react-native-screen-transitions 3.0.0-rc.3 → 3.0.0-rc.4

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 (120) hide show
  1. package/lib/commonjs/blank-stack/components/screens.js +16 -11
  2. package/lib/commonjs/blank-stack/components/screens.js.map +1 -1
  3. package/lib/commonjs/blank-stack/components/stack-view.js +42 -36
  4. package/lib/commonjs/blank-stack/components/stack-view.js.map +1 -1
  5. package/lib/commonjs/native-stack/views/NativeStackView.native.js +110 -103
  6. package/lib/commonjs/native-stack/views/NativeStackView.native.js.map +1 -1
  7. package/lib/commonjs/shared/components/controllers/blank-stack-lifecycle.js +72 -0
  8. package/lib/commonjs/shared/components/controllers/blank-stack-lifecycle.js.map +1 -0
  9. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js +79 -0
  10. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js.map +1 -0
  11. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +11 -1
  12. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  13. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +11 -6
  14. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  15. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +7 -7
  16. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  17. package/lib/commonjs/shared/providers/gestures.provider.js +32 -5
  18. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  19. package/lib/commonjs/shared/providers/register-bounds.provider.js +4 -3
  20. package/lib/commonjs/shared/providers/register-bounds.provider.js.map +1 -1
  21. package/lib/commonjs/shared/providers/routes.provider.js +48 -0
  22. package/lib/commonjs/shared/providers/routes.provider.js.map +1 -0
  23. package/lib/commonjs/shared/providers/screen-transition.provider.js.map +1 -1
  24. package/lib/commonjs/shared/types/state.types.js +9 -0
  25. package/lib/commonjs/shared/types/state.types.js.map +1 -0
  26. package/lib/commonjs/shared/utils/animation/compute-stack-progress.js +20 -0
  27. package/lib/commonjs/shared/utils/animation/compute-stack-progress.js.map +1 -0
  28. package/lib/commonjs/shared/utils/animation/derivations.js +1 -1
  29. package/lib/commonjs/shared/utils/animation/start-screen-transition.js +11 -11
  30. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +1 -1
  31. package/lib/module/blank-stack/components/screens.js +16 -11
  32. package/lib/module/blank-stack/components/screens.js.map +1 -1
  33. package/lib/module/blank-stack/components/stack-view.js +42 -36
  34. package/lib/module/blank-stack/components/stack-view.js.map +1 -1
  35. package/lib/module/native-stack/views/NativeStackView.native.js +109 -102
  36. package/lib/module/native-stack/views/NativeStackView.native.js.map +1 -1
  37. package/lib/module/shared/components/controllers/blank-stack-lifecycle.js +66 -0
  38. package/lib/module/shared/components/controllers/blank-stack-lifecycle.js.map +1 -0
  39. package/lib/module/shared/components/controllers/native-stack-lifecycle.js +73 -0
  40. package/lib/module/shared/components/controllers/native-stack-lifecycle.js.map +1 -0
  41. package/lib/module/shared/hooks/animation/use-screen-animation.js +11 -1
  42. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  43. package/lib/module/shared/hooks/gestures/use-build-gestures.js +11 -6
  44. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  45. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +7 -7
  46. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  47. package/lib/module/shared/providers/gestures.provider.js +31 -4
  48. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  49. package/lib/module/shared/providers/register-bounds.provider.js +4 -3
  50. package/lib/module/shared/providers/register-bounds.provider.js.map +1 -1
  51. package/lib/module/shared/providers/routes.provider.js +42 -0
  52. package/lib/module/shared/providers/routes.provider.js.map +1 -0
  53. package/lib/module/shared/providers/screen-transition.provider.js.map +1 -1
  54. package/lib/module/shared/types/state.types.js +5 -0
  55. package/lib/module/shared/types/state.types.js.map +1 -0
  56. package/lib/module/shared/utils/animation/compute-stack-progress.js +15 -0
  57. package/lib/module/shared/utils/animation/compute-stack-progress.js.map +1 -0
  58. package/lib/module/shared/utils/animation/derivations.js +1 -1
  59. package/lib/module/shared/utils/animation/start-screen-transition.js +11 -11
  60. package/lib/module/shared/utils/animation/start-screen-transition.js.map +1 -1
  61. package/lib/typescript/blank-stack/components/screens.d.ts.map +1 -1
  62. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -1
  63. package/lib/typescript/blank-stack/types.d.ts +1 -39
  64. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  65. package/lib/typescript/native-stack/views/NativeStackView.native.d.ts.map +1 -1
  66. package/lib/typescript/shared/components/controllers/blank-stack-lifecycle.d.ts +8 -0
  67. package/lib/typescript/shared/components/controllers/blank-stack-lifecycle.d.ts.map +1 -0
  68. package/lib/typescript/shared/components/controllers/native-stack-lifecycle.d.ts +8 -0
  69. package/lib/typescript/shared/components/controllers/native-stack-lifecycle.d.ts.map +1 -0
  70. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  71. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +2 -2
  72. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  73. package/lib/typescript/shared/providers/gestures.provider.d.ts +8 -2
  74. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  75. package/lib/typescript/shared/providers/register-bounds.provider.d.ts.map +1 -1
  76. package/lib/typescript/shared/providers/routes.provider.d.ts +19 -0
  77. package/lib/typescript/shared/providers/routes.provider.d.ts.map +1 -0
  78. package/lib/typescript/shared/providers/screen-transition.provider.d.ts +2 -2
  79. package/lib/typescript/shared/providers/screen-transition.provider.d.ts.map +1 -1
  80. package/lib/typescript/shared/types/animation.types.d.ts +12 -0
  81. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  82. package/lib/typescript/shared/types/state.types.d.ts +3 -0
  83. package/lib/typescript/shared/types/state.types.d.ts.map +1 -0
  84. package/lib/typescript/shared/utils/animation/compute-stack-progress.d.ts +3 -0
  85. package/lib/typescript/shared/utils/animation/compute-stack-progress.d.ts.map +1 -0
  86. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +1 -1
  87. package/package.json +1 -1
  88. package/src/blank-stack/components/screens.tsx +21 -15
  89. package/src/blank-stack/components/stack-view.tsx +55 -44
  90. package/src/blank-stack/types.ts +1 -24
  91. package/src/native-stack/views/NativeStackView.native.tsx +121 -112
  92. package/src/shared/__tests__/bounds.store.test.ts +14 -36
  93. package/src/shared/components/controllers/blank-stack-lifecycle.tsx +70 -0
  94. package/src/shared/components/controllers/native-stack-lifecycle.tsx +87 -0
  95. package/src/shared/hooks/animation/use-screen-animation.tsx +11 -6
  96. package/src/shared/hooks/gestures/use-build-gestures.tsx +12 -6
  97. package/src/shared/hooks/gestures/use-scroll-registry.tsx +7 -7
  98. package/src/shared/providers/gestures.provider.tsx +34 -5
  99. package/src/shared/providers/register-bounds.provider.tsx +4 -3
  100. package/src/shared/providers/routes.provider.tsx +54 -0
  101. package/src/shared/providers/screen-transition.provider.tsx +2 -2
  102. package/src/shared/types/animation.types.ts +13 -0
  103. package/src/shared/types/state.types.ts +2 -0
  104. package/src/shared/utils/animation/compute-stack-progress.ts +16 -0
  105. package/src/shared/utils/animation/derivations.ts +1 -1
  106. package/src/shared/utils/animation/start-screen-transition.ts +13 -10
  107. package/lib/commonjs/shared/components/controllers/screen-lifecycle.js +0 -142
  108. package/lib/commonjs/shared/components/controllers/screen-lifecycle.js.map +0 -1
  109. package/lib/commonjs/shared/hooks/gestures/use-parent-gesture-registry.js +0 -28
  110. package/lib/commonjs/shared/hooks/gestures/use-parent-gesture-registry.js.map +0 -1
  111. package/lib/module/shared/components/controllers/screen-lifecycle.js +0 -136
  112. package/lib/module/shared/components/controllers/screen-lifecycle.js.map +0 -1
  113. package/lib/module/shared/hooks/gestures/use-parent-gesture-registry.js +0 -23
  114. package/lib/module/shared/hooks/gestures/use-parent-gesture-registry.js.map +0 -1
  115. package/lib/typescript/shared/components/controllers/screen-lifecycle.d.ts +0 -12
  116. package/lib/typescript/shared/components/controllers/screen-lifecycle.d.ts.map +0 -1
  117. package/lib/typescript/shared/hooks/gestures/use-parent-gesture-registry.d.ts +0 -6
  118. package/lib/typescript/shared/hooks/gestures/use-parent-gesture-registry.d.ts.map +0 -1
  119. package/src/shared/components/controllers/screen-lifecycle.tsx +0 -154
  120. package/src/shared/hooks/gestures/use-parent-gesture-registry.tsx +0 -18
