@react-navigation/stack 8.0.0-alpha.2 → 8.0.0-alpha.20

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 (57) hide show
  1. package/lib/module/utils/useCardAnimation.js +1 -1
  2. package/lib/module/utils/useCardAnimation.js.map +1 -1
  3. package/lib/module/utils/useGestureHandlerRef.js +1 -1
  4. package/lib/module/utils/useGestureHandlerRef.js.map +1 -1
  5. package/lib/module/utils/useKeyboardManager.js +50 -26
  6. package/lib/module/utils/useKeyboardManager.js.map +1 -1
  7. package/lib/module/views/Header/Header.js +2 -2
  8. package/lib/module/views/Header/Header.js.map +1 -1
  9. package/lib/module/views/Header/HeaderContainer.js +8 -4
  10. package/lib/module/views/Header/HeaderContainer.js.map +1 -1
  11. package/lib/module/views/Header/HeaderSegment.js +2 -2
  12. package/lib/module/views/Header/HeaderSegment.js.map +1 -1
  13. package/lib/module/views/Stack/Card.js +25 -20
  14. package/lib/module/views/Stack/Card.js.map +1 -1
  15. package/lib/module/views/Stack/CardA11yWrapper.js +1 -2
  16. package/lib/module/views/Stack/CardA11yWrapper.js.map +1 -1
  17. package/lib/module/views/Stack/CardContainer.js +17 -19
  18. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  19. package/lib/module/views/Stack/CardStack.js +72 -117
  20. package/lib/module/views/Stack/CardStack.js.map +1 -1
  21. package/lib/module/views/Stack/StackView.js +59 -23
  22. package/lib/module/views/Stack/StackView.js.map +1 -1
  23. package/lib/typescript/src/types.d.ts +46 -47
  24. package/lib/typescript/src/types.d.ts.map +1 -1
  25. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +1 -1
  26. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/useKeyboardManager.d.ts +9 -2
  28. package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
  29. package/lib/typescript/src/views/Header/Header.d.ts +1 -1
  30. package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
  31. package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
  32. package/lib/typescript/src/views/Header/HeaderSegment.d.ts +2 -2
  33. package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
  34. package/lib/typescript/src/views/Stack/Card.d.ts +3 -3
  35. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  36. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +0 -1
  37. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +1 -1
  38. package/lib/typescript/src/views/Stack/CardContainer.d.ts +2 -2
  39. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  40. package/lib/typescript/src/views/Stack/CardStack.d.ts +1 -1
  41. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  42. package/lib/typescript/src/views/Stack/StackView.d.ts +6 -6
  43. package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
  44. package/package.json +16 -16
  45. package/src/types.tsx +73 -67
  46. package/src/utils/gestureActivationCriteria.tsx +1 -1
  47. package/src/utils/useCardAnimation.tsx +1 -1
  48. package/src/utils/useGestureHandlerRef.tsx +1 -1
  49. package/src/utils/useKeyboardManager.tsx +67 -33
  50. package/src/views/Header/Header.tsx +2 -2
  51. package/src/views/Header/HeaderContainer.tsx +8 -3
  52. package/src/views/Header/HeaderSegment.tsx +4 -4
  53. package/src/views/Stack/Card.tsx +32 -23
  54. package/src/views/Stack/CardA11yWrapper.tsx +2 -14
  55. package/src/views/Stack/CardContainer.tsx +9 -21
  56. package/src/views/Stack/CardStack.tsx +102 -155
  57. package/src/views/Stack/StackView.tsx +106 -34
@@ -57,7 +57,7 @@ type Props = {
57
57
  }) => void;
58
58
  isParentHeaderShown: boolean;
59
59
  isNextScreenTransparent: boolean;
60
- detachCurrentScreen: boolean;
60
+ children: React.ReactNode;
61
61
  };
62
62
 
63
63
  const EPSILON = 0.1;
