react-native-screen-transitions 2.0.1 → 2.0.3

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/package.json +4 -3
  2. package/src/__tests__ /geometry.test.ts +127 -0
  3. package/src/components/bounds-activator.tsx +29 -0
  4. package/src/components/controllers/screen-lifecycle.tsx +72 -0
  5. package/src/components/create-transition-aware-component.tsx +99 -0
  6. package/src/components/root-transition-aware.tsx +56 -0
  7. package/src/configs/index.ts +2 -0
  8. package/src/configs/presets.ts +227 -0
  9. package/src/configs/specs.ts +9 -0
  10. package/src/hooks/animation/use-associated-style.tsx +28 -0
  11. package/src/hooks/animation/use-screen-animation.tsx +142 -0
  12. package/src/hooks/bounds/use-bound-measurer.tsx +71 -0
  13. package/src/hooks/gestures/use-build-gestures.tsx +369 -0
  14. package/src/hooks/gestures/use-scroll-progress.tsx +60 -0
  15. package/src/hooks/use-stable-callback.tsx +15 -0
  16. package/src/index.ts +32 -0
  17. package/src/integrations/native-stack/navigators/createNativeStackNavigator.tsx +112 -0
  18. package/src/integrations/native-stack/utils/debounce.tsx +14 -0
  19. package/src/integrations/native-stack/utils/getModalRoutesKeys.ts +21 -0
  20. package/src/integrations/native-stack/utils/useAnimatedHeaderHeight.tsx +18 -0
  21. package/src/integrations/native-stack/utils/useDismissedRouteError.tsx +30 -0
  22. package/src/integrations/native-stack/utils/useInvalidPreventRemoveError.tsx +31 -0
  23. package/src/integrations/native-stack/views/FontProcessor.native.tsx +12 -0
  24. package/src/integrations/native-stack/views/FontProcessor.tsx +5 -0
  25. package/src/integrations/native-stack/views/FooterComponent.tsx +10 -0
  26. package/src/integrations/native-stack/views/NativeStackView.native.tsx +657 -0
  27. package/src/integrations/native-stack/views/NativeStackView.tsx +214 -0
  28. package/src/integrations/native-stack/views/useHeaderConfigProps.tsx +295 -0
  29. package/src/providers/gestures.tsx +89 -0
  30. package/src/providers/keys.tsx +38 -0
  31. package/src/stores/animations.ts +45 -0
  32. package/src/stores/bounds.ts +71 -0
  33. package/src/stores/gestures.ts +55 -0
  34. package/src/stores/navigator-dismiss-state.ts +17 -0
  35. package/src/stores/utils/reset-stores-for-screen.ts +14 -0
  36. package/src/types/animation.ts +76 -0
  37. package/src/types/bounds.ts +82 -0
  38. package/src/types/core.ts +50 -0
  39. package/src/types/gesture.ts +33 -0
  40. package/src/types/navigator.ts +744 -0
  41. package/src/types/utils.ts +3 -0
  42. package/src/utils/animation/animate.ts +28 -0
  43. package/src/utils/animation/run-transition.ts +49 -0
  44. package/src/utils/bounds/_types/builder.ts +35 -0
  45. package/src/utils/bounds/_types/geometry.ts +17 -0
  46. package/src/utils/bounds/_types/get-bounds.ts +10 -0
  47. package/src/utils/bounds/build-bound-styles.ts +184 -0
  48. package/src/utils/bounds/constants.ts +28 -0
  49. package/src/utils/bounds/flatten-styles.ts +21 -0
  50. package/src/utils/bounds/geometry.ts +113 -0
  51. package/src/utils/bounds/get-bounds.ts +56 -0
  52. package/src/utils/bounds/index.ts +46 -0
  53. package/src/utils/bounds/style-composers.ts +172 -0
  54. package/src/utils/gesture/apply-gesture-activation-criteria.ts +109 -0
  55. package/src/utils/gesture/map-gesture-to-progress.ts +11 -0
  56. package/src/utils/gesture/normalize-gesture-translation.ts +20 -0
  57. package/src/utils/index.ts +1 -0
