@react-navigation/native-stack 6.6.1 → 6.8.0

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 (30) hide show
  1. package/lib/commonjs/utils/useDismissedRouteError.js +29 -0
  2. package/lib/commonjs/utils/useDismissedRouteError.js.map +1 -0
  3. package/lib/commonjs/utils/useInvalidPreventRemoveError.js +33 -0
  4. package/lib/commonjs/utils/useInvalidPreventRemoveError.js.map +1 -0
  5. package/lib/commonjs/views/HeaderConfig.js +6 -3
  6. package/lib/commonjs/views/HeaderConfig.js.map +1 -1
  7. package/lib/commonjs/views/NativeStackView.js +16 -14
  8. package/lib/commonjs/views/NativeStackView.js.map +1 -1
  9. package/lib/commonjs/views/NativeStackView.native.js +99 -24
  10. package/lib/commonjs/views/NativeStackView.native.js.map +1 -1
  11. package/lib/module/utils/useDismissedRouteError.js +17 -0
  12. package/lib/module/utils/useDismissedRouteError.js.map +1 -0
  13. package/lib/module/utils/useInvalidPreventRemoveError.js +20 -0
  14. package/lib/module/utils/useInvalidPreventRemoveError.js.map +1 -0
  15. package/lib/module/views/HeaderConfig.js +7 -4
  16. package/lib/module/views/HeaderConfig.js.map +1 -1
  17. package/lib/module/views/NativeStackView.js +17 -15
  18. package/lib/module/views/NativeStackView.js.map +1 -1
  19. package/lib/module/views/NativeStackView.native.js +99 -26
  20. package/lib/module/views/NativeStackView.native.js.map +1 -1
  21. package/lib/typescript/src/types.d.ts +48 -0
  22. package/lib/typescript/src/utils/useDismissedRouteError.d.ts +5 -0
  23. package/lib/typescript/src/utils/useInvalidPreventRemoveError.d.ts +2 -0
  24. package/package.json +12 -11
  25. package/src/types.tsx +48 -0
  26. package/src/utils/useDismissedRouteError.tsx +30 -0
  27. package/src/utils/useInvalidPreventRemoveError.tsx +31 -0
  28. package/src/views/HeaderConfig.tsx +8 -5
  29. package/src/views/NativeStackView.native.tsx +108 -32
  30. package/src/views/NativeStackView.tsx +27 -26
@@ -1,4 +1,4 @@
1
- import { HeaderTitle } from '@react-navigation/elements';
1
+ import { getHeaderTitle, HeaderTitle } from '@react-navigation/elements';
2
2
  import { Route, useTheme } from '@react-navigation/native';
3
3
  import * as React from 'react';
