@react-navigation/native-stack 8.0.0-alpha.12 → 8.0.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,10 @@ import {
6
6
  HeaderShownContext,
7
7
  useFrameSize,
8
8
  } from '@react-navigation/elements';
9
- import { SafeAreaProviderCompat } from '@react-navigation/elements/internal';
9
+ import {
10
+ ActivityView,
11
+ SafeAreaProviderCompat,
12
+ } from '@react-navigation/elements/internal';
10
13
  import {
11
14
  NavigationProvider,
12
15
  type ParamListBase,
@@ -44,19 +47,15 @@ import { useHeaderConfigProps } from './useHeaderConfigProps';
44
47
 
45
48
  const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
46
49
 
47
- function isFabric() {
48
- return 'nativeFabricUIManager' in global;
49
- }
50
-
51
50
  type SceneViewProps = {
52
51
  index: number;
53
52
  focused: boolean;
54
- shouldFreeze: boolean;
55
53
  descriptor: NativeStackDescriptor;
56
- previousDescriptor?: NativeStackDescriptor;
57
- nextDescriptor?: NativeStackDescriptor;
58
- isPresentationModal?: boolean;
59
- isPreloaded?: boolean;
54
+ previousDescriptor?: NativeStackDescriptor | undefined;
55
+ nextDescriptor?: NativeStackDescriptor | undefined;
56
+ isPresentationModal?: boolean | undefined;
57
+ isNextScreenTransparent?: boolean | undefined;
58
+ isPreloaded?: boolean | undefined;
60
59
  onWillDisappear: () => void;
61
60
  onWillAppear: () => void;
62
61
  onAppear: () => void;
@@ -70,13 +69,18 @@ type SceneViewProps = {
70
69
 
71
70
  const useNativeDriver = Platform.OS !== 'web';
72
71
 
72
+ const TRANSPARENT_PRESENTATIONS = [
73
+ 'transparentModal',
74
+ 'containedTransparentModal',
75
+ ];
76
+
73
77
  const SceneView = ({
74
78
  index,
75
79
  focused,
76
- shouldFreeze,
77
80
  descriptor,
78
81
  previousDescriptor,
79
82
  isPresentationModal,
83
+ isNextScreenTransparent,
80
84
  isPreloaded,
81
85
  onWillDisappear,
82
86
  onWillAppear,
@@ -91,6 +95,7 @@ const SceneView = ({
91
95
  const { route, navigation, options, render } = descriptor;
92
96
 
93
97
  const {
98
+ inactiveBehavior = 'pause',
94
99
  animation,
95
100
  animationDuration,
96
101
  animationMatchesGesture,
@@ -123,7 +128,6 @@ const SceneView = ({
123
128
  statusBarStyle,
124
129
  unstable_sheetFooter,
125
130
  scrollEdgeEffects,
126
- freezeOnBlur,
127
131
  contentStyle,
128
132
  } = options;
129
133
 
@@ -285,6 +289,55 @@ const SceneView = ({
285
289
  }
286
290
  );
287
291
 
292
+ const content = (
293
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
294
+ <HeaderHeightContext.Provider
295
+ value={headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)}
296
+ >
297
+ {headerBackground != null ? (
298
+ /**
299
+ * To show a custom header background, we render it at the top of the screen below the header
300
+ * The header also needs to be positioned absolutely (with `translucent` style)
301
+ */
302
+ <View
303
+ style={[
304
+ styles.background,
305
+ headerTransparent ? styles.translucent : null,
306
+ { height: headerHeight },
307
+ ]}
308
+ >
309
+ {headerBackground()}
310
+ </View>
311
+ ) : null}
312
+ {header != null && headerShown !== false ? (
313
+ <View
314
+ onLayout={(e) => {
315
+ const headerHeight = e.nativeEvent.layout.height;
316
+
317
+ animatedHeaderHeight.setValue(headerHeight);
318
+ setHeaderHeight(headerHeight);
319
+ }}
320
+ style={[styles.header, headerTransparent ? styles.absolute : null]}
321
+ >
322
+ {header({
323
+ back: headerBack,
324
+ options,
325
+ route,
326
+ navigation,
327
+ })}
328
+ </View>
329
+ ) : null}
330
+ <HeaderShownContext.Provider
331
+ value={isParentHeaderShown || headerShown !== false}
332
+ >
333
+ <HeaderBackContext.Provider value={headerBack}>
334
+ {render()}
335
+ </HeaderBackContext.Provider>
336
+ </HeaderShownContext.Provider>
337
+ </HeaderHeightContext.Provider>
338
+ </AnimatedHeaderHeightContext.Provider>
339
+ );
340
+
288
341
  return (
289
342
  <NavigationProvider navigation={navigation} route={route}>
290
343
  <ScreenStackItem
@@ -296,7 +349,6 @@ const SceneView = ({
296
349
  customAnimationOnSwipe={animationMatchesGesture}
297
350
  fullScreenSwipeEnabled={fullScreenGestureEnabled}
298
351
  fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
299
- freezeOnBlur={freezeOnBlur}
300
352
  gestureEnabled={
301
353
  Platform.OS === 'android'
302
354
  ? // This prop enables handling of system back gestures on Android
@@ -353,62 +405,30 @@ const SceneView = ({
353
405
  ]}
354
406
  headerConfig={headerConfig}
355
407
  unstable_sheetFooter={unstable_sheetFooter}
356
- // When ts-expect-error is added, it affects all the props below it
357
- // So we keep any props that need it at the end
358
- // Otherwise invalid props may not be caught by TypeScript
359
- shouldFreeze={shouldFreeze}
360
408
  >
361
- <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
362
- <HeaderHeightContext.Provider
363
- value={
364
- headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
365
- }
366
- >
367
- {headerBackground != null ? (
368
- /**
369
- * To show a custom header background, we render it at the top of the screen below the header
370
- * The header also needs to be positioned absolutely (with `translucent` style)
371
- */
372
- <View
373
- style={[
374
- styles.background,
375
- headerTransparent ? styles.translucent : null,
376
- { height: headerHeight },
377
- ]}
378
- >
379
- {headerBackground()}
380
- </View>
381
- ) : null}
382
- {header != null && headerShown !== false ? (
383
- <View
384
- onLayout={(e) => {
385
- const headerHeight = e.nativeEvent.layout.height;
386
-
387
- animatedHeaderHeight.setValue(headerHeight);
388
- setHeaderHeight(headerHeight);
389
- }}
390
- style={[
391
- styles.header,
392
- headerTransparent ? styles.absolute : null,
393
- ]}
394
- >
395
- {header({
396
- back: headerBack,
397
- options,
398
- route,
399
- navigation,
400
- })}
401
- </View>
402
- ) : null}
403
- <HeaderShownContext.Provider
404
- value={isParentHeaderShown || headerShown !== false}
405
- >
406
- <HeaderBackContext.Provider value={headerBack}>
407
- {render()}
408
- </HeaderBackContext.Provider>
409
- </HeaderShownContext.Provider>
410
- </HeaderHeightContext.Provider>
411
- </AnimatedHeaderHeightContext.Provider>
409
+ <ActivityView
410
+ mode={
411
+ // Render focused screens normally
412
+ // Unpause preloaded screens so updates are visible
413
+ // This lets effects on preloaded screens run
414
+ // We don't need to handle inert as it'll be handled natively
415
+ inactiveBehavior === 'none' ||
416
+ focused ||
417
+ isPreloaded ||
418
+ isNextScreenTransparent
419
+ ? 'normal'
420
+ : 'paused'
421
+ }
422
+ visible={
423
+ // We don't need to hide the content since it's handled natively
424
+ // Hiding may also cause flash due to lag after native tab switch
425
+ // So we leave it always visible
426
+ true
427
+ }
428
+ style={StyleSheet.absoluteFill}
429
+ >
430
+ {content}
431
+ </ActivityView>
412
432
  </ScreenStackItem>
413
433
  </NavigationProvider>
414
434
  );
@@ -433,7 +453,6 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
433
453
  {state.routes.concat(state.preloadedRoutes).map((route, index) => {
434
454
  const descriptor = descriptors[route.key];
435
455
  const isFocused = state.index === index;
436
- const isBelowFocused = state.index - 1 === index;
437
456
  const previousKey = state.routes[index - 1]?.key;
438
457
  const nextKey = state.routes[index + 1]?.key;
439
458
  const previousDescriptor = previousKey
@@ -441,29 +460,28 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
441
460
  : undefined;
442
461
  const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
443
462
 
463
+ const nextPresentation = nextDescriptor?.options.presentation;
464
+
465
+ const isNextScreenTransparent =
466
+ nextPresentation != null &&
467
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
468
+
444
469
  const isModal = modalRouteKeys.includes(route.key);
445
- const isModalOnIos = isModal && Platform.OS === 'ios';
446
470
 
447
471
  const isPreloaded = state.preloadedRoutes.some(
448
472
  (r) => r.key === route.key
449
473
  );
450
474
 
451
- // On Fabric, when screen is frozen, animated and reanimated values are not updated
452
- // due to component being unmounted. To avoid this, we don't freeze the previous screen there
453
- const shouldFreeze = isFabric()
454
- ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos
455
- : !isPreloaded && !isFocused && !isModalOnIos;
456
-
457
475
  return (
458
476
  <SceneView
459
477
  key={route.key}
460
478
  index={index}
461
479
  focused={isFocused}
462
- shouldFreeze={shouldFreeze}
463
480
  descriptor={descriptor}
464
481
  previousDescriptor={previousDescriptor}
465
482
  nextDescriptor={nextDescriptor}
466
483
  isPresentationModal={isModal}
484
+ isNextScreenTransparent={isNextScreenTransparent}
467
485
  isPreloaded={isPreloaded}
468
486
  onWillDisappear={() => {
469
487
  navigation.emit({
@@ -6,6 +6,7 @@ import {
6
6
  useHeaderHeight,
7
7
  } from '@react-navigation/elements';
8
8
  import {
9
+ ActivityView,
9
10
  SafeAreaProviderCompat,
10
11
  Screen,
11
12
  } from '@react-navigation/elements/internal';
@@ -66,6 +67,7 @@ export function NativeStackView({ state, descriptors }: Props) {
66
67
  const canGoBack = headerBack != null;
67
68
 
68
69
  const {
70
+ inactiveBehavior = 'pause',
69
71
  header,
70
72
  headerShown,
71
73
  headerBackIcon,
@@ -79,11 +81,15 @@ export function NativeStackView({ state, descriptors }: Props) {
79
81
 
80
82
  const nextPresentation = nextDescriptor?.options.presentation;
81
83
 
84
+ const isNextScreenTransparent =
85
+ nextPresentation != null &&
86
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
87
+
82
88
  const isPreloaded = state.preloadedRoutes.some(
83
89
  (r) => r.key === route.key
84
90
  );
85
91
 
86
- return (
92
+ const content = (
87
93
  <Screen
88
94
  key={route.key}
89
95
  focused={isFocused}
@@ -128,14 +134,7 @@ export function NativeStackView({ state, descriptors }: Props) {
128
134
  )
129
135
  }
130
136
  style={{
131
- ...StyleSheet.absoluteFillObject,
132
- display:
133
- (isFocused ||
134
- (nextPresentation != null &&
135
- TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
136
- !isPreloaded
137
- ? 'flex'
138
- : 'none',
137
+ ...StyleSheet.absoluteFill,
139
138
  ...(presentation != null &&
140
139
  TRANSPARENT_PRESENTATIONS.includes(presentation)
141
140
  ? { backgroundColor: 'transparent' }
@@ -151,6 +150,28 @@ export function NativeStackView({ state, descriptors }: Props) {
151
150
  </HeaderBackContext.Provider>
152
151
  </Screen>
153
152
  );
153
+
154
+ return (
155
+ <ActivityView
156
+ key={route.key}
157
+ mode={
158
+ // Render focused screens normally
159
+ isFocused
160
+ ? 'normal'
161
+ : // Unpause preloaded screens so updates are visible
162
+ // This lets effects on preloaded screens run
163
+ inactiveBehavior === 'none' ||
164
+ isPreloaded ||
165
+ isNextScreenTransparent
166
+ ? 'inert'
167
+ : 'paused'
168
+ }
169
+ visible={isFocused || isPreloaded || isNextScreenTransparent}
170
+ style={StyleSheet.absoluteFill}
171
+ >
172
+ {content}
173
+ </ActivityView>
174
+ );
154
175
  })}
155
176
  </SafeAreaProviderCompat>
156
177
  );
@@ -10,9 +10,10 @@ import {
10
10
  import { useMemo } from 'react';
11
11
  import { Platform, StyleSheet, type TextStyle, View } from 'react-native';
12
12
  import {
13
- type HeaderBarButtonItem,
14
13
  type HeaderBarButtonItemMenuAction,
15
14
  type HeaderBarButtonItemSubmenu,
15
+ type HeaderBarButtonItemWithAction,
16
+ type HeaderBarButtonItemWithMenu,
16
17
  isSearchBarAvailableForCurrentPlatform,
17
18
  ScreenStackHeaderBackButtonImage,
18
19
  ScreenStackHeaderCenterView,
@@ -25,6 +26,7 @@ import {
25
26
 
26
27
  import type {
27
28
  NativeStackHeaderItem,
29
+ NativeStackHeaderItemButton,
28
30
  NativeStackHeaderItemMenuAction,
29
31
  NativeStackHeaderItemMenuSubmenu,
30
32
  NativeStackNavigationOptions,
@@ -74,7 +76,7 @@ const processBarButtonItems = (
74
76
 
75
77
  const { badge, label, labelStyle, icon, ...rest } = item;
76
78
 
77
- let processedItem: HeaderBarButtonItem = {
79
+ const processedItemCommon = {
78
80
  ...rest,
79
81
  index,
80
82
  title: label,
@@ -82,32 +84,34 @@ const processBarButtonItems = (
82
84
  ...fonts.regular,
83
85
  ...labelStyle,
84
86
  },
85
- icon:
86
- icon?.type === 'image'
87
- ? icon.tinted === false
88
- ? {
89
- type: 'imageSource',
90
- imageSource: icon.source,
91
- }
92
- : {
93
- type: 'templateSource',
94
- templateSource: icon.source,
95
- }
96
- : icon,
87
+ icon: transformIcon(icon),
97
88
  };
98
89
 
99
- if (processedItem.type === 'menu' && item.type === 'menu') {
90
+ let processedItem:
91
+ | HeaderBarButtonItemWithAction
92
+ | HeaderBarButtonItemWithMenu;
93
+
94
+ if (processedItemCommon.type === 'menu' && item.type === 'menu') {
100
95
  const { multiselectable, layout } = item.menu;
101
96
 
102
97
  processedItem = {
103
- ...processedItem,
98
+ ...processedItemCommon,
104
99
  menu: {
105
- ...processedItem.menu,
100
+ ...processedItemCommon.menu,
106
101
  singleSelection: !multiselectable,
107
102
  displayAsPalette: layout === 'palette',
108
103
  items: item.menu.items.map(getMenuItem),
109
104
  },
110
105
  };
106
+ } else if (
107
+ processedItemCommon.type === 'button' &&
108
+ item.type === 'button'
109
+ ) {
110
+ processedItem = processedItemCommon;
111
+ } else {
112
+ throw new Error(
113
+ `Invalid item type: ${JSON.stringify(item)}. Valid types are 'button' and 'menu'.`
114
+ );
111
115
  }
112
116
 
113
117
  if (badge) {
@@ -140,14 +144,30 @@ const processBarButtonItems = (
140
144
  .filter((item) => item != null);
141
145
  };
142
146
 
147
+ const transformIcon = (
148
+ icon: NativeStackHeaderItemButton['icon']
149
+ ):
150
+ | HeaderBarButtonItemWithAction['icon']
151
+ | HeaderBarButtonItemWithMenu['icon'] => {
152
+ if (icon?.type === 'image') {
153
+ return icon.tinted === false
154
+ ? { type: 'imageSource', imageSource: icon.source }
155
+ : { type: 'templateSource', templateSource: icon.source };
156
+ }
157
+
158
+ return icon;
159
+ };
160
+
143
161
  const getMenuItem = (
144
162
  item: NativeStackHeaderItemMenuAction | NativeStackHeaderItemMenuSubmenu
145
163
  ): HeaderBarButtonItemMenuAction | HeaderBarButtonItemSubmenu => {
146
164
  if (item.type === 'submenu') {
147
- const { label, inline, layout, items, multiselectable, ...rest } = item;
165
+ const { label, icon, inline, layout, items, multiselectable, ...rest } =
166
+ item;
148
167
 
149
168
  return {
150
169
  ...rest,
170
+ icon: transformIcon(icon),
151
171
  title: label,
152
172
  displayAsPalette: layout === 'palette',
153
173
  displayInline: inline,
@@ -156,10 +176,11 @@ const getMenuItem = (
156
176
  };
157
177
  }
158
178
 
159
- const { label, description, ...rest } = item;
179
+ const { label, icon, description, ...rest } = item;
160
180
 
161
181
  return {
162
182
  ...rest,
183
+ icon: transformIcon(icon),
163
184
  title: label,
164
185
  subtitle: description,
165
186
  };