@react-navigation/native-stack 8.0.0-alpha.3 → 8.0.0-alpha.30

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.
Files changed (28) hide show
  1. package/lib/module/index.js.map +1 -1
  2. package/lib/module/navigators/createNativeStackNavigator.js +4 -8
  3. package/lib/module/navigators/createNativeStackNavigator.js.map +1 -1
  4. package/lib/module/utils/useAnimatedHeaderHeight.js +1 -1
  5. package/lib/module/utils/useAnimatedHeaderHeight.js.map +1 -1
  6. package/lib/module/views/NativeStackView.js +34 -24
  7. package/lib/module/views/NativeStackView.js.map +1 -1
  8. package/lib/module/views/NativeStackView.native.js +92 -68
  9. package/lib/module/views/NativeStackView.native.js.map +1 -1
  10. package/lib/module/views/useHeaderConfigProps.js +71 -21
  11. package/lib/module/views/useHeaderConfigProps.js.map +1 -1
  12. package/lib/typescript/src/index.d.ts +1 -1
  13. package/lib/typescript/src/index.d.ts.map +1 -1
  14. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts +8 -14
  15. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts.map +1 -1
  16. package/lib/typescript/src/types.d.ts +181 -146
  17. package/lib/typescript/src/types.d.ts.map +1 -1
  18. package/lib/typescript/src/views/NativeStackView.d.ts.map +1 -1
  19. package/lib/typescript/src/views/NativeStackView.native.d.ts.map +1 -1
  20. package/lib/typescript/src/views/useHeaderConfigProps.d.ts.map +1 -1
  21. package/package.json +17 -18
  22. package/src/index.tsx +1 -0
  23. package/src/navigators/createNativeStackNavigator.tsx +11 -47
  24. package/src/types.tsx +242 -184
  25. package/src/utils/useAnimatedHeaderHeight.tsx +1 -1
  26. package/src/views/NativeStackView.native.tsx +135 -89
  27. package/src/views/NativeStackView.tsx +52 -40
  28. package/src/views/useHeaderConfigProps.tsx +100 -36
@@ -6,7 +6,7 @@ export const AnimatedHeaderHeightContext = React.createContext<
6
6
  >(undefined);
7
7
 
