@react-navigation/native-stack 8.0.0-alpha.5 → 8.0.0-alpha.51

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 (43) hide show
  1. package/lib/module/index.js.map +1 -1
  2. package/lib/module/navigators/createNativeStackNavigator.js +16 -13
  3. package/lib/module/navigators/createNativeStackNavigator.js.map +1 -1
  4. package/lib/module/utils/useAnimatedHeaderHeight.js +1 -1
  5. package/lib/module/utils/useAnimatedHeaderHeight.js.map +1 -1
  6. package/lib/module/utils/useInvalidPreventRemoveError.js +1 -1
  7. package/lib/module/utils/useInvalidPreventRemoveError.js.map +1 -1
  8. package/lib/module/views/FooterComponent.js +1 -1
  9. package/lib/module/views/FooterComponent.js.map +1 -1
  10. package/lib/module/views/NativeStackView.js +42 -25
  11. package/lib/module/views/NativeStackView.js.map +1 -1
  12. package/lib/module/views/NativeStackView.native.js +124 -76
  13. package/lib/module/views/NativeStackView.native.js.map +1 -1
  14. package/lib/module/views/useHeaderConfigProps.js +76 -23
  15. package/lib/module/views/useHeaderConfigProps.js.map +1 -1
  16. package/lib/typescript/src/index.d.ts +3 -3
  17. package/lib/typescript/src/index.d.ts.map +1 -1
  18. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts +10 -15
  19. package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts.map +1 -1
  20. package/lib/typescript/src/types.d.ts +164 -156
  21. package/lib/typescript/src/types.d.ts.map +1 -1
  22. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts +1 -1
  23. package/lib/typescript/src/utils/getModalRoutesKeys.d.ts.map +1 -1
  24. package/lib/typescript/src/utils/useInvalidPreventRemoveError.d.ts +1 -1
  25. package/lib/typescript/src/utils/useInvalidPreventRemoveError.d.ts.map +1 -1
  26. package/lib/typescript/src/views/FooterComponent.d.ts +2 -2
  27. package/lib/typescript/src/views/FooterComponent.d.ts.map +1 -1
  28. package/lib/typescript/src/views/NativeStackView.d.ts +3 -2
  29. package/lib/typescript/src/views/NativeStackView.d.ts.map +1 -1
  30. package/lib/typescript/src/views/NativeStackView.native.d.ts +3 -2
  31. package/lib/typescript/src/views/NativeStackView.native.d.ts.map +1 -1
  32. package/lib/typescript/src/views/useHeaderConfigProps.d.ts +2 -3
  33. package/lib/typescript/src/views/useHeaderConfigProps.d.ts.map +1 -1
  34. package/package.json +22 -19
  35. package/src/index.tsx +1 -2
  36. package/src/navigators/createNativeStackNavigator.tsx +27 -52
  37. package/src/types.tsx +225 -196
  38. package/src/utils/useAnimatedHeaderHeight.tsx +1 -1
  39. package/src/utils/useInvalidPreventRemoveError.tsx +2 -1
  40. package/src/views/FooterComponent.tsx +1 -1
  41. package/src/views/NativeStackView.native.tsx +184 -97
  42. package/src/views/NativeStackView.tsx +70 -41
  43. package/src/views/useHeaderConfigProps.tsx +109 -39
@@ -1,16 +1,19 @@
1
1
  import { getHeaderTitle, HeaderTitle } from '@react-navigation/elements';
2
2
  import { Color } from '@react-navigation/elements/internal';
3
3
  import {
4
+ MaterialSymbol,
4
5
  type Route,
5
6
  type Theme,
6
7
  useLocale,
7
8
  useTheme,
8
9
  } from '@react-navigation/native';
10
+ import { useMemo } from 'react';
9
11
  import { Platform, StyleSheet, type TextStyle, View } from 'react-native';
