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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +32 -16
  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/CardA11yWrapper.js +6 -7
  19. package/lib/module/views/Stack/CardA11yWrapper.js.map +1 -1
  20. package/lib/module/views/Stack/CardContainer.js +18 -19
  21. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  22. package/lib/module/views/Stack/CardStack.js +105 -133
  23. package/lib/module/views/Stack/CardStack.js.map +1 -1
  24. package/lib/module/views/Stack/StackView.js +82 -49
  25. package/lib/module/views/Stack/StackView.js.map +1 -1
  26. package/lib/typescript/src/index.d.ts +1 -1
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/navigators/createStackNavigator.d.ts +8 -14
  29. package/lib/typescript/src/navigators/createStackNavigator.d.ts.map +1 -1
  30. package/lib/typescript/src/types.d.ts +46 -53
  31. package/lib/typescript/src/types.d.ts.map +1 -1
  32. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +1 -1
  33. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
  34. package/lib/typescript/src/utils/useKeyboardManager.d.ts +9 -2
  35. package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
  36. package/lib/typescript/src/views/Header/Header.d.ts +1 -1
  37. package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
  38. package/lib/typescript/src/views/Header/HeaderContainer.d.ts +2 -1
  39. package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
  40. package/lib/typescript/src/views/Header/HeaderSegment.d.ts +2 -2
  41. package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
  42. package/lib/typescript/src/views/Stack/Card.d.ts +3 -3
  43. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  44. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +2 -2
  45. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +1 -1
  46. package/lib/typescript/src/views/Stack/CardContainer.d.ts +2 -2
  47. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  48. package/lib/typescript/src/views/Stack/CardStack.d.ts +1 -1
  49. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  50. package/lib/typescript/src/views/Stack/StackView.d.ts +6 -9
  51. package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
  52. package/package.json +20 -22
  53. package/src/index.tsx +1 -0
  54. package/src/navigators/createStackNavigator.tsx +9 -40
  55. package/src/types.tsx +73 -73
  56. package/src/utils/gestureActivationCriteria.tsx +1 -1
  57. package/src/utils/useCardAnimation.tsx +1 -1
  58. package/src/utils/useGestureHandlerRef.tsx +1 -1
  59. package/src/utils/useKeyboardManager.tsx +67 -33
  60. package/src/views/Header/Header.tsx +2 -2
  61. package/src/views/Header/HeaderContainer.tsx +37 -18
  62. package/src/views/Header/HeaderSegment.tsx +4 -4
  63. package/src/views/Stack/Card.tsx +32 -23
  64. package/src/views/Stack/CardA11yWrapper.tsx +40 -51
  65. package/src/views/Stack/CardContainer.tsx +10 -21
  66. package/src/views/Stack/CardStack.tsx +291 -328
  67. package/src/views/Stack/StackView.tsx +138 -66
@@ -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,7 +46,6 @@ 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
51
  import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