4
4
  import {
@@ -79,7 +79,7 @@ export default function HeaderConfig({
79
79
  headerTitleStyleFlattened.fontFamily,
80
80
  ]);
81
81
 
82
- const titleText = title !== undefined ? title : route.name;
82
+ const titleText = getHeaderTitle({ title, headerTitle }, route.name);
83
83
  const titleColor =
84
84
  headerTitleStyleFlattened.color ?? headerTintColor ?? colors.text;
85
85
  const titleFontSize = headerTitleStyleFlattened.fontSize;
@@ -110,7 +110,10 @@ export default function HeaderConfig({
110
110
  });
111
111
  const headerTitleElement =
112
112
  typeof headerTitle === 'function'
113
- ? headerTitle({ tintColor, children: titleText })
113
+ ? headerTitle({
114
+ tintColor,
115
+ children: titleText,
116
+ })
114
117
  : null;
115
118
 
116
119
  const supportsHeaderSearchBar =
@@ -171,7 +174,7 @@ export default function HeaderConfig({
171
174
  backTitleFontSize={headerBackTitleStyleFlattened.fontSize}
172
175
  blurEffect={headerBlurEffect}
173
176
  color={tintColor}
174
- direction={I18nManager.isRTL ? 'rtl' : 'ltr'}
177
+ direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
175
178
  disableBackButtonMenu={headerBackButtonMenuEnabled === false}
176
179
  hidden={headerShown === false}
177
180
  hideBackButton={headerBackVisible === false}
@@ -187,7 +190,7 @@ export default function HeaderConfig({
187
190
  largeTitleFontSize={headerLargeTitleStyleFlattened.fontSize}
188
191
  largeTitleFontWeight={headerLargeTitleStyleFlattened.fontWeight}
189
192
  largeTitleHideShadow={headerLargeTitleShadowVisible === false}
190
- title={typeof headerTitle === 'string' ? headerTitle : titleText}
193
+ title={titleText}
191
194
  titleColor={titleColor}
192
195
  titleFontFamily={titleFontFamily}
193
196
  titleFontSize={titleFontSize}
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getDefaultHeaderHeight,
3
3
  getHeaderTitle,
4
+ HeaderBackContext,
4
5
  HeaderHeightContext,
5
6
  HeaderShownContext,
6
7
  SafeAreaProviderCompat,
@@ -12,6 +13,7 @@ import {
12
13
  Route,
13
14
  StackActions,
14
15
  StackNavigationState,
16
+ usePreventRemoveContext,
15
17
  useTheme,
16
18
  } from '@react-navigation/native';
17
19
  import * as React from 'react';
@@ -34,6 +36,8 @@ import type {
34
36
  NativeStackNavigationHelpers,
35
37
  NativeStackNavigationOptions,
36
38
  } from '../types';
39
+ import useDismissedRouteError from '../utils/useDismissedRouteError';
40
+ import useInvalidPreventRemoveError from '../utils/useInvalidPreventRemoveError';
37
41
  import DebugContainer from './DebugContainer';
38
42
  import HeaderConfig from './HeaderConfig';
39
43
 
@@ -111,37 +115,76 @@ type SceneViewProps = {
111
115
  index: number;
112
116
  descriptor: NativeStackDescriptor;
113
117
  previousDescriptor?: NativeStackDescriptor;
118
+ nextDescriptor?: NativeStackDescriptor;
114
119
  onWillDisappear: () => void;
115
120
  onAppear: () => void;
116
121
  onDisappear: () => void;
117
122
  onDismissed: ScreenProps['onDismissed'];
123
+ onHeaderBackButtonClicked: () => void;
124
+ onNativeDismissCancelled: ScreenProps['onDismissed'];
118
125
  };
119
126
 
120
127
  const SceneView = ({
121
128
  descriptor,
122
129
  previousDescriptor,
130
+ nextDescriptor,
123
131
  index,
124
132
  onWillDisappear,
125
133
  onAppear,
126
134
  onDisappear,
127
135
  onDismissed,
136
+ onHeaderBackButtonClicked,
137
+ onNativeDismissCancelled,
128
138
  }: SceneViewProps) => {
129
139
  const { route, navigation, options, render } = descriptor;
130
140
  const {
131
- animation,
141
+ animationDuration,
132
142
  animationTypeForReplace = 'push',
133
- customAnimationOnGesture,
134
- fullScreenGestureEnabled,
135
143
  gestureEnabled,
136
144
  header,
145
+ headerBackButtonMenuEnabled,
137
146
  headerShown,
147
+ autoHideHomeIndicator,
148
+ navigationBarColor,
149
+ navigationBarHidden,
138
150
  orientation,
139
151
  statusBarAnimation,
140
152
  statusBarHidden,
141
153
  statusBarStyle,
154
+ statusBarTranslucent,
155
+ statusBarColor,
156
+ } = options;
157
+
158
+ let {
159
+ animation,
160
+ customAnimationOnGesture,
161
+ fullScreenGestureEnabled,
162
+ presentation = 'card',
163
+ gestureDirection = presentation === 'card' ? 'horizontal' : 'vertical',
142
164
  } = options;
143
165
 
144
- let { presentation = 'card' } = options;
166
+ if (gestureDirection === 'vertical' && Platform.OS === 'ios') {
167
+ // for `vertical` direction to work, we need to set `fullScreenGestureEnabled` to `true`
168
+ // so the screen can be dismissed from any point on screen.
169
+ // `customAnimationOnGesture` needs to be set to `true` so the `animation` set by user can be used,
170
+ // otherwise `simple_push` will be used.
171
+ // Also, the default animation for this direction seems to be `slide_from_bottom`.
172
+ if (fullScreenGestureEnabled === undefined) {
173
+ fullScreenGestureEnabled = true;
174
+ }
175
+ if (customAnimationOnGesture === undefined) {
176
+ customAnimationOnGesture = true;
177
+ }
178
+ if (animation === undefined) {
179
+ animation = 'slide_from_bottom';
180
+ }
181
+ }
182
+
183
+ // workaround for rn-screens where gestureDirection has to be set on both
184
+ // current and previous screen - software-mansion/react-native-screens/pull/1509
185
+ const nextGestureDirection = nextDescriptor?.options.gestureDirection;
186
+ const gestureDirectionOverride =
187
+ nextGestureDirection != null ? nextGestureDirection : gestureDirection;
145
188
 
146
189
  if (index === 0) {
147
190
  // first screen should always be treated as `card`, it resolves problems with no header animation
@@ -161,13 +204,16 @@ const SceneView = ({
161
204
 
162
205
  // Modals are fullscreen in landscape only on iPhone
163
206
  const isIPhone =
164
- Platform.OS === 'ios' && !(Platform.isPad && Platform.isTVOS);
207
+ Platform.OS === 'ios' && !(Platform.isPad || Platform.isTVOS);
165
208
  const isLandscape = frame.width > frame.height;
166
209
 
167
210
  const topInset = isModal || (isIPhone && isLandscape) ? 0 : insets.top;
168
211
 
169
212
  const isParentHeaderShown = React.useContext(HeaderShownContext);
170
213
  const parentHeaderHeight = React.useContext(HeaderHeightContext);
214
+ const parentHeaderBack = React.useContext(HeaderBackContext);
215
+
216
+ const { preventedRoutes } = usePreventRemoveContext();
171
217
 
172
218
  const defaultHeaderHeight = getDefaultHeaderHeight(frame, isModal, topInset);
173
219
 
@@ -175,6 +221,16 @@ const SceneView = ({
175
221
  React.useState(defaultHeaderHeight);
176
222
 
177
223
  const headerHeight = header ? customHeaderHeight : defaultHeaderHeight;
224
+ const headerBack = previousDescriptor
225
+ ? {
226
+ title: getHeaderTitle(
227
+ previousDescriptor.options,
228
+ previousDescriptor.route.name
229
+ ),
230
+ }
231
+ : parentHeaderBack;
232
+
233
+ const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
178
234
 
179
235
  return (
180
236
  <Screen
@@ -190,6 +246,9 @@ const SceneView = ({
190
246
  false
191
247
  : gestureEnabled
192
248
  }
249
+ homeIndicatorHidden={autoHideHomeIndicator}
250
+ navigationBarColor={navigationBarColor}
251
+ navigationBarHidden={navigationBarHidden}
193
252
  replaceAnimation={animationTypeForReplace}
194
253
  stackPresentation={presentation === 'card' ? 'push' : presentation}
195
254
  stackAnimation={animation}
@@ -197,11 +256,21 @@ const SceneView = ({
197
256
  statusBarAnimation={statusBarAnimation}
198
257
  statusBarHidden={statusBarHidden}
199
258
  statusBarStyle={statusBarStyle}
259
+ statusBarColor={statusBarColor}
260
+ statusBarTranslucent={statusBarTranslucent}
261
+ swipeDirection={gestureDirectionOverride}
262
+ transitionDuration={animationDuration}
200
263
  onWillDisappear={onWillDisappear}
201
264
  onAppear={onAppear}
202
265
  onDisappear={onDisappear}
203
266
  onDismissed={onDismissed}
204
267
  isNativeStack
268
+ // Props for enabling preventing removal in native-stack
269
+ nativeBackButtonDismissalEnabled={false} // on Android
270
+ // @ts-expect-error prop not publicly exported from rn-screens
271
+ preventNativeDismiss={isRemovePrevented} // on iOS
272
+ onHeaderBackButtonClicked={onHeaderBackButtonClicked}
273
+ onNativeDismissCancelled={onNativeDismissCancelled}
205
274
  >
206
275
  <NavigationContext.Provider value={navigation}>
207
276
  <NavigationRouteContext.Provider value={route}>
@@ -222,14 +291,7 @@ const SceneView = ({
222
291
  }}
223
292
  >
224
293
  {header({
225
- back: previousDescriptor
226
- ? {
227
- title: getHeaderTitle(
228
- previousDescriptor.options,
229
- previousDescriptor.route.name
230
- ),
231
- }
232
- : undefined,
294
+ back: headerBack,
233
295
  options,
234
296
  route,
235
297
  navigation,
@@ -239,9 +301,19 @@ const SceneView = ({
239
301
  <HeaderConfig
240
302
  {...options}
241
303
  route={route}
304
+ headerBackButtonMenuEnabled={
305
+ isRemovePrevented !== undefined
306
+ ? !isRemovePrevented
307
+ : headerBackButtonMenuEnabled
308
+ }
242
309
  headerShown={isHeaderInPush}
243
310
  headerHeight={headerHeight}
244
- canGoBack={index !== 0}
311
+ headerBackTitle={
312
+ options.headerBackTitle !== undefined
313
+ ? options.headerBackTitle
314
+ : headerBack?.title
315
+ }
316
+ canGoBack={headerBack !== undefined}
245
317
  />
246
318
  )}
247
319
  <MaybeNestedStack
@@ -250,7 +322,9 @@ const SceneView = ({
250
322
  presentation={presentation}
251
323
  headerHeight={headerHeight}
252
324
  >
253
- {render()}
325
+ <HeaderBackContext.Provider value={headerBack}>
326
+ {render()}
327
+ </HeaderBackContext.Provider>
254
328
  </MaybeNestedStack>
255
329
  </HeaderHeightContext.Provider>
256
330
  </HeaderShownContext.Provider>
@@ -267,33 +341,20 @@ type Props = {
267
341
  };
268
342
 
269
343
  function NativeStackViewInner({ state, navigation, descriptors }: Props) {
270
- const [nextDismissedKey, setNextDismissedKey] = React.useState<string | null>(
271
- null
272
- );
273
-
274
- const dismissedRouteName = nextDismissedKey
275
- ? state.routes.find((route) => route.key === nextDismissedKey)?.name
276
- : null;
277
-
278
- React.useEffect(() => {
279
- if (dismissedRouteName) {
280
- const message =
281
- `The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` +
282
- `This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` +
283
- `Consider using 'gestureEnabled: false' to prevent back gesture and use a custom back button with 'headerLeft' option to override the native behavior.`;
344
+ const { setNextDismissedKey } = useDismissedRouteError(state);
284
345
 
285
- console.error(message);
286
- }
287
- }, [dismissedRouteName]);
346
+ useInvalidPreventRemoveError(descriptors);
288
347
 
289
348
  return (
290
349
  <ScreenStack style={styles.container}>
291
350
  {state.routes.map((route, index) => {
292
351
  const descriptor = descriptors[route.key];
293
352
  const previousKey = state.routes[index - 1]?.key;
353
+ const nextKey = state.routes[index + 1]?.key;
294
354
  const previousDescriptor = previousKey
295
355
  ? descriptors[previousKey]
296
356
  : undefined;
357
+ const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
297
358
 
298
359
  return (
299
360
  <SceneView
@@ -301,6 +362,7 @@ function NativeStackViewInner({ state, navigation, descriptors }: Props) {
301
362
  index={index}
302
363
  descriptor={descriptor}
303
364
  previousDescriptor={previousDescriptor}
365
+ nextDescriptor={nextDescriptor}
304
366
  onWillDisappear={() => {
305
367
  navigation.emit({
306
368
  type: 'transitionStart',
@@ -331,6 +393,20 @@ function NativeStackViewInner({ state, navigation, descriptors }: Props) {
331
393
 
332
394
  setNextDismissedKey(route.key);
333
395
  }}
396
+ onHeaderBackButtonClicked={() => {
397
+ navigation.dispatch({
398
+ ...StackActions.pop(),
399
+ source: route.key,
400
+ target: state.key,
401
+ });
402
+ }}
403
+ onNativeDismissCancelled={(event) => {
404
+ navigation.dispatch({
405
+ ...StackActions.pop(event.nativeEvent.dismissCount),
406
+ source: route.key,
407
+ target: state.key,
408
+ });
409
+ }}
334
410
  />
335
411
  );
336
412
  })}
@@ -2,6 +2,7 @@ import {
2
2
  getHeaderTitle,
3
3
  Header,
4
4
  HeaderBackButton,
5
+ HeaderBackContext,
5
6
  SafeAreaProviderCompat,
6
7
  Screen,
7
8
  } from '@react-navigation/elements';
@@ -31,20 +32,32 @@ const TRANSPARENT_PRESENTATIONS = [
31
32
  ];
32
33
 
33
34
  export default function NativeStackView({ state, descriptors }: Props) {
35
+ const parentHeaderBack = React.useContext(HeaderBackContext);
36
+
34
37
  return (
35
38
  <SafeAreaProviderCompat>
36
39
  <View style={styles.container}>
37
40
  {state.routes.map((route, i) => {
38
41
  const isFocused = state.index === i;
39
- const canGoBack = i !== 0;
40
42
  const previousKey = state.routes[i - 1]?.key;
41
43
  const nextKey = state.routes[i + 1]?.key;
42
44
  const previousDescriptor = previousKey
43
45
  ? descriptors[previousKey]
44
46
  : undefined;
45
- const nexDescriptor = nextKey ? descriptors[nextKey] : undefined;
47
+ const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
46
48
  const { options, navigation, render } = descriptors[route.key];
47
49
 
50
+ const headerBack = previousDescriptor
51
+ ? {
52
+ title: getHeaderTitle(
53
+ previousDescriptor.options,
54
+ previousDescriptor.route.name
55
+ ),
56
+ }
57
+ : parentHeaderBack;
58
+
59
+ const canGoBack = headerBack !== undefined;
60
+
48
61
  const {
49
62
  header,
50
63
  headerShown,
@@ -58,12 +71,13 @@ export default function NativeStackView({ state, descriptors }: Props) {
58
71
  headerStyle,
59
72
  headerShadowVisible,
60
73
  headerTransparent,
74
+ headerBackground,
61
75
  headerBackTitle,
62
76
  presentation,
63
77
  contentStyle,
64
78
  } = options;
65
79
 
66
- const nextPresentation = nexDescriptor?.options.presentation;
80
+ const nextPresentation = nextDescriptor?.options.presentation;
67
81
 
68
82
  return (
69
83
  <Screen
@@ -76,14 +90,7 @@ export default function NativeStackView({ state, descriptors }: Props) {
76
90
  header={
77
91
  header !== undefined ? (
78
92
  header({
79
- back: previousDescriptor
80
- ? {
81
- title: getHeaderTitle(
82
- previousDescriptor.options,
83
- previousDescriptor.route.name
84
- ),
85
- }
86
- : undefined,
93
+ back: headerBack,
87
94
  options,
88
95
  route,
89
96
  navigation,
@@ -137,18 +144,10 @@ export default function NativeStackView({ state, descriptors }: Props) {
137
144
  }
138
145
  headerTitleAlign={headerTitleAlign}
139
146
  headerTitleStyle={headerTitleStyle}
140
- headerStyle={[
141
- headerTransparent
142
- ? {
143
- position: 'absolute',
144
- backgroundColor: 'transparent',
145
- }
146
- : null,
147
- headerStyle,
148
- headerShadowVisible === false
149
- ? { shadowOpacity: 0, borderBottomWidth: 0 }
150
- : null,
151
- ]}
147
+ headerTransparent={headerTransparent}
148
+ headerShadowVisible={headerShadowVisible}
149
+ headerBackground={headerBackground}
150
+ headerStyle={headerStyle}
152
151
  />
153
152
  )
154
153
  }
@@ -168,9 +167,11 @@ export default function NativeStackView({ state, descriptors }: Props) {
168
167
  : null,
169
168
  ]}
170
169
  >
171
- <View style={[styles.contentContainer, contentStyle]}>
172
- {render()}
173
- </View>
170
+ <HeaderBackContext.Provider value={headerBack}>
171
+ <View style={[styles.contentContainer, contentStyle]}>
172
+ {render()}
173
+ </View>
174
+ </HeaderBackContext.Provider>
174
175
  </Screen>
175
176
  );
176
177
  })}