@react-navigation/native-stack 8.0.0-alpha.57 → 8.0.0-alpha.59

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.
@@ -52,22 +52,17 @@ type SceneViewProps = {
52
52
  focused: boolean;
53
53
  descriptor: NativeStackDescriptor;
54
54
  previousDescriptor?: NativeStackDescriptor | undefined;
55
- nextDescriptor?: NativeStackDescriptor | undefined;
56
55
  isPresentationModal: boolean;
57
56
  isNextScreenTransparent: boolean;
58
57
  isNextScreenSheet: boolean;
59
58
  isNextScreenModal: boolean;
60
59
  isInactive: boolean;
61
60
  isBeforeLast: boolean;
62
- onWillDisappear: () => void;
63
- onWillAppear: () => void;
64
- onAppear: () => void;
65
- onDisappear: () => void;
66
- onDismissed: ScreenProps['onDismissed'];
67
- onHeaderBackButtonClicked: ScreenProps['onHeaderBackButtonClicked'];
68
- onNativeDismissCancelled: ScreenProps['onDismissed'];
69
- onGestureCancel: ScreenProps['onGestureCancel'];
70
- onSheetDetentChanged: ScreenProps['onSheetDetentChanged'];
61
+ hasNestedState: boolean;
62
+ stateKey: string;
63
+ emit: NativeStackNavigationHelpers['emit'];
64
+ dispatch: NativeStackNavigationHelpers['dispatch'];
65
+ setNextDismissedKey: (key: string) => void;
71
66
  };
72
67
 
73
68
  const useNativeDriver = Platform.OS !== 'web';