@@ -7,8 +7,9 @@ import * as React from "react";
7
7
  import { StyleSheet } from "react-native";
8
8
  import { GestureHandlerRootView } from "react-native-gesture-handler";
9
9
  import { ScreenContainer } from "react-native-screens";
10
- import { BlankStackScreenLifecycleController } from "../../shared/components/controllers/screen-lifecycle";
10
+ import { BlankStackScreenLifecycleController } from "../../shared/components/controllers/blank-stack-lifecycle";
11
11
  import { FlagsProvider } from "../../shared/providers/flags.provider";
12
+ import { RoutesProvider } from "../../shared/providers/routes.provider";
12
13
  import { ScreenTransitionProvider } from "../../shared/providers/screen-transition.provider";
13
14
  import type { BlankStackDescriptor } from "../types";
14
15
  import { withStackNavigationProvider } from "../utils/with-stack-navigation";
@@ -45,55 +46,65 @@ export const StackView = withStackNavigationProvider(
45
46
  scenes,
46
47
  shouldShowFloatOverlay,
47
48
  }) => {
49
+ // Memoize route keys array for ScenesProvider
50
+ const routeKeys = React.useMemo(
51
+ () => routes.map((route) => route.key),
52
+ [routes],
53
+ );
54
+
48
55
  return (
49
56
  <FlagsProvider TRANSITIONS_ALWAYS_ON>
50
- <GestureHandlerRootView>
51
- <SafeAreaProviderCompat>
52
- {shouldShowFloatOverlay ? <Overlay.Float /> : null}
53
- <ScreenContainer style={styles.container}>
54
- {scenes.map((scene, sceneIndex) => {
55
- const descriptor = scene.descriptor;
56
- const route = scene.route;
57
- const isFocused = focusedIndex === sceneIndex;
58
- const isBelowFocused = focusedIndex - 1 === sceneIndex;
57
+ <RoutesProvider routeKeys={routeKeys}>
58
+ <GestureHandlerRootView>
59
+ <SafeAreaProviderCompat>
60
+ {shouldShowFloatOverlay ? <Overlay.Float /> : null}
61
+ <ScreenContainer style={styles.container}>
62
+ {scenes.map((scene, sceneIndex) => {
63
+ const descriptor = scene.descriptor;
64
+ const route = scene.route;
65
+ const isFocused = focusedIndex === sceneIndex;
66
+ const isBelowFocused = focusedIndex - 1 === sceneIndex;
59
67
 
60
- const previousDescriptor =
61
- scenes[sceneIndex - 1]?.descriptor ?? undefined;
62
- const nextDescriptor =
63
- scenes[sceneIndex + 1]?.descriptor ?? undefined;
68
+ const previousDescriptor =
69
+ scenes[sceneIndex - 1]?.descriptor ?? undefined;
70
+ const nextDescriptor =
71
+ scenes[sceneIndex + 1]?.descriptor ?? undefined;
64
72
 
65
- const isPreloaded = descriptors[route.key] === undefined;
73
+ const isPreloaded = descriptors[route.key] === undefined;
66
74
 
67
- // On Fabric, when screen is frozen, animated and reanimated values are not updated
68
- // due to component being unmounted. To avoid this, we don't freeze the previous screen there
69
- const shouldFreeze = isFabric()
70
- ? !isPreloaded && !isFocused && !isBelowFocused
71
- : !isPreloaded && !isFocused;
72
- return (
73
- <Screen
74
- key={route.key}
75
- isPreloaded={isPreloaded}
76
- index={sceneIndex}
77
- activeScreensLimit={activeScreensLimit}
78
- routeKey={route.key}
79
- routesLength={routes.length}
80
- shouldFreeze={shouldFreeze}
81
- freezeOnBlur={descriptor.options.freezeOnBlur}
82
- >
83
- <ScreenTransitionProvider
84
- previous={previousDescriptor}
85
- current={descriptor}
86
- next={nextDescriptor}
87
- LifecycleController={BlankStackScreenLifecycleController}
75
+ // On Fabric, when screen is frozen, animated and reanimated values are not updated
76
+ // due to component being unmounted. To avoid this, we don't freeze the previous screen there
77
+ const shouldFreeze = isFabric()
78
+ ? !isPreloaded && !isFocused && !isBelowFocused
79
+ : !isPreloaded && !isFocused;
80
+ return (
81
+ <Screen
82
+ key={route.key}
83
+ isPreloaded={isPreloaded}
84
+ index={sceneIndex}
85
+ activeScreensLimit={activeScreensLimit}
86
+ routeKey={route.key}
87
+ routesLength={routes.length}
88
+ shouldFreeze={shouldFreeze}
89
+ freezeOnBlur={descriptor.options.freezeOnBlur}
88
90
  >
89
- <SceneView key={route.key} descriptor={descriptor} />
90
- </ScreenTransitionProvider>
91
- </Screen>
92
- );
93
- })}
94
- </ScreenContainer>
95
- </SafeAreaProviderCompat>
96
- </GestureHandlerRootView>
91
+ <ScreenTransitionProvider
92
+ previous={previousDescriptor}
93
+ current={descriptor}
94
+ next={nextDescriptor}
95
+ LifecycleController={
96
+ BlankStackScreenLifecycleController
97
+ }
98
+ >
99
+ <SceneView key={route.key} descriptor={descriptor} />
100
+ </ScreenTransitionProvider>
101
+ </Screen>
102
+ );
103
+ })}
104
+ </ScreenContainer>
105
+ </SafeAreaProviderCompat>
106
+ </GestureHandlerRootView>
107
+ </RoutesProvider>
97
108
  </FlagsProvider>
98
109
  );
99
110
  },
