@react-navigation/native-stack 8.0.0-alpha.3 → 8.0.0-alpha.30

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