8
8
  export function useAnimatedHeaderHeight() {
9
- const animatedValue = React.useContext(AnimatedHeaderHeightContext);
9
+ const animatedValue = React.use(AnimatedHeaderHeightContext);
10
10
 
11
11
  if (animatedValue === undefined) {
12
12
  throw new Error(
@@ -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,16 @@ 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;
57
+ isNextScreenTransparent: boolean;
58
+ isInactive: boolean;
59
+ isBeforeLast: boolean;
60
60
  onWillDisappear: () => void;
61
61
  onWillAppear: () => void;
62
62
  onAppear: () => void;
@@ -70,14 +70,20 @@ type SceneViewProps = {
70
70
 
71
71
  const useNativeDriver = Platform.OS !== 'web';
72
72
 
73
+ const TRANSPARENT_PRESENTATIONS = [
74
+ 'transparentModal',
75
+ 'containedTransparentModal',
76
+ ];
77
+
73
78
  const SceneView = ({
74
79
  index,
75
80
  focused,
76
- shouldFreeze,
77
81
  descriptor,
78
82
  previousDescriptor,
79
83
  isPresentationModal,
80
- isPreloaded,
84
+ isNextScreenTransparent,
85
+ isInactive,
86
+ isBeforeLast,
81
87
  onWillDisappear,
82
88
  onWillAppear,
83
89
  onAppear,
@@ -91,6 +97,7 @@ const SceneView = ({
91
97
  const { route, navigation, options, render } = descriptor;
92
98
 
93
99
  const {
100
+ inactiveBehavior = 'pause',
94
101
  animation,
95
102
  animationDuration,
96
103
  animationMatchesGesture,
@@ -116,12 +123,13 @@ const SceneView = ({
116
123
  sheetElevation = 24,
117
124
  sheetExpandsWhenScrolledToEdge = true,
118
125
  sheetInitialDetentIndex = 0,
126
+ sheetShouldOverflowTopInset = false,
127
+ sheetResizeAnimationEnabled = true,
119
128
  statusBarAnimation,
120
129
  statusBarHidden,
121
130
  statusBarStyle,
122
131
  unstable_sheetFooter,
123
132
  scrollEdgeEffects,
124
- freezeOnBlur,
125
133
  contentStyle,
126
134
  } = options;
127
135
 
@@ -145,9 +153,9 @@ const SceneView = ({
145
153
  // Modals are fullscreen in landscape only on iPhone
146
154
  const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV);
147
155
 
148
- const isParentHeaderShown = React.useContext(HeaderShownContext);
149
- const parentHeaderHeight = React.useContext(HeaderHeightContext);
150
- const parentHeaderBack = React.useContext(HeaderBackContext);
156
+ const isParentHeaderShown = React.use(HeaderShownContext);
157
+ const parentHeaderHeight = React.use(HeaderHeightContext);
158
+ const parentHeaderBack = React.use(HeaderBackContext);
151
159
 
152
160
  const isLandscape = useFrameSize((frame) => frame.width > frame.height);
153
161
 
@@ -263,7 +271,8 @@ const SceneView = ({
263
271
  if (
264
272
  Platform.OS === 'android' &&
265
273
  headerHeight !== 0 &&
266
- headerHeight <= ANDROID_DEFAULT_HEADER_HEIGHT
274
+ // On some devices, height maybe slightly off (e.g. 56.17 instead of 56)
275
+ Math.round(headerHeight) <= ANDROID_DEFAULT_HEADER_HEIGHT
267
276
  ) {
268
277
  // FIXME: On Android, events may get delivered out-of-order
269
278
  // https://github.com/facebook/react-native/issues/54636
@@ -283,18 +292,95 @@ const SceneView = ({
283
292
  }
284
293
  );
285
294
 
295
+ const activityMode =
296
+ // Render focused screens normally
297
+ // Unpause preloaded and retained screens so updates are visible
298
+ // This lets effects on those screens run
299
+ // We don't need to handle inert as it'll be handled natively
300
+ inactiveBehavior === 'none' ||
301
+ focused ||
302
+ isInactive ||
303
+ isNextScreenTransparent
304
+ ? 'normal'
305
+ : inactiveBehavior === 'unmount' &&
306
+ !isBeforeLast &&
307
+ !('state' in route && route.state)
308
+ ? 'unmounted'
309
+ : 'paused';
310
+
311
+ const content = (
312
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
313
+ <HeaderHeightContext.Provider
314
+ value={headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)}
315
+ >
316
+ {headerBackground != null ? (
317
+ /**
318
+ * To show a custom header background, we render it at the top of the screen below the header
319
+ * The header also needs to be positioned absolutely (with `translucent` style)
320
+ */
321
+ <View
322
+ style={[
323
+ styles.background,
324
+ headerTransparent ? styles.translucent : null,
325
+ { height: headerHeight },
326
+ ]}
327
+ >
328
+ {headerBackground()}
329
+ </View>
330
+ ) : null}
331
+ {header != null && headerShown !== false ? (
332
+ <View
333
+ style={[
334
+ styles.header,
335
+ headerTransparent
336
+ ? [
337
+ styles.absolute,
338
+ // Specify an explicit min height for Android screen readers
339
+ { minHeight: headerHeight },
340
+ ]
341
+ : null,
342
+ ]}
343
+ >
344
+ <View
345
+ onLayout={(e) => {
346
+ const headerHeight = e.nativeEvent.layout.height;
347
+
348
+ animatedHeaderHeight.setValue(headerHeight);
349
+ setHeaderHeight(headerHeight);
350
+ }}
351
+ style={{ pointerEvents: 'box-none' }}
352
+ >
353
+ {header({
354
+ back: headerBack,
355
+ options,
356
+ route,
357
+ navigation,
358
+ })}
359
+ </View>
360
+ </View>
361
+ ) : null}
362
+ <HeaderShownContext.Provider
363
+ value={isParentHeaderShown || headerShown !== false}
364
+ >
365
+ <HeaderBackContext.Provider value={headerBack}>
366
+ {render()}
367
+ </HeaderBackContext.Provider>
368
+ </HeaderShownContext.Provider>
369
+ </HeaderHeightContext.Provider>
370
+ </AnimatedHeaderHeightContext.Provider>
371
+ );
372
+
286
373
  return (
287
374
  <NavigationProvider navigation={navigation} route={route}>
288
375
  <ScreenStackItem
289
376
  key={route.key}
290
377
  screenId={route.key}
291
- activityState={isPreloaded ? 0 : 2}
378
+ activityState={isInactive ? 0 : 2}
292
379
  style={StyleSheet.absoluteFill}
293
380
  aria-hidden={!focused}
294
381
  customAnimationOnSwipe={animationMatchesGesture}
295
382
  fullScreenSwipeEnabled={fullScreenGestureEnabled}
296
383
  fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
297
- freezeOnBlur={freezeOnBlur}
298
384
  gestureEnabled={
299
385
  Platform.OS === 'android'
300
386
  ? // This prop enables handling of system back gestures on Android
@@ -316,6 +402,8 @@ const SceneView = ({
316
402
  sheetCornerRadius={sheetCornerRadius}
317
403
  sheetElevation={sheetElevation}
318
404
  sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
405
+ sheetShouldOverflowTopInset={sheetShouldOverflowTopInset}
406
+ sheetDefaultResizeAnimationEnabled={sheetResizeAnimationEnabled}
319
407
  statusBarAnimation={statusBarAnimation}
320
408
  statusBarHidden={statusBarHidden}
321
409
  statusBarStyle={statusBarStyle}
@@ -349,62 +437,21 @@ const SceneView = ({
349
437
  ]}
350
438
  headerConfig={headerConfig}
351
439
  unstable_sheetFooter={unstable_sheetFooter}
352
- // When ts-expect-error is added, it affects all the props below it
353
- // So we keep any props that need it at the end
354
- // Otherwise invalid props may not be caught by TypeScript
355
- shouldFreeze={shouldFreeze}
356
440
  >
357
- <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
358
- <HeaderHeightContext.Provider
359
- value={
360
- headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
441
+ {activityMode === 'unmounted' ? null : (
442
+ <ActivityView
443
+ mode={activityMode}
444
+ visible={
445
+ // We don't need to hide the content since it's handled natively
446
+ // Hiding may also cause flash due to lag after native tab switch
447
+ // So we leave it always visible
448
+ true
361
449
  }
450
+ style={styles.content}
362
451
  >
363
- {headerBackground != null ? (
364
- /**
365
- * To show a custom header background, we render it at the top of the screen below the header
366
- * The header also needs to be positioned absolutely (with `translucent` style)
367
- */
368
- <View
369
- style={[
370
- styles.background,
371
- headerTransparent ? styles.translucent : null,
372
- { height: headerHeight },
373
- ]}
374
- >
375
- {headerBackground()}
376
- </View>
377
- ) : null}
378
- {header != null && headerShown !== false ? (
379
- <View
380
- onLayout={(e) => {
381
- const headerHeight = e.nativeEvent.layout.height;
382
-
383
- animatedHeaderHeight.setValue(headerHeight);
384
- setHeaderHeight(headerHeight);
385
- }}
386
- style={[
387
- styles.header,
388
- headerTransparent ? styles.absolute : null,
389
- ]}
390
- >
391
- {header({
392
- back: headerBack,
393
- options,
394
- route,
395
- navigation,
396
- })}
397
- </View>
398
- ) : null}
399
- <HeaderShownContext.Provider
400
- value={isParentHeaderShown || headerShown !== false}
401
- >
402
- <HeaderBackContext.Provider value={headerBack}>
403
- {render()}
404
- </HeaderBackContext.Provider>
405
- </HeaderShownContext.Provider>
406
- </HeaderHeightContext.Provider>
407
- </AnimatedHeaderHeightContext.Provider>
452
+ {content}
453
+ </ActivityView>
454
+ )}
408
455
  </ScreenStackItem>
409
456
  </NavigationProvider>
410
457
  );
@@ -421,46 +468,42 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
421
468
 
422
469
  useInvalidPreventRemoveError(descriptors);
423
470
 
424
- const modalRouteKeys = getModalRouteKeys(state.routes, descriptors);
471
+ const activeRoutes = state.routes.slice(0, state.index + 1);
472
+ const modalRouteKeys = getModalRouteKeys(activeRoutes, descriptors);
425
473
 
426
474
  return (
427
475
  <SafeAreaProviderCompat>
428
476
  <ScreenStack style={styles.container}>
429
- {state.routes.concat(state.preloadedRoutes).map((route, index) => {
477
+ {state.routes.map((route, index) => {
430
478
  const descriptor = descriptors[route.key];
431
479
  const isFocused = state.index === index;
432
- const isBelowFocused = state.index - 1 === index;
433
- const previousKey = state.routes[index - 1]?.key;
434
- const nextKey = state.routes[index + 1]?.key;
480
+ const previousKey = activeRoutes[index - 1]?.key;
481
+ const nextKey = activeRoutes[index + 1]?.key;
435
482
  const previousDescriptor = previousKey
436
483
  ? descriptors[previousKey]
437
484
  : undefined;
438
485
  const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
439
486
 
440
- const isModal = modalRouteKeys.includes(route.key);
441
- const isModalOnIos = isModal && Platform.OS === 'ios';
487
+ const nextPresentation = nextDescriptor?.options.presentation;
442
488
 
443
- const isPreloaded = state.preloadedRoutes.some(
444
- (r) => r.key === route.key
445
- );
489
+ const isNextScreenTransparent =
490
+ nextPresentation != null &&
491
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
446
492
 
447
- // On Fabric, when screen is frozen, animated and reanimated values are not updated
448
- // due to component being unmounted. To avoid this, we don't freeze the previous screen there
449
- const shouldFreeze = isFabric()
450
- ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos
451
- : !isPreloaded && !isFocused && !isModalOnIos;
493
+ const isModal = modalRouteKeys.includes(route.key);
452
494
 
453
495
  return (
454
496
  <SceneView
455
497
  key={route.key}
456
498
  index={index}
457
499
  focused={isFocused}
458
- shouldFreeze={shouldFreeze}
459
500
  descriptor={descriptor}
460
501
  previousDescriptor={previousDescriptor}
461
502
  nextDescriptor={nextDescriptor}
462
503
  isPresentationModal={isModal}
463
- isPreloaded={isPreloaded}
504
+ isNextScreenTransparent={isNextScreenTransparent}
505
+ isInactive={index > state.index}
506
+ isBeforeLast={index === activeRoutes.length - 2}
464
507
  onWillDisappear={() => {
465
508
  navigation.emit({
466
509
  type: 'transitionStart',
@@ -540,6 +583,9 @@ const styles = StyleSheet.create({
540
583
  container: {
541
584
  flex: 1,
542
585
  },
586
+ content: {
587
+ flex: 1,
588
+ },
543
589
  header: {
544
590
  zIndex: 1,
545
591
  },
@@ -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';
@@ -15,7 +16,7 @@ import {
15
16
  useLinkBuilder,
16
17
  } from '@react-navigation/native';
17
18
  import * as React from 'react';
18
- import { Animated, Image, StyleSheet, View } from 'react-native';
19
+ import { Animated, StyleSheet, View } from 'react-native';
19
20
 
20
21
  import type {
21
22
  NativeStackDescriptorMap,
@@ -35,15 +36,17 @@ const TRANSPARENT_PRESENTATIONS = [
35
36
  ];
36
37
 
37
38
  export function NativeStackView({ state, descriptors }: Props) {
38
- const parentHeaderBack = React.useContext(HeaderBackContext);
39
+ const parentHeaderBack = React.use(HeaderBackContext);
39
40
  const { buildHref } = useLinkBuilder();
40
41
 
42
+ const activeRoutes = state.routes.slice(0, state.index + 1);
43
+
41
44
  return (
42
45
  <SafeAreaProviderCompat>
43
- {state.routes.concat(state.preloadedRoutes).map((route, i) => {
46
+ {state.routes.map((route, i) => {
44
47
  const isFocused = state.index === i;
45
- const previousKey = state.routes[i - 1]?.key;
46
- const nextKey = state.routes[i + 1]?.key;
48
+ const previousKey = activeRoutes[i - 1]?.key;
49
+ const nextKey = activeRoutes[i + 1]?.key;
47
50
  const previousDescriptor = previousKey
48
51
  ? descriptors[previousKey]
49
52
  : undefined;
@@ -66,6 +69,7 @@ export function NativeStackView({ state, descriptors }: Props) {
66
69
  const canGoBack = headerBack != null;
67
70
 
68
71
  const {
72
+ inactiveBehavior = 'pause',
69
73
  header,
70
74
  headerShown,
71
75
  headerBackIcon,
@@ -79,11 +83,33 @@ export function NativeStackView({ state, descriptors }: Props) {
79
83
 
80
84
  const nextPresentation = nextDescriptor?.options.presentation;
81
85
 
82
- const isPreloaded = state.preloadedRoutes.some(
83
- (r) => r.key === route.key
84
- );
86
+ const isNextScreenTransparent =
87
+ nextPresentation != null &&
88
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
85
89
 
86
- return (
90
+ const isInactive = i > state.index;
91
+
92
+ const isBeforeLast = i === activeRoutes.length - 2;
93
+
94
+ const activityMode =
95
+ // Render focused screens normally
96
+ isFocused
97
+ ? 'normal'
98
+ : // Unpause preloaded and retained screens so updates are visible
99
+ // This lets effects on those screens run
100
+ inactiveBehavior === 'none' ||
101
+ isInactive ||
102
+ isNextScreenTransparent
103
+ ? 'inert'
104
+ : inactiveBehavior === 'unmount' && !isBeforeLast && !route.state
105
+ ? 'unmounted'
106
+ : 'paused';
107
+
108
+ if (activityMode === 'unmounted') {
109
+ return null;
110
+ }
111
+
112
+ const content = (
87
113
  <Screen
88
114
  key={route.key}
89
115
  focused={isFocused}
@@ -117,18 +143,7 @@ export function NativeStackView({ state, descriptors }: Props) {
117
143
  {...rest}
118
144
  label={headerBackTitle ?? label}
119
145
  tintColor={tintColor}
120
- backImage={
121
- headerBackIcon !== undefined
122
- ? () => (
123
- <Image
124
- source={headerBackIcon.source}
125
- resizeMode="contain"
126
- tintColor={tintColor}
127
- style={styles.backImage}
128
- />
129
- )
130
- : undefined
131
- }
146
+ icon={headerBackIcon}
132
147
  onPress={navigation.goBack}
133
148
  />
134
149
  )
@@ -138,22 +153,13 @@ export function NativeStackView({ state, descriptors }: Props) {
138
153
  />
139
154
  )
140
155
  }
141
- style={[
142
- StyleSheet.absoluteFill,
143
- {
144
- display:
145
- (isFocused ||
146
- (nextPresentation != null &&
147
- TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
148
- !isPreloaded
149
- ? 'flex'
150
- : 'none',
151
- },
152
- presentation != null &&
156
+ style={{
157
+ ...StyleSheet.absoluteFill,
158
+ ...(presentation != null &&
153
159
  TRANSPARENT_PRESENTATIONS.includes(presentation)
154
160
  ? { backgroundColor: 'transparent' }
155
- : null,
156
- ]}
161
+ : null),
162
+ }}
157
163
  >
158
164
  <HeaderBackContext.Provider value={headerBack}>
159
165
  <AnimatedHeaderHeightProvider>
@@ -164,6 +170,17 @@ export function NativeStackView({ state, descriptors }: Props) {
164
170
  </HeaderBackContext.Provider>
165
171
  </Screen>
166
172
  );
173
+
174
+ return (
175
+ <ActivityView
176
+ key={route.key}
177
+ mode={activityMode}
178
+ visible={isFocused || isNextScreenTransparent}
179
+ style={StyleSheet.absoluteFill}
180
+ >
181
+ {content}
182
+ </ActivityView>
183
+ );
167
184
  })}
168
185
  </SafeAreaProviderCompat>
169
186
  );
@@ -194,9 +211,4 @@ const styles = StyleSheet.create({
194
211
  contentContainer: {
195
212
  flex: 1,
196
213
  },
197
- backImage: {
198
- height: 24,
199
- width: 24,
200
- margin: 3,
201
- },
202
214
  });