@@ -62,6 +63,7 @@ type Props = {
62
63
  routes: Route<string>[];
63
64
  openingRouteKeys: string[];
64
65
  closingRouteKeys: string[];
66
+ replacingRouteKeys: string[];
65
67
  onOpenRoute: (props: { route: Route<string> }) => void;
66
68
  onCloseRoute: (props: { route: Route<string> }) => void;
67
69
  getPreviousRoute: (props: {
@@ -87,7 +89,6 @@ type State = {
87
89
  scenes: Scene[];
88
90
  gestures: GestureValues;
89
91
  layout: Layout;
90
- activeStates: (0 | 1 | Animated.AnimatedInterpolation<0 | 1>)[];
91
92
  headerHeights: Record<string, number>;
92
93
  };
93
94
 
@@ -107,12 +108,6 @@ const NAMED_TRANSITIONS_PRESETS = {
107
108
  }),
108
109
  } as const satisfies Record<StackAnimationName, TransitionPreset>;
109
110
 
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
111
  const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
117
112
 
118
113
  const getInterpolationIndex = (scenes: Scene[], index: number) => {
@@ -263,6 +258,27 @@ export function getAnimationEnabled(animation: StackAnimationName | undefined) {
263
258
  return getDefaultAnimation(animation) !== 'none';
264
259
  }
265
260
 
261
+ const getAllRoutes = (
262
+ routes: Route<string>[],
263
+ state: StackNavigationState<ParamListBase>
264
+ ) => {
265
+ const routeKeys = new Set(routes.map((route) => route.key));
266
+ const inactiveRoutes = state.routes.slice(state.index + 1);
267
+
268
+ // If a route is moved from active routes to inactive routes,
269
+ // It can still be in the local copy of `routes` until the animation ends
270
+ // So we need to deduplicate the routes to avoid rendering the same route twice
271
+ return [
272
+ ...routes,
273
+ ...inactiveRoutes.filter((route) => !routeKeys.has(route.key)),
274
+ ];
275
+ };
276
+
277
+ const isInactiveRoute = (route: Route<string>, routes: Route<string>[]) =>
278
+ // `routes` contains active routes and routes animating during transitions.
279
+ // Any route added by `getAllRoutes` that's missing from this list is inactive.
280
+ !routes.some((currentRoute) => currentRoute.key === route.key);
281
+
266
282
  export class CardStack extends React.Component<Props, State> {
267
283
  static getDerivedStateFromProps(
268
284
  props: Props,
@@ -275,10 +291,9 @@ export class CardStack extends React.Component<Props, State> {
275
291
  return null;
276
292
  }
277
293
 
278
- const gestures = [
279
- ...props.routes,
280
- ...props.state.preloadedRoutes,
281
- ].reduce<GestureValues>((acc, curr) => {
294
+ const allRoutes = getAllRoutes(props.routes, props.state);
295
+
296
+ const gestures = allRoutes.reduce<GestureValues>((acc, curr) => {
282
297
  const descriptor = props.descriptors[curr.key];
283
298
  const { animation } = descriptor?.options || {};
284
299
 
@@ -287,7 +302,7 @@ export class CardStack extends React.Component<Props, State> {
287
302
  new Animated.Value(
288
303
  (props.openingRouteKeys.includes(curr.key) &&
289
304
  getAnimationEnabled(animation)) ||
290
- props.state.preloadedRoutes.includes(curr)
305
+ isInactiveRoute(curr, props.routes)
291
306
  ? getDistanceFromOptions(
292
307
  state.layout,
293
308
  descriptor?.options,
@@ -299,254 +314,170 @@ export class CardStack extends React.Component<Props, State> {
299
314
  return acc;
300
315
  }, {});
301
316
 
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
- );
317
+ const modalRouteKeys = getModalRouteKeys(allRoutes, props.descriptors);
318
+
319
+ const scenes = allRoutes.map((route, index, self) => {
320
+ // For preloaded or retained screens, we don't care about the previous and the next screen
321
+ const isInactive = isInactiveRoute(route, props.routes);
322
+ const previousRoute = isInactive ? undefined : self[index - 1];
323
+ const nextRoute = isInactive ? undefined : self[index + 1];
324
+
325
+ const oldScene = state.scenes[index];
326
+
327
+ const currentGesture = gestures[route.key];
328
+ const previousGesture = previousRoute
329
+ ? gestures[previousRoute.key]
330
+ : undefined;
331
+ const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
332
+
333
+ const descriptor =
334
+ props.descriptors[route.key] ||
335
+ state.descriptors[route.key] ||
336
+ (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
337
+
338
+ const nextOptions =
339
+ nextRoute &&
340
+ (props.descriptors[nextRoute?.key] || state.descriptors[nextRoute?.key])
341
+ ?.options;
342
+
343
+ const previousOptions =
344
+ previousRoute &&
345
+ (
346
+ props.descriptors[previousRoute?.key] ||
347
+ state.descriptors[previousRoute?.key]
348
+ )?.options;
349
+
350
+ // When a screen is not the last, it should use next screen's transition config
351
+ // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
352
+ // For example combining a slide and a modal transition would look wrong otherwise
353
+ // With this approach, combining different transition styles in the same navigator mostly looks right
354
+ // This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
355
+ // but the majority of the transitions look alright
356
+ const optionsForTransitionConfig =
357
+ index !== self.length - 1 &&
358
+ nextOptions &&
359
+ nextOptions?.presentation !== 'transparentModal'
360
+ ? nextOptions
361
+ : descriptor.options;
362
+
363
+ // Assume modal if there are already modal screens in the stack
364
+ // or current screen is a modal when no presentation is specified
365
+ const isModal = modalRouteKeys.includes(route.key);
366
+
367
+ const animation = getDefaultAnimation(
368
+ optionsForTransitionConfig.animation
369
+ );
361
370
 
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
- },
371
+ const isAnimationEnabled = getAnimationEnabled(animation);
372
+
373
+ const transitionPreset =
374
+ animation !== 'default'
375
+ ? NAMED_TRANSITIONS_PRESETS[animation]
376
+ : optionsForTransitionConfig.presentation === 'transparentModal'
377
+ ? ModalFadeTransition
378
+ : optionsForTransitionConfig.presentation === 'modal' || isModal
379
+ ? ModalTransition
380
+ : DefaultTransition;
381
+
382
+ const {
383
+ gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled,
384
+ gestureDirection = transitionPreset.gestureDirection,
385
+ transitionSpec = transitionPreset.transitionSpec,
386
+ cardStyleInterpolator = isAnimationEnabled
387
+ ? transitionPreset.cardStyleInterpolator
388
+ : forNoAnimationCard,
389
+ headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
390
+ cardOverlayEnabled = (Platform.OS !== 'ios' &&
391
+ optionsForTransitionConfig.presentation !== 'transparentModal') ||
392
+ getIsModalPresentation(cardStyleInterpolator),
393
+ } = optionsForTransitionConfig;
394
+
395
+ const headerMode: StackHeaderMode =
396
+ descriptor.options.headerMode ??
397
+ (!(
398
+ optionsForTransitionConfig.presentation === 'modal' ||
399
+ optionsForTransitionConfig.presentation === 'transparentModal' ||
400
+ nextOptions?.presentation === 'modal' ||
401
+ nextOptions?.presentation === 'transparentModal' ||
402
+ getIsModalPresentation(cardStyleInterpolator)
403
+ ) &&
404
+ Platform.OS === 'ios' &&
405
+ descriptor.options.header === undefined
406
+ ? 'float'
407
+ : 'screen');
408
+
409
+ const isRTL = props.direction === 'rtl';
410
+
411
+ const scene = {
412
+ route,
413
+ descriptor: {
414
+ ...descriptor,
415
+ options: {
416
+ ...descriptor.options,
417
+ animation,
418
+ cardOverlayEnabled,
419
+ cardStyleInterpolator,
420
+ gestureDirection,
421
+ gestureEnabled,
422
+ headerStyleInterpolator,
423
+ transitionSpec,
424
+ headerMode,
417
425
  },
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
426
+ },
427
+ progress: {
428
+ current: getProgressFromGesture(
429
+ currentGesture,
430
+ state.layout,
431
+ descriptor.options,
432
+ isRTL
433
+ ),
434
+ next:
435
+ nextGesture && nextOptions?.presentation !== 'transparentModal'
435
436
  ? getProgressFromGesture(
436
- previousGesture,
437
+ nextGesture,
437
438
  state.layout,
438
- previousOptions,
439
+ nextOptions,
439
440
  isRTL
440
441
  )
441
442
  : 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
- );
443
+ previous: previousGesture
444
+ ? getProgressFromGesture(
445
+ previousGesture,
446
+ state.layout,
447
+ previousOptions,
448
+ isRTL
449
+ )
450
+ : undefined,
451
+ },
452
+ __memo: [
453
+ state.layout,
454
+ descriptor,
455
+ nextOptions,
456
+ previousOptions,
457
+ currentGesture,
458
+ nextGesture,
459
+ previousGesture,
460
+ ],
461
+ };
467
462
 
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
- }
463
+ if (
464
+ oldScene &&
465
+ scene.__memo.every((it, i) => {
466
+ // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
467
+ return oldScene.__memo[i] === it;
468
+ })
469
+ ) {
470
+ return oldScene;
503
471
  }
504
472
 
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
- }
473
+ return scene;
474
+ });
543
475
 
544
476
  return {
545
477
  routes: props.routes,
546
478
  scenes,
547
479
  gestures,
548
480
  descriptors: props.descriptors,
549
- activeStates,
550
481
  headerHeights: getHeaderHeights(
551
482
  scenes,
552
483
  props.insets,
@@ -567,7 +498,6 @@ export class CardStack extends React.Component<Props, State> {
567
498
  gestures: {},
568
499
  layout: SafeAreaProviderCompat.initialMetrics.frame,
569
500
  descriptors: this.props.descriptors,
570
- activeStates: [],
571
501
  // Used when card's header is null and mode is float to make transition
572
502
  // between screens with headers and those without headers smooth.
573
503
  // This is not a great heuristic here. We don't know synchronously
@@ -654,23 +584,22 @@ export class CardStack extends React.Component<Props, State> {
654
584
  routes,
655
585
  openingRouteKeys,
656
586
  closingRouteKeys,
587
+ replacingRouteKeys,
657
588
  onOpenRoute,
658
589
  onCloseRoute,
590
+ onGestureStart,
591
+ onGestureEnd,
592
+ onGestureCancel,
659
593
  renderHeader,
660
594
  isParentHeaderShown,
661
595
  isParentModal,
662
596
  onTransitionStart,
663
597
  onTransitionEnd,
664
- onGestureStart,
665
- onGestureEnd,
666
- onGestureCancel,
667
- detachInactiveScreens = Platform.OS === 'web' ||
668
- Platform.OS === 'android' ||
669
- Platform.OS === 'ios',
670
598
  } = this.props;
671
599
 
672
- const { scenes, layout, gestures, activeStates, headerHeights } =
673
- this.state;
600
+ const { scenes, layout, gestures, headerHeights } = this.state;
601
+
602
+ const allRoutes = getAllRoutes(routes, state);
674
603
 
675
604
  const focusedRoute = state.routes[state.index];
676
605
 
@@ -700,6 +629,7 @@ export class CardStack extends React.Component<Props, State> {
700
629
  scenes,
701
630
  getPreviousScene: this.getPreviousScene,
702
631
  getFocusedRoute: this.getFocusedRoute,
632
+ contentHeight: headerHeights[focusedRoute.key],
703
633
  onContentHeightChange: this.handleHeaderLayout,
704
634
  style: [
705
635
  isFloatHeaderAbsolute && [
@@ -712,34 +642,18 @@ export class CardStack extends React.Component<Props, State> {
712
642
  ],
713
643
  ],
714
644
  })}
715
- <ScreenContainer
716
- enabled={detachInactiveScreens}
717
- style={styles.container}
718
- onLayout={this.handleLayout}
719
- >
720
- {[...routes, ...state.preloadedRoutes].map((route, index) => {
645
+ <View style={styles.container} onLayout={this.handleLayout}>
646
+ {allRoutes.map((route, index, self) => {
721
647
  const focused = focusedRoute.key === route.key;
722
648
  const gesture = gestures[route.key];
723
649
  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
- }
650
+ const isInactive = isInactiveRoute(route, this.props.routes);
737
651
 
738
652
  const {
653
+ inactiveBehavior = 'pause',
654
+ animation,
739
655
  headerShown = true,
740
656
  headerTransparent,
741
- freezeOnBlur,
742
- autoHideHomeIndicator,
743
657
  } = scene.descriptor.options;
744
658
 
745
659
  const safeAreaInsetTop = insets.top;
@@ -758,67 +672,116 @@ export class CardStack extends React.Component<Props, State> {
758
672
  isParentModal
759
673
  );
760
674
 
675
+ const isAnimationEnabled =
676
+ getAnimationEnabled(animation) ||
677
+ // Also check next screen's animation,
678
+ // As it will result in both screens being visible
679
+ (scenes[index + 1]
680
+ ? getAnimationEnabled(
681
+ scenes[index + 1].descriptor.options.animation
682
+ )
683
+ : false);
684
+
761
685
  const isNextScreenTransparent =
762
686
  scenes[index + 1]?.descriptor.options.presentation ===
763
687
  'transparentModal';
764
688
 
765
- const detachCurrentScreen =
766
- scenes[index + 1]?.descriptor.options.detachPreviousScreen !==
767
- false;
768
-
769
- const activityState = isPreloaded
770
- ? STATE_INACTIVE
771
- : activeStates[index];
689
+ const isRemoving =
690
+ replacingRouteKeys.includes(route.key) ||
691
+ closingRouteKeys.includes(route.key);
692
+
693
+ const isFocusing =
694
+ openingRouteKeys.includes(route.key) ||
695
+ [...closingRouteKeys, ...replacingRouteKeys].includes(
696
+ self[index + 1]?.key
697
+ );
698
+
699
+ const isBeforeLast = index === routes.length - 2;
700
+
701
+ // Keep animating and the last two rendered routes visible for smoother transitions
702
+ const isVisible =
703
+ focused ||
704
+ isNextScreenTransparent ||
705
+ // We only need to keep other screens visible when animation is enabled
706
+ (isAnimationEnabled &&
707
+ (isFocusing ||
708
+ isRemoving ||
709
+ // Preloaded and retained screens should stay mounted, but remain hidden until focused
710
+ (!isInactive && index >= routes.length - 2)));
711
+
712
+ const activityMode = // Render focused and animating screens normally
713
+ focused || isFocusing
714
+ ? 'normal'
715
+ : inactiveBehavior === 'none' ||
716
+ // Unpause preloaded or retained screens so updates are visible
717
+ isInactive ||
718
+ // Keep the screen before transparent screen active
719
+ // This lets the screen under the transparent screen update and animate
720
+ isNextScreenTransparent ||
721
+ // Keep the screen before last screen active
722
+ // Otherwise it breaks animation when going back
723
+ isBeforeLast
724
+ ? 'inert'
725
+ : inactiveBehavior === 'unmount' &&
726
+ // Don't unmount screens that needs to stay visible
727
+ !isVisible &&
728
+ // Don't unmount screens with nested navigators
729
+ // So we don't lose their state
730
+ !('state' in route && route.state)
731
+ ? 'unmounted'
732
+ : 'paused';
733
+
734
+ if (activityMode === 'unmounted') {
735
+ return null;
736
+ }
772
737
 
773
738
  return (
774
- <Screen
739
+ <CardContainer
775
740
  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}
741
+ index={index}
742
+ interpolationIndex={interpolationIndex}
743
+ modal={isModal}
744
+ active={index === routes.length - 1}
745
+ focused={focused}
746
+ opening={openingRouteKeys.includes(route.key)}
747
+ closing={closingRouteKeys.includes(route.key)}
748
+ layout={layout}
749
+ gesture={gesture}
750
+ scene={scene}
751
+ safeAreaInsetTop={safeAreaInsetTop}
752
+ safeAreaInsetRight={safeAreaInsetRight}
753
+ safeAreaInsetBottom={safeAreaInsetBottom}
754
+ safeAreaInsetLeft={safeAreaInsetLeft}
755
+ onGestureStart={onGestureStart}
756
+ onGestureCancel={onGestureCancel}
757
+ onGestureEnd={onGestureEnd}
758
+ headerHeight={headerHeight}
759
+ isParentHeaderShown={isParentHeaderShown}
760
+ onHeaderHeightChange={this.handleHeaderLayout}
761
+ getPreviousScene={this.getPreviousScene}
762
+ getFocusedRoute={this.getFocusedRoute}
763
+ hasAbsoluteFloatHeader={
764
+ isFloatHeaderAbsolute && !headerTransparent
765
+ }
766
+ renderHeader={renderHeader}
767
+ onOpenRoute={onOpenRoute}
768
+ onCloseRoute={onCloseRoute}
769
+ onTransitionStart={onTransitionStart}
770
+ onTransitionEnd={onTransitionEnd}
771
+ isNextScreenTransparent={isNextScreenTransparent}
772
+ preloaded={isInactive}
782
773
  >
783
- <CardContainer
784
- index={index}
785
- interpolationIndex={interpolationIndex}
786
- modal={isModal}
787
- active={index === routes.length - 1}
788
- focused={focused}
789
- opening={openingRouteKeys.includes(route.key)}
790
- closing={closingRouteKeys.includes(route.key)}
791
- layout={layout}
792
- gesture={gesture}
793
- scene={scene}
794
- safeAreaInsetTop={safeAreaInsetTop}
795
- safeAreaInsetRight={safeAreaInsetRight}
796
- safeAreaInsetBottom={safeAreaInsetBottom}
797
- safeAreaInsetLeft={safeAreaInsetLeft}
798
- onGestureStart={onGestureStart}
799
- onGestureCancel={onGestureCancel}
800
- onGestureEnd={onGestureEnd}
801
- headerHeight={headerHeight}
802
- isParentHeaderShown={isParentHeaderShown}
803
- onHeaderHeightChange={this.handleHeaderLayout}
804
- getPreviousScene={this.getPreviousScene}
805
- getFocusedRoute={this.getFocusedRoute}
806
- hasAbsoluteFloatHeader={
807
- isFloatHeaderAbsolute && !headerTransparent
808
- }
809
- renderHeader={renderHeader}
810
- onOpenRoute={onOpenRoute}
811
- onCloseRoute={onCloseRoute}
812
- onTransitionStart={onTransitionStart}
813
- onTransitionEnd={onTransitionEnd}
814
- isNextScreenTransparent={isNextScreenTransparent}
815
- detachCurrentScreen={detachCurrentScreen}
816
- preloaded={isPreloaded}
817
- />
818
- </Screen>
774
+ <ActivityView
775
+ mode={activityMode}
776
+ visible={isVisible}
777
+ style={StyleSheet.absoluteFill}
778
+ >
779
+ {scene.descriptor.render()}
780
+ </ActivityView>
781
+ </CardContainer>
819
782
  );
820
783
  })}
821
- </ScreenContainer>
784
+ </View>
822
785
  </View>
823
786
  );
824
787
  }