@react-navigation/native-stack 8.0.0-alpha.5 → 8.0.0-alpha.51

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 (43) hide show
  1. package/lib/module/index.js.map +1 -1
  2. package/lib/module/navigators/createNativeStackNavigator.js +16 -13
  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/utils/useInvalidPreventRemoveError.js +1 -1
  7. package/lib/module/utils/useInvalidPreventRemoveError.js.map +1 -1
  8. package/lib/module/views/FooterComponent.js +1 -1
  9. package/lib/module/views/FooterComponent.js.map +1 -1
  10. package/lib/module/views/NativeStackView.js +42 -25
  11. package/lib/module/views/NativeStackView.js.map +1 -1
  12. package/lib/module/views/NativeStackView.native.js +124 -76
  13. package/lib/module/views/NativeStackView.native.js.map +1 -1
  14. package/lib/module/views/useHeaderConfigProps.js +76 -23
  15. package/lib/module/views/useHeaderConfigProps.js.map +1 -1
  16. package/lib/typescript/src/index.d.ts +3 -3
  17. package/lib/typescript/src/index.d.ts.map +1 -1
  18. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts +10 -15
  19. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts.map +1 -1
  20. package/lib/typescript/src/types.d.ts +164 -156
  21. package/lib/typescript/src/types.d.ts.map +1 -1
  22. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts +1 -1
  23. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts.map +1 -1
  24. package/lib/typescript/src/utils/useInvalidPreventRemoveError.d.ts +1 -1
  25. package/lib/typescript/src/utils/useInvalidPreventRemoveError.d.ts.map +1 -1
  26. package/lib/typescript/src/views/FooterComponent.d.ts +2 -2
  27. package/lib/typescript/src/views/FooterComponent.d.ts.map +1 -1
  28. package/lib/typescript/src/views/NativeStackView.d.ts +3 -2
  29. package/lib/typescript/src/views/NativeStackView.d.ts.map +1 -1
  30. package/lib/typescript/src/views/NativeStackView.native.d.ts +3 -2
  31. package/lib/typescript/src/views/NativeStackView.native.d.ts.map +1 -1
  32. package/lib/typescript/src/views/useHeaderConfigProps.d.ts +2 -3
  33. package/lib/typescript/src/views/useHeaderConfigProps.d.ts.map +1 -1
  34. package/package.json +22 -19
  35. package/src/index.tsx +1 -2
  36. package/src/navigators/createNativeStackNavigator.tsx +27 -52
  37. package/src/types.tsx +225 -196
  38. package/src/utils/useAnimatedHeaderHeight.tsx +1 -1
  39. package/src/utils/useInvalidPreventRemoveError.tsx +2 -1
  40. package/src/views/FooterComponent.tsx +1 -1
  41. package/src/views/NativeStackView.native.tsx +184 -97
  42. package/src/views/NativeStackView.tsx +70 -41
  43. package/src/views/useHeaderConfigProps.tsx +109 -39