@@ -18,29 +18,7 @@ import type {
18
18
  ScreenInterpolationProps,
19
19
  } from "../shared/types/animation.types";
20
20
 
21
- export type BlankStackNavigationEventMap = {
22
- /**
23
- * Event which fires when a transition animation starts.
24
- */
25
- transitionStart: { data: { closing: boolean } };
26
- /**
27
- * Event which fires when a transition animation ends.
28
- */
29
- transitionEnd: { data: { closing: boolean } };
30
- /**
31
- * Event which fires when a swipe back is canceled on iOS.
32
- */
33
- gestureCancel: { data: undefined };
34
- /**
35
- * Event which fires when screen is in sheet presentation & it's detent changes.
36
- *
37
- * In payload it caries two fields:
38
- *
39
- * * `index` - current detent index in the `sheetAllowedDetents` array,
40
- * * `stable` - on Android `false` value means that the user is dragging the sheet or it is settling; on iOS it is always `true`.
41
- */
42
- sheetDetentChange: { data: { index: number; stable: boolean } };
43
- };
21
+ export type BlankStackNavigationEventMap = {};
44
22
 
45
23
  export type BlankStackNavigationProp<
46
24
  ParamList extends ParamListBase,
@@ -132,7 +110,6 @@ export type BlankStackScreenTransitionConfig = ScreenTransitionConfig & {
132
110
  * Whether to detach the previous screen from the view hierarchy to save memory.
133
111
  * Set it to `false` if you need the previous screen to be seen through the active screen.
134
112
  * Only applicable if `detachInactiveScreens` isn't set to `false`.
135
- * Defaults to `false` for the last screen for modals, otherwise `true`.
136
113
  */
137
114
  detachPreviousScreen?: boolean;
138
115
  };
