@react-navigation/native-stack 7.20.0 → 7.21.1

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 (31) hide show
  1. package/README.md +1 -1
  2. package/lib/module/utils/useAnimatedHeaderHeight.js.map +1 -1
  3. package/lib/module/views/NativeStackView.js +100 -77
  4. package/lib/module/views/NativeStackView.js.map +1 -1
  5. package/lib/module/views/NativeStackView.native.js +101 -95
  6. package/lib/module/views/NativeStackView.native.js.map +1 -1
  7. package/lib/module/views/useHeaderConfigProps.js +26 -15
  8. package/lib/module/views/useHeaderConfigProps.js.map +1 -1
  9. package/lib/typescript/src/types.d.ts +40 -5
  10. package/lib/typescript/src/types.d.ts.map +1 -1
  11. package/lib/typescript/src/utils/useAnimatedHeaderHeight.d.ts.map +1 -1
  12. package/lib/typescript/src/views/NativeStackView.d.ts.map +1 -1
  13. package/lib/typescript/src/views/NativeStackView.native.d.ts.map +1 -1
  14. package/lib/typescript/src/views/useHeaderConfigProps.d.ts +1 -1
  15. package/lib/typescript/src/views/useHeaderConfigProps.d.ts.map +1 -1
  16. package/package.json +15 -15
  17. package/src/types.tsx +44 -7
  18. package/src/utils/useAnimatedHeaderHeight.tsx +5 -3
  19. package/src/views/NativeStackView.native.tsx +114 -108
  20. package/src/views/NativeStackView.tsx +149 -113
  21. package/src/views/useHeaderConfigProps.tsx +41 -28
  22. package/lib/module/views/FontProcessor.js +0 -6
  23. package/lib/module/views/FontProcessor.js.map +0 -1
  24. package/lib/module/views/FontProcessor.native.js +0 -12
  25. package/lib/module/views/FontProcessor.native.js.map +0 -1
  26. package/lib/typescript/src/views/FontProcessor.d.ts +0 -2
  27. package/lib/typescript/src/views/FontProcessor.d.ts.map +0 -1
  28. package/lib/typescript/src/views/FontProcessor.native.d.ts +0 -2
  29. package/lib/typescript/src/views/FontProcessor.native.d.ts.map +0 -1
  30. package/src/views/FontProcessor.native.tsx +0 -12
  31. package/src/views/FontProcessor.tsx +0 -5
@@ -37,6 +37,7 @@ import type {
37
37
  NativeStackDescriptor,
38
38
  NativeStackDescriptorMap,
39
39
  NativeStackNavigationHelpers,
40
+ NativeStackNavigationOptions,
40
41
  } from '../types';
41
42
  import { debounce } from '../utils/debounce';
42
43
  import { getModalRouteKeys } from '../utils/getModalRoutesKeys';
@@ -57,43 +58,119 @@ type SceneViewProps = {
57
58
  shouldFreeze: boolean;
58
59
  descriptor: NativeStackDescriptor;
59
60
  previousDescriptor?: NativeStackDescriptor;
60
- nextDescriptor?: NativeStackDescriptor;
61
+ nextGestureDirection?: NativeStackNavigationOptions['gestureDirection'];
61
62
  isPresentationModal?: boolean;
62
63
  isPreloaded?: boolean;
63
- onWillDisappear: () => void;
64
- onWillAppear: () => void;
65
- onAppear: () => void;
66
- onDisappear: () => void;
67
- onDismissed: ScreenProps['onDismissed'];
68
- onHeaderBackButtonClicked: ScreenProps['onHeaderBackButtonClicked'];
69
- onNativeDismissCancelled: ScreenProps['onDismissed'];
70
- onGestureCancel: ScreenProps['onGestureCancel'];
71
- onSheetDetentChanged: ScreenProps['onSheetDetentChanged'];
64
+ stateKey: string;
65
+ emit: NativeStackNavigationHelpers['emit'];
66
+ dispatch: NativeStackNavigationHelpers['dispatch'];
67
+ getState: NativeStackNavigationHelpers['getState'];
68
+ setNextDismissedKey: (key: string) => void;
72
69
  };