10
12
  import {
11
- type HeaderBarButtonItem,
12
13
  type HeaderBarButtonItemMenuAction,
13
14
  type HeaderBarButtonItemSubmenu,
15
+ type HeaderBarButtonItemWithAction,
16
+ type HeaderBarButtonItemWithMenu,
14
17
  isSearchBarAvailableForCurrentPlatform,
15
18
  ScreenStackHeaderBackButtonImage,
16
19
  ScreenStackHeaderCenterView,
@@ -23,18 +26,20 @@ import {
23
26
 
24
27
  import type {
25
28
  NativeStackHeaderItem,
29
+ NativeStackHeaderItemButton,
26
30
  NativeStackHeaderItemMenuAction,
27
31
  NativeStackHeaderItemMenuSubmenu,
28
32
  NativeStackNavigationOptions,
29
33
  } from '../types';
30
34
 
31
35
  type Props = NativeStackNavigationOptions & {
32
- headerTopInsetEnabled: boolean;
33
36
  headerHeight: number;
34
37
  headerBack: { title?: string | undefined; href: undefined } | undefined;
35
38
  route: Route<string>;
36
39
  };
37
40
 
41
+ const ICON_SIZE = 24;
42
+
38
43
  const processBarButtonItems = (
39
44
  items: NativeStackHeaderItem[] | undefined,
40
45
  colors: Theme['colors'],
@@ -70,7 +75,7 @@ const processBarButtonItems = (
70
75
 
71
76
  const { badge, label, labelStyle, icon, ...rest } = item;
72
77
 
73
- let processedItem: HeaderBarButtonItem = {
78
+ const processedItemCommon = {
74
79
  ...rest,
75
80
  index,
76
81
  title: label,
@@ -78,40 +83,43 @@ const processBarButtonItems = (
78
83
  ...fonts.regular,
79
84
  ...labelStyle,
80
85
  },
81
- icon:
82
- icon?.type === 'image'
83
- ? icon.tinted === false
84
- ? {
85
- type: 'imageSource',
86
- imageSource: icon.source,
87
- }
88
- : {
89
- type: 'templateSource',
90
- templateSource: icon.source,
91
- }
92
- : icon,
86
+ icon: transformIcon(icon),
93
87
  };
94
88
 
95
- if (processedItem.type === 'menu' && item.type === 'menu') {
89
+ let processedItem:
90
+ | HeaderBarButtonItemWithAction
91
+ | HeaderBarButtonItemWithMenu;
92
+
93
+ if (processedItemCommon.type === 'menu' && item.type === 'menu') {
96
94
  const { multiselectable, layout } = item.menu;
97
95
 
98
96
  processedItem = {
99
- ...processedItem,
97
+ ...processedItemCommon,
100
98
  menu: {
101
- ...processedItem.menu,
102
- singleSelection: !multiselectable,
99
+ ...processedItemCommon.menu,
100
+ singleSelection:
101
+ typeof multiselectable === 'boolean'
102
+ ? !multiselectable
103
+ : undefined,
103
104
  displayAsPalette: layout === 'palette',
104
105
  items: item.menu.items.map(getMenuItem),
105
106
  },
106
107
  };
108
+ } else if (
109
+ processedItemCommon.type === 'button' &&
110
+ item.type === 'button'
111
+ ) {
112
+ processedItem = processedItemCommon;
113
+ } else {
114
+ throw new Error(
115
+ `Invalid item type: ${JSON.stringify(item)}. Valid types are 'button' and 'menu'.`
116
+ );
107
117
  }
108
118
 
109
119
  if (badge) {
110
120
  const badgeBackgroundColor =
111
121
  badge.style?.backgroundColor ?? colors.notification;
112
- const badgeTextColor = Color(badgeBackgroundColor)?.isLight()
113
- ? 'black'
114
- : 'white';
122
+ const badgeTextColor = Color.foreground(badgeBackgroundColor);
115
123
 
116
124
  processedItem = {
117
125
  ...processedItem,
@@ -138,26 +146,44 @@ const processBarButtonItems = (
138
146
  .filter((item) => item != null);
139
147
  };
140
148
 
149
+ const transformIcon = (
150
+ icon: NativeStackHeaderItemButton['icon']
151
+ ):
152
+ | HeaderBarButtonItemWithAction['icon']
153
+ | HeaderBarButtonItemWithMenu['icon'] => {
154
+ if (icon?.type === 'image') {
155
+ return icon.tinted === false
156
+ ? { type: 'imageSource', imageSource: icon.source }
157
+ : { type: 'templateSource', templateSource: icon.source };
158
+ }
159
+
160
+ return icon;
161
+ };
162
+
141
163
  const getMenuItem = (
142
164
  item: NativeStackHeaderItemMenuAction | NativeStackHeaderItemMenuSubmenu
143
165
  ): HeaderBarButtonItemMenuAction | HeaderBarButtonItemSubmenu => {
144
166
  if (item.type === 'submenu') {
145
- const { label, inline, layout, items, multiselectable, ...rest } = item;
167
+ const { label, icon, inline, layout, items, multiselectable, ...rest } =
168
+ item;
146
169
 
147
170
  return {
148
171
  ...rest,
172
+ icon: transformIcon(icon),
149
173
  title: label,
150
174
  displayAsPalette: layout === 'palette',
151
175
  displayInline: inline,
152
- singleSelection: !multiselectable,
176
+ singleSelection:
177
+ typeof multiselectable === 'boolean' ? !multiselectable : undefined,
153
178
  items: items.map(getMenuItem),
154
179
  };
155
180
  }
156
181
 
157
- const { label, description, ...rest } = item;
182
+ const { label, icon, description, ...rest } = item;
158
183
 
159
184
  return {
160
185
  ...rest,
186
+ icon: transformIcon(icon),
161
187
  title: label,
162
188
  subtitle: description,
163
189
  };
@@ -189,10 +215,10 @@ export function useHeaderConfigProps({
189
215
  headerTitleStyle,
190
216
  headerTransparent,
191
217
  headerSearchBarOptions,
192
- headerTopInsetEnabled,
193
218
  headerBack,
194
219
  route,
195
220
  title,
221
+ unstable_headerInsets: headerInsets,
196
222
  unstable_headerLeftItems: headerLeftItems,
197
223
  unstable_headerRightItems: headerRightItems,
198
224
  }: Props): ScreenStackHeaderConfigProps {
@@ -216,6 +242,15 @@ export function useHeaderConfigProps({
216
242
  const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
217
243
  const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
218
244
 
245
+ const headerBackgroundColor =
246
+ headerStyleFlattened.backgroundColor ??
247
+ (headerBackground != null ||
248
+ headerTransparent ||
249
+ // The title becomes invisible if background color is set with large title on iOS 26
250
+ (Platform.OS === 'ios' && headerLargeTitleEnabled)
251
+ ? 'transparent'
252
+ : colors.card);
253
+
219
254
  const backTitleFontSize =
220
255
  'fontSize' in headerBackTitleStyleFlattened
221
256
  ? headerBackTitleStyleFlattened.fontSize
@@ -226,7 +261,13 @@ export function useHeaderConfigProps({
226
261
  const titleColor =
227
262
  'color' in headerTitleStyleFlattened
228
263
  ? headerTitleStyleFlattened.color
229
- : (headerTintColor ?? colors.text);
264
+ : Platform.OS === 'ios' &&
265
+ (headerTransparent || headerBackgroundColor === 'transparent')
266
+ ? // On iOS 26, we want header title to change color based on content underneath
267
+ // So we don't set an explicit color when header is transparent
268
+ // Unless a custom tint color is explicitly provided
269
+ headerTintColor
270
+ : (headerTintColor ?? colors.text);
230
271
  const titleFontSize =
231
272
  'fontSize' in headerTitleStyleFlattened
232
273
  ? headerTitleStyleFlattened.fontSize
@@ -260,15 +301,6 @@ export function useHeaderConfigProps({
260
301
  headerTitleStyleSupported.fontWeight = titleFontWeight;
261
302
  }
262
303
 
263
- const headerBackgroundColor =
264
- headerStyleFlattened.backgroundColor ??
265
- (headerBackground != null ||
266
- headerTransparent ||
267
- // The title becomes invisible if background color is set with large title on iOS 26
268
- (Platform.OS === 'ios' && headerLargeTitleEnabled)
269
- ? 'transparent'
270
- : colors.card);
271
-
272
304
  const canGoBack = headerBack != null;
273
305
 
274
306
  const headerLeftElement = headerLeft?.({
@@ -342,6 +374,37 @@ export function useHeaderConfigProps({
342
374
  rightItems = [...rightItems].reverse();
343
375
  }
344
376
 
377
+ const backImageSource = useMemo(() => {
378
+ if (headerBackIcon == null && Platform.OS === 'android') {
379
+ try {
380
+ // Use Material Symbol as default back icon on Android
381
+ // So it's consistent with other material icons
382
+ // Based on the available variant and weight
383
+ return MaterialSymbol.getImageSource({
384
+ name: 'arrow_back',
385
+ color: tintColor,
386
+ size: ICON_SIZE,
387
+ });
388
+ } catch (e) {
389
+ // Fallback to default if symbol is not available
390
+ // This can happen if no font, or multiple fonts are available
391
+ // Or in tests where native module is not available
392
+ }
393
+ } else if (headerBackIcon?.type === 'image') {
394
+ return headerBackIcon.source;
395
+ } else if (headerBackIcon?.type === 'materialSymbol') {
396
+ return MaterialSymbol.getImageSource({
397
+ name: headerBackIcon.name,
398
+ variant: headerBackIcon.variant,
399
+ weight: headerBackIcon.weight,
400
+ color: tintColor,
401
+ size: ICON_SIZE,
402
+ });
403
+ }
404
+
405
+ return undefined;
406
+ }, [headerBackIcon, tintColor]);
407
+
345
408
  const children = (
346
409
  <>
347
410
  {Platform.OS === 'ios' ? (
@@ -417,8 +480,8 @@ export function useHeaderConfigProps({
417
480
  ) : null}
418
481
  </>
419
482
  )}
420
- {headerBackIcon !== undefined ? (
421
- <ScreenStackHeaderBackButtonImage source={headerBackIcon.source} />
483
+ {backImageSource != null ? (
484
+ <ScreenStackHeaderBackButtonImage source={backImageSource} />
422
485
  ) : null}
423
486
  {Platform.OS === 'ios' && rightItems ? (
424
487
  rightItems.map((item, index) => {
@@ -488,7 +551,14 @@ export function useHeaderConfigProps({
488
551
  titleFontFamily,
489
552
  titleFontSize,
490
553
  titleFontWeight: String(titleFontWeight),
491
- topInsetEnabled: headerTopInsetEnabled,
554
+ disableTopInsetApplication:
555
+ headerInsets?.top !== undefined ? !headerInsets.top : undefined,
556
+ disableLeftInsetApplication:
557
+ headerInsets?.left !== undefined ? !headerInsets.left : undefined,
558
+ disableRightInsetApplication:
559
+ headerInsets?.right !== undefined ? !headerInsets.right : undefined,
560
+ disableBottomInsetApplication:
561
+ headerInsets?.bottom !== undefined ? !headerInsets.bottom : undefined,
492
562
  translucent: translucent === true,
493
563
  children,
494
564
  headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),