@react-navigation/stack 8.0.0-alpha.6 → 8.0.0-alpha.60

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 (147) hide show
  1. package/lib/module/TransitionConfigs/CardStyleInterpolators.js +280 -84
  2. package/lib/module/TransitionConfigs/CardStyleInterpolators.js.map +1 -1
  3. package/lib/module/TransitionConfigs/HeaderStyleInterpolators.js +11 -44
  4. package/lib/module/TransitionConfigs/HeaderStyleInterpolators.js.map +1 -1
  5. package/lib/module/TransitionConfigs/TransitionEasings.js +71 -0
  6. package/lib/module/TransitionConfigs/TransitionEasings.js.map +1 -0
  7. package/lib/module/TransitionConfigs/TransitionPresets.js +50 -10
  8. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -1
  9. package/lib/module/TransitionConfigs/TransitionSpecs.js +69 -34
  10. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -1
  11. package/lib/module/index.js +2 -2
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/navigators/createStackNavigator.js +37 -33
  14. package/lib/module/navigators/createStackNavigator.js.map +1 -1
  15. package/lib/module/utils/GestureHandlerContext.js +5 -0
  16. package/lib/module/utils/GestureHandlerContext.js.map +1 -0
  17. package/lib/module/utils/gestureActivationCriteria.js +10 -8
  18. package/lib/module/utils/gestureActivationCriteria.js.map +1 -1
  19. package/lib/module/utils/useCardAnimation.js +1 -1
  20. package/lib/module/utils/useCardAnimation.js.map +1 -1
  21. package/lib/module/utils/useGestureHandler.js +9 -0
  22. package/lib/module/utils/useGestureHandler.js.map +1 -0
  23. package/lib/module/utils/useKeyboardManager.js +71 -26
  24. package/lib/module/utils/useKeyboardManager.js.map +1 -1
  25. package/lib/module/views/GestureHandler.js +15 -14
  26. package/lib/module/views/GestureHandler.js.map +1 -1
  27. package/lib/module/views/GestureHandlerNative.js +16 -10
  28. package/lib/module/views/GestureHandlerNative.js.map +1 -1
  29. package/lib/module/views/Header/Header.js +4 -2
  30. package/lib/module/views/Header/Header.js.map +1 -1
  31. package/lib/module/views/Header/HeaderContainer.js +60 -21
  32. package/lib/module/views/Header/HeaderContainer.js.map +1 -1
  33. package/lib/module/views/Header/HeaderSegment.js +6 -9
  34. package/lib/module/views/Header/HeaderSegment.js.map +1 -1
  35. package/lib/module/views/Stack/Card.js +170 -123
  36. package/lib/module/views/Stack/Card.js.map +1 -1
  37. package/lib/module/views/Stack/CardContainer.js +96 -98
  38. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  39. package/lib/module/views/Stack/CardContent.js +10 -37
  40. package/lib/module/views/Stack/CardContent.js.map +1 -1
  41. package/lib/module/views/Stack/CardStack.js +211 -147
  42. package/lib/module/views/Stack/CardStack.js.map +1 -1
  43. package/lib/module/views/Stack/StackView.js +106 -57
  44. package/lib/module/views/Stack/StackView.js.map +1 -1
  45. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts +20 -12
  46. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts.map +1 -1
  47. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts +4 -4
  48. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts.map +1 -1
  49. package/lib/typescript/src/TransitionConfigs/TransitionEasings.d.ts +38 -0
  50. package/lib/typescript/src/TransitionConfigs/TransitionEasings.d.ts.map +1 -0
  51. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +15 -3
  52. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  53. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +44 -15
  54. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  55. package/lib/typescript/src/index.d.ts +12 -12
  56. package/lib/typescript/src/index.d.ts.map +1 -1
  57. package/lib/typescript/src/navigators/createStackNavigator.d.ts +10 -15
  58. package/lib/typescript/src/navigators/createStackNavigator.d.ts.map +1 -1
  59. package/lib/typescript/src/types.d.ts +120 -108
  60. package/lib/typescript/src/types.d.ts.map +1 -1
  61. package/lib/typescript/src/utils/CardAnimationContext.d.ts +1 -1
  62. package/lib/typescript/src/utils/CardAnimationContext.d.ts.map +1 -1
  63. package/lib/typescript/src/utils/GestureHandlerContext.d.ts +3 -0
  64. package/lib/typescript/src/utils/GestureHandlerContext.d.ts.map +1 -0
  65. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +4 -51
  66. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
  67. package/lib/typescript/src/utils/getDistanceForDirection.d.ts +1 -1
  68. package/lib/typescript/src/utils/getDistanceForDirection.d.ts.map +1 -1
  69. package/lib/typescript/src/utils/getInvertedMultiplier.d.ts +1 -1
  70. package/lib/typescript/src/utils/getInvertedMultiplier.d.ts.map +1 -1
  71. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts +1 -1
  72. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts.map +1 -1
  73. package/lib/typescript/src/utils/getShadowStyle.d.ts +5 -5
  74. package/lib/typescript/src/utils/useCardAnimation.d.ts +1 -1
  75. package/lib/typescript/src/utils/useCardAnimation.d.ts.map +1 -1
  76. package/lib/typescript/src/utils/useGestureHandler.d.ts +2 -0
  77. package/lib/typescript/src/utils/useGestureHandler.d.ts.map +1 -0
  78. package/lib/typescript/src/utils/useKeyboardManager.d.ts +12 -2
  79. package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
  80. package/lib/typescript/src/views/GestureHandler.android.d.ts +1 -1
  81. package/lib/typescript/src/views/GestureHandler.android.d.ts.map +1 -1
  82. package/lib/typescript/src/views/GestureHandler.d.ts +7 -11
  83. package/lib/typescript/src/views/GestureHandler.d.ts.map +1 -1
  84. package/lib/typescript/src/views/GestureHandler.ios.d.ts +1 -1
  85. package/lib/typescript/src/views/GestureHandler.ios.d.ts.map +1 -1
  86. package/lib/typescript/src/views/GestureHandlerNative.d.ts +9 -4
  87. package/lib/typescript/src/views/GestureHandlerNative.d.ts.map +1 -1
  88. package/lib/typescript/src/views/Header/Header.d.ts +2 -2
  89. package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
  90. package/lib/typescript/src/views/Header/HeaderContainer.d.ts +9 -3
  91. package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
  92. package/lib/typescript/src/views/Header/HeaderSegment.d.ts +6 -4
  93. package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
  94. package/lib/typescript/src/views/Stack/Card.d.ts +14 -8
  95. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  96. package/lib/typescript/src/views/Stack/CardContainer.d.ts +7 -7
  97. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  98. package/lib/typescript/src/views/Stack/CardContent.d.ts +1 -1
  99. package/lib/typescript/src/views/Stack/CardContent.d.ts.map +1 -1
  100. package/lib/typescript/src/views/Stack/CardStack.d.ts +10 -6
  101. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  102. package/lib/typescript/src/views/Stack/StackView.d.ts +14 -17
  103. package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
  104. package/package.json +23 -24
  105. package/src/TransitionConfigs/CardStyleInterpolators.tsx +399 -100
  106. package/src/TransitionConfigs/HeaderStyleInterpolators.tsx +15 -46
  107. package/src/TransitionConfigs/TransitionEasings.tsx +90 -0
  108. package/src/TransitionConfigs/TransitionPresets.tsx +56 -10
  109. package/src/TransitionConfigs/TransitionSpecs.tsx +68 -34
  110. package/src/index.tsx +3 -2
  111. package/src/navigators/createStackNavigator.tsx +74 -91
  112. package/src/types.tsx +149 -123
  113. package/src/utils/GestureHandlerContext.tsx +5 -0
  114. package/src/utils/gestureActivationCriteria.tsx +14 -10
  115. package/src/utils/useCardAnimation.tsx +1 -1
  116. package/src/utils/useGestureHandler.tsx +9 -0
  117. package/src/utils/useKeyboardManager.tsx +96 -33
  118. package/src/views/GestureHandler.tsx +24 -16
  119. package/src/views/GestureHandlerNative.tsx +24 -13
  120. package/src/views/Header/Header.tsx +4 -2
  121. package/src/views/Header/HeaderContainer.tsx +79 -37
  122. package/src/views/Header/HeaderSegment.tsx +10 -11
  123. package/src/views/Stack/Card.tsx +241 -154
  124. package/src/views/Stack/CardContainer.tsx +111 -127
  125. package/src/views/Stack/CardContent.tsx +13 -51
  126. package/src/views/Stack/CardStack.tsx +478 -335
  127. package/src/views/Stack/StackView.tsx +181 -82
  128. package/lib/module/utils/GestureHandlerRefContext.js +0 -5
  129. package/lib/module/utils/GestureHandlerRefContext.js.map +0 -1
  130. package/lib/module/utils/findLastIndex.js +0 -11
  131. package/lib/module/utils/findLastIndex.js.map +0 -1
  132. package/lib/module/utils/useGestureHandlerRef.js +0 -12
  133. package/lib/module/utils/useGestureHandlerRef.js.map +0 -1
  134. package/lib/module/views/Stack/CardA11yWrapper.js +0 -43
  135. package/lib/module/views/Stack/CardA11yWrapper.js.map +0 -1
  136. package/lib/typescript/src/utils/GestureHandlerRefContext.d.ts +0 -3
  137. package/lib/typescript/src/utils/GestureHandlerRefContext.d.ts.map +0 -1
  138. package/lib/typescript/src/utils/findLastIndex.d.ts +0 -2
  139. package/lib/typescript/src/utils/findLastIndex.d.ts.map +0 -1
  140. package/lib/typescript/src/utils/useGestureHandlerRef.d.ts +0 -3
  141. package/lib/typescript/src/utils/useGestureHandlerRef.d.ts.map +0 -1
  142. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +0 -15
  143. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +0 -1
  144. package/src/utils/GestureHandlerRefContext.tsx +0 -5
  145. package/src/utils/findLastIndex.tsx +0 -9
  146. package/src/utils/useGestureHandlerRef.tsx +0 -15
  147. package/src/views/Stack/CardA11yWrapper.tsx +0 -67
