react-native-screen-transitions 2.0.2 → 2.0.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 (77) hide show
  1. package/lib/commonjs/utils/bounds/constants.js +3 -3
  2. package/lib/commonjs/utils/bounds/constants.js.map +1 -1
  3. package/lib/commonjs/utils/bounds/get-bounds.js +3 -3
  4. package/lib/commonjs/utils/bounds/get-bounds.js.map +1 -1
  5. package/lib/module/utils/bounds/constants.js +3 -3
  6. package/lib/module/utils/bounds/constants.js.map +1 -1
  7. package/lib/module/utils/bounds/get-bounds.js +3 -3
  8. package/lib/module/utils/bounds/get-bounds.js.map +1 -1
  9. package/lib/typescript/utils/bounds/_types/get-bounds.d.ts +2 -2
  10. package/lib/typescript/utils/bounds/_types/get-bounds.d.ts.map +1 -1
  11. package/lib/typescript/utils/bounds/constants.d.ts +4 -4
  12. package/lib/typescript/utils/bounds/constants.d.ts.map +1 -1
  13. package/lib/typescript/utils/bounds/get-bounds.d.ts +2 -2
  14. package/lib/typescript/utils/bounds/get-bounds.d.ts.map +1 -1
  15. package/lib/typescript/utils/bounds/index.d.ts +1 -1
  16. package/lib/typescript/utils/bounds/index.d.ts.map +1 -1
  17. package/package.json +4 -2
  18. package/src/__tests__/geometry.test.ts +127 -0
  19. package/src/components/bounds-activator.tsx +29 -0
  20. package/src/components/controllers/screen-lifecycle.tsx +72 -0
  21. package/src/components/create-transition-aware-component.tsx +99 -0
  22. package/src/components/root-transition-aware.tsx +56 -0
  23. package/src/configs/index.ts +2 -0
  24. package/src/configs/presets.ts +227 -0
  25. package/src/configs/specs.ts +9 -0
  26. package/src/hooks/animation/use-associated-style.tsx +28 -0
  27. package/src/hooks/animation/use-screen-animation.tsx +142 -0
  28. package/src/hooks/bounds/use-bound-measurer.tsx +71 -0
  29. package/src/hooks/gestures/use-build-gestures.tsx +369 -0
  30. package/src/hooks/gestures/use-scroll-progress.tsx +60 -0
  31. package/src/hooks/use-stable-callback.tsx +15 -0
  32. package/src/index.ts +32 -0
  33. package/src/integrations/native-stack/navigators/createNativeStackNavigator.tsx +112 -0
  34. package/src/integrations/native-stack/utils/debounce.tsx +14 -0
  35. package/src/integrations/native-stack/utils/getModalRoutesKeys.ts +21 -0
  36. package/src/integrations/native-stack/utils/useAnimatedHeaderHeight.tsx +18 -0
  37. package/src/integrations/native-stack/utils/useDismissedRouteError.tsx +30 -0
  38. package/src/integrations/native-stack/utils/useInvalidPreventRemoveError.tsx +31 -0
  39. package/src/integrations/native-stack/views/FontProcessor.native.tsx +12 -0
  40. package/src/integrations/native-stack/views/FontProcessor.tsx +5 -0
  41. package/src/integrations/native-stack/views/FooterComponent.tsx +10 -0
  42. package/src/integrations/native-stack/views/NativeStackView.native.tsx +657 -0
  43. package/src/integrations/native-stack/views/NativeStackView.tsx +214 -0
  44. package/src/integrations/native-stack/views/useHeaderConfigProps.tsx +295 -0
  45. package/src/providers/gestures.tsx +89 -0
  46. package/src/providers/keys.tsx +38 -0
  47. package/src/stores/animations.ts +45 -0
  48. package/src/stores/bounds.ts +71 -0
  49. package/src/stores/gestures.ts +55 -0
  50. package/src/stores/navigator-dismiss-state.ts +17 -0
  51. package/src/stores/utils/reset-stores-for-screen.ts +14 -0
  52. package/src/types/animation.ts +76 -0
  53. package/src/types/bounds.ts +82 -0
  54. package/src/types/core.ts +50 -0
  55. package/src/types/gesture.ts +33 -0
  56. package/src/types/navigator.ts +744 -0
  57. package/src/types/utils.ts +3 -0
  58. package/src/utils/animation/animate.ts +28 -0
  59. package/src/utils/animation/run-transition.ts +49 -0
  60. package/src/utils/bounds/_types/builder.ts +35 -0
  61. package/src/utils/bounds/_types/geometry.ts +17 -0
  62. package/src/utils/bounds/_types/get-bounds.ts +10 -0
  63. package/src/utils/bounds/build-bound-styles.ts +184 -0
  64. package/src/utils/bounds/constants.ts +25 -0
  65. package/src/utils/bounds/flatten-styles.ts +21 -0
  66. package/src/utils/bounds/geometry.ts +113 -0
  67. package/src/utils/bounds/get-bounds.ts +56 -0
  68. package/src/utils/bounds/index.ts +46 -0
  69. package/src/utils/bounds/style-composers.ts +172 -0
  70. package/src/utils/gesture/apply-gesture-activation-criteria.ts +109 -0
  71. package/src/utils/gesture/map-gesture-to-progress.ts +11 -0
  72. package/src/utils/gesture/normalize-gesture-translation.ts +20 -0
  73. package/src/utils/index.ts +1 -0
  74. package/lib/commonjs/__tests__ /geometry.test.js +0 -178
  75. package/lib/commonjs/__tests__ /geometry.test.js.map +0 -1
  76. package/lib/module/__tests__ /geometry.test.js +0 -178
  77. package/lib/module/__tests__ /geometry.test.js.map +0 -1