@@ -0,0 +1,657 @@
1
+ import {
2
+ getDefaultHeaderHeight,
3
+ getHeaderTitle,
4
+ HeaderBackContext,
5
+ HeaderHeightContext,
6
+ HeaderShownContext,
7
+ SafeAreaProviderCompat,
8
+ useFrameSize,
9
+ } from "@react-navigation/elements";
10
+ import {
11
+ NavigationContext,
12
+ NavigationRouteContext,
13
+ type ParamListBase,
14
+ type RouteProp,
15
+ StackActions,
16
+ type StackNavigationState,
17
+ usePreventRemoveContext,
18
+ useTheme,
19
+ } from "@react-navigation/native";
20
+ import * as React from "react";
21
+ import {
22
+ Animated,
23
+ Platform,
24
+ StatusBar,
25
+ StyleSheet,
26
+ useAnimatedValue,
27
+ View,
28
+ } from "react-native";
29
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
30
+ import {
31
+ type ScreenProps,
32
+ ScreenStack,
33
+ ScreenStackItem,
34
+ } from "react-native-screens";
35
+ import { ScreenLifecycleController } from "../../../components/controllers/screen-lifecycle";
36
+ import { RootTransitionAware } from "../../../components/root-transition-aware";
37
+ import { ScreenGestureProvider } from "../../../providers/gestures";
38
+ import { KeysProvider } from "../../../providers/keys";
39
+ import type {
40
+ NativeStackDescriptor,
41
+ NativeStackDescriptorMap,
42
+ NativeStackNavigationHelpers,
43
+ } from "../../../types/navigator";
44
+ import { debounce } from "../utils/debounce";
45
+ import { getModalRouteKeys } from "../utils/getModalRoutesKeys";
46
+ import { AnimatedHeaderHeightContext } from "../utils/useAnimatedHeaderHeight";
47
+ import { useDismissedRouteError } from "../utils/useDismissedRouteError";
48
+ import { useInvalidPreventRemoveError } from "../utils/useInvalidPreventRemoveError";
49
+ import { useHeaderConfigProps } from "./useHeaderConfigProps";
50
+
51
+ const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
52
+
53
+ function isFabric() {
54
+ return "nativeFabricUIManager" in global;
55
+ }
56
+
57
+ type SceneViewProps = {
58
+ index: number;
59
+ focused: boolean;
60
+ shouldFreeze: boolean;
61
+ descriptor: NativeStackDescriptor;
62
+ previousDescriptor?: NativeStackDescriptor;
63
+ nextDescriptor?: NativeStackDescriptor;
64
+ isPresentationModal?: boolean;
65
+ isPreloaded?: boolean;
66
+ onWillDisappear: () => void;
67
+ onWillAppear: () => void;
68
+ onAppear: () => void;
69
+ onDisappear: () => void;
70
+ onDismissed: ScreenProps["onDismissed"];
71
+ onHeaderBackButtonClicked: ScreenProps["onHeaderBackButtonClicked"];
72
+ onNativeDismissCancelled: ScreenProps["onDismissed"];
73
+ onGestureCancel: ScreenProps["onGestureCancel"];
74
+ onSheetDetentChanged: ScreenProps["onSheetDetentChanged"];
75
+ };
76
+
77
+ const useNativeDriver = Platform.OS !== "web";
78
+
79
+ const SceneView = ({
80
+ index,
81
+ focused,
82
+ shouldFreeze,
83
+ descriptor,
84
+ previousDescriptor,
85
+ nextDescriptor,
86
+ isPresentationModal,
87
+ isPreloaded,
88
+ onWillDisappear,
89
+ onWillAppear,
90
+ onAppear,
91
+ onDisappear,
92
+ onDismissed,
93
+ onHeaderBackButtonClicked,
94
+ onNativeDismissCancelled,
95
+ onGestureCancel,
96
+ onSheetDetentChanged,
97
+ }: SceneViewProps) => {
98
+ const { route, navigation, options, render } = descriptor;
99
+
100
+ let {
101
+ animation,
102
+ animationMatchesGesture,
103
+ presentation = isPresentationModal ? "modal" : "card",
104
+ fullScreenGestureEnabled,
105
+ } = options;
106
+
107
+ const {
108
+ animationDuration,
109
+ animationTypeForReplace = "push",
110
+ fullScreenGestureShadowEnabled = true,
111
+ nativeGestureEnabled,
112
+ nativeGestureDirection = presentation === "card"
113
+ ? "horizontal"
114
+ : "vertical",
115
+ nativeGestureResponseDistance,
116
+ header,
117
+ headerBackButtonMenuEnabled,
118
+ headerShown,
119
+ headerBackground,
120
+ headerTransparent,
121
+ autoHideHomeIndicator,
122
+ keyboardHandlingEnabled,
123
+ navigationBarColor,
124
+ navigationBarTranslucent,
125
+ navigationBarHidden,
126
+ orientation,
127
+ sheetAllowedDetents = [1.0],
128
+ sheetLargestUndimmedDetentIndex = -1,
129
+ sheetGrabberVisible = false,
130
+ sheetCornerRadius = -1.0,
131
+ sheetElevation = 24,
132
+ sheetExpandsWhenScrolledToEdge = true,
133
+ sheetInitialDetentIndex = 0,
134
+ statusBarAnimation,
135
+ statusBarHidden,
136
+ statusBarStyle,
137
+ statusBarTranslucent,
138
+ statusBarBackgroundColor,
139
+ unstable_sheetFooter,
140
+ freezeOnBlur,
141
+ contentStyle,
142
+ enableTransitions,
143
+ } = options;
144
+
145
+ if (nativeGestureDirection === "vertical" && Platform.OS === "ios") {
146
+ // for `vertical` direction to work, we need to set `fullScreenGestureEnabled` to `true`
147
+ // so the screen can be dismissed from any point on screen.
148
+ // `animationMatchesGesture` needs to be set to `true` so the `animation` set by user can be used,
149
+ // otherwise `simple_push` will be used.
150
+ // Also, the default animation for this direction seems to be `slide_from_bottom`.
151
+ if (fullScreenGestureEnabled === undefined) {
152
+ fullScreenGestureEnabled = true;
153
+ }
154
+
155
+ if (animationMatchesGesture === undefined) {
156
+ animationMatchesGesture = true;
157
+ }
158
+
159
+ if (animation === undefined) {
160
+ animation = "slide_from_bottom";
161
+ }
162
+ }
163
+
164
+ // workaround for rn-screens where gestureDirection has to be set on both
165
+ // current and previous screen - software-mansion/react-native-screens/pull/1509
166
+ const nextGestureDirection = nextDescriptor?.options.nativeGestureDirection;
167
+ const gestureDirectionOverride =
168
+ nextGestureDirection != null
169
+ ? nextGestureDirection
170
+ : nativeGestureDirection;
171
+
172
+ if (index === 0) {
173
+ // first screen should always be treated as `card`, it resolves problems with no header animation
174
+ // for navigator with first screen as `modal` and the next as `card`
175
+ presentation = "card";
176
+ }
177
+
178
+ const { colors } = useTheme();
179
+ const insets = useSafeAreaInsets();
180
+
181
+ // `modal` and `formSheet` presentations do not take whole screen, so should not take the inset.
182
+ const isModal = presentation === "modal" || presentation === "formSheet";
183
+
184
+ // Modals are fullscreen in landscape only on iPhone
185
+ const isIPhone = Platform.OS === "ios" && !(Platform.isPad || Platform.isTV);
186
+
187
+ const isParentHeaderShown = React.useContext(HeaderShownContext);
188
+ const parentHeaderHeight = React.useContext(HeaderHeightContext);
189
+ const parentHeaderBack = React.useContext(HeaderBackContext);
190
+
191
+ const isLandscape = useFrameSize((frame) => frame.width > frame.height);
192
+
193
+ const topInset =
194
+ isParentHeaderShown ||
195
+ (Platform.OS === "ios" && isModal) ||
196
+ (isIPhone && isLandscape)
197
+ ? 0
198
+ : insets.top;
199
+
200
+ const defaultHeaderHeight = useFrameSize((frame) =>
201
+ Platform.select({
202
+ // FIXME: Currently screens isn't using Material 3
203
+ // So our `getDefaultHeaderHeight` doesn't return the correct value
204
+ // So we hardcode the value here for now until screens is updated
205
+ android: ANDROID_DEFAULT_HEADER_HEIGHT + topInset,
206
+ default: getDefaultHeaderHeight(frame, isModal, topInset),
207
+ }),
208
+ );
209
+
210
+ const { preventedRoutes } = usePreventRemoveContext();
211
+
212
+ const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight);
213
+
214
+ // eslint-disable-next-line react-hooks/exhaustive-deps
215
+ const setHeaderHeightDebounced = React.useCallback(
216
+ // Debounce the header height updates to avoid excessive re-renders
217
+ debounce(setHeaderHeight, 100),
218
+ [],
219
+ );
220
+
221
+ const hasCustomHeader = header != null;
222
+
223
+ let headerHeightCorrectionOffset = 0;
224
+
225
+ if (Platform.OS === "android" && !hasCustomHeader) {
226
+ const statusBarHeight = StatusBar.currentHeight ?? 0;
227
+
228
+ // FIXME: On Android, the native header height is not correctly calculated
229
+ // It includes status bar height even if statusbar is not translucent
230
+ // And the statusbar value itself doesn't match the actual status bar height
231
+ // So we subtract the bogus status bar height and add the actual top inset
232
+ headerHeightCorrectionOffset = -statusBarHeight + topInset;
233
+ }
234
+
235
+ const rawAnimatedHeaderHeight = useAnimatedValue(defaultHeaderHeight);
236
+ const animatedHeaderHeight = React.useMemo(
237
+ () =>
238
+ Animated.add<number>(
239
+ rawAnimatedHeaderHeight,
240
+ headerHeightCorrectionOffset,
241
+ ),
242
+ [headerHeightCorrectionOffset, rawAnimatedHeaderHeight],
243
+ );
244
+
245
+ // During the very first render topInset is > 0 when running
246
+ // in non edge-to-edge mode on Android, while on every consecutive render
247
+ // topInset === 0, causing header content to jump, as we add padding on the first frame,
248
+ // just to remove it in next one. To prevent this, when statusBarTranslucent is set,
249
+ // we apply additional padding in header only if its true.
250
+ // For more details see: https://github.com/react-navigation/react-navigation/pull/12014
251
+ const headerTopInsetEnabled =
252
+ typeof statusBarTranslucent === "boolean"
253
+ ? statusBarTranslucent
254
+ : topInset !== 0;
255
+
256
+ const canGoBack = previousDescriptor != null || parentHeaderBack != null;
257
+ const backTitle = previousDescriptor
258
+ ? getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name)
259
+ : parentHeaderBack?.title;
260
+
261
+ const headerBack = React.useMemo(() => {
262
+ if (canGoBack) {
263
+ return {
264
+ href: undefined, // No href needed for native
265
+ title: backTitle,
266
+ };
267
+ }
268
+
269
+ return undefined;
270
+ }, [canGoBack, backTitle]);
271
+
272
+ const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
273
+ const modifiedPresentation = enableTransitions
274
+ ? "containedTransparentModal"
275
+ : presentation === "card"
276
+ ? "push"
277
+ : presentation;
278
+
279
+ const modifiedAnimation = enableTransitions ? "none" : animation;
280
+ const modifiedHeaderShown =
281
+ enableTransitions || header !== undefined ? false : headerShown;
282
+
283
+ const headerConfig = useHeaderConfigProps({
284
+ ...options,
285
+ route,
286
+ headerBackButtonMenuEnabled:
287
+ isRemovePrevented !== undefined
288
+ ? !isRemovePrevented
289
+ : headerBackButtonMenuEnabled,
290
+ headerBackTitle:
291
+ options.headerBackTitle !== undefined
292
+ ? options.headerBackTitle
293
+ : undefined,
294
+ headerHeight,
295
+ headerShown: modifiedHeaderShown,
296
+ headerTopInsetEnabled,
297
+ headerBack,
298
+ });
299
+
300
+ return (
301
+ <NavigationContext.Provider value={navigation}>
302
+ <NavigationRouteContext.Provider value={route}>
303
+ <ScreenStackItem
304
+ key={route.key}
305
+ screenId={route.key}
306
+ activityState={isPreloaded ? 0 : 2}
307
+ style={StyleSheet.absoluteFill}
308
+ aria-hidden={!focused}
309
+ customAnimationOnSwipe={animationMatchesGesture}
310
+ fullScreenSwipeEnabled={fullScreenGestureEnabled}
311
+ fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
312
+ freezeOnBlur={freezeOnBlur}
313
+ gestureEnabled={
314
+ Platform.OS === "android"
315
+ ? // This prop enables handling of system back gestures on Android
316
+ // Since we handle them in JS side, we disable this
317
+ false
318
+ : nativeGestureEnabled
319
+ }
320
+ homeIndicatorHidden={autoHideHomeIndicator}
321
+ hideKeyboardOnSwipe={keyboardHandlingEnabled}
322
+ navigationBarColor={navigationBarColor}
323
+ navigationBarTranslucent={navigationBarTranslucent}
324
+ navigationBarHidden={navigationBarHidden}
325
+ replaceAnimation={animationTypeForReplace}
326
+ stackPresentation={modifiedPresentation}
327
+ stackAnimation={modifiedAnimation}
328
+ screenOrientation={orientation}
329
+ sheetAllowedDetents={sheetAllowedDetents}
330
+ sheetLargestUndimmedDetentIndex={sheetLargestUndimmedDetentIndex}
331
+ sheetGrabberVisible={sheetGrabberVisible}
332
+ sheetInitialDetentIndex={sheetInitialDetentIndex}
333
+ sheetCornerRadius={sheetCornerRadius}
334
+ sheetElevation={sheetElevation}
335
+ sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
336
+ statusBarAnimation={statusBarAnimation}
337
+ statusBarHidden={statusBarHidden}
338
+ statusBarStyle={statusBarStyle}
339
+ statusBarColor={statusBarBackgroundColor}
340
+ statusBarTranslucent={statusBarTranslucent}
341
+ swipeDirection={gestureDirectionOverride}
342
+ transitionDuration={animationDuration}
343
+ onWillAppear={onWillAppear}
344
+ onWillDisappear={onWillDisappear}
345
+ onAppear={onAppear}
346
+ onDisappear={onDisappear}
347
+ onDismissed={onDismissed}
348
+ onGestureCancel={onGestureCancel}
349
+ onSheetDetentChanged={onSheetDetentChanged}
350
+ gestureResponseDistance={nativeGestureResponseDistance}
351
+ nativeBackButtonDismissalEnabled={false} // on Android
352
+ onHeaderBackButtonClicked={onHeaderBackButtonClicked}
353
+ preventNativeDismiss={isRemovePrevented} // on iOS
354
+ onNativeDismissCancelled={onNativeDismissCancelled}
355
+ // Unfortunately, because of the bug that exists on Fabric, where native event drivers
356
+ // for Animated objects are being created after the first notifications about the header height
357
+ // from the native side, `onHeaderHeightChange` event does not notify
358
+ // `animatedHeaderHeight` about initial values on appearing screens at the moment.
359
+ onHeaderHeightChange={Animated.event(
360
+ [
361
+ {
362
+ nativeEvent: {
363
+ headerHeight: rawAnimatedHeaderHeight,
364
+ },
365
+ },
366
+ ],
367
+ {
368
+ useNativeDriver,
369
+ listener: (e) => {
370
+ if (hasCustomHeader) {
371
+ // If we have a custom header, don't use native header height
372
+ return;
373
+ }
374
+
375
+ if (
376
+ Platform.OS === "android" &&
377
+ (options.headerBackground != null ||
378
+ options.headerTransparent)
379
+ ) {
380
+ // FIXME: On Android, we get 0 if the header is translucent
381
+ // So we set a default height in that case
382
+ setHeaderHeight(ANDROID_DEFAULT_HEADER_HEIGHT + topInset);
383
+ return;
384
+ }
385
+
386
+ if (
387
+ e.nativeEvent &&
388
+ typeof e.nativeEvent === "object" &&
389
+ "headerHeight" in e.nativeEvent &&
390
+ typeof e.nativeEvent.headerHeight === "number"
391
+ ) {
392
+ const headerHeight =
393
+ e.nativeEvent.headerHeight + headerHeightCorrectionOffset;
394
+
395
+ // Only debounce if header has large title or search bar
396
+ // As it's the only case where the header height can change frequently
397
+ const doesHeaderAnimate =
398
+ Platform.OS === "ios" &&
399
+ (options.headerLargeTitle ||
400
+ options.headerSearchBarOptions);
401
+
402
+ if (doesHeaderAnimate) {
403
+ setHeaderHeightDebounced(headerHeight);
404
+ } else {
405
+ setHeaderHeight(headerHeight);
406
+ }
407
+ }
408
+ },
409
+ },
410
+ )}
411
+ contentStyle={[
412
+ modifiedPresentation !== "transparentModal" &&
413
+ modifiedPresentation !== "containedTransparentModal" && {
414
+ backgroundColor: colors.background,
415
+ },
416
+ contentStyle,
417
+ ]}
418
+ headerConfig={headerConfig}
419
+ unstable_sheetFooter={unstable_sheetFooter}
420
+ // When ts-expect-error is added, it affects all the props below it
421
+ // So we keep any props that need it at the end
422
+ // Otherwise invalid props may not be caught by TypeScript
423
+ shouldFreeze={shouldFreeze}
424
+ >
425
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
426
+ <HeaderHeightContext.Provider
427
+ value={
428
+ headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
429
+ }
430
+ >
431
+ {headerBackground != null ? (
432
+ /**
433
+ * To show a custom header background, we render it at the top of the screen below the header
434
+ * The header also needs to be positioned absolutely (with `translucent` style)
435
+ */
436
+ <View
437
+ style={[
438
+ styles.background,
439
+ headerTransparent ? styles.translucent : null,
440
+ { height: headerHeight },
441
+ ]}
442
+ >
443
+ {headerBackground()}
444
+ </View>
445
+ ) : null}
446
+ {header != null && headerShown !== false ? (
447
+ <View
448
+ onLayout={(e) => {
449
+ const headerHeight = e.nativeEvent.layout.height;
450
+
451
+ setHeaderHeight(headerHeight);
452
+ rawAnimatedHeaderHeight.setValue(headerHeight);
453
+ }}
454
+ style={[
455
+ styles.header,
456
+ headerTransparent ? styles.absolute : null,
457
+ ]}
458
+ >
459
+ {header({
460
+ back: headerBack,
461
+ options,
462
+ route,
463
+ navigation,
464
+ })}
465
+ </View>
466
+ ) : null}
467
+ <HeaderShownContext.Provider
468
+ value={isParentHeaderShown || headerShown !== false}
469
+ >
470
+ <HeaderBackContext.Provider value={headerBack}>
471
+ <KeysProvider
472
+ previous={previousDescriptor}
473
+ current={descriptor}
474
+ next={nextDescriptor}
475
+ >
476
+ <ScreenLifecycleController>
477
+ <ScreenGestureProvider>
478
+ <RootTransitionAware>{render()}</RootTransitionAware>
479
+ </ScreenGestureProvider>
480
+ </ScreenLifecycleController>
481
+ </KeysProvider>
482
+ </HeaderBackContext.Provider>
483
+ </HeaderShownContext.Provider>
484
+ </HeaderHeightContext.Provider>
485
+ </AnimatedHeaderHeightContext.Provider>
486
+ </ScreenStackItem>
487
+ </NavigationRouteContext.Provider>
488
+ </NavigationContext.Provider>
489
+ );
490
+ };
491
+
492
+ type Props = {
493
+ state: StackNavigationState<ParamListBase>;
494
+ navigation: NativeStackNavigationHelpers;
495
+ descriptors: NativeStackDescriptorMap;
496
+ describe: (
497
+ route: RouteProp<ParamListBase>,
498
+ placeholder: boolean,
499
+ ) => NativeStackDescriptor;
500
+ };
501
+
502
+ export function NativeStackView({
503
+ state,
504
+ navigation,
505
+ descriptors,
506
+ describe,
507
+ }: Props) {
508
+ const { setNextDismissedKey } = useDismissedRouteError(state);
509
+
510
+ useInvalidPreventRemoveError(descriptors);
511
+
512
+ const modalRouteKeys = getModalRouteKeys(state.routes, descriptors);
513
+
514
+ const preloadedDescriptors =
515
+ state.preloadedRoutes.reduce<NativeStackDescriptorMap>((acc, route) => {
516
+ acc[route.key] = acc[route.key] || describe(route, true);
517
+ return acc;
518
+ }, {});
519
+
520
+ return (
521
+ <SafeAreaProviderCompat>
522
+ <ScreenStack style={styles.container}>
523
+ {state.routes.concat(state.preloadedRoutes).map((route, index) => {
524
+ const descriptor =
525
+ descriptors[route.key] ?? preloadedDescriptors[route.key];
526
+ const isFocused = state.index === index;
527
+ const isBelowFocused = state.index - 1 === index;
528
+ const previousKey = state.routes[index - 1]?.key;
529
+ const nextKey = state.routes[index + 1]?.key;
530
+ const previousDescriptor = previousKey
531
+ ? descriptors[previousKey]
532
+ : undefined;
533
+ const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
534
+
535
+ const isModal = modalRouteKeys.includes(route.key);
536
+
537
+ const isPreloaded =
538
+ preloadedDescriptors[route.key] !== undefined &&
539
+ descriptors[route.key] === undefined;
540
+
541
+ // On Fabric, when screen is frozen, animated and reanimated values are not updated
542
+ // due to component being unmounted. To avoid this, we don't freeze the previous screen there
543
+ const shouldFreeze = isFabric()
544
+ ? !isPreloaded && !isFocused && !isBelowFocused
545
+ : !isPreloaded && !isFocused;
546
+
547
+ return (
548
+ <SceneView
549
+ key={route.key}
550
+ index={index}
551
+ focused={isFocused}
552
+ shouldFreeze={shouldFreeze}
553
+ descriptor={descriptor}
554
+ previousDescriptor={previousDescriptor}
555
+ nextDescriptor={nextDescriptor}
556
+ isPresentationModal={isModal}
557
+ isPreloaded={isPreloaded}
558
+ onWillDisappear={() => {
559
+ navigation.emit({
560
+ type: "transitionStart",
561
+ data: { closing: true },
562
+ target: route.key,
563
+ });
564
+ }}
565
+ onWillAppear={() => {
566
+ navigation.emit({
567
+ type: "transitionStart",
568
+ data: { closing: false },
569
+ target: route.key,
570
+ });
571
+ }}
572
+ onAppear={() => {
573
+ navigation.emit({
574
+ type: "transitionEnd",
575
+ data: { closing: false },
576
+ target: route.key,
577
+ });
578
+ }}
579
+ onDisappear={() => {
580
+ navigation.emit({
581
+ type: "transitionEnd",
582
+ data: { closing: true },
583
+ target: route.key,
584
+ });
585
+ }}
586
+ onDismissed={(event) => {
587
+ navigation.dispatch({
588
+ ...StackActions.pop(event.nativeEvent.dismissCount),
589
+ source: route.key,
590
+ target: state.key,
591
+ });
592
+
593
+ setNextDismissedKey(route.key);
594
+ }}
595
+ onHeaderBackButtonClicked={() => {
596
+ navigation.dispatch({
597
+ ...StackActions.pop(),
598
+ source: route.key,
599
+ target: state.key,
600
+ });
601
+ }}
602
+ onNativeDismissCancelled={(event) => {
603
+ navigation.dispatch({
604
+ ...StackActions.pop(event.nativeEvent.dismissCount),
605
+ source: route.key,
606
+ target: state.key,
607
+ });
608
+ }}
609
+ onGestureCancel={() => {
610
+ navigation.emit({
611
+ type: "gestureCancel",
612
+ target: route.key,
613
+ });
614
+ }}
615
+ onSheetDetentChanged={(event) => {
616
+ navigation.emit({
617
+ type: "sheetDetentChange",
618
+ target: route.key,
619
+ data: {
620
+ index: event.nativeEvent.index,
621
+ stable: event.nativeEvent.isStable,
622
+ },
623
+ });
624
+ }}
625
+ />
626
+ );
627
+ })}
628
+ </ScreenStack>
629
+ </SafeAreaProviderCompat>
630
+ );
631
+ }
632
+
633
+ const styles = StyleSheet.create({
634
+ container: {
635
+ flex: 1,
636
+ },
637
+ header: {
638
+ zIndex: 1,
639
+ },
640
+ absolute: {
641
+ position: "absolute",
642
+ top: 0,
643
+ start: 0,
644
+ end: 0,
645
+ },
646
+ translucent: {
647
+ position: "absolute",
648
+ top: 0,
649
+ start: 0,
650
+ end: 0,
651
+ zIndex: 1,
652
+ elevation: 1,
653
+ },
654
+ background: {
655
+ overflow: "hidden",
656
+ },
657
+ });