@react-navigation/stack 8.0.0-alpha.4 → 8.0.0-alpha.41

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 (126) hide show
  1. package/lib/module/index.js +2 -2
  2. package/lib/module/index.js.map +1 -1
  3. package/lib/module/navigators/createStackNavigator.js +3 -7
  4. package/lib/module/navigators/createStackNavigator.js.map +1 -1
  5. package/lib/module/utils/GestureHandlerContext.js +5 -0
  6. package/lib/module/utils/GestureHandlerContext.js.map +1 -0
  7. package/lib/module/utils/gestureActivationCriteria.js +10 -8
  8. package/lib/module/utils/gestureActivationCriteria.js.map +1 -1
  9. package/lib/module/utils/useCardAnimation.js +1 -1
  10. package/lib/module/utils/useCardAnimation.js.map +1 -1
  11. package/lib/module/utils/useGestureHandler.js +9 -0
  12. package/lib/module/utils/useGestureHandler.js.map +1 -0
  13. package/lib/module/utils/useKeyboardManager.js +71 -26
  14. package/lib/module/utils/useKeyboardManager.js.map +1 -1
  15. package/lib/module/views/GestureHandler.js +15 -14
  16. package/lib/module/views/GestureHandler.js.map +1 -1
  17. package/lib/module/views/GestureHandlerNative.js +16 -10
  18. package/lib/module/views/GestureHandlerNative.js.map +1 -1
  19. package/lib/module/views/Header/Header.js +2 -2
  20. package/lib/module/views/Header/Header.js.map +1 -1
  21. package/lib/module/views/Header/HeaderContainer.js +52 -18
  22. package/lib/module/views/Header/HeaderContainer.js.map +1 -1
  23. package/lib/module/views/Header/HeaderSegment.js +2 -2
  24. package/lib/module/views/Header/HeaderSegment.js.map +1 -1
  25. package/lib/module/views/Stack/Card.js +90 -87
  26. package/lib/module/views/Stack/Card.js.map +1 -1
  27. package/lib/module/views/Stack/CardContainer.js +72 -98
  28. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  29. package/lib/module/views/Stack/CardStack.js +171 -126
  30. package/lib/module/views/Stack/CardStack.js.map +1 -1
  31. package/lib/module/views/Stack/StackView.js +103 -53
  32. package/lib/module/views/Stack/StackView.js.map +1 -1
  33. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts +1 -1
  34. package/lib/typescript/src/TransitionConfigs/CardStyleInterpolators.d.ts.map +1 -1
  35. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts +1 -1
  36. package/lib/typescript/src/TransitionConfigs/HeaderStyleInterpolators.d.ts.map +1 -1
  37. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +1 -1
  38. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  39. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +1 -1
  40. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +12 -12
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/navigators/createStackNavigator.d.ts +10 -15
  44. package/lib/typescript/src/navigators/createStackNavigator.d.ts.map +1 -1
  45. package/lib/typescript/src/types.d.ts +46 -53
  46. package/lib/typescript/src/types.d.ts.map +1 -1
  47. package/lib/typescript/src/utils/CardAnimationContext.d.ts +1 -1
  48. package/lib/typescript/src/utils/CardAnimationContext.d.ts.map +1 -1
  49. package/lib/typescript/src/utils/GestureHandlerContext.d.ts +3 -0
  50. package/lib/typescript/src/utils/GestureHandlerContext.d.ts.map +1 -0
  51. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +4 -51
  52. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
  53. package/lib/typescript/src/utils/getDistanceForDirection.d.ts +1 -1
  54. package/lib/typescript/src/utils/getDistanceForDirection.d.ts.map +1 -1
  55. package/lib/typescript/src/utils/getInvertedMultiplier.d.ts +1 -1
  56. package/lib/typescript/src/utils/getInvertedMultiplier.d.ts.map +1 -1
  57. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts +1 -1
  58. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts.map +1 -1
  59. package/lib/typescript/src/utils/useCardAnimation.d.ts +1 -1
  60. package/lib/typescript/src/utils/useCardAnimation.d.ts.map +1 -1
  61. package/lib/typescript/src/utils/useGestureHandler.d.ts +2 -0
  62. package/lib/typescript/src/utils/useGestureHandler.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/useKeyboardManager.d.ts +12 -2
  64. package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
  65. package/lib/typescript/src/views/GestureHandler.android.d.ts +1 -1
  66. package/lib/typescript/src/views/GestureHandler.android.d.ts.map +1 -1
  67. package/lib/typescript/src/views/GestureHandler.d.ts +7 -11
  68. package/lib/typescript/src/views/GestureHandler.d.ts.map +1 -1
  69. package/lib/typescript/src/views/GestureHandler.ios.d.ts +1 -1
  70. package/lib/typescript/src/views/GestureHandler.ios.d.ts.map +1 -1
  71. package/lib/typescript/src/views/GestureHandlerNative.d.ts +9 -4
  72. package/lib/typescript/src/views/GestureHandlerNative.d.ts.map +1 -1
  73. package/lib/typescript/src/views/Header/Header.d.ts +2 -2
  74. package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
  75. package/lib/typescript/src/views/Header/HeaderContainer.d.ts +9 -3
  76. package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
  77. package/lib/typescript/src/views/Header/HeaderSegment.d.ts +5 -4
  78. package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
  79. package/lib/typescript/src/views/Stack/Card.d.ts +5 -5
  80. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  81. package/lib/typescript/src/views/Stack/CardContainer.d.ts +5 -7
  82. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  83. package/lib/typescript/src/views/Stack/CardContent.d.ts +1 -1
  84. package/lib/typescript/src/views/Stack/CardContent.d.ts.map +1 -1
  85. package/lib/typescript/src/views/Stack/CardStack.d.ts +6 -5
  86. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  87. package/lib/typescript/src/views/Stack/StackView.d.ts +10 -13
  88. package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
  89. package/package.json +21 -23
  90. package/src/index.tsx +3 -2
  91. package/src/navigators/createStackNavigator.tsx +9 -40
  92. package/src/types.tsx +73 -73
  93. package/src/utils/GestureHandlerContext.tsx +5 -0
  94. package/src/utils/gestureActivationCriteria.tsx +14 -10
  95. package/src/utils/useCardAnimation.tsx +1 -1
  96. package/src/utils/useGestureHandler.tsx +9 -0
  97. package/src/utils/useKeyboardManager.tsx +96 -33
  98. package/src/views/GestureHandler.tsx +27 -16
  99. package/src/views/GestureHandlerNative.tsx +26 -12
  100. package/src/views/Header/Header.tsx +2 -2
  101. package/src/views/Header/HeaderContainer.tsx +67 -24
  102. package/src/views/Header/HeaderSegment.tsx +4 -4
  103. package/src/views/Stack/Card.tsx +134 -115
  104. package/src/views/Stack/CardContainer.tsx +86 -127
  105. package/src/views/Stack/CardStack.tsx +380 -323
  106. package/src/views/Stack/StackView.tsx +173 -71
  107. package/lib/module/utils/GestureHandlerRefContext.js +0 -5
  108. package/lib/module/utils/GestureHandlerRefContext.js.map +0 -1
  109. package/lib/module/utils/findLastIndex.js +0 -11
  110. package/lib/module/utils/findLastIndex.js.map +0 -1
  111. package/lib/module/utils/useGestureHandlerRef.js +0 -12
  112. package/lib/module/utils/useGestureHandlerRef.js.map +0 -1
  113. package/lib/module/views/Stack/CardA11yWrapper.js +0 -43
  114. package/lib/module/views/Stack/CardA11yWrapper.js.map +0 -1
  115. package/lib/typescript/src/utils/GestureHandlerRefContext.d.ts +0 -3
  116. package/lib/typescript/src/utils/GestureHandlerRefContext.d.ts.map +0 -1
  117. package/lib/typescript/src/utils/findLastIndex.d.ts +0 -2
  118. package/lib/typescript/src/utils/findLastIndex.d.ts.map +0 -1
  119. package/lib/typescript/src/utils/useGestureHandlerRef.d.ts +0 -3
  120. package/lib/typescript/src/utils/useGestureHandlerRef.d.ts.map +0 -1
  121. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +0 -15
  122. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +0 -1
  123. package/src/utils/GestureHandlerRefContext.tsx +0 -5
  124. package/src/utils/findLastIndex.tsx +0 -9
  125. package/src/utils/useGestureHandlerRef.tsx +0 -15
  126. 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,7 +18,6 @@ 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 {
21
23
  forModalPresentationIOS,
@@ -44,16 +46,20 @@ import type {
44
46
  StackNavigationOptions,
45
47
  TransitionPreset,
46
48
  } from '../../types';
47
- import { findLastIndex } from '../../utils/findLastIndex';
48
49
  import { getDistanceForDirection } from '../../utils/getDistanceForDirection';
49
50
  import { getModalRouteKeys } from '../../utils/getModalRoutesKeys';
50
- import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
51
+ import type {
52
+ HeaderHeight,
53
+ Props as HeaderContainerProps,
54
+ } from '../Header/HeaderContainer';
51
55
  import { CardContainer } from './CardContainer';
52
56
 
53
57
  type GestureValues = {
54
58
  [key: string]: Animated.Value;
55
59
  };
56
60
 
61
+ type HeaderHeights = Record<string, HeaderHeight>;
62
+
57
63
  type Props = {
58
64
  direction: LocaleDirection;
59
65
  insets: EdgeInsets;
@@ -62,6 +68,7 @@ type Props = {
62
68
  routes: Route<string>[];
63
69
  openingRouteKeys: string[];
64
70
  closingRouteKeys: string[];
71
+ replacingRouteKeys: string[];
65
72
  onOpenRoute: (props: { route: Route<string> }) => void;
66
73
  onCloseRoute: (props: { route: Route<string> }) => void;
67
74
  getPreviousRoute: (props: {
@@ -87,8 +94,7 @@ type State = {
87
94
  scenes: Scene[];
88
95
  gestures: GestureValues;
89
96
  layout: Layout;
90
- activeStates: (0 | 1 | Animated.AnimatedInterpolation<0 | 1>)[];
91
- headerHeights: Record<string, number>;
97
+ headerHeights: HeaderHeights;
92
98
  };
93
99
 
94
100
  const NAMED_TRANSITIONS_PRESETS = {
@@ -107,16 +113,16 @@ const NAMED_TRANSITIONS_PRESETS = {
107
113
  }),
108
114
  } as const satisfies Record<StackAnimationName, TransitionPreset>;
109
115
 
110
- const EPSILON = 1e-5;
116
+ const HIDDEN_HEADER_HEIGHT = { measured: false, value: 0 };
111
117
 
112
- const STATE_INACTIVE = 0;
113
- const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
114
- const STATE_ON_TOP = 2;
118
+ const getInterpolationIndex = (scenes: Scene[], index: number) => {
119
+ const scene = scenes[index];
115
120
 
116
- const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
121
+ if (scene == null) {
122
+ throw new Error(`Couldn't find a scene at index ${index}.`);
123
+ }
117
124
 
118
- const getInterpolationIndex = (scenes: Scene[], index: number) => {
119
- const { cardStyleInterpolator } = scenes[index].descriptor.options;
125
+ const { cardStyleInterpolator } = scene.descriptor.options;
120
126
 
121
127
  // Start from current card and count backwards the number of cards with same interpolation
122
128
  let interpolationIndex = 0;
@@ -167,9 +173,9 @@ const getHeaderHeights = (
167
173
  isParentHeaderShown: boolean,
168
174
  isParentModal: boolean,
169
175
  layout: Layout,
170
- previous: Record<string, number>
176
+ previous: HeaderHeights
171
177
  ) => {
172
- return scenes.reduce<Record<string, number>>((acc, curr, index) => {
178
+ return scenes.reduce<HeaderHeights>((acc, curr, index) => {
173
179
  const {
174
180
  headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
175
181
  headerStyle,
@@ -180,19 +186,34 @@ const getHeaderHeights = (
180
186
  const height =
181
187
  'height' in style && typeof style.height === 'number'
182
188
  ? style.height
183
- : previous[curr.route.key];
189
+ : undefined;
184
190
 
185
191
  const interpolationIndex = getInterpolationIndex(scenes, index);
186
192
  const isModal = getIsModal(curr, interpolationIndex, isParentModal);
187
193
 
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
- });
194
+ if (typeof height === 'number') {
195
+ acc[curr.route.key] = { measured: true, value: height };
196
+ } else {
197
+ const previousHeight = previous[curr.route.key];
198
+ const defaultHeight = getDefaultHeaderHeight({
199
+ landscape: layout.width > layout.height,
200
+ modalPresentation: isModal,
201
+ topInset: headerStatusBarHeight,
202
+ });
203
+
204
+ // If we hadn't measured before, use re-computed default height
205
+ if (
206
+ !previousHeight?.measured &&
207
+ defaultHeight !== previousHeight?.value
208
+ ) {
209
+ acc[curr.route.key] = {
210
+ measured: false,
211
+ value: defaultHeight,
212
+ };
213
+ } else if (previousHeight) {
214
+ acc[curr.route.key] = previousHeight;
215
+ }
216
+ }
196
217
 
197
218
  return acc;
198
219
  }, {});
@@ -263,6 +284,27 @@ export function getAnimationEnabled(animation: StackAnimationName | undefined) {
263
284
  return getDefaultAnimation(animation) !== 'none';
264
285
  }
265
286
 
287
+ const getAllRoutes = (
288
+ routes: Route<string>[],
289
+ state: StackNavigationState<ParamListBase>
290
+ ) => {
291
+ const routeKeys = new Set(routes.map((route) => route.key));
292
+ const inactiveRoutes = state.routes.slice(state.index + 1);
293
+
294
+ // If a route is moved from active routes to inactive routes,
295
+ // It can still be in the local copy of `routes` until the animation ends
296
+ // So we need to deduplicate the routes to avoid rendering the same route twice
297
+ return [
298
+ ...routes,
299
+ ...inactiveRoutes.filter((route) => !routeKeys.has(route.key)),
300
+ ];
301
+ };
302
+
303
+ const isInactiveRoute = (route: Route<string>, routes: Route<string>[]) =>
304
+ // `routes` contains active routes and routes animating during transitions.
305
+ // Any route added by `getAllRoutes` that's missing from this list is inactive.
306
+ !routes.some((currentRoute) => currentRoute.key === route.key);
307
+
266
308
  export class CardStack extends React.Component<Props, State> {
267
309
  static getDerivedStateFromProps(
268
310
  props: Props,
@@ -275,10 +317,9 @@ export class CardStack extends React.Component<Props, State> {
275
317
  return null;
276
318
  }
277
319
 
278
- const gestures = [
279
- ...props.routes,
280
- ...props.state.preloadedRoutes,
281
- ].reduce<GestureValues>((acc, curr) => {
320
+ const allRoutes = getAllRoutes(props.routes, props.state);
321
+
322
+ const gestures = allRoutes.reduce<GestureValues>((acc, curr) => {
282
323
  const descriptor = props.descriptors[curr.key];
283
324
  const { animation } = descriptor?.options || {};
284
325
 
@@ -287,7 +328,7 @@ export class CardStack extends React.Component<Props, State> {
287
328
  new Animated.Value(
288
329
  (props.openingRouteKeys.includes(curr.key) &&
289
330
  getAnimationEnabled(animation)) ||
290
- props.state.preloadedRoutes.includes(curr)
331
+ isInactiveRoute(curr, props.routes)
291
332
  ? getDistanceFromOptions(
292
333
  state.layout,
293
334
  descriptor?.options,
@@ -299,254 +340,179 @@ export class CardStack extends React.Component<Props, State> {
299
340
  return acc;
300
341
  }, {});
301
342
 
302
- const modalRouteKeys = getModalRouteKeys(
303
- [...props.routes, ...props.state.preloadedRoutes],
304
- props.descriptors
305
- );
343
+ const modalRouteKeys = getModalRouteKeys(allRoutes, props.descriptors);
306
344
 
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
- );
345
+ const scenes = allRoutes.map((route, index, self) => {
346
+ // For preloaded or retained screens, we don't care about the previous and the next screen
347
+ const isInactive = isInactiveRoute(route, props.routes);
348
+ const previousRoute = isInactive ? undefined : self[index - 1];
349
+ const nextRoute = isInactive ? undefined : self[index + 1];
361
350
 
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
- },
351
+ const oldScene = state.scenes[index];
352
+
353
+ const currentGesture = gestures[route.key];
354
+
355
+ if (currentGesture == null) {
356
+ throw new Error(`Couldn't find a gesture for route '${route.key}'.`);
357
+ }
358
+
359
+ const previousGesture = previousRoute
360
+ ? gestures[previousRoute.key]
361
+ : undefined;
362
+ const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
363
+
364
+ const descriptor =
365
+ props.descriptors[route.key] ??
366
+ state.descriptors[route.key] ??
367
+ oldScene?.descriptor;
368
+
369
+ if (descriptor == null) {
370
+ throw new Error(`Couldn't find a descriptor for route '${route.key}'.`);
371
+ }
372
+
373
+ const nextOptions = nextRoute
374
+ ? (props.descriptors[nextRoute.key] ?? state.descriptors[nextRoute.key])
375
+ ?.options
376
+ : undefined;
377
+
378
+ const previousOptions = previousRoute
379
+ ? (
380
+ props.descriptors[previousRoute.key] ??
381
+ state.descriptors[previousRoute.key]
382
+ )?.options
383
+ : undefined;
384
+
385
+ // When a screen is not the last, it should use next screen's transition config
386
+ // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
387
+ // For example combining a slide and a modal transition would look wrong otherwise
388
+ // With this approach, combining different transition styles in the same navigator mostly looks right
389
+ // This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
390
+ // but the majority of the transitions look alright
391
+ const optionsForTransitionConfig =
392
+ index !== self.length - 1 &&
393
+ nextOptions &&
394
+ nextOptions?.presentation !== 'transparentModal'
395
+ ? nextOptions
396
+ : descriptor.options;
397
+
398
+ // Assume modal if there are already modal screens in the stack
399
+ // or current screen is a modal when no presentation is specified
400
+ const isModal = modalRouteKeys.includes(route.key);
401
+
402
+ const animation = getDefaultAnimation(
403
+ optionsForTransitionConfig.animation
404
+ );
405
+
406
+ const isAnimationEnabled = getAnimationEnabled(animation);
407
+
408
+ const transitionPreset =
409
+ animation !== 'default'
410
+ ? NAMED_TRANSITIONS_PRESETS[animation]
411
+ : optionsForTransitionConfig.presentation === 'transparentModal'
412
+ ? ModalFadeTransition
413
+ : optionsForTransitionConfig.presentation === 'modal' || isModal
414
+ ? ModalTransition
415
+ : DefaultTransition;
416
+
417
+ const {
418
+ gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled,
419
+ gestureDirection = transitionPreset.gestureDirection,
420
+ transitionSpec = transitionPreset.transitionSpec,
421
+ cardStyleInterpolator = isAnimationEnabled
422
+ ? transitionPreset.cardStyleInterpolator
423
+ : forNoAnimationCard,
424
+ headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
425
+ cardOverlayEnabled = (Platform.OS !== 'ios' &&
426
+ optionsForTransitionConfig.presentation !== 'transparentModal') ||
427
+ getIsModalPresentation(cardStyleInterpolator),
428
+ } = optionsForTransitionConfig;
429
+
430
+ const headerMode: StackHeaderMode =
431
+ descriptor.options.headerMode ??
432
+ (!(
433
+ optionsForTransitionConfig.presentation === 'modal' ||
434
+ optionsForTransitionConfig.presentation === 'transparentModal' ||
435
+ nextOptions?.presentation === 'modal' ||
436
+ nextOptions?.presentation === 'transparentModal' ||
437
+ getIsModalPresentation(cardStyleInterpolator)
438
+ ) &&
439
+ Platform.OS === 'ios' &&
440
+ descriptor.options.header === undefined
441
+ ? 'float'
442
+ : 'screen');
443
+
444
+ const isRTL = props.direction === 'rtl';
445
+
446
+ const scene = {
447
+ route,
448
+ descriptor: {
449
+ ...descriptor,
450
+ options: {
451
+ ...descriptor.options,
452
+ animation,
453
+ cardOverlayEnabled,
454
+ cardStyleInterpolator,
455
+ gestureDirection,
456
+ gestureEnabled,
457
+ headerStyleInterpolator,
458
+ transitionSpec,
459
+ headerMode,
417
460
  },
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
461
+ },
462
+ progress: {
463
+ current: getProgressFromGesture(
464
+ currentGesture,
465
+ state.layout,
466
+ descriptor.options,
467
+ isRTL
468
+ ),
469
+ next:
470
+ nextGesture && nextOptions?.presentation !== 'transparentModal'
435
471
  ? getProgressFromGesture(
436
- previousGesture,
472
+ nextGesture,
437
473
  state.layout,
438
- previousOptions,
474
+ nextOptions,
439
475
  isRTL
440
476
  )
441
477
  : 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
- );
478
+ previous: previousGesture
479
+ ? getProgressFromGesture(
480
+ previousGesture,
481
+ state.layout,
482
+ previousOptions,
483
+ isRTL
484
+ )
485
+ : undefined,
486
+ },
487
+ __memo: [
488
+ state.layout,
489
+ descriptor,
490
+ nextOptions,
491
+ previousOptions,
492
+ currentGesture,
493
+ nextGesture,
494
+ previousGesture,
495
+ ],
496
+ };
467
497
 
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
- }
498
+ if (
499
+ oldScene &&
500
+ scene.__memo.every((it, i) => {
501
+ // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
502
+ return oldScene.__memo[i] === it;
503
+ })
504
+ ) {
505
+ return oldScene;
503
506
  }
504
507
 
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
- }
508
+ return scene;
509
+ });
543
510
 
544
511
  return {
545
512
  routes: props.routes,
546
513
  scenes,
547
514
  gestures,
548
515
  descriptors: props.descriptors,
549
- activeStates,
550
516
  headerHeights: getHeaderHeights(
551
517
  scenes,
552
518
  props.insets,
@@ -567,7 +533,6 @@ export class CardStack extends React.Component<Props, State> {
567
533
  gestures: {},
568
534
  layout: SafeAreaProviderCompat.initialMetrics.frame,
569
535
  descriptors: this.props.descriptors,
570
- activeStates: [],
571
536
  // Used when card's header is null and mode is float to make transition
572
537
  // between screens with headers and those without headers smooth.
573
538
  // This is not a great heuristic here. We don't know synchronously
@@ -611,14 +576,17 @@ export class CardStack extends React.Component<Props, State> {
611
576
  this.setState(({ headerHeights }) => {
612
577
  const previousHeight = headerHeights[route.key];
613
578
 
614
- if (previousHeight === height) {
579
+ if (previousHeight?.value === height) {
615
580
  return null;
616
581
  }
617
582
 
618
583
  return {
619
584
  headerHeights: {
620
585
  ...headerHeights,
621
- [route.key]: height,
586
+ [route.key]: {
587
+ measured: true,
588
+ value: height,
589
+ },
622
590
  },
623
591
  };
624
592
  });
@@ -627,7 +595,13 @@ export class CardStack extends React.Component<Props, State> {
627
595
  private getFocusedRoute = () => {
628
596
  const { state } = this.props;
629
597
 
630
- return state.routes[state.index];
598
+ const route = state.routes[state.index];
599
+
600
+ if (route == null) {
601
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
602
+ }
603
+
604
+ return route;
631
605
  };
632
606
 
633
607
  private getPreviousScene = ({ route }: { route: Route<string> }) => {
@@ -654,27 +628,37 @@ export class CardStack extends React.Component<Props, State> {
654
628
  routes,
655
629
  openingRouteKeys,
656
630
  closingRouteKeys,
631
+ replacingRouteKeys,
657
632
  onOpenRoute,
658
633
  onCloseRoute,
634
+ onGestureStart,
635
+ onGestureEnd,
636
+ onGestureCancel,
659
637
  renderHeader,
660
638
  isParentHeaderShown,
661
639
  isParentModal,
662
640
  onTransitionStart,
663
641
  onTransitionEnd,
664
- onGestureStart,
665
- onGestureEnd,
666
- onGestureCancel,
667
- detachInactiveScreens = Platform.OS === 'web' ||
668
- Platform.OS === 'android' ||
669
- Platform.OS === 'ios',
670
642
  } = this.props;
671
643
 
672
- const { scenes, layout, gestures, activeStates, headerHeights } =
673
- this.state;
644
+ const { scenes, layout, gestures, headerHeights } = this.state;
645
+
646
+ const allRoutes = getAllRoutes(routes, state);
674
647
 
675
648
  const focusedRoute = state.routes[state.index];
676
649
 
677
- const isFloatHeaderAbsolute = scenes.slice(-2).some((scene) => {
650
+ if (focusedRoute == null) {
651
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
652
+ }
653
+
654
+ // Render only two top-most active headers as a workaround for
655
+ // https://github.com/react-navigation/react-navigation/issues/12456.
656
+ // If the header is persisted, it might be placed incorrectly when navigating back
657
+ const activeHeaderScenes = scenes
658
+ .filter((scene) => !isInactiveRoute(scene.route, routes))
659
+ .slice(-2);
660
+
661
+ const isFloatHeaderAbsolute = activeHeaderScenes.some((scene) => {
678
662
  const options = scene.descriptor.options ?? {};
679
663
  const { headerMode, headerTransparent, headerShown = true } = options;
680
664
 
@@ -693,13 +677,16 @@ export class CardStack extends React.Component<Props, State> {
693
677
  scenes.findLast((scene) => scene.route.key === focusedRoute.key)
694
678
  ?.descriptor.options.headerMode === 'float';
695
679
 
680
+ const focusedHeaderHeight = headerHeights[focusedRoute.key];
681
+
696
682
  return (
697
683
  <View style={styles.container}>
698
684
  {renderHeader({
699
685
  mode: 'float',
700
- scenes,
686
+ scenes: activeHeaderScenes,
701
687
  getPreviousScene: this.getPreviousScene,
702
688
  getFocusedRoute: this.getFocusedRoute,
689
+ contentHeight: headerHeights[focusedRoute.key],
703
690
  onContentHeightChange: this.handleHeaderLayout,
704
691
  style: [
705
692
  isFloatHeaderAbsolute && [
@@ -707,39 +694,36 @@ export class CardStack extends React.Component<Props, State> {
707
694
  // The header title can't be found with maestro without this
708
695
  // In some cases, header buttons may not be touchable as well
709
696
  isFloatHeaderFocused && {
710
- height: headerHeights[focusedRoute.key],
697
+ height: focusedHeaderHeight?.value,
711
698
  },
712
699
  ],
713
700
  ],
714
701
  })}
715
- <ScreenContainer
716
- enabled={detachInactiveScreens}
717
- style={styles.container}
718
- onLayout={this.handleLayout}
719
- >
720
- {[...routes, ...state.preloadedRoutes].map((route, index) => {
702
+ <View style={styles.container} onLayout={this.handleLayout}>
703
+ {allRoutes.map((route, index, self) => {
721
704
  const focused = focusedRoute.key === route.key;
722
705
  const gesture = gestures[route.key];
723
706
  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;
707
+
708
+ if (gesture == null) {
709
+ throw new Error(
710
+ `Couldn't find a gesture for route '${route.key}'.`
711
+ );
736
712
  }
737
713
 
714
+ if (scene == null) {
715
+ throw new Error(`Couldn't find a scene at index ${index}.`);
716
+ }
717
+
718
+ const nextScene = scenes[index + 1];
719
+ const nextRoute = self[index + 1];
720
+
738
721
  const {
722
+ inactiveBehavior = 'pause',
723
+ animation,
739
724
  headerShown = true,
725
+ headerMode,
740
726
  headerTransparent,
741
- freezeOnBlur,
742
- autoHideHomeIndicator,
743
727
  } = scene.descriptor.options;
744
728
 
745
729
  const safeAreaInsetTop = insets.top;
@@ -748,7 +732,9 @@ export class CardStack extends React.Component<Props, State> {
748
732
  const safeAreaInsetLeft = insets.left;
749
733
 
750
734
  const headerHeight =
751
- headerShown !== false ? headerHeights[route.key] : 0;
735
+ headerShown !== false
736
+ ? headerHeights[route.key]
737
+ : HIDDEN_HEADER_HEIGHT;
752
738
 
753
739
  // Start from current card and count backwards the number of cards with same interpolation
754
740
  const interpolationIndex = getInterpolationIndex(scenes, index);
@@ -758,33 +744,107 @@ export class CardStack extends React.Component<Props, State> {
758
744
  isParentModal
759
745
  );
760
746
 
761
- const isNextScreenTransparent =
762
- scenes[index + 1]?.descriptor.options.presentation ===
763
- 'transparentModal';
764
-
765
- const detachCurrentScreen =
766
- scenes[index + 1]?.descriptor.options.detachPreviousScreen !==
767
- false;
747
+ const isAnimationEnabled =
748
+ getAnimationEnabled(animation) ||
749
+ // Also check next screen's animation,
750
+ // As it will result in both screens being visible
751
+ (nextScene
752
+ ? getAnimationEnabled(nextScene.descriptor.options.animation)
753
+ : false);
768
754
 
769
- const activityState = isPreloaded
770
- ? STATE_INACTIVE
771
- : activeStates[index];
755
+ const isNextScreenTransparent =
756
+ nextScene?.descriptor.options.presentation === 'transparentModal';
757
+
758
+ const isRemoving =
759
+ replacingRouteKeys.includes(route.key) ||
760
+ closingRouteKeys.includes(route.key);
761
+
762
+ const isFocusing =
763
+ openingRouteKeys.includes(route.key) ||
764
+ (nextRoute != null &&
765
+ [...closingRouteKeys, ...replacingRouteKeys].includes(
766
+ nextRoute.key
767
+ ));
768
+
769
+ const isInactive = isInactiveRoute(route, this.props.routes);
770
+ const isRetained = state.retainedRouteKeys?.includes(route.key);
771
+ const isPreloaded = isInactive && !isRetained;
772
+
773
+ const isTopmost = index === routes.length - 1;
774
+ const isBeforeLast = index === routes.length - 2;
775
+
776
+ // Keep animating and the last two rendered routes visible for smoother transitions
777
+ const isVisible =
778
+ focused ||
779
+ isNextScreenTransparent ||
780
+ // React native destroys the view when hidden
781
+ // So we keep retained screens visible
782
+ // This makes sure any videos being played don't stop etc.
783
+ isRetained ||
784
+ // We only need to keep other screens visible when animation is enabled
785
+ (isAnimationEnabled &&
786
+ (isFocusing ||
787
+ isRemoving ||
788
+ // Preloaded screens should stay mounted, but remain hidden until focused
789
+ (!isPreloaded && index >= routes.length - 2)));
790
+
791
+ const activityMode = // Render focused and animating screens normally
792
+ focused || isFocusing
793
+ ? 'normal'
794
+ : inactiveBehavior === 'none' ||
795
+ // Unpause preloaded or retained screens so updates are visible
796
+ // Handle retained explicitly as isInactive won't be updated until animation end
797
+ isInactive ||
798
+ isRetained ||
799
+ // Keep the screen before transparent screen active
800
+ // This lets the screen under the transparent screen update and animate
801
+ isNextScreenTransparent ||
802
+ // Keep the screen before last screen active
803
+ // Otherwise it breaks animation when going back
804
+ isBeforeLast
805
+ ? 'inert'
806
+ : inactiveBehavior === 'unmount' &&
807
+ // Don't unmount screens that needs to stay visible
808
+ !isVisible &&
809
+ // Don't unmount screens with nested navigators
810
+ // So we don't lose their state
811
+ !('state' in route && route.state)
812
+ ? 'unmounted'
813
+ : 'paused';
814
+
815
+ if (activityMode === 'unmounted') {
816
+ return null;
817
+ }
772
818
 
773
819
  return (
774
- <Screen
820
+ <ActivityView
775
821
  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}
822
+ mode={activityMode}
823
+ visible={isVisible}
824
+ style={{
825
+ ...StyleSheet.absoluteFill,
826
+ // Avoid unfocused larger pages increasing scroll area
827
+ // e.g. when a smaller screen is pushed over a larger one on web
828
+ overflow: isTopmost ? undefined : 'hidden',
829
+ marginTop:
830
+ headerTransparent || headerMode === 'screen'
831
+ ? 0
832
+ : // Add offset for non-transparent floating header
833
+ isFloatHeaderAbsolute
834
+ ? // If header is absolute, offset the card by the header height
835
+ headerHeight?.value
836
+ : // Otherwise, the in-flow float header already takes the
837
+ // focused header's space, so offset by the difference
838
+ headerHeight && focusedHeaderHeight
839
+ ? headerHeight.value - focusedHeaderHeight.value
840
+ : 0,
841
+ }}
782
842
  >
783
843
  <CardContainer
784
844
  index={index}
785
845
  interpolationIndex={interpolationIndex}
786
846
  modal={isModal}
787
- active={index === routes.length - 1}
847
+ active={isTopmost}
788
848
  focused={focused}
789
849
  opening={openingRouteKeys.includes(route.key)}
790
850
  closing={closingRouteKeys.includes(route.key)}
@@ -803,22 +863,19 @@ export class CardStack extends React.Component<Props, State> {
803
863
  onHeaderHeightChange={this.handleHeaderLayout}
804
864
  getPreviousScene={this.getPreviousScene}
805
865
  getFocusedRoute={this.getFocusedRoute}
806
- hasAbsoluteFloatHeader={
807
- isFloatHeaderAbsolute && !headerTransparent
808
- }
809
866
  renderHeader={renderHeader}
810
867
  onOpenRoute={onOpenRoute}
811
868
  onCloseRoute={onCloseRoute}
812
869
  onTransitionStart={onTransitionStart}
813
870
  onTransitionEnd={onTransitionEnd}
814
- isNextScreenTransparent={isNextScreenTransparent}
815
- detachCurrentScreen={detachCurrentScreen}
816
- preloaded={isPreloaded}
817
- />
818
- </Screen>
871
+ preloaded={isInactive}
872
+ >
873
+ {scene.descriptor.render()}
874
+ </CardContainer>
875
+ </ActivityView>
819
876
  );
820
877
  })}
821
- </ScreenContainer>
878
+ </View>
822
879
  </View>
823
880
  );
824
881
  }