@@ -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(
@@ -8,7 +8,8 @@ export function useInvalidPreventRemoveError(
8
8
  ) {
9
9
  const { preventedRoutes } = usePreventRemoveContext();
10
10
  const preventedRouteKey = Object.keys(preventedRoutes)[0];
11
- const preventedDescriptor = descriptors[preventedRouteKey];
11
+ const preventedDescriptor =
12
+ preventedRouteKey == null ? undefined : descriptors[preventedRouteKey];
12
13
  const isHeaderBackButtonMenuEnabledOnPreventedScreen =
13
14
  preventedDescriptor?.options?.headerBackButtonMenuEnabled;
14
15
  const preventedRouteName = preventedDescriptor?.route?.name;
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
2
  import { ScreenFooter } from 'react-native-screens';
3
3
 
4
4
  type FooterProps = {
@@ -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,18 @@ 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
+ isNextScreenSheet: boolean;
59
+ isNextScreenModal: boolean;
60
+ isInactive: boolean;
61
+ isBeforeLast: boolean;
60
62
  onWillDisappear: () => void;
61
63
  onWillAppear: () => void;
62
64
  onAppear: () => void;
@@ -70,14 +72,24 @@ type SceneViewProps = {
70
72
 
71
73
  const useNativeDriver = Platform.OS !== 'web';
72
74
 
75
+ const TRANSPARENT_PRESENTATIONS = [
76
+ 'transparentModal',
77
+ 'containedTransparentModal',
78
+ ];
79
+
80
+ const SHEET_PRESENTATIONS = ['formSheet', 'pageSheet'];
81
+
73
82
  const SceneView = ({
74
83
  index,
75
84
  focused,
76
- shouldFreeze,
77
85
  descriptor,
78
86
  previousDescriptor,
79
87
  isPresentationModal,
80
- isPreloaded,
88
+ isNextScreenTransparent,
89
+ isNextScreenSheet,
90
+ isNextScreenModal,
91
+ isInactive,
92
+ isBeforeLast,
81
93
  onWillDisappear,
82
94
  onWillAppear,
83
95
  onAppear,
@@ -91,6 +103,7 @@ const SceneView = ({
91
103
  const { route, navigation, options, render } = descriptor;
92
104
 
93
105
  const {
106
+ inactiveBehavior = 'pause',
94
107
  animation,
95
108
  animationDuration,
96
109
  animationMatchesGesture,
@@ -121,9 +134,9 @@ const SceneView = ({
121
134
  statusBarAnimation,
122
135
  statusBarHidden,
123
136
  statusBarStyle,
137
+ unstable_headerInsets,
124
138
  unstable_sheetFooter,
125
139
  scrollEdgeEffects,
126
- freezeOnBlur,
127
140
  contentStyle,
128
141
  } = options;
129
142
 
@@ -147,14 +160,15 @@ const SceneView = ({
147
160
  // Modals are fullscreen in landscape only on iPhone
148
161
  const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV);
149
162
 
150
- const isParentHeaderShown = React.useContext(HeaderShownContext);
151
- const parentHeaderHeight = React.useContext(HeaderHeightContext);
152
- const parentHeaderBack = React.useContext(HeaderBackContext);
163
+ const isParentHeaderShown = React.use(HeaderShownContext);
164
+ const parentHeaderHeight = React.use(HeaderHeightContext);
165
+ const parentHeaderBack = React.use(HeaderBackContext);
153
166
 
154
167
  const isLandscape = useFrameSize((frame) => frame.width > frame.height);
155
168
 
156
169
  const topInset =
157
170
  isParentHeaderShown ||
171
+ (Platform.OS === 'android' && unstable_headerInsets?.top === false) ||
158
172
  (Platform.OS === 'ios' && isModal) ||
159
173
  (isIPhone && isLandscape)
160
174
  ? 0
@@ -187,8 +201,6 @@ const SceneView = ({
187
201
 
188
202
  const hasCustomHeader = header != null;
189
203
 
190
- const headerTopInsetEnabled = topInset !== 0;
191
-
192
204
  const canGoBack = previousDescriptor != null || parentHeaderBack != null;
193
205
  const backTitle = previousDescriptor
194
206
  ? getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name)
@@ -222,10 +234,19 @@ const SceneView = ({
222
234
  : undefined,
223
235
  headerHeight,
224
236
  headerShown: header !== undefined ? false : headerShown,
225
- headerTopInsetEnabled,
226
237
  headerBack,
227
238
  });
228
239
 
240
+ const headerContainerRef =
241
+ React.useRef<React.ComponentRef<typeof View>>(null);
242
+
243
+ React.useLayoutEffect(() => {
244
+ headerContainerRef.current?.measure((_x, _y, _width, height) => {
245
+ animatedHeaderHeight.setValue(height);
246
+ setHeaderHeight(height);
247
+ });
248
+ }, [animatedHeaderHeight]);
249
+
229
250
  const onHeaderHeightChange = hasCustomHeader
230
251
  ? // If we have a custom header, don't use native header height
231
252
  undefined
@@ -265,7 +286,8 @@ const SceneView = ({
265
286
  if (
266
287
  Platform.OS === 'android' &&
267
288
  headerHeight !== 0 &&
268
- headerHeight <= ANDROID_DEFAULT_HEADER_HEIGHT
289
+ // On some devices, height maybe slightly off (e.g. 56.17 instead of 56)
290
+ Math.round(headerHeight) <= ANDROID_DEFAULT_HEADER_HEIGHT
269
291
  ) {
270
292
  // FIXME: On Android, events may get delivered out-of-order
271
293
  // https://github.com/facebook/react-native/issues/54636
@@ -285,18 +307,102 @@ const SceneView = ({
285
307
  }
286
308
  );
287
309
 
310
+ const activityMode =
311
+ // Render focused screens normally
312
+ // Unpause preloaded and retained screens so updates are visible
313
+ // Unpause previous screen so update isn't delayed for swipe back
314
+ // This lets effects on those screens run
315
+ // We don't need to handle inert as it'll be handled natively
316
+ inactiveBehavior === 'none' ||
317
+ focused ||
318
+ isInactive ||
319
+ isNextScreenTransparent ||
320
+ // Unpause the screen behind a sheet so it stays up to date
321
+ // Sheets don't cover the whole screen, so the screen behind is visible
322
+ isNextScreenSheet ||
323
+ // On iPadOS, the base screen is visible while modal is in a smaller area
324
+ // So also keep the screens unpaused
325
+ (isNextScreenModal && !isModal) ||
326
+ isBeforeLast
327
+ ? 'normal'
328
+ : inactiveBehavior === 'unmount' && !('state' in route && route.state)
329
+ ? 'unmounted'
330
+ : 'paused';
331
+
332
+ const content = (
333
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
334
+ <HeaderHeightContext.Provider
335
+ value={headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)}
336
+ >
337
+ {headerBackground != null ? (
338
+ /**
339
+ * To show a custom header background, we render it at the top of the screen below the header
340
+ * The header also needs to be positioned absolutely (with `translucent` style)
341
+ */
342
+ <View
343
+ style={[
344
+ styles.background,
345
+ headerTransparent ? styles.translucent : null,
346
+ { height: headerHeight },
347
+ ]}
348
+ >
349
+ {headerBackground()}
350
+ </View>
351
+ ) : null}
352
+ {header != null && headerShown !== false ? (
353
+ <View
354
+ style={[
355
+ styles.header,
356
+ headerTransparent
357
+ ? [
358
+ styles.absolute,
359
+ // Specify an explicit min height for Android screen readers
360
+ { minHeight: headerHeight },
361
+ ]
362
+ : null,
363
+ ]}
364
+ >
365
+ <View
366
+ ref={headerContainerRef}
367
+ onLayout={(e) => {
368
+ const headerHeight = e.nativeEvent.layout.height;
369
+
370
+ animatedHeaderHeight.setValue(headerHeight);
371
+ setHeaderHeight(headerHeight);
372
+ }}
373
+ style={{ pointerEvents: 'box-none' }}
374
+ >
375
+ {header({
376
+ back: headerBack,
377
+ options,
378
+ route,
379
+ navigation,
380
+ })}
381
+ </View>
382
+ </View>
383
+ ) : null}
384
+ <HeaderShownContext.Provider
385
+ value={isParentHeaderShown || headerShown !== false}
386
+ >
387
+ <HeaderBackContext.Provider value={headerBack}>
388
+ {render()}
389
+ </HeaderBackContext.Provider>
390
+ </HeaderShownContext.Provider>
391
+ </HeaderHeightContext.Provider>
392
+ </AnimatedHeaderHeightContext.Provider>
393
+ );
394
+
288
395
  return (
289
396
  <NavigationProvider navigation={navigation} route={route}>
290
397
  <ScreenStackItem
291
398
  key={route.key}
292
399
  screenId={route.key}
293
- activityState={isPreloaded ? 0 : 2}
400
+ activityState={isInactive ? 0 : 2}
294
401
  style={StyleSheet.absoluteFill}
295
402
  aria-hidden={!focused}
296
403
  customAnimationOnSwipe={animationMatchesGesture}
297
404
  fullScreenSwipeEnabled={fullScreenGestureEnabled}
298
405
  fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
299
- freezeOnBlur={freezeOnBlur}
300
406
  gestureEnabled={
301
407
  Platform.OS === 'android'
302
408
  ? // This prop enables handling of system back gestures on Android
@@ -353,62 +459,21 @@ const SceneView = ({
353
459
  ]}
354
460
  headerConfig={headerConfig}
355
461
  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
462
  >
361
- <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
362
- <HeaderHeightContext.Provider
363
- value={
364
- headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
463
+ {activityMode === 'unmounted' ? null : (
464
+ <ActivityView
465
+ mode={activityMode}
466
+ visible={
467
+ // We don't need to hide the content since it's handled natively
468
+ // Hiding may also cause flash due to lag after native tab switch
469
+ // So we leave it always visible
470
+ true
365
471
  }
472
+ style={styles.content}
366
473
  >
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>
474
+ {content}
475
+ </ActivityView>
476
+ )}
412
477
  </ScreenStackItem>
413
478
  </NavigationProvider>
414
479
  );
@@ -425,46 +490,57 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
425
490
 
426
491
  useInvalidPreventRemoveError(descriptors);
427
492
 
428
- const modalRouteKeys = getModalRouteKeys(state.routes, descriptors);
493
+ const activeRoutes = state.routes.slice(0, state.index + 1);
494
+ const modalRouteKeys = getModalRouteKeys(activeRoutes, descriptors);
429
495
 
430
496
  return (
431
497
  <SafeAreaProviderCompat>
432
498
  <ScreenStack style={styles.container}>
433
- {state.routes.concat(state.preloadedRoutes).map((route, index) => {
499
+ {state.routes.map((route, index) => {
434
500
  const descriptor = descriptors[route.key];
501
+
502
+ if (descriptor == null) {
503
+ throw new Error(
504
+ `Couldn't find a descriptor for route '${route.key}'.`
505
+ );
506
+ }
507
+
435
508
  const isFocused = state.index === index;
436
- const isBelowFocused = state.index - 1 === index;
437
- const previousKey = state.routes[index - 1]?.key;
438
- const nextKey = state.routes[index + 1]?.key;
509
+ const previousKey = activeRoutes[index - 1]?.key;
510
+ const nextKey = activeRoutes[index + 1]?.key;
439
511
  const previousDescriptor = previousKey
440
512
  ? descriptors[previousKey]
441
513
  : undefined;
442
514
  const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
443
515
 
444
- const isModal = modalRouteKeys.includes(route.key);
445
- const isModalOnIos = isModal && Platform.OS === 'ios';
516
+ const nextPresentation = nextDescriptor?.options.presentation;
446
517
 
447
- const isPreloaded = state.preloadedRoutes.some(
448
- (r) => r.key === route.key
449
- );
518
+ const isNextScreenTransparent =
519
+ nextPresentation != null &&
520
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
521
+ const isNextScreenSheet =
522
+ nextPresentation != null &&
523
+ SHEET_PRESENTATIONS.includes(nextPresentation);
524
+ const isNextScreenModal = nextKey
525
+ ? modalRouteKeys.includes(nextKey)
526
+ : false;
450
527
 
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;
528
+ const isModal = modalRouteKeys.includes(route.key);
456
529
 
457
530
  return (
458
531
  <SceneView
459
532
  key={route.key}
460
533
  index={index}
461
534
  focused={isFocused}
462
- shouldFreeze={shouldFreeze}
463
535
  descriptor={descriptor}
464
536
  previousDescriptor={previousDescriptor}
465
537
  nextDescriptor={nextDescriptor}
466
538
  isPresentationModal={isModal}
467
- isPreloaded={isPreloaded}
539
+ isNextScreenTransparent={isNextScreenTransparent}
540
+ isNextScreenSheet={isNextScreenSheet}
541
+ isNextScreenModal={isNextScreenModal}
542
+ isInactive={index > state.index}
543
+ isBeforeLast={index === activeRoutes.length - 2}
468
544
  onWillDisappear={() => {
469
545
  navigation.emit({
470
546
  type: 'transitionStart',
@@ -494,11 +570,19 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
494
570
  });
495
571
  }}
496
572
  onDismissed={(event) => {
497
- navigation.dispatch({
498
- ...StackActions.pop(event.nativeEvent.dismissCount),
499
- source: route.key,
500
- target: state.key,
501
- });
573
+ const currentState = navigation.getState();
574
+ const currentActiveRoutes = currentState.routes.slice(
575
+ 0,
576
+ currentState.index + 1
577
+ );
578
+
579
+ if (currentActiveRoutes.some((r) => r.key === route.key)) {
580
+ navigation.dispatch({
581
+ ...StackActions.pop(event.nativeEvent.dismissCount),
582
+ source: route.key,
583
+ target: currentState.key,
584
+ });
585
+ }
502
586
 
503
587
  setNextDismissedKey(route.key);
504
588
  }}
@@ -544,6 +628,9 @@ const styles = StyleSheet.create({
544
628
  container: {
545
629
  flex: 1,
546
630
  },
631
+ content: {
632
+ flex: 1,
633
+ },
547
634
  header: {
548
635
  zIndex: 1,
549
636
  },
@@ -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,12 +16,13 @@ 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,
22
23
  NativeStackNavigationHelpers,
23
24
  } from '../types';
25
+ import { getModalRouteKeys } from '../utils/getModalRoutesKeys';
24
26
  import { AnimatedHeaderHeightContext } from '../utils/useAnimatedHeaderHeight';
25
27
 
26
28
  type Props = {
@@ -35,20 +37,31 @@ const TRANSPARENT_PRESENTATIONS = [
35
37
  ];
36
38
 
37
39
  export function NativeStackView({ state, descriptors }: Props) {
38
- const parentHeaderBack = React.useContext(HeaderBackContext);
40
+ const parentHeaderBack = React.use(HeaderBackContext);
39
41
  const { buildHref } = useLinkBuilder();
40
42
 
43
+ const activeRoutes = state.routes.slice(0, state.index + 1);
44
+ const modalRouteKeys = getModalRouteKeys(activeRoutes, descriptors);
45
+
41
46
  return (
42
47
  <SafeAreaProviderCompat>
43
- {state.routes.concat(state.preloadedRoutes).map((route, i) => {
48
+ {state.routes.map((route, i) => {
44
49
  const isFocused = state.index === i;
45
- const previousKey = state.routes[i - 1]?.key;
46
- const nextKey = state.routes[i + 1]?.key;
50
+ const previousKey = activeRoutes[i - 1]?.key;
51
+ const nextKey = activeRoutes[i + 1]?.key;
47
52
  const previousDescriptor = previousKey
48
53
  ? descriptors[previousKey]
49
54
  : undefined;
50
55
  const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
51
- const { options, navigation, render } = descriptors[route.key];
56
+ const descriptor = descriptors[route.key];
57
+
58
+ if (descriptor == null) {
59
+ throw new Error(
60
+ `Couldn't find a descriptor for route '${route.key}'.`
61
+ );
62
+ }
63
+
64
+ const { options, navigation, render } = descriptor;
52
65
 
53
66
  const headerBack = previousDescriptor
54
67
  ? {
@@ -66,6 +79,7 @@ export function NativeStackView({ state, descriptors }: Props) {
66
79
  const canGoBack = headerBack != null;
67
80
 
68
81
  const {
82
+ inactiveBehavior = 'pause',
69
83
  header,
70
84
  headerShown,
71
85
  headerBackIcon,
@@ -79,11 +93,40 @@ export function NativeStackView({ state, descriptors }: Props) {
79
93
 
80
94
  const nextPresentation = nextDescriptor?.options.presentation;
81
95
 
82
- const isPreloaded = state.preloadedRoutes.some(
83
- (r) => r.key === route.key
84
- );
96
+ const isNextScreenTransparent =
97
+ nextPresentation != null &&
98
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
85
99
 
86
- return (
100
+ const isInactive = i > state.index;
101
+
102
+ const isBeforeLast = i === activeRoutes.length - 2;
103
+
104
+ const isNextScreenModal = nextKey
105
+ ? modalRouteKeys.includes(nextKey)
106
+ : false;
107
+
108
+ const activityMode =
109
+ // Render focused screens normally
110
+ isFocused
111
+ ? 'normal'
112
+ : // Unpause preloaded and retained screens so updates are visible
113
+ // This lets effects on those screens run
114
+ inactiveBehavior === 'none' ||
115
+ isInactive ||
116
+ isNextScreenTransparent
117
+ ? 'inert'
118
+ : inactiveBehavior === 'unmount' &&
119
+ !isNextScreenModal &&
120
+ !isBeforeLast &&
121
+ !route.state
122
+ ? 'unmounted'
123
+ : 'paused';
124
+
125
+ if (activityMode === 'unmounted') {
126
+ return null;
127
+ }
128
+
129
+ const content = (
87
130
  <Screen
88
131
  key={route.key}
89
132
  focused={isFocused}
@@ -117,18 +160,7 @@ export function NativeStackView({ state, descriptors }: Props) {
117
160
  {...rest}
118
161
  label={headerBackTitle ?? label}
119
162
  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
- }
163
+ icon={headerBackIcon}
132
164
  onPress={navigation.goBack}
133
165
  />
134
166
  )
@@ -138,22 +170,13 @@ export function NativeStackView({ state, descriptors }: Props) {
138
170
  />
139
171
  )
140
172
  }
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 &&
173
+ style={{
174
+ ...StyleSheet.absoluteFill,
175
+ ...(presentation != null &&
153
176
  TRANSPARENT_PRESENTATIONS.includes(presentation)
154
177
  ? { backgroundColor: 'transparent' }
155
- : null,
156
- ]}
178
+ : null),
179
+ }}
157
180
  >
158
181
  <HeaderBackContext.Provider value={headerBack}>
159
182
  <AnimatedHeaderHeightProvider>
@@ -164,6 +187,17 @@ export function NativeStackView({ state, descriptors }: Props) {
164
187
  </HeaderBackContext.Provider>
165
188
  </Screen>
166
189
  );
190
+
191
+ return (
192
+ <ActivityView
193
+ key={route.key}
194
+ mode={activityMode}
195
+ visible={isFocused || isNextScreenTransparent}
196
+ style={StyleSheet.absoluteFill}
197
+ >
198
+ {content}
199
+ </ActivityView>
200
+ );
167
201
  })}
168
202
  </SafeAreaProviderCompat>
169
203
  );
@@ -194,9 +228,4 @@ const styles = StyleSheet.create({
194
228
  contentContainer: {
195
229
  flex: 1,
196
230
  },
197
- backImage: {
198
- height: 24,
199
- width: 24,
200
- margin: 3,
201
- },
202
231
  });