@@ -79,7 +74,7 @@ const TRANSPARENT_PRESENTATIONS = [
79
74
 
80
75
  const SHEET_PRESENTATIONS = ['formSheet', 'pageSheet'];
81
76
 
82
- const SceneView = ({
77
+ const SceneView = React.memo(function SceneView({
83
78
  index,
84
79
  focused,
85
80
  descriptor,
@@ -90,18 +85,94 @@ const SceneView = ({
90
85
  isNextScreenModal,
91
86
  isInactive,
92
87
  isBeforeLast,
93
- onWillDisappear,
94
- onWillAppear,
95
- onAppear,
96
- onDisappear,
97
- onDismissed,
98
- onHeaderBackButtonClicked,
99
- onNativeDismissCancelled,
100
- onGestureCancel,
101
- onSheetDetentChanged,
102
- }: SceneViewProps) => {
88
+ hasNestedState,
89
+ stateKey,
90
+ emit,
91
+ dispatch,
92
+ setNextDismissedKey,
93
+ }: SceneViewProps) {
103
94
  const { route, navigation, options, render } = descriptor;
104
95
 
96
+ const onWillDisappear = () => {
97
+ emit({
98
+ type: 'transitionStart',
99
+ data: { closing: true },
100
+ target: route.key,
101
+ });
102
+ };
103
+
104
+ const onWillAppear = () => {
105
+ emit({
106
+ type: 'transitionStart',
107
+ data: { closing: false },
108
+ target: route.key,
109
+ });
110
+ };
111
+
112
+ const onAppear = () => {
113
+ emit({
114
+ type: 'transitionEnd',
115
+ data: { closing: false },
116
+ target: route.key,
117
+ });
118
+ };
119
+
120
+ const onDisappear = () => {
121
+ emit({
122
+ type: 'transitionEnd',
123
+ data: { closing: true },
124
+ target: route.key,
125
+ });
126
+ };
127
+
128
+ const onDismissed: ScreenProps['onDismissed'] = (event) => {
129
+ if (isInactive) {
130
+ return;
131
+ }
132
+
133
+ dispatch({
134
+ ...StackActions.dismiss(event.nativeEvent.dismissCount),
135
+ source: route.key,
136
+ target: stateKey,
137
+ });
138
+
139
+ setNextDismissedKey(route.key);
140
+ };
141
+
142
+ const onHeaderBackButtonClicked = () => {
143
+ dispatch({
144
+ ...StackActions.pop(),
145
+ source: route.key,
146
+ target: stateKey,
147
+ });
148
+ };
149
+
150
+ const onNativeDismissCancelled: ScreenProps['onDismissed'] = (event) => {
151
+ dispatch({
152
+ ...StackActions.dismiss(event.nativeEvent.dismissCount),
153
+ source: route.key,
154
+ target: stateKey,
155
+ });
156
+ };
157
+
158
+ const onGestureCancel = () => {
159
+ emit({
160
+ type: 'gestureCancel',
161
+ target: route.key,
162
+ });
163
+ };
164
+
165
+ const onSheetDetentChanged: ScreenProps['onSheetDetentChanged'] = (event) => {
166
+ emit({
167
+ type: 'sheetDetentChange',
168
+ target: route.key,
169
+ data: {
170
+ index: event.nativeEvent.index,
171
+ stable: event.nativeEvent.isStable,
172
+ },
173
+ });
174
+ };
175
+
105
176
  const {
106
177
  inactiveBehavior = 'pause',
107
178
  animation,
@@ -307,6 +378,14 @@ const SceneView = ({
307
378
  }
308
379
  );
309
380
 
381
+ // Nested navigators clean up `route.state` when the screen is paused with `Activity`
382
+ // So we remember if the route had state until the screen is removed
383
+ const [hadNestedState, setHadNestedState] = React.useState(hasNestedState);
384
+
385
+ if (hasNestedState && !hadNestedState) {
386
+ setHadNestedState(true);
387
+ }
388
+
310
389
  const activityMode =
311
390
  // Render focused screens normally
312
391
  // Unpause preloaded and retained screens so updates are visible
@@ -325,7 +404,7 @@ const SceneView = ({
325
404
  (isNextScreenModal && !isModal) ||
326
405
  isBeforeLast
327
406
  ? 'normal'
328
- : inactiveBehavior === 'unmount' && !('state' in route && route.state)
407
+ : inactiveBehavior === 'unmount' && !hadNestedState
329
408
  ? 'unmounted'
330
409
  : 'paused';
331
410
 
@@ -477,7 +556,7 @@ const SceneView = ({
477
556
  </ScreenStackItem>
478
557
  </NavigationProvider>
479
558
  );
480
- };
559
+ });
481
560
 
482
561
  type Props = {
483
562
  state: StackNavigationState<ParamListBase>;
@@ -534,86 +613,17 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
534
613
  focused={isFocused}
535
614
  descriptor={descriptor}
536
615
  previousDescriptor={previousDescriptor}
537
- nextDescriptor={nextDescriptor}
538
616
  isPresentationModal={isModal}
539
617
  isNextScreenTransparent={isNextScreenTransparent}
540
618
  isNextScreenSheet={isNextScreenSheet}
541
619
  isNextScreenModal={isNextScreenModal}
542
620
  isInactive={index > state.index}
543
621
  isBeforeLast={index === activeRoutes.length - 2}
544
- onWillDisappear={() => {
545
- navigation.emit({
546
- type: 'transitionStart',
547
- data: { closing: true },
548
- target: route.key,
549
- });
550
- }}
551
- onWillAppear={() => {
552
- navigation.emit({
553
- type: 'transitionStart',
554
- data: { closing: false },
555
- target: route.key,
556
- });
557
- }}
558
- onAppear={() => {
559
- navigation.emit({
560
- type: 'transitionEnd',
561
- data: { closing: false },
562
- target: route.key,
563
- });
564
- }}
565
- onDisappear={() => {
566
- navigation.emit({
567
- type: 'transitionEnd',
568
- data: { closing: true },
569
- target: route.key,
570
- });
571
- }}
572
- onDismissed={(event) => {
573
- navigation.dispatch({
574
- ...StackActions.remove(
575
- route.name,
576
- event.nativeEvent.dismissCount
577
- ),
578
- source: route.key,
579
- target: state.key,
580
- });
581
-
582
- setNextDismissedKey(route.key);
583
- }}
584
- onHeaderBackButtonClicked={() => {
585
- navigation.dispatch({
586
- ...StackActions.pop(),
587
- source: route.key,
588
- target: state.key,
589
- });
590
- }}
591
- onNativeDismissCancelled={(event) => {
592
- navigation.dispatch({
593
- ...StackActions.remove(
594
- route.name,
595
- event.nativeEvent.dismissCount
596
- ),
597
- source: route.key,
598
- target: state.key,
599
- });
600
- }}
601
- onGestureCancel={() => {
602
- navigation.emit({
603
- type: 'gestureCancel',
604
- target: route.key,
605
- });
606
- }}
607
- onSheetDetentChanged={(event) => {
608
- navigation.emit({
609
- type: 'sheetDetentChange',
610
- target: route.key,
611
- data: {
612
- index: event.nativeEvent.index,
613
- stable: event.nativeEvent.isStable,
614
- },
615
- });
616
- }}
622
+ hasNestedState={route.state != null}
623
+ stateKey={state.key}
624
+ emit={navigation.emit}
625
+ dispatch={navigation.dispatch}
626
+ setNextDismissedKey={setNextDismissedKey}
617
627
  />
618
628
  );
619
629
  })}
@@ -11,6 +11,7 @@ import {
11
11
  Screen,
12
12
  } from '@react-navigation/elements/internal';
13
13
  import {
14
+ type NavigationRoute,
14
15
  type ParamListBase,
15
16
  type StackNavigationState,
16
17
  useLinkBuilder,
@@ -19,6 +20,7 @@ import * as React from 'react';
19
20
  import { Animated, StyleSheet } from 'react-native';
20
21
 
21
22
  import type {
23
+ NativeStackDescriptor,
22
24
  NativeStackDescriptorMap,
23
25
  NativeStackNavigationHelpers,
24
26
  } from '../types';
@@ -43,6 +45,24 @@ export function NativeStackView({ state, descriptors }: Props) {
43
45
  const activeRoutes = state.routes.slice(0, state.index + 1);
44
46
  const modalRouteKeys = getModalRouteKeys(activeRoutes, descriptors);
45
47
 
48
+ const [savedNestedStateRouteKeys, setSavedNestedStateRouteKeys] =
49
+ React.useState<string[]>([]);
50
+
51
+ // Nested navigators clean up `route.state` when the screen is paused with `Activity`
52
+ // So we remember routes which had state until they are removed
53
+ const nestedStateRouteKeys = state.routes
54
+ .filter(
55
+ (route) =>
56
+ route.state != null || savedNestedStateRouteKeys.includes(route.key)
57
+ )
58
+ .map((route) => route.key);
59
+
60
+ if (
61
+ nestedStateRouteKeys.some((key) => !savedNestedStateRouteKeys.includes(key))
62
+ ) {
63
+ setSavedNestedStateRouteKeys(nestedStateRouteKeys);
64
+ }
65
+
46
66
  return (
47
67
  <SafeAreaProviderCompat>
48
68
  {state.routes.map((route, i) => {
@@ -61,35 +81,7 @@ export function NativeStackView({ state, descriptors }: Props) {
61
81
  );
62
82
  }
63
83
 
64
- const { options, navigation, render } = descriptor;
65
-
66
- const headerBack = previousDescriptor
67
- ? {
68
- title: getHeaderTitle(
69
- previousDescriptor.options,
70
- previousDescriptor.route.name
71
- ),
72
- href: buildHref(
73
- previousDescriptor.route.name,
74
- previousDescriptor.route.params
75
- ),
76
- }
77
- : parentHeaderBack;
78
-
79
- const canGoBack = headerBack != null;
80
-
81
- const {
82
- inactiveBehavior = 'pause',
83
- header,
84
- headerShown,
85
- headerBackIcon,
86
- headerLeft,
87
- headerTransparent,
88
- headerBackTitle,
89
- presentation,
90
- contentStyle,
91
- ...rest
92
- } = options;
84
+ const { inactiveBehavior = 'pause' } = descriptor.options;
93
85
 
94
86
  const nextPresentation = nextDescriptor?.options.presentation;
95
87
 
@@ -118,7 +110,7 @@ export function NativeStackView({ state, descriptors }: Props) {
118
110
  : inactiveBehavior === 'unmount' &&
119
111
  !isNextScreenModal &&
120
112
  !isBeforeLast &&
121
- !route.state
113
+ !nestedStateRouteKeys.includes(route.key)
122
114
  ? 'unmounted'
123
115
  : 'paused';
124
116
 
@@ -126,84 +118,144 @@ export function NativeStackView({ state, descriptors }: Props) {
126
118
  return null;
127
119
  }
128
120
 
129
- const content = (
130
- <Screen
131
- key={route.key}
132
- focused={isFocused}
133
- route={route}
134
- navigation={navigation}
135
- headerShown={headerShown}
136
- headerTransparent={headerTransparent}
137
- pageOverflowEnabled={
138
- presentation == null || presentation === 'card'
139
- }
140
- contentStyle={contentStyle}
141
- header={
142
- header !== undefined ? (
143
- header({
144
- back: headerBack,
145
- options,
146
- route,
147
- navigation,
148
- })
149
- ) : (
150
- <Header
151
- {...rest}
152
- back={headerBack}
153
- title={getHeaderTitle(options, route.name)}
154
- headerLeft={
155
- typeof headerLeft === 'function'
156
- ? ({ label, ...rest }) =>
157
- headerLeft({
158
- ...rest,
159
- label: headerBackTitle ?? label,
160
- })
161
- : headerLeft === undefined && canGoBack
162
- ? ({ tintColor, label, ...rest }) => (
163
- <HeaderBackButton
164
- {...rest}
165
- label={headerBackTitle ?? label}
166
- tintColor={tintColor}
167
- icon={headerBackIcon}
168
- onPress={navigation.goBack}
169
- />
170
- )
171
- : headerLeft
172
- }
173
- headerTransparent={headerTransparent}
174
- />
175
- )
176
- }
177
- style={{
178
- ...(presentation != null &&
179
- TRANSPARENT_PRESENTATIONS.includes(presentation)
180
- ? { backgroundColor: 'transparent' }
181
- : null),
182
- }}
183
- >
184
- <HeaderBackContext.Provider value={headerBack}>
185
- <AnimatedHeaderHeightProvider>
186
- {render()}
187
- </AnimatedHeaderHeightProvider>
188
- </HeaderBackContext.Provider>
189
- </Screen>
190
- );
191
-
192
121
  return (
193
- <ActivityView
122
+ <SceneView
194
123
  key={route.key}
195
- mode={activityMode}
124
+ route={route}
125
+ descriptor={descriptor}
126
+ previousDescriptor={previousDescriptor}
127
+ parentHeaderBack={parentHeaderBack}
128
+ buildHref={buildHref}
129
+ focused={isFocused}
130
+ activityMode={activityMode}
196
131
  visible={isFocused || isNextScreenTransparent}
197
- style={StyleSheet.absoluteFill}
198
- >
199
- {content}
200
- </ActivityView>
132
+ />
201
133
  );
202
134
  })}
203
135
  </SafeAreaProviderCompat>
204
136
  );
205
137
  }
206
138
 
139
+ type SceneViewProps = {
140
+ route: NavigationRoute<ParamListBase, string>;
141
+ descriptor: NativeStackDescriptor;
142
+ previousDescriptor: NativeStackDescriptor | undefined;
143
+ parentHeaderBack: React.ContextType<typeof HeaderBackContext>;
144
+ buildHref: ReturnType<typeof useLinkBuilder>['buildHref'];
145
+ focused: boolean;
146
+ activityMode: 'normal' | 'inert' | 'paused';
147
+ visible: boolean;
148
+ };
149
+
150
+ const SceneView = React.memo(function SceneView({
151
+ route,
152
+ descriptor,
153
+ previousDescriptor,
154
+ parentHeaderBack,
155
+ buildHref,
156
+ focused,
157
+ activityMode,
158
+ visible,
159
+ }: SceneViewProps) {
160
+ const { options, navigation, render } = descriptor;
161
+
162
+ const headerBack = previousDescriptor
163
+ ? {
164
+ title: getHeaderTitle(
165
+ previousDescriptor.options,
166
+ previousDescriptor.route.name
167
+ ),
168
+ href: buildHref(
169
+ previousDescriptor.route.name,
170
+ previousDescriptor.route.params
171
+ ),
172
+ }
173
+ : parentHeaderBack;
174
+
175
+ const canGoBack = headerBack != null;
176
+
177
+ const {
178
+ header,
179
+ headerShown,
180
+ headerBackIcon,
181
+ headerLeft,
182
+ headerTransparent,
183
+ headerBackTitle,
184
+ presentation,
185
+ contentStyle,
186
+ ...rest
187
+ } = options;
188
+
189
+ const content = (
190
+ <Screen
191
+ key={route.key}
192
+ focused={focused}
193
+ route={route}
194
+ navigation={navigation}
195
+ headerShown={headerShown}
196
+ headerTransparent={headerTransparent}
197
+ pageOverflowEnabled={presentation == null || presentation === 'card'}
198
+ contentStyle={contentStyle}
199
+ header={
200
+ header !== undefined ? (
201
+ header({
202
+ back: headerBack,
203
+ options,
204
+ route,
205
+ navigation,
206
+ })
207
+ ) : (
208
+ <Header
209
+ {...rest}
210
+ back={headerBack}
211
+ title={getHeaderTitle(options, route.name)}
212
+ headerLeft={
213
+ typeof headerLeft === 'function'
214
+ ? ({ label, ...rest }) =>
215
+ headerLeft({
216
+ ...rest,
217
+ label: headerBackTitle ?? label,
218
+ })
219
+ : headerLeft === undefined && canGoBack
220
+ ? ({ tintColor, label, ...rest }) => (
221
+ <HeaderBackButton
222
+ {...rest}
223
+ label={headerBackTitle ?? label}
224
+ tintColor={tintColor}
225
+ icon={headerBackIcon}
226
+ onPress={navigation.goBack}
227
+ />
228
+ )
229
+ : headerLeft
230
+ }
231
+ headerTransparent={headerTransparent}
232
+ />
233
+ )
234
+ }
235
+ style={{
236
+ ...(presentation != null &&
237
+ TRANSPARENT_PRESENTATIONS.includes(presentation)
238
+ ? { backgroundColor: 'transparent' }
239
+ : null),
240
+ }}
241
+ >
242
+ <HeaderBackContext.Provider value={headerBack}>
243
+ <AnimatedHeaderHeightProvider>{render()}</AnimatedHeaderHeightProvider>
244
+ </HeaderBackContext.Provider>
245
+ </Screen>
246
+ );
247
+
248
+ return (
249
+ <ActivityView
250
+ mode={activityMode}
251
+ visible={visible}
252
+ style={StyleSheet.absoluteFill}
253
+ >
254
+ {content}
255
+ </ActivityView>
256
+ );
257
+ });
258
+
207
259
  const AnimatedHeaderHeightProvider = ({
208
260
  children,
209
261
  }: {
@@ -12,6 +12,7 @@ import { Platform, StyleSheet, type TextStyle, View } from 'react-native';
12
12
  import {
13
13
  type HeaderBarButtonItemMenuAction,
14
14
  type HeaderBarButtonItemSubmenu,
15
+ type HeaderBarButtonItemVisibilityPriority,
15
16
  type HeaderBarButtonItemWithAction,
16
17
  type HeaderBarButtonItemWithMenu,
17
18
  isSearchBarAvailableForCurrentPlatform,
@@ -73,7 +74,13 @@ const processBarButtonItems = (
73
74
  );
74
75
  }
75
76
 
76
- const { badge, label, labelStyle, icon, ...rest } = item;
77
+ const { badge, label, labelStyle, icon, visibilityPriority, ...rest } =
78
+ item;
79
+
80
+ const processedVisibilityPriority:
81
+ | HeaderBarButtonItemVisibilityPriority
82
+ | undefined =
83
+ visibilityPriority === 'default' ? 'standard' : visibilityPriority;
77
84
 
78
85
  const processedItemCommon = {
79
86
  ...rest,
@@ -84,6 +91,7 @@ const processBarButtonItems = (
84
91
  ...labelStyle,
85
92
  },
86
93
  icon: transformIcon(icon),
94
+ visibilityPriority: processedVisibilityPriority,
87
95
  };
88
96
 
89
97
  let processedItem:
@@ -164,13 +172,22 @@ const getMenuItem = (
164
172
  item: NativeStackHeaderItemMenuAction | NativeStackHeaderItemMenuSubmenu
165
173
  ): HeaderBarButtonItemMenuAction | HeaderBarButtonItemSubmenu => {
166
174
  if (item.type === 'submenu') {
167
- const { label, icon, inline, layout, items, multiselectable, ...rest } =
168
- item;
175
+ const {
176
+ label,
177
+ description,
178
+ icon,
179
+ inline,
180
+ layout,
181
+ items,
182
+ multiselectable,
183
+ ...rest
184
+ } = item;
169
185
 
170
186
  return {
171
187
  ...rest,
172
188
  icon: transformIcon(icon),
173
189
  title: label,
190
+ subtitle: description,
174
191
  displayAsPalette: layout === 'palette',
175
192
  displayInline: inline,
176
193
  singleSelection:
@@ -379,21 +396,20 @@ export function useHeaderConfigProps({
379
396
  return undefined;
380
397
  }
381
398
 
382
- if (headerBackIcon == null && Platform.OS === 'android') {
383
- try {
384
- // Use Material Symbol as default back icon on Android
385
- // So it's consistent with other material icons
386
- // Based on the available variant and weight
387
- return MaterialSymbol.getImageSource({
388
- name: 'arrow_back',
389
- color: tintColor,
390
- size: ICON_SIZE,
391
- });
392
- } catch (e) {
393
- // Fallback to default if symbol is not available
394
- // This can happen if no font, or multiple fonts are available
395
- // Or in tests where native module is not available
396
- }
399
+ if (
400
+ headerBackIcon == null &&
401
+ Platform.OS === 'android' &&
402
+ MaterialSymbol.isAvailable()
403
+ ) {
404
+ // Use Material Symbol as default back icon on Android
405
+ // So it's consistent with other material icons
406
+ // Based on the available variant and weight
407
+ return MaterialSymbol.getImageSource({
408
+ name: 'arrow_back',
409
+ color: tintColor,
410
+ size: ICON_SIZE,
411
+ direction,
412
+ });
397
413
  } else if (headerBackIcon?.type === 'image') {
398
414
  return headerBackIcon.source;
399
415
  } else if (headerBackIcon?.type === 'materialSymbol') {
@@ -401,13 +417,15 @@ export function useHeaderConfigProps({
401
417
  name: headerBackIcon.name,
402
418
  variant: headerBackIcon.variant,
403
419
  weight: headerBackIcon.weight,
420
+ fill: headerBackIcon.fill,
404
421
  color: tintColor,
405
422
  size: ICON_SIZE,
423
+ direction,
406
424
  });
407
425
  }
408
426
 
409
427
  return undefined;
410
- }, [headerBackIcon, headerBackVisible, headerShown, tintColor]);
428
+ }, [direction, headerBackIcon, headerBackVisible, headerShown, tintColor]);
411
429
 
412
430
  const children = (
413
431
  <>
@@ -446,7 +464,7 @@ export function useHeaderConfigProps({
446
464
  <>
447
465
  {headerLeftElement != null || typeof headerTitle === 'function' ? (
448
466
  // The style passed to header left, together with title element being wrapped
449
- // in flex view is reqruied for proper header layout, in particular,
467
+ // in flex view is required for proper header layout, in particular,
450
468
  // for the text truncation to work.
451
469
  <ScreenStackHeaderLeftView
452
470
  style={!isCenterViewRenderedAndroid ? { flex: 1 } : null}