@@ -0,0 +1,369 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { useWindowDimensions } from "react-native";
3
+ import {
4
+ Gesture,
5
+ type GestureStateChangeEvent,
6
+ type GestureTouchEvent,
7
+ type GestureUpdateEvent,
8
+ type PanGestureHandlerEventPayload,
9
+ } from "react-native-gesture-handler";
10
+ import type { GestureStateManagerType } from "react-native-gesture-handler/lib/typescript/handlers/gestures/gestureStateManager";
11
+ import {
12
+ interpolate,
13
+ runOnJS,
14
+ type SharedValue,
15
+ useSharedValue,
16
+ } from "react-native-reanimated";
17
+ import type { ScrollProgress } from "../../providers/gestures";
18
+ import { useKeys } from "../../providers/keys";
19
+ import { Animations } from "../../stores/animations";
20
+ import { Gestures } from "../../stores/gestures";
21
+ import { NavigatorDismissState } from "../../stores/navigator-dismiss-state";
22
+ import { animate } from "../../utils/animation/animate";
23
+ import { runTransition } from "../../utils/animation/run-transition";
24
+ import { applyGestureActivationCriteria } from "../../utils/gesture/apply-gesture-activation-criteria";
25
+ import { mapGestureToProgress } from "../../utils/gesture/map-gesture-to-progress";
26
+
27
+ const GESTURE_VELOCITY_IMPACT = 0.3;
28
+ const DEFAULT_GESTURE_RESPONSE_DISTANCE = 50;
29
+ const DEFAULT_GESTURE_DIRECTION = "horizontal";
30
+ const DEFAULT_GESTURE_ENABLED = false;
31
+ const DEFAULT_GESTURE_DRIVES_PROGRESS = true;
32
+
33
+ interface BuildGesturesHookProps {
34
+ scrollProgress: SharedValue<ScrollProgress>;
35
+ }
36
+
37
+ export const useBuildGestures = ({
38
+ scrollProgress,
39
+ }: BuildGesturesHookProps) => {
40
+ const dimensions = useWindowDimensions();
41
+ const { current } = useKeys();
42
+
43
+ const initialTouch = useSharedValue({
44
+ x: 0,
45
+ y: 0,
46
+ });
47
+
48
+ const gestures = Gestures.getRouteGestures(current.route.key);
49
+
50
+ const animations = Animations.getAll(current.route.key);
51
+
52
+ const {
53
+ gestureDirection = DEFAULT_GESTURE_DIRECTION,
54
+ gestureEnabled = DEFAULT_GESTURE_ENABLED,
55
+ transitionSpec,
56
+ gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
57
+ gestureResponseDistance = DEFAULT_GESTURE_RESPONSE_DISTANCE,
58
+ gestureDrivesProgress = DEFAULT_GESTURE_DRIVES_PROGRESS,
59
+ } = current.options;
60
+
61
+ const directions = Array.isArray(gestureDirection)
62
+ ? gestureDirection
63
+ : [gestureDirection];
64
+
65
+ const allowed = useMemo(
66
+ () => ({
67
+ bidirectional: directions.includes("bidirectional"),
68
+ vertical: directions.includes("vertical"),
69
+ verticalInverted: directions.includes("vertical-inverted"),
70
+ horizontal: directions.includes("horizontal"),
71
+ horizontalInverted: directions.includes("horizontal-inverted"),
72
+ }),
73
+ [directions],
74
+ );
75
+
76
+ const nativeGesture = useMemo(() => Gesture.Native(), []);
77
+
78
+ const onTouchesDown = useCallback(
79
+ (e: GestureTouchEvent) => {
80
+ "worklet";
81
+ const firstTouch = e.changedTouches[0];
82
+ initialTouch.value = { x: firstTouch.x, y: firstTouch.y };
83
+ },
84
+ [initialTouch],
85
+ );
86
+
87
+ const onTouchesMove = useCallback(
88
+ (e: GestureTouchEvent, manager: GestureStateManagerType) => {
89
+ "worklet";
90
+
91
+ const touch = e.changedTouches[0];
92
+ const deltaX = touch.x - initialTouch.value.x;
93
+ const deltaY = touch.y - initialTouch.value.y;
94
+
95
+ const isVerticalSwipe = Math.abs(deltaY) > Math.abs(deltaX);
96
+ const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
97
+
98
+ const isSwipingDown = isVerticalSwipe && deltaY > 0;
99
+ const isSwipingUp = isVerticalSwipe && deltaY < 0;
100
+ const isSwipingRight = isHorizontalSwipe && deltaX > 0;
101
+ const isSwipingLeft = isHorizontalSwipe && deltaX < 0;
102
+
103
+ const minMovement = 5;
104
+ const hasEnoughMovement =
105
+ Math.abs(deltaX) > minMovement || Math.abs(deltaY) > minMovement;
106
+
107
+ if (!hasEnoughMovement) return;
108
+
109
+ if (gestures.isDragging?.value) {
110
+ manager.activate();
111
+ return;
112
+ }
113
+
114
+ let shouldActivate = false;
115
+
116
+ if (allowed.vertical && isSwipingDown) {
117
+ shouldActivate = scrollProgress.value.y <= 0;
118
+ }
119
+ if (allowed.verticalInverted && isSwipingUp) {
120
+ const maxScrollableY =
121
+ scrollProgress.value.contentHeight -
122
+ scrollProgress.value.layoutHeight;
123
+
124
+ shouldActivate = scrollProgress.value.y >= maxScrollableY;
125
+ }
126
+ if (allowed.horizontal && isSwipingRight) {
127
+ shouldActivate = scrollProgress.value.x <= 0;
128
+ }
129
+ if (allowed.horizontalInverted && isSwipingLeft) {
130
+ const maxProgress =
131
+ scrollProgress.value.contentWidth - scrollProgress.value.layoutWidth;
132
+ shouldActivate = scrollProgress.value.x >= maxProgress;
133
+ }
134
+ if (allowed.bidirectional) {
135
+ if (isSwipingDown) {
136
+ shouldActivate = scrollProgress.value.y >= 0;
137
+ } else if (isSwipingUp) {
138
+ shouldActivate = scrollProgress.value.y <= 0;
139
+ } else if (isSwipingRight || isSwipingLeft) {
140
+ shouldActivate = true;
141
+ }
142
+ }
143
+
144
+ if (
145
+ (shouldActivate || gestures.isDragging?.value) &&
146
+ !gestures.isDismissing?.value
147
+ ) {
148
+ manager.activate();
149
+ } else {
150
+ manager.fail();
151
+ }
152
+ },
153
+ [initialTouch, scrollProgress, gestures, allowed],
154
+ );
155
+
156
+ const onStart = useCallback(() => {
157
+ "worklet";
158
+ gestures.isDragging.value = 1;
159
+ gestures.isDismissing.value = 0;
160
+ }, [gestures]);
161
+
162
+ const onUpdate = useCallback(
163
+ (event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
164
+ "worklet";
165
+
166
+ let gestureProgress = 0;
167
+
168
+ gestures.x.value = event.translationX;
169
+ gestures.y.value = event.translationY;
170
+
171
+ gestures.normalizedX.value = interpolate(
172
+ event.translationX,
173
+ [-dimensions.width, dimensions.width],
174
+ [-1, 1],
175
+ "clamp",
176
+ );
177
+ gestures.normalizedY.value = interpolate(
178
+ event.translationY,
179
+ [-dimensions.height, dimensions.height],
180
+ [-1, 1],
181
+ "clamp",
182
+ );
183
+
184
+ if (allowed.bidirectional) {
185
+ const distance = Math.sqrt(
186
+ event.translationX ** 2 + event.translationY ** 2,
187
+ );
188
+ gestureProgress = mapGestureToProgress(distance, dimensions.width);
189
+ } else {
190
+ let maxProgress = 0;
191
+
192
+ const allowedDown = allowed.vertical;
193
+ const allowedUp = allowed.verticalInverted;
194
+ const allowedRight = allowed.horizontal;
195
+ const allowedLeft = allowed.horizontalInverted;
196
+
197
+ if (allowedRight && event.translationX > 0) {
198
+ const currentProgress = mapGestureToProgress(
199
+ event.translationX,
200
+ dimensions.width,
201
+ );
202
+ maxProgress = Math.max(maxProgress, currentProgress);
203
+ }
204
+
205
+ if (allowedLeft && event.translationX < 0) {
206
+ const currentProgress = mapGestureToProgress(
207
+ -event.translationX,
208
+ dimensions.width,
209
+ );
210
+ maxProgress = Math.max(maxProgress, currentProgress);
211
+ }
212
+
213
+ if (allowedDown && event.translationY > 0) {
214
+ const currentProgress = mapGestureToProgress(
215
+ event.translationY,
216
+ dimensions.height,
217
+ );
218
+ maxProgress = Math.max(maxProgress, currentProgress);
219
+ }
220
+
221
+ if (allowedUp && event.translationY < 0) {
222
+ const currentProgress = mapGestureToProgress(
223
+ -event.translationY,
224
+ dimensions.height,
225
+ );
226
+ maxProgress = Math.max(maxProgress, currentProgress);
227
+ }
228
+
229
+ gestureProgress = maxProgress;
230
+ }
231
+
232
+ if (gestureDrivesProgress) {
233
+ animations.progress.value = 1 - gestureProgress;
234
+ }
235
+ },
236
+ [dimensions, gestures, animations, gestureDrivesProgress, allowed],
237
+ );
238
+
239
+ const setNavigatorDismissal = useCallback(() => {
240
+ const key = current.navigation.getState().key;
241
+
242
+ NavigatorDismissState.set(key, true);
243
+ }, [current]);
244
+
245
+ const handleDismiss = useCallback(() => {
246
+ const key = current.navigation.getState().key;
247
+ current.navigation.goBack();
248
+ NavigatorDismissState.remove(key);
249
+ }, [current]);
250
+
251
+ const onEnd = useCallback(
252
+ (event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
253
+ "worklet";
254
+
255
+ const { translationX, translationY, velocityX, velocityY } = event;
256
+
257
+ // reminder: we should make this into an option
258
+ const dismissThreshold = 0.5;
259
+
260
+ const finalX = translationX + velocityX * gestureVelocityImpact;
261
+ const finalY = translationY + velocityY * gestureVelocityImpact;
262
+
263
+ const diagonal = Math.sqrt(
264
+ dimensions.width * dimensions.width +
265
+ dimensions.height * dimensions.height,
266
+ );
267
+
268
+ let shouldDismiss = false;
269
+
270
+ const horizontalDistance = Math.abs(finalX);
271
+ const verticalDistance = Math.abs(finalY);
272
+ const crossAxisThreshold = diagonal * dismissThreshold * 0.7;
273
+
274
+ if (allowed.bidirectional) {
275
+ // For bidirectional, use the original distance-based logic
276
+ const finalDistance = Math.sqrt(finalX * finalX + finalY * finalY);
277
+ shouldDismiss = finalDistance > diagonal * dismissThreshold;
278
+ } else {
279
+ // Check primary direction dismissal
280
+ if (allowed.vertical && finalY > 0) {
281
+ shouldDismiss = verticalDistance > diagonal * dismissThreshold;
282
+ } else if (allowed.verticalInverted && finalY < 0) {
283
+ shouldDismiss = verticalDistance > diagonal * dismissThreshold;
284
+ } else if (allowed.horizontal && finalX > 0) {
285
+ shouldDismiss = horizontalDistance > diagonal * dismissThreshold;
286
+ } else if (allowed.horizontalInverted && finalX < 0) {
287
+ shouldDismiss = horizontalDistance > diagonal * dismissThreshold;
288
+ }
289
+
290
+ // Allow dismissal on perpendicular axis if movement is significant
291
+ if (!shouldDismiss) {
292
+ if (
293
+ (allowed.vertical || allowed.verticalInverted) &&
294
+ horizontalDistance > crossAxisThreshold
295
+ ) {
296
+ shouldDismiss = true;
297
+ } else if (
298
+ (allowed.horizontal || allowed.horizontalInverted) &&
299
+ verticalDistance > crossAxisThreshold
300
+ ) {
301
+ shouldDismiss = true;
302
+ }
303
+ }
304
+ }
305
+
306
+ gestures.isDismissing.value = Number(shouldDismiss);
307
+
308
+ if (gestures.isDismissing.value) {
309
+ runOnJS(setNavigatorDismissal)();
310
+ }
311
+
312
+ runTransition({
313
+ target: gestures.isDismissing.value ? "close" : "open",
314
+ spec: transitionSpec,
315
+ onFinish: gestures.isDismissing.value ? handleDismiss : undefined,
316
+ animations,
317
+ });
318
+
319
+ const spec = gestures.isDismissing.value
320
+ ? transitionSpec?.close
321
+ : transitionSpec?.open;
322
+
323
+ gestures.x.value = animate(0, spec);
324
+ gestures.y.value = animate(0, spec);
325
+ gestures.normalizedX.value = animate(0, spec);
326
+ gestures.normalizedY.value = animate(0, spec);
327
+ gestures.isDragging.value = 0;
328
+ },
329
+ [
330
+ dimensions,
331
+ animations,
332
+ transitionSpec,
333
+ gestureVelocityImpact,
334
+ setNavigatorDismissal,
335
+ handleDismiss,
336
+ gestures,
337
+ allowed,
338
+ ],
339
+ );
340
+
341
+ const panGesture = useMemo(
342
+ () =>
343
+ Gesture.Pan()
344
+ .enabled(gestureEnabled)
345
+ .manualActivation(true)
346
+ .onTouchesDown(onTouchesDown)
347
+ .onTouchesMove(onTouchesMove)
348
+ .onStart(onStart)
349
+ .onUpdate(onUpdate)
350
+ .onEnd(onEnd)
351
+ .blocksExternalGesture(nativeGesture),
352
+ [
353
+ gestureEnabled,
354
+ nativeGesture,
355
+ onTouchesDown,
356
+ onTouchesMove,
357
+ onStart,
358
+ onUpdate,
359
+ onEnd,
360
+ ],
361
+ );
362
+
363
+ applyGestureActivationCriteria({
364
+ gestureDirection,
365
+ gestureResponseDistance,
366
+ panGesture,
367
+ });
368
+ return { panGesture, nativeGesture };
369
+ };
@@ -0,0 +1,60 @@
1
+ import { useCallback } from "react";
2
+ import type { LayoutChangeEvent } from "react-native";
3
+ import { useAnimatedScrollHandler } from "react-native-reanimated";
4
+ import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
5
+ import { useGestureContext } from "../../providers/gestures";
6
+
7
+ interface ScrollProgressHookProps {
8
+ onScroll?: (event: ReanimatedScrollEvent) => void;
9
+ onContentSizeChange?: (width: number, height: number) => void;
10
+ onLayout?: (event: LayoutChangeEvent) => void;
11
+ }
12
+
13
+ export const useScrollProgress = (props: ScrollProgressHookProps) => {
14
+ const { scrollProgress } = useGestureContext();
15
+ const scrollHandler = useAnimatedScrollHandler({
16
+ onScroll: (event) => {
17
+ props.onScroll?.(event);
18
+ scrollProgress.modify((v) => {
19
+ "worklet";
20
+ v.x = event.contentOffset.x;
21
+ v.y = event.contentOffset.y;
22
+ return v;
23
+ });
24
+ },
25
+ });
26
+
27
+ const onContentSizeChange = useCallback(
28
+ (width: number, height: number) => {
29
+ props.onContentSizeChange?.(width, height);
30
+
31
+ scrollProgress.modify((v) => {
32
+ "worklet";
33
+ v.contentWidth = width;
34
+ v.contentHeight = height;
35
+ return v;
36
+ });
37
+ },
38
+ [scrollProgress, props.onContentSizeChange],
39
+ );
40
+
41
+ const onLayout = useCallback(
42
+ (event: LayoutChangeEvent) => {
43
+ props.onLayout?.(event);
44
+ const { width, height } = event.nativeEvent.layout;
45
+ scrollProgress.modify((v) => {
46
+ "worklet";
47
+ v.layoutHeight = height;
48
+ v.layoutWidth = width;
49
+ return v;
50
+ });
51
+ },
52
+ [scrollProgress, props.onLayout],
53
+ );
54
+
55
+ return {
56
+ scrollHandler,
57
+ onContentSizeChange,
58
+ onLayout,
59
+ };
60
+ };
@@ -0,0 +1,15 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ export default function useStableCallback<C extends (...args: any[]) => any>(
4
+ callback: C,
5
+ ) {
6
+ const callbackRef = useRef(callback);
7
+
8
+ useEffect(() => {
9
+ callbackRef.current = callback;
10
+ }, [callback]);
11
+
12
+ return useCallback((...args: Parameters<C>) => {
13
+ callbackRef.current(...args);
14
+ }, []);
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { FlatList, Pressable, ScrollView, View } from "react-native";
2
+ import { createTransitionAwareComponent } from "./components/create-transition-aware-component";
3
+ import { presets, specs } from "./configs";
4
+
5
+ export default {
6
+ View: createTransitionAwareComponent(View),
7
+ Pressable: createTransitionAwareComponent(Pressable),
8
+ ScrollView: createTransitionAwareComponent(ScrollView, {
9
+ isScrollable: true,
10
+ }),
11
+ FlatList: createTransitionAwareComponent(FlatList, {
12
+ isScrollable: true,
13
+ }),
14
+ presets,
15
+ specs,
16
+ createTransitionAwareComponent,
17
+ };
18
+
19
+ export { useScreenAnimation } from "./hooks/animation/use-screen-animation";
20
+ export { createNativeStackNavigator } from "./integrations/native-stack/navigators/createNativeStackNavigator";
21
+
22
+ export type {
23
+ NativeStackHeaderLeftProps,
24
+ NativeStackHeaderProps,
25
+ NativeStackHeaderRightProps,
26
+ NativeStackNavigationEventMap,
27
+ NativeStackNavigationOptions,
28
+ NativeStackNavigationProp,
29
+ NativeStackNavigatorProps,
30
+ NativeStackOptionsArgs,
31
+ NativeStackScreenProps,
32
+ } from "./types/navigator";
@@ -0,0 +1,112 @@
1
+ import {
2
+ createNavigatorFactory,
3
+ type EventArg,
4
+ type NavigatorTypeBagBase,
5
+ type ParamListBase,
6
+ type StackActionHelpers,
7
+ StackActions,
8
+ type StackNavigationState,
9
+ StackRouter,
10
+ type StackRouterOptions,
11
+ type StaticConfig,
12
+ type TypedNavigator,
13
+ useNavigationBuilder,
14
+ } from "@react-navigation/native";
15
+ import * as React from "react";
16
+
17
+ import type {
18
+ NativeStackNavigationEventMap,
19
+ NativeStackNavigationOptions,
20
+ NativeStackNavigationProp,
21
+ NativeStackNavigatorProps,
22
+ } from "../../../types/navigator";
23
+ import { NativeStackView } from "../views/NativeStackView";
24
+
25
+ function NativeStackNavigator({
26
+ id,
27
+ initialRouteName,
28
+ children,
29
+ layout,
30
+ screenListeners,
31
+ screenOptions,
32
+ screenLayout,
33
+ ...rest
34
+ }: NativeStackNavigatorProps) {
35
+ const { state, describe, descriptors, navigation, NavigationContent } =
36
+ useNavigationBuilder<
37
+ StackNavigationState<ParamListBase>,
38
+ StackRouterOptions,
39
+ StackActionHelpers<ParamListBase>,
40
+ NativeStackNavigationOptions,
41
+ NativeStackNavigationEventMap
42
+ >(StackRouter, {
43
+ id,
44
+ initialRouteName,
45
+ children,
46
+ layout,
47
+ screenListeners,
48
+ screenOptions,
49
+ screenLayout,
50
+ });
51
+
52
+ React.useEffect(
53
+ () =>
54
+ // @ts-expect-error: there may not be a tab navigator in parent
55
+ navigation?.addListener?.("tabPress", (e: any) => {
56
+ const isFocused = navigation.isFocused();
57
+
58
+ // Run the operation in the next frame so we're sure all listeners have been run
59
+ // This is necessary to know if preventDefault() has been called
60
+ requestAnimationFrame(() => {
61
+ if (
62
+ state.index > 0 &&
63
+ isFocused &&
64
+ !(e as EventArg<"tabPress", true>).defaultPrevented
65
+ ) {
66
+ // When user taps on already focused tab and we're inside the tab,
67
+ // reset the stack to replicate native behaviour
68
+ navigation.dispatch({
69
+ ...StackActions.popToTop(),
70
+ target: state.key,
71
+ });
72
+ }
73
+ });
74
+ }),
75
+ [navigation, state.index, state.key],
76
+ );
77
+
78
+ return (
79
+ <NavigationContent>
80
+ <NativeStackView
81
+ {...rest}
82
+ state={state}
83
+ navigation={navigation}
84
+ descriptors={descriptors}
85
+ describe={describe}
86
+ />
87
+ </NavigationContent>
88
+ );
89
+ }
90
+
91
+ export function createNativeStackNavigator<
92
+ const ParamList extends ParamListBase,
93
+ const NavigatorID extends string | undefined = undefined,
94
+ const TypeBag extends NavigatorTypeBagBase = {
95
+ ParamList: ParamList;
96
+ NavigatorID: NavigatorID;
97
+ State: StackNavigationState<ParamList>;
98
+ ScreenOptions: NativeStackNavigationOptions;
99
+ EventMap: NativeStackNavigationEventMap;
100
+ NavigationList: {
101
+ [RouteName in keyof ParamList]: NativeStackNavigationProp<
102
+ ParamList,
103
+ RouteName,
104
+ NavigatorID
105
+ >;
106
+ };
107
+ Navigator: typeof NativeStackNavigator;
108
+ },
109
+ const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
110
+ >(config?: Config): TypedNavigator<TypeBag, Config> {
111
+ return createNavigatorFactory(NativeStackNavigator)(config);
112
+ }
@@ -0,0 +1,14 @@
1
+ export function debounce<T extends (...args: any[]) => void>(
2
+ func: T,
3
+ duration: number
4
+ ): T {
5
+ let timeout: ReturnType<typeof setTimeout>;
6
+
7
+ return function (this: unknown, ...args) {
8
+ clearTimeout(timeout);
9
+
10
+ timeout = setTimeout(() => {
11
+ func.apply(this, args);
12
+ }, duration);
13
+ } as T;
14
+ }
@@ -0,0 +1,21 @@
1
+ import type { Route } from "@react-navigation/native";
2
+
3
+ import type { NativeStackDescriptorMap } from "../../../types/navigator";
4
+
5
+ export const getModalRouteKeys = (
6
+ routes: Route<string>[],
7
+ descriptors: NativeStackDescriptorMap,
8
+ ) =>
9
+ routes.reduce<string[]>((acc, route) => {
10
+ const { presentation } = descriptors[route.key]?.options ?? {};
11
+
12
+ if (
13
+ (acc.length && !presentation) ||
14
+ presentation === "modal" ||
15
+ presentation === "transparentModal"
16
+ ) {
17
+ acc.push(route.key);
18
+ }
19
+
20
+ return acc;
21
+ }, []);
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import type { Animated } from 'react-native';
3
+
4
+ export const AnimatedHeaderHeightContext = React.createContext<
5
+ Animated.AnimatedInterpolation<number> | undefined
6
+ >(undefined);
7
+
8
+ export function useAnimatedHeaderHeight() {
9
+ const animatedValue = React.useContext(AnimatedHeaderHeightContext);
10
+
11
+ if (animatedValue === undefined) {
12
+ throw new Error(
13
+ "Couldn't find the header height. Are you inside a screen in a native stack navigator?"
14
+ );
15
+ }
16
+
17
+ return animatedValue;
18
+ }
@@ -0,0 +1,30 @@
1
+ import type {
2
+ ParamListBase,
3
+ StackNavigationState,
4
+ } from '@react-navigation/native';
5
+ import * as React from 'react';
6
+
7
+ export function useDismissedRouteError(
8
+ state: StackNavigationState<ParamListBase>
9
+ ) {
10
+ const [nextDismissedKey, setNextDismissedKey] = React.useState<string | null>(
11
+ null
12
+ );
13
+
14
+ const dismissedRouteName = nextDismissedKey
15
+ ? state.routes.find((route) => route.key === nextDismissedKey)?.name
16
+ : null;
17
+
18
+ React.useEffect(() => {
19
+ if (dismissedRouteName) {
20
+ const message =
21
+ `The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` +
22
+ `This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` +
23
+ `Consider using a 'usePreventRemove' hook with 'headerBackButtonMenuEnabled: false' to prevent users from natively going back multiple screens.`;
24
+
25
+ console.error(message);
26
+ }
27
+ }, [dismissedRouteName]);
28
+
29
+ return { setNextDismissedKey };
30
+ }