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

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 (127) hide show
  1. package/lib/commonjs/blank-stack/components/screens.js +22 -13
  2. package/lib/commonjs/blank-stack/components/screens.js.map +1 -1
  3. package/lib/commonjs/blank-stack/components/stack-view.js +42 -39
  4. package/lib/commonjs/blank-stack/components/stack-view.js.map +1 -1
  5. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js +11 -10
  6. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  7. package/lib/commonjs/native-stack/views/NativeStackView.native.js +110 -103
  8. package/lib/commonjs/native-stack/views/NativeStackView.native.js.map +1 -1
  9. package/lib/commonjs/shared/components/controllers/blank-stack-lifecycle.js +72 -0
  10. package/lib/commonjs/shared/components/controllers/blank-stack-lifecycle.js.map +1 -0
  11. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js +81 -0
  12. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js.map +1 -0
  13. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +11 -1
  14. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  15. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +11 -6
  16. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  17. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +7 -7
  18. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  19. package/lib/commonjs/shared/providers/gestures.provider.js +36 -20
  20. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  21. package/lib/commonjs/shared/providers/register-bounds.provider.js +4 -3
  22. package/lib/commonjs/shared/providers/register-bounds.provider.js.map +1 -1
  23. package/lib/commonjs/shared/providers/routes.provider.js +48 -0
  24. package/lib/commonjs/shared/providers/routes.provider.js.map +1 -0
  25. package/lib/commonjs/shared/providers/screen-transition.provider.js.map +1 -1
  26. package/lib/commonjs/shared/types/state.types.js +9 -0
  27. package/lib/commonjs/shared/types/state.types.js.map +1 -0
  28. package/lib/commonjs/shared/utils/animation/compute-stack-progress.js +20 -0
  29. package/lib/commonjs/shared/utils/animation/compute-stack-progress.js.map +1 -0
  30. package/lib/commonjs/shared/utils/animation/derivations.js +1 -1
  31. package/lib/commonjs/shared/utils/animation/start-screen-transition.js +11 -11
  32. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +1 -1
  33. package/lib/module/blank-stack/components/screens.js +22 -13
  34. package/lib/module/blank-stack/components/screens.js.map +1 -1
  35. package/lib/module/blank-stack/components/stack-view.js +42 -39
  36. package/lib/module/blank-stack/components/stack-view.js.map +1 -1
  37. package/lib/module/blank-stack/utils/with-stack-navigation/index.js +11 -10
  38. package/lib/module/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  39. package/lib/module/native-stack/views/NativeStackView.native.js +109 -102
  40. package/lib/module/native-stack/views/NativeStackView.native.js.map +1 -1
  41. package/lib/module/shared/components/controllers/blank-stack-lifecycle.js +66 -0
  42. package/lib/module/shared/components/controllers/blank-stack-lifecycle.js.map +1 -0
  43. package/lib/module/shared/components/controllers/native-stack-lifecycle.js +74 -0
  44. package/lib/module/shared/components/controllers/native-stack-lifecycle.js.map +1 -0
  45. package/lib/module/shared/hooks/animation/use-screen-animation.js +11 -1
  46. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  47. package/lib/module/shared/hooks/gestures/use-build-gestures.js +11 -6
  48. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  49. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +7 -7
  50. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  51. package/lib/module/shared/providers/gestures.provider.js +36 -21
  52. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  53. package/lib/module/shared/providers/register-bounds.provider.js +4 -3
  54. package/lib/module/shared/providers/register-bounds.provider.js.map +1 -1
  55. package/lib/module/shared/providers/routes.provider.js +42 -0
  56. package/lib/module/shared/providers/routes.provider.js.map +1 -0
  57. package/lib/module/shared/providers/screen-transition.provider.js.map +1 -1
  58. package/lib/module/shared/types/state.types.js +5 -0
  59. package/lib/module/shared/types/state.types.js.map +1 -0
  60. package/lib/module/shared/utils/animation/compute-stack-progress.js +15 -0
  61. package/lib/module/shared/utils/animation/compute-stack-progress.js.map +1 -0
  62. package/lib/module/shared/utils/animation/derivations.js +1 -1
  63. package/lib/module/shared/utils/animation/start-screen-transition.js +11 -11
  64. package/lib/module/shared/utils/animation/start-screen-transition.js.map +1 -1
  65. package/lib/typescript/blank-stack/components/screens.d.ts +1 -3
  66. package/lib/typescript/blank-stack/components/screens.d.ts.map +1 -1
  67. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -1
  68. package/lib/typescript/blank-stack/types.d.ts +1 -39
  69. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  70. package/lib/typescript/blank-stack/utils/with-stack-navigation/index.d.ts.map +1 -1
  71. package/lib/typescript/native-stack/views/NativeStackView.native.d.ts.map +1 -1
  72. package/lib/typescript/shared/components/controllers/blank-stack-lifecycle.d.ts +8 -0
  73. package/lib/typescript/shared/components/controllers/blank-stack-lifecycle.d.ts.map +1 -0
  74. package/lib/typescript/shared/components/controllers/native-stack-lifecycle.d.ts +8 -0
  75. package/lib/typescript/shared/components/controllers/native-stack-lifecycle.d.ts.map +1 -0
  76. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  77. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +2 -2
  78. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  79. package/lib/typescript/shared/providers/gestures.provider.d.ts +10 -7
  80. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  81. package/lib/typescript/shared/providers/register-bounds.provider.d.ts.map +1 -1
  82. package/lib/typescript/shared/providers/routes.provider.d.ts +19 -0
  83. package/lib/typescript/shared/providers/routes.provider.d.ts.map +1 -0
  84. package/lib/typescript/shared/providers/screen-transition.provider.d.ts +2 -2
  85. package/lib/typescript/shared/providers/screen-transition.provider.d.ts.map +1 -1
  86. package/lib/typescript/shared/types/animation.types.d.ts +12 -0
  87. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  88. package/lib/typescript/shared/types/state.types.d.ts +3 -0
  89. package/lib/typescript/shared/types/state.types.d.ts.map +1 -0
  90. package/lib/typescript/shared/utils/animation/compute-stack-progress.d.ts +3 -0
  91. package/lib/typescript/shared/utils/animation/compute-stack-progress.d.ts.map +1 -0
  92. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +1 -1
  93. package/package.json +1 -1
  94. package/src/blank-stack/components/screens.tsx +25 -19
  95. package/src/blank-stack/components/stack-view.tsx +57 -53
  96. package/src/blank-stack/types.ts +1 -24
  97. package/src/blank-stack/utils/with-stack-navigation/index.tsx +17 -3
  98. package/src/native-stack/views/NativeStackView.native.tsx +121 -112
  99. package/src/shared/__tests__/bounds.store.test.ts +14 -36
  100. package/src/shared/components/controllers/blank-stack-lifecycle.tsx +70 -0
  101. package/src/shared/components/controllers/native-stack-lifecycle.tsx +89 -0
  102. package/src/shared/hooks/animation/use-screen-animation.tsx +11 -6
  103. package/src/shared/hooks/gestures/use-build-gestures.tsx +12 -6
  104. package/src/shared/hooks/gestures/use-scroll-registry.tsx +7 -7
  105. package/src/shared/providers/gestures.provider.tsx +49 -48
  106. package/src/shared/providers/register-bounds.provider.tsx +4 -3
  107. package/src/shared/providers/routes.provider.tsx +54 -0
  108. package/src/shared/providers/screen-transition.provider.tsx +2 -2
  109. package/src/shared/types/animation.types.ts +13 -0
  110. package/src/shared/types/state.types.ts +2 -0
  111. package/src/shared/utils/animation/compute-stack-progress.ts +16 -0
  112. package/src/shared/utils/animation/derivations.ts +1 -1
  113. package/src/shared/utils/animation/start-screen-transition.ts +13 -10
  114. package/lib/commonjs/shared/components/controllers/screen-lifecycle.js +0 -142
  115. package/lib/commonjs/shared/components/controllers/screen-lifecycle.js.map +0 -1
  116. package/lib/commonjs/shared/hooks/gestures/use-parent-gesture-registry.js +0 -28
  117. package/lib/commonjs/shared/hooks/gestures/use-parent-gesture-registry.js.map +0 -1
  118. package/lib/module/shared/components/controllers/screen-lifecycle.js +0 -136
  119. package/lib/module/shared/components/controllers/screen-lifecycle.js.map +0 -1
  120. package/lib/module/shared/hooks/gestures/use-parent-gesture-registry.js +0 -23
  121. package/lib/module/shared/hooks/gestures/use-parent-gesture-registry.js.map +0 -1
  122. package/lib/typescript/shared/components/controllers/screen-lifecycle.d.ts +0 -12
  123. package/lib/typescript/shared/components/controllers/screen-lifecycle.d.ts.map +0 -1
  124. package/lib/typescript/shared/hooks/gestures/use-parent-gesture-registry.d.ts +0 -6
  125. package/lib/typescript/shared/hooks/gestures/use-parent-gesture-registry.d.ts.map +0 -1
  126. package/src/shared/components/controllers/screen-lifecycle.tsx +0 -154
  127. package/src/shared/hooks/gestures/use-parent-gesture-registry.tsx +0 -18