@@ -33,7 +33,8 @@ import {
33
33
  ScreenStack,
34
34
  ScreenStackItem,
35
35
  } from "react-native-screens";
36
- import { NativeStackScreenLifecycleController } from "../../shared/components/controllers/screen-lifecycle";
36
+ import { NativeStackScreenLifecycleController } from "../../shared/components/controllers/native-stack-lifecycle";
37
+ import { RoutesProvider } from "../../shared/providers/routes.provider";
37
38
  import { ScreenTransitionProvider } from "../../shared/providers/screen-transition.provider";
38
39
  import type {
39
40
  NativeStackDescriptor,
@@ -515,118 +516,126 @@ export function NativeStackView({
515
516
 
516
517
  const routes = state.routes.concat(state.preloadedRoutes);
517
518
 
519
+ // Memoize route keys array for RoutesProvider
520
+ const routeKeys = React.useMemo(
521
+ () => routes.map((route) => route.key),
522
+ [routes],
523
+ );
524
+
518
525
  return (
519
- <GestureHandlerRootView>
520
- <SafeAreaProviderCompat>
521
- <ScreenStack style={styles.container}>
522
- {routes.map((route, index) => {
523
- const descriptor =
524
- descriptors[route.key] ?? preloadedDescriptors[route.key];
525
- const isFocused = state.index === index;
526
- const isBelowFocused = state.index - 1 === index;
527
- const previousKey = state.routes[index - 1]?.key;
528
- const nextKey = state.routes[index + 1]?.key;
529
- const previousDescriptor = previousKey
530
- ? descriptors[previousKey]
531
- : undefined;
532
- const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
533
-
534
- const isModal = modalRouteKeys.includes(route.key);
535
-
536
- const isPreloaded =
537
- preloadedDescriptors[route.key] !== undefined &&
538
- descriptors[route.key] === undefined;
539
-
540
- // On Fabric, when screen is frozen, animated and reanimated values are not updated
541
- // due to component being unmounted. To avoid this, we don't freeze the previous screen there
542
- const shouldFreeze = isFabric()
543
- ? !isPreloaded && !isFocused && !isBelowFocused
544
- : !isPreloaded && !isFocused;
545
-
546
- return (
547
- <SceneView
548
- key={route.key}
549
- index={index}
550
- focused={isFocused}
551
- shouldFreeze={shouldFreeze}
552
- descriptor={descriptor}
553
- previousDescriptor={previousDescriptor}
554
- nextDescriptor={nextDescriptor}
555
- isPresentationModal={isModal}
556
- isPreloaded={isPreloaded}
557
- onWillDisappear={() => {
558
- navigation.emit({
559
- type: "transitionStart",
560
- data: { closing: true },
561
- target: route.key,
562
- });
563
- }}
564
- onWillAppear={() => {
565
- navigation.emit({
566
- type: "transitionStart",
567
- data: { closing: false },
568
- target: route.key,
569
- });
570
- }}
571
- onAppear={() => {
572
- navigation.emit({
573
- type: "transitionEnd",
574
- data: { closing: false },
575
- target: route.key,
576
- });
577
- }}
578
- onDisappear={() => {
579
- navigation.emit({
580
- type: "transitionEnd",
581
- data: { closing: true },
582
- target: route.key,
583
- });
584
- }}
585
- onDismissed={(event) => {
586
- navigation.dispatch({
587
- ...StackActions.pop(event.nativeEvent.dismissCount),
588
- source: route.key,
589
- target: state.key,
590
- });
591
-
592
- setNextDismissedKey(route.key);
593
- }}
594
- onHeaderBackButtonClicked={() => {
595
- navigation.dispatch({
596
- ...StackActions.pop(),
597
- source: route.key,
598
- target: state.key,
599
- });
600
- }}
601
- onNativeDismissCancelled={(event) => {
602
- navigation.dispatch({
603
- ...StackActions.pop(event.nativeEvent.dismissCount),
604
- source: route.key,
605
- target: state.key,
606
- });
607
- }}
608
- onGestureCancel={() => {
609
- navigation.emit({
610
- type: "gestureCancel",
611
- target: route.key,
612
- });
613
- }}
614
- onSheetDetentChanged={(event) => {
615
- navigation.emit({
616
- type: "sheetDetentChange",
617
- target: route.key,
618
- data: {
619
- index: event.nativeEvent.index,
620
- stable: event.nativeEvent.isStable,
621
- },
622
- });
623
- }}
624
- />
625
- );
626
- })}
627
- </ScreenStack>
628
- </SafeAreaProviderCompat>
629
- </GestureHandlerRootView>
526
+ <RoutesProvider routeKeys={routeKeys}>
527
+ <GestureHandlerRootView>
528
+ <SafeAreaProviderCompat>
529
+ <ScreenStack style={styles.container}>
530
+ {routes.map((route, index) => {
531
+ const descriptor =
532
+ descriptors[route.key] ?? preloadedDescriptors[route.key];
533
+ const isFocused = state.index === index;
534
+ const isBelowFocused = state.index - 1 === index;
535
+ const previousKey = state.routes[index - 1]?.key;
536
+ const nextKey = state.routes[index + 1]?.key;
537
+ const previousDescriptor = previousKey
538
+ ? descriptors[previousKey]
539
+ : undefined;
540
+ const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
541
+
542
+ const isModal = modalRouteKeys.includes(route.key);
543
+
544
+ const isPreloaded =
545
+ preloadedDescriptors[route.key] !== undefined &&
546
+ descriptors[route.key] === undefined;
547
+
548
+ // On Fabric, when screen is frozen, animated and reanimated values are not updated
549
+ // due to component being unmounted. To avoid this, we don't freeze the previous screen there
550
+ const shouldFreeze = isFabric()
551
+ ? !isPreloaded && !isFocused && !isBelowFocused
552
+ : !isPreloaded && !isFocused;
553
+
554
+ return (
555
+ <SceneView
556
+ key={route.key}
557
+ index={index}
558
+ focused={isFocused}
559
+ shouldFreeze={shouldFreeze}
560
+ descriptor={descriptor}
561
+ previousDescriptor={previousDescriptor}
562
+ nextDescriptor={nextDescriptor}
563
+ isPresentationModal={isModal}
564
+ isPreloaded={isPreloaded}
565
+ onWillDisappear={() => {
566
+ navigation.emit({
567
+ type: "transitionStart",
568
+ data: { closing: true },
569
+ target: route.key,
570
+ });
571
+ }}
572
+ onWillAppear={() => {
573
+ navigation.emit({
574
+ type: "transitionStart",
575
+ data: { closing: false },
576
+ target: route.key,
577
+ });
578
+ }}
579
+ onAppear={() => {
580
+ navigation.emit({
581
+ type: "transitionEnd",
582
+ data: { closing: false },
583
+ target: route.key,
584
+ });
585
+ }}
586
+ onDisappear={() => {
587
+ navigation.emit({
588
+ type: "transitionEnd",
589
+ data: { closing: true },
590
+ target: route.key,
591
+ });
592
+ }}
593
+ onDismissed={(event) => {
594
+ navigation.dispatch({
595
+ ...StackActions.pop(event.nativeEvent.dismissCount),
596
+ source: route.key,
597
+ target: state.key,
598
+ });
599
+
600
+ setNextDismissedKey(route.key);
601
+ }}
602
+ onHeaderBackButtonClicked={() => {
603
+ navigation.dispatch({
604
+ ...StackActions.pop(),
605
+ source: route.key,
606
+ target: state.key,
607
+ });
608
+ }}
609
+ onNativeDismissCancelled={(event) => {
610
+ navigation.dispatch({
611
+ ...StackActions.pop(event.nativeEvent.dismissCount),
612
+ source: route.key,
613
+ target: state.key,
614
+ });
615
+ }}
616
+ onGestureCancel={() => {
617
+ navigation.emit({
618
+ type: "gestureCancel",
619
+ target: route.key,
620
+ });
621
+ }}
622
+ onSheetDetentChanged={(event) => {
623
+ navigation.emit({
624
+ type: "sheetDetentChange",
625
+ target: route.key,
626
+ data: {
627
+ index: event.nativeEvent.index,
628
+ stable: event.nativeEvent.isStable,
629
+ },
630
+ });
631
+ }}
632
+ />
633
+ );
634
+ })}
635
+ </ScreenStack>
636
+ </SafeAreaProviderCompat>
637
+ </GestureHandlerRootView>
638
+ </RoutesProvider>
630
639
  );
631
640
  }
632
641
 
@@ -45,12 +45,8 @@ describe("BoundStore.registerSnapshot", () => {
45
45
  BoundStore.registerSnapshot("card", "screen-a", boundsA);
46
46
  BoundStore.registerSnapshot("card", "screen-b", boundsB);
47
47
 
48
- expect(BoundStore.getSnapshot("card", "screen-a")?.bounds).toEqual(
49
- boundsA,
50
- );
51
- expect(BoundStore.getSnapshot("card", "screen-b")?.bounds).toEqual(
52
- boundsB,
53
- );
48
+ expect(BoundStore.getSnapshot("card", "screen-a")?.bounds).toEqual(boundsA);
49
+ expect(BoundStore.getSnapshot("card", "screen-b")?.bounds).toEqual(boundsB);
54
50
  });
55
51
 
56
52
  it("stores ancestorKeys correctly", () => {
@@ -192,13 +188,9 @@ describe("BoundStore.getSnapshot", () => {
192
188
  const ancestorBounds = createBounds(200, 200, 50, 50);
193
189
 
194
190
  // Register with ancestor that matches another screen's key
195
- BoundStore.registerSnapshot(
196
- "card",
197
- "screen-a",
198
- ancestorBounds,
199
- {},
200
- ["stack-a"],
201
- );
191
+ BoundStore.registerSnapshot("card", "screen-a", ancestorBounds, {}, [
192
+ "stack-a",
193
+ ]);
202
194
  BoundStore.registerSnapshot("card", "stack-a", directBounds);
203
195
 
204
196
  // Direct match should win
@@ -242,32 +234,23 @@ describe("BoundStore.getActiveLink", () => {
242
234
 
243
235
  // Query from source screen = closing (going back)
244
236
  const linkFromSource = BoundStore.getActiveLink("card", "screen-a");
245
- expect(linkFromSource?.isClosing).toBe(true);
246
237
  expect(linkFromSource?.source.screenKey).toBe("screen-a");
247
238
 
248
239
  // Query from destination screen = opening
249
240
  const linkFromDest = BoundStore.getActiveLink("card", "screen-b");
250
- expect(linkFromDest?.isClosing).toBe(false);
251
241
  expect(linkFromDest?.destination?.screenKey).toBe("screen-b");
252
242
  });
253
243
 
254
244
  it("ancestor matching works in link lookup", () => {
255
245
  const ancestors = ["stack-a"];
256
246
 
257
- BoundStore.setLinkSource(
258
- "card",
259
- "screen-a",
260
- createBounds(),
261
- {},
262
- ancestors,
263
- );
247
+ BoundStore.setLinkSource("card", "screen-a", createBounds(), {}, ancestors);
264
248
  BoundStore.setLinkDestination("card", "screen-b", createBounds());
265
249
 
266
250
  // Query by ancestor key (matches source)
267
251
  const link = BoundStore.getActiveLink("card", "stack-a");
268
252
  expect(link).not.toBeNull();
269
253
  expect(link?.source.screenKey).toBe("screen-a");
270
- expect(link?.isClosing).toBe(true); // Ancestor of source = closing
271
254
  });
272
255
 
273
256
  it("returns null when screenKey does not match any link", () => {
@@ -296,13 +279,11 @@ describe("Scenario: Simple push/pop navigation", () => {
296
279
 
297
280
  // Verify link is complete - query from destination (opening)
298
281
  const openingLink = BoundStore.getActiveLink("card", "screen-b");
299
- expect(openingLink?.isClosing).toBe(false);
300
282
  expect(openingLink?.source.bounds).toEqual(srcBounds);
301
283
  expect(openingLink?.destination?.bounds).toEqual(dstBounds);
302
284
 
303
285
  // 3. Query from source (closing - going back)
304
286
  const closingLink = BoundStore.getActiveLink("card", "screen-a");
305
- expect(closingLink?.isClosing).toBe(true);
306
287
  expect(closingLink?.source.screenKey).toBe("screen-a");
307
288
  expect(closingLink?.destination?.screenKey).toBe("screen-b");
308
289
  });
@@ -355,19 +336,14 @@ describe("Scenario: Nested navigator with ancestor keys", () => {
355
336
 
356
337
  it("getActiveLink respects ancestor chain", () => {
357
338
  // Navigation from Stack A to detail screen
358
- BoundStore.setLinkSource(
359
- "profile",
360
- "a1",
361
- createBounds(10, 10),
362
- {},
363
- ["stack-a"],
364
- );
339
+ BoundStore.setLinkSource("profile", "a1", createBounds(10, 10), {}, [
340
+ "stack-a",
341
+ ]);
365
342
  BoundStore.setLinkDestination("profile", "detail", createBounds(0, 0));
366
343
 
367
344
  // Query by ancestor should find the link
368
345
  const link = BoundStore.getActiveLink("profile", "stack-a");
369
346
  expect(link?.source.screenKey).toBe("a1");
370
- expect(link?.isClosing).toBe(true); // Ancestor matches source = closing
371
347
  });
372
348
  });
373
349
 
@@ -388,12 +364,10 @@ describe("Scenario: Rapid navigation A → B → C → pop → pop", () => {
388
364
 
389
365
  // Query from C (destination of B→C) = opening
390
366
  const fromC = BoundStore.getActiveLink("card", "screen-c");
391
- expect(fromC?.isClosing).toBe(false);
392
367
  expect(fromC?.destination?.screenKey).toBe("screen-c");
393
368
 
394
369
  // Query from B - B is source of B→C link, so isClosing=true
395
370
  const fromB = BoundStore.getActiveLink("card", "screen-b");
396
- expect(fromB?.isClosing).toBe(true);
397
371
  expect(fromB?.source.screenKey).toBe("screen-b");
398
372
  });
399
373
  });
@@ -401,7 +375,11 @@ describe("Scenario: Rapid navigation A → B → C → pop → pop", () => {
401
375
  describe("Scenario: Global bounds (fullscreen target)", () => {
402
376
  it("getActiveLink with no screenKey returns most recent for fullscreen", () => {
403
377
  // Source exists, destination will be fullscreen (no specific screenKey needed)
404
- BoundStore.setLinkSource("image", "gallery", createBounds(50, 50, 100, 100));
378
+ BoundStore.setLinkSource(
379
+ "image",
380
+ "gallery",
381
+ createBounds(50, 50, 100, 100),
382
+ );
405
383
  BoundStore.setLinkDestination(
406
384
  "image",
407
385
  "fullscreen-viewer",
@@ -0,0 +1,70 @@
1
+ import { useLayoutEffect } from "react";
2
+ import { useAnimatedReaction } from "react-native-reanimated";
3
+ import type { BlankStackDescriptor } from "../../../blank-stack/types";
4
+ import { useStackNavigationContext } from "../../../blank-stack/utils/with-stack-navigation";
5
+ import useStableCallback from "../../hooks/use-stable-callback";
6
+ import { useKeys } from "../../providers/keys.provider";
7
+ import { AnimationStore } from "../../stores/animation.store";
8
+ import { startScreenTransition } from "../../utils/animation/start-screen-transition";
9
+ import { resetStoresForScreen } from "../../utils/reset-stores-for-screen";
10
+
11
+ export interface Props {
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ /**
16
+ * Lifecycle controller built out for Blank Stack implementation.
17
+ */
18
+ export const BlankStackScreenLifecycleController = ({ children }: Props) => {
19
+ const { current } = useKeys<BlankStackDescriptor>();
20
+ const { handleCloseRoute, closingRouteKeysShared } =
21
+ useStackNavigationContext();
22
+
23
+ const animations = AnimationStore.getAll(current.route.key);
24
+
25
+ const handleInitialize = useStableCallback(() => {
26
+ startScreenTransition({
27
+ target: "open",
28
+ spec: current.options.transitionSpec,
29
+ animations,
30
+ });
31
+ });
32
+
33
+ const handleCleanup = useStableCallback(() => {
34
+ resetStoresForScreen(current);
35
+ });
36
+
37
+ const handleCloseEnd = useStableCallback((finished: boolean) => {
38
+ if (!finished) {
39
+ return;
40
+ }
41
+ handleCloseRoute({ route: current.route });
42
+ });
43
+
44
+ useAnimatedReaction(
45
+ () => ({
46
+ keys: closingRouteKeysShared.value,
47
+ }),
48
+ ({ keys }) => {
49
+ if (!keys.includes(current.route.key)) {
50
+ return;
51
+ }
52
+
53
+ startScreenTransition({
54
+ target: "close",
55
+ spec: current.options.transitionSpec,
56
+ animations,
57
+ onAnimationFinish: handleCloseEnd,
58
+ });
59
+ },
60
+ );
61
+
62
+ useLayoutEffect(() => {
63
+ handleInitialize();
64
+ return () => {
65
+ handleCleanup();
66
+ };
67
+ }, [handleInitialize, handleCleanup]);
68
+
69
+ return children;
70
+ };