@react-navigation/stack 8.0.0-alpha.3 → 8.0.0-alpha.31

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