@@ -1,5 +1,8 @@
1
1
  import { getDefaultHeaderHeight } from '@react-navigation/elements';
2
- import { SafeAreaProviderCompat } from '@react-navigation/elements/internal';
2
+ import {
3
+ ActivityView,
4
+ SafeAreaProviderCompat,
5
+ } from '@react-navigation/elements/internal';
3
6
  import type {
4
7
  LocaleDirection,
5
8
  ParamListBase,
@@ -15,18 +18,26 @@ import {
15
18
  View,
16
19
  } from 'react-native';
17
20
  import type { EdgeInsets } from 'react-native-safe-area-context';
18
- import { Screen, ScreenContainer } from 'react-native-screens';
19
21
 
20
22
  import {
23
+ forBottomSheetAndroid,
24
+ forCrossDissolveIOS,
25
+ forDialogAndroid,
26
+ forFadeFromBottomAndroid,
27
+ forFadeFromRightAndroid,
28
+ forFlipIOS,
21
29
  forModalPresentationIOS,
22
30
  forNoAnimation as forNoAnimationCard,
31
+ forRevealFromBottomAndroid,
32
+ forScaleFromCenterAndroid,
33
+ forVerticalIOS,
23
34
  } from '../../TransitionConfigs/CardStyleInterpolators';
24
35
  import {
25
- BottomSheetAndroid,
26
36
  DefaultTransition,
27
37
  FadeFromBottomAndroid,
28
38
  FadeFromRightAndroid,
29
39
  ModalFadeTransition,
40
+ ModalFlipIOS,
30
41
  ModalSlideFromBottomIOS,
31
42
  ModalTransition,
32
43
  RevealFromBottomAndroid,
@@ -44,16 +55,20 @@ import type {
44
55
  StackNavigationOptions,
45
56
  TransitionPreset,
46
57
  } from '../../types';
47
- import { findLastIndex } from '../../utils/findLastIndex';
48
58
  import { getDistanceForDirection } from '../../utils/getDistanceForDirection';
49
59
  import { getModalRouteKeys } from '../../utils/getModalRoutesKeys';
50
- import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
60
+ import type {
61
+ HeaderHeight,
62
+ Props as HeaderContainerProps,
63
+ } from '../Header/HeaderContainer';
51
64
  import { CardContainer } from './CardContainer';
52
65
 
53
66
  type GestureValues = {
54
67
  [key: string]: Animated.Value;
55
68
  };
56
69
 
70
+ type HeaderHeights = Record<string, HeaderHeight>;
71
+
57
72
  type Props = {
58
73
  direction: LocaleDirection;
59
74
  insets: EdgeInsets;
@@ -62,6 +77,7 @@ type Props = {
62
77
  routes: Route<string>[];
63
78
  openingRouteKeys: string[];
64
79
  closingRouteKeys: string[];
80
+ replacingRouteKeys: string[];
65
81
  onOpenRoute: (props: { route: Route<string> }) => void;
66
82
  onCloseRoute: (props: { route: Route<string> }) => void;
67
83
  getPreviousRoute: (props: {
@@ -84,11 +100,11 @@ type Props = {
84
100
  type State = {
85
101
  routes: Route<string>[];
86
102
  descriptors: StackDescriptorMap;
87
- scenes: Scene[];
103
+ scenes: (Scene & { __memo: unknown[] })[];
88
104
  gestures: GestureValues;
89
105
  layout: Layout;
90
- activeStates: (0 | 1 | Animated.AnimatedInterpolation<0 | 1>)[];
91
- headerHeights: Record<string, number>;
106
+ headerHeights: HeaderHeights;
107
+ nestedStateRouteKeys: string[];
92
108
  };
93
109
 
94
110
  const NAMED_TRANSITIONS_PRESETS = {
@@ -96,27 +112,43 @@ const NAMED_TRANSITIONS_PRESETS = {
96
112
  fade: ModalFadeTransition,
97
113
  fade_from_bottom: FadeFromBottomAndroid,
98
114
  fade_from_right: FadeFromRightAndroid,
115
+ flip: ModalFlipIOS,
116
+ ios_from_left: SlideFromLeftIOS,
117
+ ios_from_right: SlideFromRightIOS,
99
118
  none: DefaultTransition,
100
119
  reveal_from_bottom: RevealFromBottomAndroid,
101
120
  scale_from_center: ScaleFromCenterAndroid,
102
121
  slide_from_left: SlideFromLeftIOS,
103
122
  slide_from_right: SlideFromRightIOS,
104
- slide_from_bottom: Platform.select({
105
- ios: ModalSlideFromBottomIOS,
106
- default: BottomSheetAndroid,
107
- }),
123
+ slide_from_bottom: ModalSlideFromBottomIOS,
108
124
  } as const satisfies Record<StackAnimationName, TransitionPreset>;
109
125
 
110
- const EPSILON = 1e-5;
126
+ const GESTURE_DISABLED_INTERPOLATORS = [
127
+ forCrossDissolveIOS,
128
+ forDialogAndroid,
129
+ forFadeFromBottomAndroid,
130
+ forFadeFromRightAndroid,
131
+ forFlipIOS,
132
+ forRevealFromBottomAndroid,
133
+ forScaleFromCenterAndroid,
134
+ forVerticalIOS,
135
+ ];
111
136
 
112
- const STATE_INACTIVE = 0;
113
- const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
114
- const STATE_ON_TOP = 2;
137
+ const SCREEN_HEADER_INTERPOLATORS = [
138
+ ...GESTURE_DISABLED_INTERPOLATORS,
139
+ forBottomSheetAndroid,
140
+ ];
115
141
 
116
- const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
142
+ const HIDDEN_HEADER_HEIGHT = { measured: false, value: 0 };
117
143
 
118
144
  const getInterpolationIndex = (scenes: Scene[], index: number) => {
119
- const { cardStyleInterpolator } = scenes[index].descriptor.options;
145
+ const scene = scenes[index];
146
+
147
+ if (scene == null) {
148
+ throw new Error(`Couldn't find a scene at index ${index}.`);
149
+ }
150
+
151
+ const { cardStyleInterpolator } = scene.descriptor.options;
120
152
 
121
153
  // Start from current card and count backwards the number of cards with same interpolation
122
154
  let interpolationIndex = 0;
@@ -140,12 +172,27 @@ const getIsModalPresentation = (
140
172
  ) => {
141
173
  return (
142
174
  cardStyleInterpolator === forModalPresentationIOS ||
143
- // Handle custom modal presentation interpolators as well
144
- cardStyleInterpolator.name === 'forModalPresentationIOS'
175
+ cardStyleInterpolator.name === forModalPresentationIOS.name
145
176
  );
146
177
  };
147
178
 
148
- const getIsModal = (
179
+ const getIsGestureEnabledByDefault = (
180
+ cardStyleInterpolator: StackCardStyleInterpolator
181
+ ) =>
182
+ !GESTURE_DISABLED_INTERPOLATORS.some(
183
+ (interpolator) =>
184
+ cardStyleInterpolator === interpolator ||
185
+ cardStyleInterpolator.name === interpolator.name
186
+ );
187
+
188
+ const getIsScreenHeader = (cardStyleInterpolator: StackCardStyleInterpolator) =>
189
+ SCREEN_HEADER_INTERPOLATORS.some(
190
+ (interpolator) =>
191
+ cardStyleInterpolator === interpolator ||
192
+ cardStyleInterpolator.name === interpolator.name
193
+ );
194
+
195
+ const getIsInModalPresentation = (
149
196
  scene: Scene,
150
197
  interpolationIndex: number,
151
198
  isParentModal: boolean
@@ -155,6 +202,7 @@ const getIsModal = (
155
202
  }
156
203
 
157
204
  const { cardStyleInterpolator } = scene.descriptor.options;
205
+
158
206
  const isModalPresentation = getIsModalPresentation(cardStyleInterpolator);
159
207
  const isModal = isModalPresentation && interpolationIndex !== 0;
160
208
 
@@ -167,32 +215,51 @@ const getHeaderHeights = (
167
215
  isParentHeaderShown: boolean,
168
216
  isParentModal: boolean,
169
217
  layout: Layout,
170
- previous: Record<string, number>
218
+ previous: HeaderHeights
171
219
  ) => {
172
- return scenes.reduce<Record<string, number>>((acc, curr, index) => {
220
+ return scenes.reduce<HeaderHeights>((acc, curr, index) => {
173
221
  const {
174
222
  headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
175
223
  headerStyle,
176
224
  } = curr.descriptor.options;
177
225
 
178
- const style = StyleSheet.flatten(headerStyle || {});
226
+ const style = StyleSheet.flatten(headerStyle);
179
227
 
180
228
  const height =
181
- 'height' in style && typeof style.height === 'number'
229
+ style != null && 'height' in style && typeof style.height === 'number'
182
230
  ? style.height
183
- : previous[curr.route.key];
231
+ : undefined;
184
232
 
185
233
  const interpolationIndex = getInterpolationIndex(scenes, index);
186
- const isModal = getIsModal(curr, interpolationIndex, isParentModal);
234
+ const isModal = getIsInModalPresentation(
235
+ curr,
236
+ interpolationIndex,
237
+ isParentModal
238
+ );
187
239
 
188
- acc[curr.route.key] =
189
- typeof height === 'number'
190
- ? height
191
- : getDefaultHeaderHeight({
192
- landscape: layout.width > layout.height,
193
- modalPresentation: isModal,
194
- topInset: headerStatusBarHeight,
195
- });
240
+ if (typeof height === 'number') {
241
+ acc[curr.route.key] = { measured: true, value: height };
242
+ } else {
243
+ const previousHeight = previous[curr.route.key];
244
+ const defaultHeight = getDefaultHeaderHeight({
245
+ landscape: layout.width > layout.height,
246
+ modalPresentation: isModal,
247
+ topInset: headerStatusBarHeight,
248
+ });
249
+
250
+ // If we hadn't measured before, use re-computed default height
251
+ if (
252
+ !previousHeight?.measured &&
253
+ defaultHeight !== previousHeight?.value
254
+ ) {
255
+ acc[curr.route.key] = {
256
+ measured: false,
257
+ value: defaultHeight,
258
+ };
259
+ } else if (previousHeight) {
260
+ acc[curr.route.key] = previousHeight;
261
+ }
262
+ }
196
263
 
197
264
  return acc;
198
265
  }, {});
@@ -263,6 +330,27 @@ export function getAnimationEnabled(animation: StackAnimationName | undefined) {
263
330
  return getDefaultAnimation(animation) !== 'none';
264
331
  }
265
332
 
333
+ const getAllRoutes = (
334
+ routes: Route<string>[],
335
+ state: StackNavigationState<ParamListBase>
336
+ ) => {
337
+ const routeKeys = new Set(routes.map((route) => route.key));
338
+ const inactiveRoutes = state.routes.slice(state.index + 1);
339
+
340
+ // If a route is moved from active routes to inactive routes,
341
+ // It can still be in the local copy of `routes` until the animation ends
342
+ // So we need to deduplicate the routes to avoid rendering the same route twice
343
+ return [
344
+ ...routes,
345
+ ...inactiveRoutes.filter((route) => !routeKeys.has(route.key)),
346
+ ];
347
+ };
348
+
349
+ const isInactiveRoute = (route: Route<string>, routes: Route<string>[]) =>
350
+ // `routes` contains active routes and routes animating during transitions.
351
+ // Any route added by `getAllRoutes` that's missing from this list is inactive.
352
+ !routes.some((currentRoute) => currentRoute.key === route.key);
353
+
266
354
  export class CardStack extends React.Component<Props, State> {
267
355
  static getDerivedStateFromProps(
268
356
  props: Props,
@@ -275,10 +363,8 @@ export class CardStack extends React.Component<Props, State> {
275
363
  return null;
276
364
  }
277
365
 
278
- const gestures = [
279
- ...props.routes,
280
- ...props.state.preloadedRoutes,
281
- ].reduce<GestureValues>((acc, curr) => {
366
+ const allRoutes = getAllRoutes(props.routes, props.state);
367
+ const gestures = allRoutes.reduce<GestureValues>((acc, curr) => {
282
368
  const descriptor = props.descriptors[curr.key];
283
369
  const { animation } = descriptor?.options || {};
284
370
 
@@ -287,7 +373,7 @@ export class CardStack extends React.Component<Props, State> {
287
373
  new Animated.Value(
288
374
  (props.openingRouteKeys.includes(curr.key) &&
289
375
  getAnimationEnabled(animation)) ||
290
- props.state.preloadedRoutes.includes(curr)
376
+ isInactiveRoute(curr, props.routes)
291
377
  ? getDistanceFromOptions(
292
378
  state.layout,
293
379
  descriptor?.options,
@@ -299,254 +385,189 @@ export class CardStack extends React.Component<Props, State> {
299
385
  return acc;
300
386
  }, {});
301
387
 
302
- const modalRouteKeys = getModalRouteKeys(
303
- [...props.routes, ...props.state.preloadedRoutes],
304
- props.descriptors
305
- );
388
+ const modalRouteKeys = getModalRouteKeys(allRoutes, props.descriptors);
306
389
 
307
- const scenes = [...props.routes, ...props.state.preloadedRoutes].map(
308
- (route, index, self) => {
309
- // For preloaded screens, we don't care about the previous and the next screen
310
- const isPreloaded = props.state.preloadedRoutes.includes(route);
311
- const previousRoute = isPreloaded ? undefined : self[index - 1];
312
- const nextRoute = isPreloaded ? undefined : self[index + 1];
313
-
314
- const oldScene = state.scenes[index];
315
-
316
- const currentGesture = gestures[route.key];
317
- const previousGesture = previousRoute
318
- ? gestures[previousRoute.key]
319
- : undefined;
320
- const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
321
-
322
- const descriptor =
323
- props.descriptors[route.key] ||
324
- state.descriptors[route.key] ||
325
- (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
326
-
327
- const nextOptions =
328
- nextRoute &&
329
- (
330
- props.descriptors[nextRoute?.key] ||
331
- state.descriptors[nextRoute?.key]
332
- )?.options;
333
-
334
- const previousOptions =
335
- previousRoute &&
336
- (
337
- props.descriptors[previousRoute?.key] ||
338
- state.descriptors[previousRoute?.key]
339
- )?.options;
340
-
341
- // When a screen is not the last, it should use next screen's transition config
342
- // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
343
- // For example combining a slide and a modal transition would look wrong otherwise
344
- // With this approach, combining different transition styles in the same navigator mostly looks right
345
- // This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
346
- // but the majority of the transitions look alright
347
- const optionsForTransitionConfig =
348
- index !== self.length - 1 &&
349
- nextOptions &&
350
- nextOptions?.presentation !== 'transparentModal'
351
- ? nextOptions
352
- : descriptor.options;
353
-
354
- // Assume modal if there are already modal screens in the stack
355
- // or current screen is a modal when no presentation is specified
356
- const isModal = modalRouteKeys.includes(route.key);
357
-
358
- const animation = getDefaultAnimation(
359
- optionsForTransitionConfig.animation
360
- );
390
+ const scenes = allRoutes.map((route, index, self) => {
391
+ // For preloaded or retained screens, we don't care about the previous and the next screen
392
+ const isInactive = isInactiveRoute(route, props.routes);
393
+ const previousRoute = isInactive ? undefined : self[index - 1];
394
+ const nextRoute = isInactive ? undefined : self[index + 1];
361
395
 
362
- const isAnimationEnabled = getAnimationEnabled(animation);
363
-
364
- const transitionPreset =
365
- animation !== 'default'
366
- ? NAMED_TRANSITIONS_PRESETS[animation]
367
- : optionsForTransitionConfig.presentation === 'transparentModal'
368
- ? ModalFadeTransition
369
- : optionsForTransitionConfig.presentation === 'modal' || isModal
370
- ? ModalTransition
371
- : DefaultTransition;
372
-
373
- const {
374
- gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled,
375
- gestureDirection = transitionPreset.gestureDirection,
376
- transitionSpec = transitionPreset.transitionSpec,
377
- cardStyleInterpolator = isAnimationEnabled
378
- ? transitionPreset.cardStyleInterpolator
379
- : forNoAnimationCard,
380
- headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
381
- cardOverlayEnabled = (Platform.OS !== 'ios' &&
382
- optionsForTransitionConfig.presentation !== 'transparentModal') ||
383
- getIsModalPresentation(cardStyleInterpolator),
384
- } = optionsForTransitionConfig;
385
-
386
- const headerMode: StackHeaderMode =
387
- descriptor.options.headerMode ??
388
- (!(
389
- optionsForTransitionConfig.presentation === 'modal' ||
390
- optionsForTransitionConfig.presentation === 'transparentModal' ||
391
- nextOptions?.presentation === 'modal' ||
392
- nextOptions?.presentation === 'transparentModal' ||
393
- getIsModalPresentation(cardStyleInterpolator)
394
- ) &&
395
- Platform.OS === 'ios' &&
396
- descriptor.options.header === undefined
397
- ? 'float'
398
- : 'screen');
399
-
400
- const isRTL = props.direction === 'rtl';
401
-
402
- const scene = {
403
- route,
404
- descriptor: {
405
- ...descriptor,
406
- options: {
407
- ...descriptor.options,
408
- animation,
409
- cardOverlayEnabled,
410
- cardStyleInterpolator,
411
- gestureDirection,
412
- gestureEnabled,
413
- headerStyleInterpolator,
414
- transitionSpec,
415
- headerMode,
416
- },
396
+ const oldScene = state.scenes[index];
397
+ const previousScene =
398
+ oldScene?.route.key === route.key
399
+ ? oldScene
400
+ : state.scenes.find((scene) => scene.route.key === route.key);
401
+ const closing =
402
+ previousScene?.closing ??
403
+ new Animated.Value(props.closingRouteKeys.includes(route.key) ? 1 : 0);
404
+
405
+ const currentGesture = gestures[route.key];
406
+
407
+ if (currentGesture == null) {
408
+ throw new Error(`Couldn't find a gesture for route '${route.key}'.`);
409
+ }
410
+
411
+ const previousGesture = previousRoute
412
+ ? gestures[previousRoute.key]
413
+ : undefined;
414
+ const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
415
+
416
+ const descriptor =
417
+ props.descriptors[route.key] ??
418
+ state.descriptors[route.key] ??
419
+ oldScene?.descriptor;
420
+
421
+ if (descriptor == null) {
422
+ throw new Error(`Couldn't find a descriptor for route '${route.key}'.`);
423
+ }
424
+
425
+ const nextOptions = nextRoute
426
+ ? (props.descriptors[nextRoute.key] ?? state.descriptors[nextRoute.key])
427
+ ?.options
428
+ : undefined;
429
+
430
+ const previousOptions = previousRoute
431
+ ? (
432
+ props.descriptors[previousRoute.key] ??
433
+ state.descriptors[previousRoute.key]
434
+ )?.options
435
+ : undefined;
436
+
437
+ // When a screen is not the last, it should use next screen's transition config
438
+ // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
439
+ // For example combining a slide and a modal transition would look wrong otherwise
440
+ // With this approach, combining different transition styles in the same navigator mostly looks right
441
+ // This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
442
+ // but the majority of the transitions look alright
443
+ const optionsForTransitionConfig =
444
+ index !== self.length - 1 &&
445
+ nextOptions &&
446
+ nextOptions?.presentation !== 'transparentModal'
447
+ ? nextOptions
448
+ : descriptor.options;
449
+
450
+ // Assume modal if there are already modal screens in the stack
451
+ // or current screen is a modal when no presentation is specified
452
+ const isModal = modalRouteKeys.includes(route.key);
453
+
454
+ const animation = getDefaultAnimation(
455
+ optionsForTransitionConfig.animation
456
+ );
457
+
458
+ const isAnimationEnabled = getAnimationEnabled(animation);
459
+
460
+ const transitionPreset =
461
+ animation !== 'default'
462
+ ? NAMED_TRANSITIONS_PRESETS[animation]
463
+ : optionsForTransitionConfig.presentation === 'transparentModal'
464
+ ? ModalFadeTransition
465
+ : optionsForTransitionConfig.presentation === 'modal' || isModal
466
+ ? ModalTransition
467
+ : DefaultTransition;
468
+
469
+ const {
470
+ gestureDirection = transitionPreset.gestureDirection,
471
+ transitionSpec = transitionPreset.transitionSpec,
472
+ cardStyleInterpolator = isAnimationEnabled
473
+ ? transitionPreset.cardStyleInterpolator
474
+ : forNoAnimationCard,
475
+ gestureEnabled = Platform.OS === 'ios' &&
476
+ isAnimationEnabled &&
477
+ getIsGestureEnabledByDefault(cardStyleInterpolator),
478
+ headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
479
+ cardOverlayEnabled,
480
+ cardShadowEnabled,
481
+ } = optionsForTransitionConfig;
482
+
483
+ const headerMode: StackHeaderMode =
484
+ descriptor.options.headerMode ??
485
+ (!(
486
+ optionsForTransitionConfig.presentation === 'modal' ||
487
+ optionsForTransitionConfig.presentation === 'transparentModal' ||
488
+ nextOptions?.presentation === 'modal' ||
489
+ nextOptions?.presentation === 'transparentModal' ||
490
+ getIsModalPresentation(cardStyleInterpolator) ||
491
+ getIsScreenHeader(cardStyleInterpolator)
492
+ ) &&
493
+ Platform.OS === 'ios' &&
494
+ descriptor.options.header === undefined
495
+ ? 'float'
496
+ : 'screen');
497
+
498
+ const isRTL = props.direction === 'rtl';
499
+
500
+ const scene = {
501
+ route,
502
+ descriptor: {
503
+ ...descriptor,
504
+ options: {
505
+ ...descriptor.options,
506
+ animation,
507
+ cardOverlayEnabled,
508
+ cardShadowEnabled,
509
+ cardStyleInterpolator,
510
+ gestureDirection,
511
+ gestureEnabled,
512
+ headerStyleInterpolator,
513
+ transitionSpec,
514
+ headerMode,
417
515
  },
418
- progress: {
419
- current: getProgressFromGesture(
420
- currentGesture,
421
- state.layout,
422
- descriptor.options,
423
- isRTL
424
- ),
425
- next:
426
- nextGesture && nextOptions?.presentation !== 'transparentModal'
427
- ? getProgressFromGesture(
428
- nextGesture,
429
- state.layout,
430
- nextOptions,
431
- isRTL
432
- )
433
- : undefined,
434
- previous: previousGesture
516
+ },
517
+ progress: {
518
+ current: getProgressFromGesture(
519
+ currentGesture,
520
+ state.layout,
521
+ descriptor.options,
522
+ isRTL
523
+ ),
524
+ next:
525
+ nextGesture && nextOptions?.presentation !== 'transparentModal'
435
526
  ? getProgressFromGesture(
436
- previousGesture,
527
+ nextGesture,
437
528
  state.layout,
438
- previousOptions,
529
+ nextOptions,
439
530
  isRTL
440
531
  )
441
532
  : undefined,
442
- },
443
- __memo: [
444
- state.layout,
445
- descriptor,
446
- nextOptions,
447
- previousOptions,
448
- currentGesture,
449
- nextGesture,
450
- previousGesture,
451
- ],
452
- };
453
-
454
- if (
455
- oldScene &&
456
- scene.__memo.every((it, i) => {
457
- // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
458
- return oldScene.__memo[i] === it;
459
- })
460
- ) {
461
- return oldScene;
462
- }
463
-
464
- return scene;
465
- }
466
- );
533
+ previous: previousGesture
534
+ ? getProgressFromGesture(
535
+ previousGesture,
536
+ state.layout,
537
+ previousOptions,
538
+ isRTL
539
+ )
540
+ : undefined,
541
+ },
542
+ closing,
543
+ __memo: [
544
+ state.layout,
545
+ descriptor,
546
+ nextOptions,
547
+ previousOptions,
548
+ currentGesture,
549
+ nextGesture,
550
+ previousGesture,
551
+ ],
552
+ };
467
553
 
468
- let activeStates = state.activeStates;
469
-
470
- if (props.routes.length !== state.routes.length) {
471
- let activeScreensLimit = 1;
472
-
473
- for (let i = props.routes.length - 1; i >= 0; i--) {
474
- const { options } = scenes[i].descriptor;
475
-
476
- const {
477
- // By default, we don't want to detach the previous screen of the active one for modals
478
- detachPreviousScreen = options.presentation === 'transparentModal'
479
- ? false
480
- : getIsModalPresentation(options.cardStyleInterpolator)
481
- ? i !==
482
- findLastIndex(scenes, (scene) => {
483
- const { cardStyleInterpolator } = scene.descriptor.options;
484
-
485
- return (
486
- cardStyleInterpolator === forModalPresentationIOS ||
487
- cardStyleInterpolator?.name === 'forModalPresentationIOS'
488
- );
489
- })
490
- : true,
491
- } = options;
492
-
493
- if (detachPreviousScreen === false) {
494
- activeScreensLimit++;
495
- } else {
496
- // Check at least last 2 screens before stopping
497
- // This will make sure that screen isn't detached when another screen is animating on top of the transparent one
498
- // e.g. opaque -> transparent -> opaque
499
- if (i <= props.routes.length - 2) {
500
- break;
501
- }
502
- }
554
+ if (
555
+ oldScene &&
556
+ scene.__memo.every((it, i) => {
557
+ return oldScene.__memo[i] === it;
558
+ })
559
+ ) {
560
+ return oldScene;
503
561
  }
504
562
 
505
- activeStates = props.routes.map((_, index, self) => {
506
- // The activity state represents state of the screen:
507
- // 0 - inactive, the screen is detached
508
- // 1 - transitioning or below the top screen, the screen is mounted but interaction is disabled
509
- // 2 - on top of the stack, the screen is mounted and interaction is enabled
510
- let activityState:
511
- | Animated.AnimatedInterpolation<0 | 1 | 2>
512
- | 0
513
- | 1
514
- | 2;
515
-
516
- const lastActiveState = state.activeStates[index];
517
- const activeAfterTransition = index >= self.length - activeScreensLimit;
518
-
519
- if (lastActiveState === STATE_INACTIVE && !activeAfterTransition) {
520
- // screen was inactive before and it will still be inactive after the transition
521
- activityState = STATE_INACTIVE;
522
- } else {
523
- const sceneForActivity = scenes[self.length - 1];
524
- const outputValue =
525
- index === self.length - 1
526
- ? STATE_ON_TOP // the screen is on top after the transition
527
- : activeAfterTransition
528
- ? STATE_TRANSITIONING_OR_BELOW_TOP // the screen should stay active after the transition, it is not on top but is in activeLimit
529
- : STATE_INACTIVE; // the screen should be active only during the transition, it is at the edge of activeLimit
530
-
531
- activityState = sceneForActivity
532
- ? sceneForActivity.progress.current.interpolate({
533
- inputRange: [0, 1 - EPSILON, 1],
534
- outputRange: [1, 1, outputValue],
535
- extrapolate: 'clamp',
536
- })
537
- : STATE_TRANSITIONING_OR_BELOW_TOP;
538
- }
539
-
540
- return activityState;
541
- });
542
- }
563
+ return scene;
564
+ });
543
565
 
544
566
  return {
545
567
  routes: props.routes,
546
568
  scenes,
547
569
  gestures,
548
570
  descriptors: props.descriptors,
549
- activeStates,
550
571
  headerHeights: getHeaderHeights(
551
572
  scenes,
552
573
  props.insets,
@@ -555,6 +576,15 @@ export class CardStack extends React.Component<Props, State> {
555
576
  state.layout,
556
577
  state.headerHeights
557
578
  ),
579
+ // Nested navigators clean up `route.state` when the screen is paused with `Activity`
580
+ // So we remember routes which had state until they are removed
581
+ nestedStateRouteKeys: allRoutes
582
+ .filter(
583
+ (route) =>
584
+ ('state' in route && route.state) ||
585
+ state.nestedStateRouteKeys.includes(route.key)
586
+ )
587
+ .map((route) => route.key),
558
588
  };
559
589
  }
560
590
 
@@ -567,13 +597,13 @@ export class CardStack extends React.Component<Props, State> {
567
597
  gestures: {},
568
598
  layout: SafeAreaProviderCompat.initialMetrics.frame,
569
599
  descriptors: this.props.descriptors,
570
- activeStates: [],
571
600
  // Used when card's header is null and mode is float to make transition
572
601
  // between screens with headers and those without headers smooth.
573
602
  // This is not a great heuristic here. We don't know synchronously
574
603
  // on mount what the header height is so we have just used the most
575
604
  // common cases here.
576
605
  headerHeights: {},
606
+ nestedStateRouteKeys: [],
577
607
  };
578
608
  }
579
609
 
@@ -611,14 +641,17 @@ export class CardStack extends React.Component<Props, State> {
611
641
  this.setState(({ headerHeights }) => {
612
642
  const previousHeight = headerHeights[route.key];
613
643
 
614
- if (previousHeight === height) {
644
+ if (previousHeight?.value === height) {
615
645
  return null;
616
646
  }
617
647
 
618
648
  return {
619
649
  headerHeights: {
620
650
  ...headerHeights,
621
- [route.key]: height,
651
+ [route.key]: {
652
+ measured: true,
653
+ value: height,
654
+ },
622
655
  },
623
656
  };
624
657
  });
@@ -627,7 +660,13 @@ export class CardStack extends React.Component<Props, State> {
627
660
  private getFocusedRoute = () => {
628
661
  const { state } = this.props;
629
662
 
630
- return state.routes[state.index];
663
+ const route = state.routes[state.index];
664
+
665
+ if (route == null) {
666
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
667
+ }
668
+
669
+ return route;
631
670
  };
632
671
 
633
672
  private getPreviousScene = ({ route }: { route: Route<string> }) => {
@@ -647,34 +686,45 @@ export class CardStack extends React.Component<Props, State> {
647
686
  return undefined;
648
687
  };
649
688
 
650
- render() {
689
+ override render() {
651
690
  const {
652
691
  insets,
653
692
  state,
654
693
  routes,
655
694
  openingRouteKeys,
656
695
  closingRouteKeys,
696
+ replacingRouteKeys,
657
697
  onOpenRoute,
658
698
  onCloseRoute,
699
+ onGestureStart,
700
+ onGestureEnd,
701
+ onGestureCancel,
659
702
  renderHeader,
660
703
  isParentHeaderShown,
661
704
  isParentModal,
662
705
  onTransitionStart,
663
706
  onTransitionEnd,
664
- onGestureStart,
665
- onGestureEnd,
666
- onGestureCancel,
667
- detachInactiveScreens = Platform.OS === 'web' ||
668
- Platform.OS === 'android' ||
669
- Platform.OS === 'ios',
670
707
  } = this.props;
671
708
 
672
- const { scenes, layout, gestures, activeStates, headerHeights } =
709
+ const { scenes, layout, gestures, headerHeights, nestedStateRouteKeys } =
673
710
  this.state;
674
711
 
712
+ const allRoutes = getAllRoutes(routes, state);
713
+
675
714
  const focusedRoute = state.routes[state.index];
676
715
 
677
- const isFloatHeaderAbsolute = scenes.slice(-2).some((scene) => {
716
+ if (focusedRoute == null) {
717
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
718
+ }
719
+
720
+ // Render only two top-most active headers as a workaround for
721
+ // https://github.com/react-navigation/react-navigation/issues/12456.
722
+ // If the header is persisted, it might be placed incorrectly when navigating back
723
+ const activeHeaderScenes = scenes
724
+ .filter((scene) => !isInactiveRoute(scene.route, routes))
725
+ .slice(-2);
726
+
727
+ const isFloatHeaderAbsolute = activeHeaderScenes.some((scene) => {
678
728
  const options = scene.descriptor.options ?? {};
679
729
  const { headerMode, headerTransparent, headerShown = true } = options;
680
730
 
@@ -693,13 +743,16 @@ export class CardStack extends React.Component<Props, State> {
693
743
  scenes.findLast((scene) => scene.route.key === focusedRoute.key)
694
744
  ?.descriptor.options.headerMode === 'float';
695
745
 
746
+ const focusedHeaderHeight = headerHeights[focusedRoute.key];
747
+
696
748
  return (
697
749
  <View style={styles.container}>
698
750
  {renderHeader({
699
751
  mode: 'float',
700
- scenes,
752
+ scenes: activeHeaderScenes,
701
753
  getPreviousScene: this.getPreviousScene,
702
754
  getFocusedRoute: this.getFocusedRoute,
755
+ contentHeight: headerHeights[focusedRoute.key],
703
756
  onContentHeightChange: this.handleHeaderLayout,
704
757
  style: [
705
758
  isFloatHeaderAbsolute && [
@@ -707,39 +760,36 @@ export class CardStack extends React.Component<Props, State> {
707
760
  // The header title can't be found with maestro without this
708
761
  // In some cases, header buttons may not be touchable as well
709
762
  isFloatHeaderFocused && {
710
- height: headerHeights[focusedRoute.key],
763
+ height: focusedHeaderHeight?.value,
711
764
  },
712
765
  ],
713
766
  ],
714
767
  })}
715
- <ScreenContainer
716
- enabled={detachInactiveScreens}
717
- style={styles.container}
718
- onLayout={this.handleLayout}
719
- >
720
- {[...routes, ...state.preloadedRoutes].map((route, index) => {
768
+ <View style={styles.container} onLayout={this.handleLayout}>
769
+ {allRoutes.map((route, index, self) => {
721
770
  const focused = focusedRoute.key === route.key;
722
771
  const gesture = gestures[route.key];
723
772
  const scene = scenes[index];
724
- // It is possible that for a short period the route appears in both arrays.
725
- // Particularly, if the screen is removed with `retain`, then it needs a moment to execute the animation.
726
- // However, due to the router action, it immediately populates the `preloadedRoutes` array.
727
- // Practically, the logic below takes care that it is rendered only once.
728
- const isPreloaded =
729
- state.preloadedRoutes.includes(route) && !routes.includes(route);
730
- if (
731
- state.preloadedRoutes.includes(route) &&
732
- routes.includes(route) &&
733
- index >= routes.length
734
- ) {
735
- return null;
773
+
774
+ if (gesture == null) {
775
+ throw new Error(
776
+ `Couldn't find a gesture for route '${route.key}'.`
777
+ );
778
+ }
779
+
780
+ if (scene == null) {
781
+ throw new Error(`Couldn't find a scene at index ${index}.`);
736
782
  }
737
783
 
784
+ const nextScene = scenes[index + 1];
785
+ const nextRoute = self[index + 1];
786
+
738
787
  const {
788
+ inactiveBehavior = 'pause',
789
+ animation,
739
790
  headerShown = true,
791
+ headerMode,
740
792
  headerTransparent,
741
- freezeOnBlur,
742
- autoHideHomeIndicator,
743
793
  } = scene.descriptor.options;
744
794
 
745
795
  const safeAreaInsetTop = insets.top;
@@ -748,46 +798,142 @@ export class CardStack extends React.Component<Props, State> {
748
798
  const safeAreaInsetLeft = insets.left;
749
799
 
750
800
  const headerHeight =
751
- headerShown !== false ? headerHeights[route.key] : 0;
801
+ headerShown !== false
802
+ ? headerHeights[route.key]
803
+ : HIDDEN_HEADER_HEIGHT;
752
804
 
753
805
  // Start from current card and count backwards the number of cards with same interpolation
754
806
  const interpolationIndex = getInterpolationIndex(scenes, index);
755
- const isModal = getIsModal(
807
+ const isInModalPresentation = getIsInModalPresentation(
756
808
  scene,
757
809
  interpolationIndex,
758
810
  isParentModal
759
811
  );
760
812
 
761
- const isNextScreenTransparent =
762
- scenes[index + 1]?.descriptor.options.presentation ===
763
- 'transparentModal';
813
+ const isModal =
814
+ scene.descriptor.options.presentation === 'modal' ||
815
+ getIsModalPresentation(
816
+ scene.descriptor.options.cardStyleInterpolator
817
+ );
764
818
 
765
- const detachCurrentScreen =
766
- scenes[index + 1]?.descriptor.options.detachPreviousScreen !==
767
- false;
819
+ const isNextScreenModal =
820
+ nextScene != null &&
821
+ (nextScene.descriptor.options.presentation === 'modal' ||
822
+ getIsModalPresentation(
823
+ nextScene.descriptor.options.cardStyleInterpolator
824
+ ));
768
825
 
769
- const activityState = isPreloaded
770
- ? STATE_INACTIVE
771
- : activeStates[index];
826
+ const isNextScreenTransparent =
827
+ nextScene?.descriptor.options.presentation === 'transparentModal';
828
+
829
+ const isAnimationEnabled =
830
+ getAnimationEnabled(animation) ||
831
+ // Also check next screen's animation,
832
+ // As it will result in both screens being visible
833
+ (nextScene
834
+ ? getAnimationEnabled(nextScene.descriptor.options.animation)
835
+ : false);
836
+
837
+ const isRemoving =
838
+ replacingRouteKeys.includes(route.key) ||
839
+ closingRouteKeys.includes(route.key);
840
+
841
+ const isFocusing =
842
+ openingRouteKeys.includes(route.key) ||
843
+ (nextRoute != null &&
844
+ [...closingRouteKeys, ...replacingRouteKeys].includes(
845
+ nextRoute.key
846
+ ));
847
+
848
+ const isInactive = isInactiveRoute(route, this.props.routes);
849
+ const isRetained = state.retainedRouteKeys?.includes(route.key);
850
+ const isPreloaded = isInactive && !isRetained;
851
+
852
+ const isTopmost = index === routes.length - 1;
853
+ const isBeforeLast = index === routes.length - 2;
854
+
855
+ // Keep animating and the last two rendered routes visible for smoother transitions
856
+ const isVisible =
857
+ focused ||
858
+ // React native destroys the view when hidden
859
+ // So we keep retained screens visible
860
+ // This makes sure any videos being played don't stop etc.
861
+ isRetained ||
862
+ isNextScreenTransparent ||
863
+ // Modal presentation on iOS has multiple screens visible at the same time
864
+ // The base screen before the modals renders the dim, which is always persistent
865
+ // On iPadOS, the base screen is visible while modal is in a smaller area on top
866
+ // The top edge of the last 3 screens can be seen animating behind the modal
867
+ (isNextScreenModal && (!isModal || index >= routes.length - 3)) ||
868
+ // We only need to keep other screens visible when animation is enabled
869
+ (isAnimationEnabled &&
870
+ (isFocusing ||
871
+ isRemoving ||
872
+ // Preloaded screens should stay mounted, but remain hidden until focused
873
+ (!isPreloaded && index >= routes.length - 2)));
874
+
875
+ const activityMode =
876
+ // Keep focused and focusing screens interactive
877
+ focused || isFocusing
878
+ ? 'normal'
879
+ : inactiveBehavior === 'none' ||
880
+ // Keep effects running until the closing animation finishes
881
+ isRemoving ||
882
+ // Unpause preloaded or retained screens so updates are visible
883
+ // Handle retained explicitly as isInactive won't be updated until animation end
884
+ isInactive ||
885
+ isRetained ||
886
+ // Keep the screen before transparent screen active
887
+ // This lets the screen under the transparent screen update and animate
888
+ isNextScreenTransparent ||
889
+ // Keep the screen before last screen active
890
+ // Otherwise it breaks animation when going back
891
+ isBeforeLast
892
+ ? 'inert'
893
+ : inactiveBehavior === 'unmount' &&
894
+ // Don't unmount screens that needs to stay visible
895
+ !isVisible &&
896
+ // Don't unmount screens with nested navigators
897
+ // So we don't lose their state
898
+ !nestedStateRouteKeys.includes(route.key)
899
+ ? 'unmounted'
900
+ : 'paused';
901
+
902
+ if (activityMode === 'unmounted') {
903
+ return null;
904
+ }
772
905
 
773
906
  return (
774
- <Screen
907
+ <ActivityView
775
908
  key={route.key}
776
- style={[StyleSheet.absoluteFill, { pointerEvents: 'box-none' }]}
777
- enabled={detachInactiveScreens}
778
- activityState={activityState}
779
- freezeOnBlur={freezeOnBlur}
780
- shouldFreeze={activityState === STATE_INACTIVE && !isPreloaded}
781
- homeIndicatorHidden={autoHideHomeIndicator}
909
+ mode={activityMode}
910
+ visible={isVisible}
911
+ style={{
912
+ ...StyleSheet.absoluteFill,
913
+ marginTop:
914
+ headerTransparent || headerMode === 'screen'
915
+ ? 0
916
+ : // Add offset for non-transparent floating header
917
+ isFloatHeaderAbsolute
918
+ ? // If header is absolute, offset the card by the header height
919
+ headerHeight?.value
920
+ : // Otherwise, the in-flow float header already takes the
921
+ // focused header's space, so offset by the difference
922
+ headerHeight && focusedHeaderHeight
923
+ ? headerHeight.value - focusedHeaderHeight.value
924
+ : 0,
925
+ }}
782
926
  >
783
927
  <CardContainer
784
928
  index={index}
785
929
  interpolationIndex={interpolationIndex}
786
- modal={isModal}
787
- active={index === routes.length - 1}
930
+ modal={isInModalPresentation}
931
+ active={isTopmost}
788
932
  focused={focused}
789
933
  opening={openingRouteKeys.includes(route.key)}
790
934
  closing={closingRouteKeys.includes(route.key)}
935
+ closingAnimated={scene.closing}
936
+ nextClosingAnimated={nextScene?.closing}
791
937
  layout={layout}
792
938
  gesture={gesture}
793
939
  scene={scene}
@@ -803,22 +949,19 @@ export class CardStack extends React.Component<Props, State> {
803
949
  onHeaderHeightChange={this.handleHeaderLayout}
804
950
  getPreviousScene={this.getPreviousScene}
805
951
  getFocusedRoute={this.getFocusedRoute}
806
- hasAbsoluteFloatHeader={
807
- isFloatHeaderAbsolute && !headerTransparent
808
- }
809
952
  renderHeader={renderHeader}
810
953
  onOpenRoute={onOpenRoute}
811
954
  onCloseRoute={onCloseRoute}
812
955
  onTransitionStart={onTransitionStart}
813
956
  onTransitionEnd={onTransitionEnd}
814
- isNextScreenTransparent={isNextScreenTransparent}
815
- detachCurrentScreen={detachCurrentScreen}
816
- preloaded={isPreloaded}
817
- />
818
- </Screen>
957
+ preloaded={isInactive}
958
+ >
959
+ {scene.descriptor.render()}
960
+ </CardContainer>
961
+ </ActivityView>
819
962
  );
820
963
  })}
821
- </ScreenContainer>
964
+ </View>
822
965
  </View>
823
966
  );
824
967
  }