@@ -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
+ };
@@ -0,0 +1,89 @@
1
+ /** biome-ignore-all lint/style/noNonNullAssertion: <Lifecycles are rendered right under the gesture provider> */
2
+ import { useEffect, useLayoutEffect } from "react";
3
+ import { useDerivedValue } from "react-native-reanimated";
4
+ import type { NativeStackDescriptor } from "../../../native-stack/types";
5
+ import { useSharedValueState } from "../../hooks/reanimated/use-shared-value-state";
6
+ import useStableCallback from "../../hooks/use-stable-callback";
7
+ import { useGestureContext } from "../../providers/gestures.provider";
8
+ import { useKeys } from "../../providers/keys.provider";
9
+ import { AnimationStore } from "../../stores/animation.store";
10
+ import { TRUE } from "../../types/state.types";
11
+ import type { Any } from "../../types/utils.types";
12
+ import { startScreenTransition } from "../../utils/animation/start-screen-transition";
13
+ import { resetStoresForScreen } from "../../utils/reset-stores-for-screen";
14
+
15
+ export interface Props {
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ /**
20
+ * Lifecycle controller built out for Native Stack implementation.
21
+ */
22
+ export const NativeStackScreenLifecycleController = ({ children }: Props) => {
23
+ const { current } = useKeys<NativeStackDescriptor>();
24
+ const { ancestorContext } = useGestureContext()!;
25
+
26
+ const isAncestorDismissingViaGesture = useSharedValueState(
27
+ useDerivedValue(() => {
28
+ "worklet";
29
+ return (
30
+ ancestorContext?.gestureAnimationValues.isDismissing?.value ?? false
31
+ );
32
+ }),
33
+ );
34
+
35
+ const animations = AnimationStore.getAll(current.route.key);
36
+
37
+ const handleBeforeRemove = useStableCallback((e: Any) => {
38
+ const isEnabled = current.options.enableTransitions;
39
+
40
+ const isFirstScreen = current.navigation.getState().index === 0;
41
+
42
+ // If transitions are disabled, or an ancestor is dismissing via gesture, or this is the first screen of the stack, reset the stores
43
+ if (!isEnabled || isAncestorDismissingViaGesture || isFirstScreen) {
44
+ animations.closing.set(TRUE);
45
+ resetStoresForScreen(current);
46
+ return;
47
+ }
48
+
49
+ e.preventDefault();
50
+ const onAnimationFinish = (finished: boolean) => {
51
+ if (finished) {
52
+ current.navigation.dispatch(e.data.action);
53
+
54
+ // we'll ensure the dispatch is complete before resetting stores
55
+ requestAnimationFrame(() => {
56
+ resetStoresForScreen(current);
57
+ });
58
+ }
59
+ };
60
+
61
+ startScreenTransition({
62
+ target: "close",
63
+ spec: current.options.transitionSpec,
64
+ onAnimationFinish,
65
+ animations,
66
+ });
67
+ });
68
+
69
+ const handleInitialize = useStableCallback(() => {
70
+ startScreenTransition({
71
+ target: "open",
72
+ spec: current.options.transitionSpec,
73
+ animations,
74
+ });
75
+ });
76
+
77
+ useEffect(() => {
78
+ const unsubscribe = current.navigation.addListener(
79
+ "beforeRemove",
80
+ handleBeforeRemove,
81
+ );
82
+
83
+ return unsubscribe;
84
+ }, [current.navigation, handleBeforeRemove]);
85
+
86
+ useLayoutEffect(handleInitialize, []);
87
+
88
+ return children;
89
+ };
@@ -10,6 +10,7 @@ import {
10
10
  type TransitionDescriptor,
11
11
  useKeys,
12
12
  } from "../../providers/keys.provider";
13
+ import { useStackAnimationValues } from "../../providers/routes.provider";
13
14
  import { AnimationStore } from "../../stores/animation.store";
14
15
  import { GestureStore, type GestureStoreMap } from "../../stores/gesture.store";
15
16
  import type {
@@ -17,7 +18,7 @@ import type {
17
18
  ScreenTransitionState,
18
19
  } from "../../types/animation.types";
19
20
  import type { ScreenTransitionConfig } from "../../types/core.types";
20
- import type { GestureDirection } from "../../types/gesture.types";
21
+ import { computeStackProgress } from "../../utils/animation/compute-stack-progress";
21
22
  import { derivations } from "../../utils/animation/derivations";
22
23
  import { createBounds } from "../../utils/bounds";
23
24
 
@@ -60,10 +61,7 @@ const unwrapInto = (s: BuiltState): ScreenTransitionState => {
60
61
  out.gesture.normalizedY = s.gesture.normalizedY.value;
61
62
  out.gesture.isDismissing = s.gesture.isDismissing.value;
62
63
  out.gesture.isDragging = s.gesture.isDragging.value;
63
- out.gesture.direction = s.gesture.direction.value as Omit<
64
- GestureDirection,
65
- "bidirectional"
66
- > | null;
64
+ out.gesture.direction = s.gesture.direction.value;
67
65
 
68
66
  return out;
69
67
  };
@@ -112,6 +110,9 @@ export function _useScreenAnimation() {
112
110
  const nextAnimation = useBuildScreenTransitionState(nextDescriptor);
113
111
  const prevAnimation = useBuildScreenTransitionState(previousDescriptor);
114
112
 
113
+ const currentRouteKey = currentDescriptor?.route?.key;
114
+ const stackAnimationValues = useStackAnimationValues(currentRouteKey);
115
+
115
116
  const screenInterpolatorProps = useDerivedValue<
116
117
  Omit<ScreenInterpolationProps, "bounds">
117
118
  >(() => {
@@ -129,17 +130,21 @@ export function _useScreenAnimation() {
129
130
  ? unwrapInto(currentAnimation)
130
131
  : DEFAULT_SCREEN_TRANSITION_STATE;
131
132
 
132
- const helpers = derivations({
133
+ const { progress, ...helpers } = derivations({
133
134
  current,
134
135
  next,
135
136
  });
136
137
 
138
+ const stackProgress = computeStackProgress(stackAnimationValues, progress);
139
+
137
140
  return {
138
141
  layouts: { screen: dimensions },
139
142
  insets,
140
143
  previous,
141
144
  current,
142
145
  next,
146
+ progress,
147
+ stackProgress,
143
148
  ...helpers,
144
149
  };
145
150
  });
@@ -40,12 +40,12 @@ import useStableCallbackValue from "../use-stable-callback-value";
40
40
 
41
41
  interface BuildGesturesHookProps {
42
42
  scrollConfig: SharedValue<ScrollConfig | null>;
43
- parentContext?: GestureContextType | null;
43
+ ancestorContext?: GestureContextType | null;
44
44
  }
45
45
 
46
46
  export const useBuildGestures = ({
47
47
  scrollConfig,
48
- parentContext,
48
+ ancestorContext,
49
49
  }: BuildGesturesHookProps): {
50
50
  panGesture: GestureType;
51
51
  nativeGesture: GestureType;
@@ -96,8 +96,8 @@ export const useBuildGestures = ({
96
96
 
97
97
  const handleDismiss = useCallback(() => {
98
98
  // If an ancestor navigator is already dismissing, skip this dismiss to
99
- // avoid racing with the parent
100
- if (parentContext?.gestureAnimationValues.isDismissing?.value) {
99
+ // avoid racing with the ancestor
100
+ if (ancestorContext?.gestureAnimationValues.isDismissing?.value) {
101
101
  return;
102
102
  }
103
103
 
@@ -116,7 +116,7 @@ export const useBuildGestures = ({
116
116
  source: current.route.key,
117
117
  target: state.key,
118
118
  });
119
- }, [current, parentContext]);
119
+ }, [current, ancestorContext]);
120
120
 
121
121
  const onTouchesDown = useStableCallbackValue((e: GestureTouchEvent) => {
122
122
  "worklet";
@@ -130,7 +130,7 @@ export const useBuildGestures = ({
130
130
  "worklet";
131
131
 
132
132
  // If an ancestor navigator is already dismissing via gesture, block new gestures here.
133
- if (parentContext?.gestureAnimationValues.isDismissing?.value) {
133
+ if (ancestorContext?.gestureAnimationValues.isDismissing?.value) {
134
134
  gestureOffsetState.value = GestureOffsetState.FAILED;
135
135
  manager.fail();
136
136
  return;
@@ -338,6 +338,11 @@ export const useBuildGestures = ({
338
338
  .onEnd(onEnd)
339
339
  .blocksExternalGesture(nativeGesture);
340
340
 
341
+ // Allow ancestors to block child native gestures
342
+ if (ancestorContext?.panGesture && nativeGesture) {
343
+ ancestorContext.panGesture.blocksExternalGesture(nativeGesture);
344
+ }
345
+
341
346
  return {
342
347
  panGesture,
343
348
  nativeGesture,
@@ -351,5 +356,6 @@ export const useBuildGestures = ({
351
356
  onUpdate,
352
357
  onEnd,
353
358
  gestureAnimationValues,
359
+ ancestorContext,
354
360
  ]);
355
361
  };