@@ -78,7 +78,6 @@ function CardContainerInner({
78
78
  onHeaderHeightChange,
79
79
  isParentHeaderShown,
80
80
  isNextScreenTransparent,
81
- detachCurrentScreen,
82
81
  layout,
83
82
  onCloseRoute,
84
83
  onOpenRoute,
@@ -94,23 +93,19 @@ function CardContainerInner({
94
93
  safeAreaInsetRight,
95
94
  safeAreaInsetTop,
96
95
  scene,
96
+ children,
97
97
  }: Props) {
98
98
  const wrapperRef = React.useRef<CardA11yWrapperRef>(null);
99
99
 
100
100
  const { direction } = useLocale();
101
101
 
102
- const parentHeaderHeight = React.useContext(HeaderHeightContext);
102
+ const parentHeaderHeight = React.use(HeaderHeightContext);
103
+
104
+ const { options } = scene.descriptor;
105
+ const enabled = focused && options.keyboardHandlingEnabled !== false;
103
106
 
104
107
  const { onPageChangeStart, onPageChangeCancel, onPageChangeConfirm } =
105
- useKeyboardManager(
106
- React.useCallback(() => {
107
- const { options, navigation } = scene.descriptor;
108
-
109
- return (
110
- navigation.isFocused() && options.keyboardHandlingEnabled !== false
111
- );
112
- }, [scene.descriptor])
113
- );
108
+ useKeyboardManager({ enabled, focused });
114
109
 
115
110
  const handleOpen = () => {
116
111
  const { route } = scene.descriptor;
@@ -157,13 +152,7 @@ function CardContainerInner({
157
152
 
158
153
  const { route } = scene.descriptor;
159
154
 
160
- if (!gesture) {
161
- onPageChangeConfirm?.(true);
162
- } else if (active && closing) {
163
- onPageChangeConfirm?.(false);
164
- } else {
165
- onPageChangeCancel?.();
166
- }
155
+ onPageChangeConfirm?.({ gesture, active, closing });
167
156
 
168
157
  onTransitionStart?.({ route }, closing);
169
158
  };
@@ -242,7 +231,6 @@ function CardContainerInner({
242
231
  active={active}
243
232
  animated={animated}
244
233
  isNextScreenTransparent={isNextScreenTransparent}
245
- detachCurrentScreen={detachCurrentScreen}
246
234
  >
247
235
  <Card
248
236
  animated={animated}
@@ -311,7 +299,7 @@ function CardContainerInner({
311
299
  : (parentHeaderHeight ?? 0)
312
300
  }
313
301
  >
314
- {scene.descriptor.render()}
302
+ {children}
315
303
  </HeaderHeightContext.Provider>
316
304
  </HeaderShownContext.Provider>
317
305
  </HeaderBackContext.Provider>
@@ -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) => {
@@ -465,88 +460,11 @@ export class CardStack extends React.Component<Props, State> {
465
460
  }
466
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
- }
503
- }
504
-
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
- }
543
-
544
463
  return {
545
464
  routes: props.routes,
546
465
  scenes,
547
466
  gestures,
548
467
  descriptors: props.descriptors,
549
- activeStates,
550
468
  headerHeights: getHeaderHeights(
551
469
  scenes,
552
470
  props.insets,
@@ -567,7 +485,6 @@ export class CardStack extends React.Component<Props, State> {
567
485
  gestures: {},
568
486
  layout: SafeAreaProviderCompat.initialMetrics.frame,
569
487
  descriptors: this.props.descriptors,
570
- activeStates: [],
571
488
  // Used when card's header is null and mode is float to make transition
572
489
  // between screens with headers and those without headers smooth.
573
490
  // This is not a great heuristic here. We don't know synchronously
@@ -654,23 +571,20 @@ export class CardStack extends React.Component<Props, State> {
654
571
  routes,
655
572
  openingRouteKeys,
656
573
  closingRouteKeys,
574
+ replacingRouteKeys,
657
575
  onOpenRoute,
658
576
  onCloseRoute,
577
+ onGestureStart,
578
+ onGestureEnd,
579
+ onGestureCancel,
659
580
  renderHeader,
660
581
  isParentHeaderShown,
661
582
  isParentModal,
662
583
  onTransitionStart,
663
584
  onTransitionEnd,
664
- onGestureStart,
665
- onGestureEnd,
666
- onGestureCancel,
667
- detachInactiveScreens = Platform.OS === 'web' ||
668
- Platform.OS === 'android' ||
669
- Platform.OS === 'ios',
670
585
  } = this.props;
671
586
 
672
- const { scenes, layout, gestures, activeStates, headerHeights } =
673
- this.state;
587
+ const { scenes, layout, gestures, headerHeights } = this.state;
674
588
 
675
589
  const focusedRoute = state.routes[state.index];
676
590
 
@@ -712,12 +626,8 @@ export class CardStack extends React.Component<Props, State> {
712
626
  ],
713
627
  ],
714
628
  })}
715
- <ScreenContainer
716
- enabled={detachInactiveScreens}
717
- style={styles.container}
718
- onLayout={this.handleLayout}
719
- >
720
- {[...routes, ...state.preloadedRoutes].map((route, index) => {
629
+ <View style={styles.container} onLayout={this.handleLayout}>
630
+ {[...routes, ...state.preloadedRoutes].map((route, index, self) => {
721
631
  const focused = focusedRoute.key === route.key;
722
632
  const gesture = gestures[route.key];
723
633
  const scene = scenes[index];
@@ -736,10 +646,9 @@ export class CardStack extends React.Component<Props, State> {
736
646
  }
737
647
 
738
648
  const {
649
+ inactiveBehavior = 'pause',
739
650
  headerShown = true,
740
651
  headerTransparent,
741
- freezeOnBlur,
742
- autoHideHomeIndicator,
743
652
  } = scene.descriptor.options;
744
653
 
745
654
  const safeAreaInsetTop = insets.top;
@@ -762,63 +671,101 @@ export class CardStack extends React.Component<Props, State> {
762
671
  scenes[index + 1]?.descriptor.options.presentation ===
763
672
  'transparentModal';
764
673
 
765
- const detachCurrentScreen =
766
- scenes[index + 1]?.descriptor.options.detachPreviousScreen !==
767
- false;
768
-
769
- const activityState = isPreloaded
770
- ? STATE_INACTIVE
771
- : activeStates[index];
674
+ const isRemoving =
675
+ replacingRouteKeys.includes(route.key) ||
676
+ closingRouteKeys.includes(route.key);
677
+
678
+ const isFocusing =
679
+ openingRouteKeys.includes(route.key) ||
680
+ [...closingRouteKeys, ...replacingRouteKeys].includes(
681
+ self[index + 1]?.key
682
+ );
683
+
684
+ const isBeforeLast = index === routes.length - 2;
685
+
686
+ // Keep animating, preloaded & last two screens visible for smoother transitions
687
+ const isVisible =
688
+ focused ||
689
+ isFocusing ||
690
+ isRemoving ||
691
+ isPreloaded ||
692
+ isNextScreenTransparent ||
693
+ index >= routes.length - 2;
694
+
695
+ const activityMode = // Render focused and animating screens normally
696
+ focused || isFocusing
697
+ ? 'normal'
698
+ : inactiveBehavior === 'none' ||
699
+ // Unpause preloaded screens so updates are visible
700
+ // This lets preloaded screens initialize
701
+ isPreloaded ||
702
+ // Keep the screen before transparent screen active
703
+ // This lets the screen under the transparent screen update and animate
704
+ isNextScreenTransparent ||
705
+ // Keep the screen before last screen active
706
+ // Otherwise it breaks animation when going back
707
+ isBeforeLast
708
+ ? 'inert'
709
+ : inactiveBehavior === 'unmount' &&
710
+ // Don't unmount screens that needs to stay visible
711
+ !isVisible &&
712
+ // Don't unmount screens with nested navigators
713
+ // So we don't lose their state
714
+ !('state' in route && route.state)
715
+ ? 'unmounted'
716
+ : 'paused';
717
+
718
+ if (activityMode === 'unmounted') {
719
+ return null;
720
+ }
772
721
 
773
722
  return (
774
- <Screen
723
+ <CardContainer
775
724
  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}
725
+ index={index}
726
+ interpolationIndex={interpolationIndex}
727
+ modal={isModal}
728
+ active={index === routes.length - 1}
729
+ focused={focused}
730
+ opening={openingRouteKeys.includes(route.key)}
731
+ closing={closingRouteKeys.includes(route.key)}
732
+ layout={layout}
733
+ gesture={gesture}
734
+ scene={scene}
735
+ safeAreaInsetTop={safeAreaInsetTop}
736
+ safeAreaInsetRight={safeAreaInsetRight}
737
+ safeAreaInsetBottom={safeAreaInsetBottom}
738
+ safeAreaInsetLeft={safeAreaInsetLeft}
739
+ onGestureStart={onGestureStart}
740
+ onGestureCancel={onGestureCancel}
741
+ onGestureEnd={onGestureEnd}
742
+ headerHeight={headerHeight}
743
+ isParentHeaderShown={isParentHeaderShown}
744
+ onHeaderHeightChange={this.handleHeaderLayout}
745
+ getPreviousScene={this.getPreviousScene}
746
+ getFocusedRoute={this.getFocusedRoute}
747
+ hasAbsoluteFloatHeader={
748
+ isFloatHeaderAbsolute && !headerTransparent
749
+ }
750
+ renderHeader={renderHeader}
751
+ onOpenRoute={onOpenRoute}
752
+ onCloseRoute={onCloseRoute}
753
+ onTransitionStart={onTransitionStart}
754
+ onTransitionEnd={onTransitionEnd}
755
+ isNextScreenTransparent={isNextScreenTransparent}
756
+ preloaded={isPreloaded}
782
757
  >
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>
758
+ <ActivityView
759
+ mode={activityMode}
760
+ visible={isVisible}
761
+ style={StyleSheet.absoluteFill}
762
+ >
763
+ {scene.descriptor.render()}
764
+ </ActivityView>
765
+ </CardContainer>
819
766
  );
820
767
  })}
821
- </ScreenContainer>
768
+ </View>
822
769
  </View>
823
770
  );
824
771
  }
@@ -35,8 +35,8 @@ type Props = StackNavigationConfig & {
35
35
  type State = {
36
36
  // Local copy of the routes which are actually rendered
37
37
  routes: Route<string>[];
38
- // Previous routes, to compare whether routes have changed or not
39
- previousRoutes: Route<string>[];
38
+ // Previous navigation state for comparison
39
+ previousState: StackNavigationState<ParamListBase> | undefined;
40
40
  // Previous descriptors, to compare whether descriptors have changed or not
41
41
  previousDescriptors: StackDescriptorMap;
42
42
  // List of routes being opened, we need to animate pushing of these new routes
@@ -65,22 +65,52 @@ export class StackView extends React.Component<Props, State> {
65
65
  state: Readonly<State>
66
66
  ) {
67
67
  const allRoutes = [...props.state.routes, ...props.state.preloadedRoutes];
68
+ const previousRoutes = state.previousState
69
+ ? [...state.previousState.routes, ...state.previousState.preloadedRoutes]
70
+ : [];
68
71
 
69
72
  // If there was no change in routes, we don't need to compute anything
70
73
  if (
71
74
  isArrayEqual(
72
75
  allRoutes.map((r) => r.key),
73
- state.previousRoutes.map((r) => r.key)
76
+ previousRoutes.map((r) => r.key)
74
77
  ) &&
75
78
  state.routes.length
76
79
  ) {
77
- let routes = props.state.routes;
78
- let previousRoutes = state.previousRoutes;
80
+ // If there were any routes being closed or replaced,
81
+ // We need to make sure they are preserved in the new state from props.state
82
+ // So first we get all such routes from the previous state (that included the animating routes)
83
+ // Then we add them back to the new state if they don't already exist
84
+
85
+ const closingRoutes = state.routes.filter(
86
+ (r) =>
87
+ state.closingRouteKeys.includes(r.key) &&
88
+ !props.state.routes.some((pr) => pr.key === r.key)
89
+ );
90
+
91
+ const replacingRoutes = state.routes.filter(
92
+ (r) =>
93
+ state.replacingRouteKeys.includes(r.key) &&
94
+ !props.state.routes.some((pr) => pr.key === r.key)
95
+ );
96
+
97
+ let routes: Route<string>[] = props.state.routes.slice();
98
+
99
+ // Replacing routes go before the focused route (they're being covered)
100
+ if (replacingRoutes.length) {
101
+ routes.splice(routes.length - 1, 0, ...replacingRoutes);
102
+ }
103
+
104
+ // Closing routes go at the end (they're animating out on top)
105
+ if (closingRoutes.length) {
106
+ routes.push(...closingRoutes);
107
+ }
108
+
79
109
  let descriptors = props.descriptors;
80
110
  let previousDescriptors = state.previousDescriptors;
81
111
 
82
112
  if (props.descriptors !== state.previousDescriptors) {
83
- descriptors = state.routes.reduce<StackDescriptorMap>((acc, route) => {
113
+ descriptors = routes.reduce<StackDescriptorMap>((acc, route) => {
84
114
  acc[route.key] =
85
115
  props.descriptors[route.key] || state.descriptors[route.key];
86
116
 
@@ -90,7 +120,7 @@ export class StackView extends React.Component<Props, State> {
90
120
  previousDescriptors = props.descriptors;
91
121
  }
92
122
 
93
- if (!isArrayEqual(allRoutes, state.previousRoutes)) {
123
+ if (!isArrayEqual(allRoutes, previousRoutes)) {
94
124
  // if any route objects have changed, we should update them
95
125
  const map = allRoutes.reduce<Record<string, Route<string>>>(
96
126
  (acc, route) => {
@@ -100,13 +130,12 @@ export class StackView extends React.Component<Props, State> {
100
130
  {}
101
131
  );
102
132
 
103
- routes = props.state.routes.map((route) => map[route.key] || route);
104
- previousRoutes = allRoutes;
133
+ routes = routes.map((route) => map[route.key] || route);
105
134
  }
106
135
 
107
136
  return {
108
137
  routes,
109
- previousRoutes,
138
+ previousState: props.state,
110
139
  descriptors,
111
140
  previousDescriptors,
112
141
  };
@@ -122,14 +151,24 @@ export class StackView extends React.Component<Props, State> {
122
151
  props.state.routes.slice(0, props.state.index + 1)
123
152
  : props.state.routes;
124
153
 
125
- // Now we need to determine which routes were added and removed
126
- const { previousRoutes } = state;
127
-
128
154
  let { openingRouteKeys, closingRouteKeys, replacingRouteKeys } = state;
129
155
 
130
- const previousFocusedRoute = previousRoutes[previousRoutes.length - 1] as
131
- | Route<string>
132
- | undefined;
156
+ // If a route that was closing or being replaced is now back in the routes,
157
+ // it was added back before the animation finished, so stop tracking it
158
+ closingRouteKeys = closingRouteKeys.filter(
159
+ (key) => !routes.some((r) => r.key === key)
160
+ );
161
+
162
+ replacingRouteKeys = replacingRouteKeys.filter(
163
+ (key) => !routes.some((r) => r.key === key)
164
+ );
165
+
166
+ // Get previous focused route from previousState (actual focused route, not last in previousRoutes
167
+ // which can be a preloaded route that was never focused)
168
+ const previousFocusedRoute = state.previousState
169
+ ? state.previousState.routes[state.previousState.index]
170
+ : undefined;
171
+
133
172
  const nextFocusedRoute = routes[routes.length - 1];
134
173
 
135
174
  const isAnimationEnabled = (key: string) => {
@@ -233,6 +272,18 @@ export class StackView extends React.Component<Props, State> {
233
272
  // After the push animation is completed, routes being replaced will be removed completely
234
273
  routes = routes.slice();
235
274
  routes.splice(routes.length - 1, 0, previousFocusedRoute);
275
+
276
+ // Preserve any other routes still being replaced from previous transitions
277
+ const previousReplacingRoutes = state.routes.filter(
278
+ (r) =>
279
+ replacingRouteKeys.includes(r.key) &&
280
+ !routes.some((existing) => existing.key === r.key)
281
+ );
282
+
283
+ if (previousReplacingRoutes.length) {
284
+ // Insert before the route we just added (previousFocusedRoute)
285
+ routes.splice(routes.length - 2, 0, ...previousReplacingRoutes);
286
+ }
236
287
  }
237
288
  }
238
289
  }
@@ -266,7 +317,7 @@ export class StackView extends React.Component<Props, State> {
266
317
 
267
318
  return {
268
319
  routes,
269
- previousRoutes: [...props.state.routes, ...props.state.preloadedRoutes],
320
+ previousState: props.state,
270
321
  previousDescriptors: props.descriptors,
271
322
  openingRouteKeys,
272
323
  closingRouteKeys,
@@ -277,7 +328,7 @@ export class StackView extends React.Component<Props, State> {
277
328
 
278
329
  state: State = {
279
330
  routes: [],
280
- previousRoutes: [],
331
+ previousState: undefined,
281
332
  previousDescriptors: {},
282
333
  openingRouteKeys: [],
283
334
  closingRouteKeys: [],
@@ -328,20 +379,35 @@ export class StackView extends React.Component<Props, State> {
328
379
  });
329
380
  });
330
381
  } else {
331
- this.setState((state) => ({
332
- routes: state.replacingRouteKeys.length
333
- ? state.routes.filter(
334
- (r) => !state.replacingRouteKeys.includes(r.key)
335
- )
336
- : state.routes,
337
- openingRouteKeys: state.openingRouteKeys.filter(
338
- (key) => key !== route.key
339
- ),
340
- closingRouteKeys: state.closingRouteKeys.filter(
341
- (key) => key !== route.key
342
- ),
343
- replacingRouteKeys: [],
344
- }));
382
+ this.setState((state) => {
383
+ const routeIndex = state.routes.findIndex((r) => r.key === route.key);
384
+
385
+ // Remove replacing routes that were before the route that just opened
386
+ // Those were replaced by this or earlier routes and should be cleaned up
387
+ const replacingRoutesToRemove = new Set(
388
+ state.routes
389
+ .slice(0, routeIndex)
390
+ .filter((r) => state.replacingRouteKeys.includes(r.key))
391
+ .map((r) => r.key)
392
+ );
393
+
394
+ const newRoutes = state.routes.filter(
395
+ (r) => !replacingRoutesToRemove.has(r.key)
396
+ );
397
+
398
+ return {
399
+ routes: newRoutes,
400
+ openingRouteKeys: state.openingRouteKeys.filter(
401
+ (key) => key !== route.key
402
+ ),
403
+ closingRouteKeys: state.closingRouteKeys.filter(
404
+ (key) => key !== route.key
405
+ ),
406
+ replacingRouteKeys: state.replacingRouteKeys.filter(
407
+ (key) => !replacingRoutesToRemove.has(key)
408
+ ),
409
+ };
410
+ });
345
411
  }
346
412
  };
347
413
 
@@ -420,8 +486,13 @@ export class StackView extends React.Component<Props, State> {
420
486
  ...rest
421
487
  } = this.props;
422
488
 
423
- const { routes, descriptors, openingRouteKeys, closingRouteKeys } =
424
- this.state;
489
+ const {
490
+ routes,
491
+ descriptors,
492
+ openingRouteKeys,
493
+ closingRouteKeys,
494
+ replacingRouteKeys,
495
+ } = this.state;
425
496
 
426
497
  return (
427
498
  <GestureHandlerWrapper style={styles.container}>
@@ -440,6 +511,7 @@ export class StackView extends React.Component<Props, State> {
440
511
  routes={routes}
441
512
  openingRouteKeys={openingRouteKeys}
442
513
  closingRouteKeys={closingRouteKeys}
514
+ replacingRouteKeys={replacingRouteKeys}
443
515
  onOpenRoute={this.handleOpenRoute}
444
516
  onCloseRoute={this.handleCloseRoute}
445
517
  onTransitionStart={this.handleTransitionStart}