73
70
 
74
71
  const useNativeDriver = Platform.OS !== 'web';
75
72
 
76
- const SceneView = ({
73
+ const SceneView = React.memo(function SceneView({
77
74
  index,
78
75
  focused,
79
76
  shouldFreeze,
80
77
  descriptor,
81
78
  previousDescriptor,
82
- nextDescriptor,
79
+ nextGestureDirection,
83
80
  isPresentationModal,
84
81
  isPreloaded,
85
- onWillDisappear,
86
- onWillAppear,
87
- onAppear,
88
- onDisappear,
89
- onDismissed,
90
- onHeaderBackButtonClicked,
91
- onNativeDismissCancelled,
92
- onGestureCancel,
93
- onSheetDetentChanged,
94
- }: SceneViewProps) => {
82
+ stateKey,
83
+ emit,
84
+ dispatch,
85
+ getState,
86
+ setNextDismissedKey,
87
+ }: SceneViewProps) {
95
88
  const { route, navigation, options, render } = descriptor;
96
89
 
90
+ const onWillDisappear = () => {
91
+ emit({
92
+ type: 'transitionStart',
93
+ data: { closing: true },
94
+ target: route.key,
95
+ });
96
+ };
97
+
98
+ const onWillAppear = () => {
99
+ emit({
100
+ type: 'transitionStart',
101
+ data: { closing: false },
102
+ target: route.key,
103
+ });
104
+ };
105
+
106
+ const onAppear = () => {
107
+ emit({
108
+ type: 'transitionEnd',
109
+ data: { closing: false },
110
+ target: route.key,
111
+ });
112
+ };
113
+
114
+ const onDisappear = () => {
115
+ emit({
116
+ type: 'transitionEnd',
117
+ data: { closing: true },
118
+ target: route.key,
119
+ });
120
+ };
121
+
122
+ const onDismissed: ScreenProps['onDismissed'] = (event) => {
123
+ const currentState = getState();
124
+ const currentActiveRoutes = currentState.routes.slice(
125
+ 0,
126
+ currentState.index + 1
127
+ );
128
+
129
+ if (currentActiveRoutes.some((r) => r.key === route.key)) {
130
+ dispatch({
131
+ ...StackActions.pop(event.nativeEvent.dismissCount),
132
+ source: route.key,
133
+ target: currentState.key,
134
+ });
135
+ }
136
+
137
+ setNextDismissedKey(route.key);
138
+ };
139
+
140
+ const onHeaderBackButtonClicked = () => {
141
+ dispatch({
142
+ ...StackActions.pop(),
143
+ source: route.key,
144
+ target: stateKey,
145
+ });
146
+ };
147
+
148
+ const onNativeDismissCancelled: ScreenProps['onDismissed'] = (event) => {
149
+ dispatch({
150
+ ...StackActions.pop(event.nativeEvent.dismissCount),
151
+ source: route.key,
152
+ target: stateKey,
153
+ });
154
+ };
155
+
156
+ const onGestureCancel = () => {
157
+ emit({
158
+ type: 'gestureCancel',
159
+ target: route.key,
160
+ });
161
+ };
162
+
163
+ const onSheetDetentChanged: ScreenProps['onSheetDetentChanged'] = (event) => {
164
+ emit({
165
+ type: 'sheetDetentChange',
166
+ target: route.key,
167
+ data: {
168
+ index: event.nativeEvent.index,
169
+ stable: event.nativeEvent.isStable,
170
+ },
171
+ });
172
+ };
173
+
97
174
  let {
98
175
  animation,
99
176
  animationMatchesGesture,
@@ -161,7 +238,6 @@ const SceneView = ({
161
238
 
162
239
  // workaround for rn-screens where gestureDirection has to be set on both
163
240
  // current and previous screen - software-mansion/react-native-screens/pull/1509
164
- const nextGestureDirection = nextDescriptor?.options.gestureDirection;
165
241
  const gestureDirectionOverride =
166
242
  nextGestureDirection != null ? nextGestureDirection : gestureDirection;
167
243
 
@@ -211,7 +287,7 @@ const SceneView = ({
211
287
 
212
288
  const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight);
213
289
 
214
- // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ // eslint-disable-next-line @eslint-react/exhaustive-deps
215
291
  const setHeaderHeightDebounced = React.useCallback(
216
292
  // Debounce the header height updates to avoid excessive re-renders
217
293
  debounce(setHeaderHeight, 100),
@@ -242,11 +318,7 @@ const SceneView = ({
242
318
 
243
319
  const rawAnimatedHeaderHeight = useAnimatedValue(defaultHeaderHeight);
244
320
  const animatedHeaderHeight = React.useMemo(
245
- () =>
246
- Animated.add<number>(
247
- rawAnimatedHeaderHeight,
248
- headerHeightCorrectionOffset
249
- ),
321
+ () => Animated.add(rawAnimatedHeaderHeight, headerHeightCorrectionOffset),
250
322
  [headerHeightCorrectionOffset, rawAnimatedHeaderHeight]
251
323
  );
252
324
 
@@ -439,7 +511,7 @@ const SceneView = ({
439
511
  headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
440
512
  }
441
513
  >
442
- {headerBackground != null ? (
514
+ {headerShown !== false && headerBackground != null ? (
443
515
  /**
444
516
  * To show a custom header background, we render it at the top of the screen below the header
445
517
  * The header also needs to be positioned absolutely (with `translucent` style)
@@ -497,7 +569,7 @@ const SceneView = ({
497
569
  </ScreenStackItem>
498
570
  </NavigationProvider>
499
571
  );
500
- };
572
+ });
501
573
 
502
574
  type Props = {
503
575
  state: StackNavigationState<ParamListBase>;
@@ -515,6 +587,7 @@ export function NativeStackView({
515
587
  descriptors,
516
588
  describe,
517
589
  }: Props) {
590
+ const { colors } = useTheme();
518
591
  const { setNextDismissedKey } = useDismissedRouteError(state);
519
592
 
520
593
  useInvalidPreventRemoveError(descriptors);
@@ -529,7 +602,10 @@ export function NativeStackView({
529
602
 
530
603
  return (
531
604
  <SafeAreaProviderCompat>
532
- <ScreenStack style={styles.container}>
605
+ <ScreenStack
606
+ style={styles.container}
607
+ nativeContainerStyle={{ backgroundColor: colors.background }}
608
+ >
533
609
  {state.routes.concat(state.preloadedRoutes).map((route, index) => {
534
610
  const descriptor =
535
611
  descriptors[route.key] ?? preloadedDescriptors[route.key];
@@ -563,84 +639,14 @@ export function NativeStackView({
563
639
  shouldFreeze={shouldFreeze}
564
640
  descriptor={descriptor}
565
641
  previousDescriptor={previousDescriptor}
566
- nextDescriptor={nextDescriptor}
642
+ nextGestureDirection={nextDescriptor?.options.gestureDirection}
567
643
  isPresentationModal={isModal}
568
644
  isPreloaded={isPreloaded}
569
- onWillDisappear={() => {
570
- navigation.emit({
571
- type: 'transitionStart',
572
- data: { closing: true },
573
- target: route.key,
574
- });
575
- }}
576
- onWillAppear={() => {
577
- navigation.emit({
578
- type: 'transitionStart',
579
- data: { closing: false },
580
- target: route.key,
581
- });
582
- }}
583
- onAppear={() => {
584
- navigation.emit({
585
- type: 'transitionEnd',
586
- data: { closing: false },
587
- target: route.key,
588
- });
589
- }}
590
- onDisappear={() => {
591
- navigation.emit({
592
- type: 'transitionEnd',
593
- data: { closing: true },
594
- target: route.key,
595
- });
596
- }}
597
- onDismissed={(event) => {
598
- const currentState = navigation.getState();
599
- const currentActiveRoutes = currentState.routes.slice(
600
- 0,
601
- currentState.index + 1
602
- );
603
-
604
- if (currentActiveRoutes.some((r) => r.key === route.key)) {
605
- navigation.dispatch({
606
- ...StackActions.pop(event.nativeEvent.dismissCount),
607
- source: route.key,
608
- target: currentState.key,
609
- });
610
- }
611
-
612
- setNextDismissedKey(route.key);
613
- }}
614
- onHeaderBackButtonClicked={() => {
615
- navigation.dispatch({
616
- ...StackActions.pop(),
617
- source: route.key,
618
- target: state.key,
619
- });
620
- }}
621
- onNativeDismissCancelled={(event) => {
622
- navigation.dispatch({
623
- ...StackActions.pop(event.nativeEvent.dismissCount),
624
- source: route.key,
625
- target: state.key,
626
- });
627
- }}
628
- onGestureCancel={() => {
629
- navigation.emit({
630
- type: 'gestureCancel',
631
- target: route.key,
632
- });
633
- }}
634
- onSheetDetentChanged={(event) => {
635
- navigation.emit({
636
- type: 'sheetDetentChange',
637
- target: route.key,
638
- data: {
639
- index: event.nativeEvent.index,
640
- stable: event.nativeEvent.isStable,
641
- },
642
- });
643
- }}
645
+ stateKey={state.key}
646
+ emit={navigation.emit}
647
+ dispatch={navigation.dispatch}
648
+ getState={navigation.getState}
649
+ setNextDismissedKey={setNextDismissedKey}
644
650
  />
645
651
  );
646
652
  })}
@@ -8,6 +8,7 @@ import {
8
8
  useHeaderHeight,
9
9
  } from '@react-navigation/elements';
10
10
  import {
11
+ type NavigationRoute,
11
12
  type ParamListBase,
12
13
  type RouteProp,
13
14
  type StackNavigationState,
@@ -49,143 +50,178 @@ export function NativeStackView({ state, descriptors, describe }: Props) {
49
50
  return acc;
50
51
  }, {});
51
52
 
53
+ const activeRoutes = state.routes.slice(0, state.index + 1);
54
+ const lastOpaqueIndex = activeRoutes.findLastIndex((route) => {
55
+ const presentation = descriptors[route.key]?.options.presentation;
56
+
57
+ return (
58
+ presentation == null || !TRANSPARENT_PRESENTATIONS.includes(presentation)
59
+ );
60
+ });
61
+
52
62
  return (
53
63
  <SafeAreaProviderCompat>
54
64
  {state.routes.concat(state.preloadedRoutes).map((route, i) => {
55
65
  const isFocused = state.index === i;
56
66
  const previousKey = state.routes[i - 1]?.key;
57
- const nextKey = state.routes[i + 1]?.key;
58
67
  const previousDescriptor = previousKey
59
68
  ? descriptors[previousKey]
60
69
  : undefined;
61
- const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
62
- const { options, navigation, render } =
70
+ const descriptor =
63
71
  descriptors[route.key] ?? preloadedDescriptors[route.key];
64
72
 
65
- const headerBack = previousDescriptor
66
- ? {
67
- title: getHeaderTitle(
68
- previousDescriptor.options,
69
- previousDescriptor.route.name
70
- ),
71
- href: buildHref(
72
- previousDescriptor.route.name,
73
- previousDescriptor.route.params
74
- ),
75
- }
76
- : parentHeaderBack;
77
-
78
- const canGoBack = headerBack != null;
79
-
80
- const {
81
- header,
82
- headerShown,
83
- headerBackIcon,
84
- headerBackImageSource,
85
- headerLeft,
86
- headerTransparent,
87
- headerBackTitle,
88
- presentation,
89
- contentStyle,
90
- ...rest
91
- } = options;
92
-
93
- const nextPresentation = nextDescriptor?.options.presentation;
73
+ const isVisible = i >= lastOpaqueIndex && i <= state.index;
94
74
 
95
75
  const isPreloaded =
96
76
  preloadedDescriptors[route.key] !== undefined &&
97
77
  descriptors[route.key] === undefined;
98
78
 
99
79
  return (
100
- <Screen
80
+ <SceneView
101
81
  key={route.key}
102
- focused={isFocused}
103
82
  route={route}
104
- navigation={navigation}
105
- headerShown={headerShown}
106
- headerTransparent={headerTransparent}
107
- header={
108
- header !== undefined ? (
109
- header({
110
- back: headerBack,
111
- options,
112
- route,
113
- navigation,
114
- })
115
- ) : (
116
- <Header
117
- {...rest}
118
- back={headerBack}
119
- title={getHeaderTitle(options, route.name)}
120
- headerLeft={
121
- typeof headerLeft === 'function'
122
- ? ({ label, ...rest }) =>
123
- headerLeft({
124
- ...rest,
125
- label: headerBackTitle ?? label,
126
- })
127
- : headerLeft === undefined && canGoBack
128
- ? ({ tintColor, label, ...rest }) => (
129
- <HeaderBackButton
130
- {...rest}
131
- label={headerBackTitle ?? label}
132
- tintColor={tintColor}
133
- backImage={
134
- headerBackIcon !== undefined ||
135
- headerBackImageSource !== undefined
136
- ? () => (
137
- <Image
138
- source={
139
- headerBackIcon?.source ??
140
- headerBackImageSource
141
- }
142
- resizeMode="contain"
143
- tintColor={tintColor}
144
- style={styles.backImage}
145
- />
146
- )
147
- : undefined
148
- }
149
- onPress={navigation.goBack}
150
- />
151
- )
152
- : headerLeft
153
- }
154
- headerTransparent={headerTransparent}
155
- />
156
- )
157
- }
158
- style={[
159
- StyleSheet.absoluteFill,
160
- {
161
- display:
162
- (isFocused ||
163
- (nextPresentation != null &&
164
- TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
165
- !isPreloaded
166
- ? 'flex'
167
- : 'none',
168
- },
169
- presentation != null &&
170
- TRANSPARENT_PRESENTATIONS.includes(presentation)
171
- ? { backgroundColor: 'transparent' }
172
- : null,
173
- ]}
174
- >
175
- <HeaderBackContext.Provider value={headerBack}>
176
- <AnimatedHeaderHeightProvider>
177
- <View style={[styles.contentContainer, contentStyle]}>
178
- {render()}
179
- </View>
180
- </AnimatedHeaderHeightProvider>
181
- </HeaderBackContext.Provider>
182
- </Screen>
83
+ descriptor={descriptor}
84
+ previousDescriptor={previousDescriptor}
85
+ parentHeaderBack={parentHeaderBack}
86
+ buildHref={buildHref}
87
+ focused={isFocused}
88
+ visible={isVisible && !isPreloaded}
89
+ />
183
90
  );
184
91
  })}
185
92
  </SafeAreaProviderCompat>
186
93
  );
187
94
  }
188
95
 
96
+ type SceneViewProps = {
97
+ route: NavigationRoute<ParamListBase, string>;
98
+ descriptor: NativeStackDescriptor;
99
+ previousDescriptor: NativeStackDescriptor | undefined;
100
+ parentHeaderBack: React.ContextType<typeof HeaderBackContext>;
101
+ buildHref: ReturnType<typeof useLinkBuilder>['buildHref'];
102
+ focused: boolean;
103
+ visible: boolean;
104
+ };
105
+
106
+ const SceneView = React.memo(function SceneView({
107
+ route,
108
+ descriptor,
109
+ previousDescriptor,
110
+ parentHeaderBack,
111
+ buildHref,
112
+ focused,
113
+ visible,
114
+ }: SceneViewProps) {
115
+ const { options, navigation, render } = descriptor;
116
+
117
+ const headerBack = previousDescriptor
118
+ ? {
119
+ title: getHeaderTitle(
120
+ previousDescriptor.options,
121
+ previousDescriptor.route.name
122
+ ),
123
+ href: buildHref(
124
+ previousDescriptor.route.name,
125
+ previousDescriptor.route.params
126
+ ),
127
+ }
128
+ : parentHeaderBack;
129
+
130
+ const canGoBack = headerBack != null;
131
+
132
+ const {
133
+ header,
134
+ headerShown,
135
+ headerBackIcon,
136
+ headerBackImageSource,
137
+ headerLeft,
138
+ headerTransparent,
139
+ headerBackTitle,
140
+ presentation,
141
+ contentStyle,
142
+ ...rest
143
+ } = options;
144
+
145
+ return (
146
+ <Screen
147
+ key={route.key}
148
+ focused={focused}
149
+ route={route}
150
+ navigation={navigation}
151
+ headerShown={headerShown}
152
+ headerTransparent={headerTransparent}
153
+ header={
154
+ header !== undefined ? (
155
+ header({
156
+ back: headerBack,
157
+ options,
158
+ route,
159
+ navigation,
160
+ })
161
+ ) : (
162
+ <Header
163
+ {...rest}
164
+ back={headerBack}
165
+ title={getHeaderTitle(options, route.name)}
166
+ headerLeft={
167
+ typeof headerLeft === 'function'
168
+ ? ({ label, ...rest }) =>
169
+ headerLeft({
170
+ ...rest,
171
+ label: headerBackTitle ?? label,
172
+ })
173
+ : headerLeft === undefined && canGoBack
174
+ ? ({ tintColor, label, ...rest }) => (
175
+ <HeaderBackButton
176
+ {...rest}
177
+ label={headerBackTitle ?? label}
178
+ tintColor={tintColor}
179
+ backImage={
180
+ headerBackIcon !== undefined ||
181
+ headerBackImageSource !== undefined
182
+ ? () => (
183
+ <Image
184
+ source={
185
+ headerBackIcon?.source ??
186
+ headerBackImageSource
187
+ }
188
+ resizeMode="contain"
189
+ tintColor={tintColor}
190
+ style={styles.backImage}
191
+ />
192
+ )
193
+ : undefined
194
+ }
195
+ onPress={navigation.goBack}
196
+ />
197
+ )
198
+ : headerLeft
199
+ }
200
+ headerTransparent={headerTransparent}
201
+ />
202
+ )
203
+ }
204
+ style={[
205
+ StyleSheet.absoluteFill,
206
+ {
207
+ display: visible ? 'flex' : 'none',
208
+ },
209
+ presentation != null && TRANSPARENT_PRESENTATIONS.includes(presentation)
210
+ ? { backgroundColor: 'transparent' }
211
+ : null,
212
+ ]}
213
+ >
214
+ <HeaderBackContext.Provider value={headerBack}>
215
+ <AnimatedHeaderHeightProvider>
216
+ <View style={[styles.contentContainer, contentStyle]}>
217
+ {render()}
218
+ </View>
219
+ </AnimatedHeaderHeightProvider>
220
+ </HeaderBackContext.Provider>
221
+ </Screen>
222
+ );
223
+ });
224
+
189
225
  const AnimatedHeaderHeightProvider = ({
190
226
  children,
191
227
  }: {