@react-navigation/native-stack 8.0.0-alpha.2 → 8.0.0-alpha.21

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.
@@ -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,40 @@ 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,
100
+ ...processedItemCommon.menu,
102
101
  singleSelection: !multiselectable,
103
102
  displayAsPalette: layout === 'palette',
104
103
  items: item.menu.items.map(getMenuItem),
105
104
  },
106
105
  };
106
+ } else if (
107
+ processedItemCommon.type === 'button' &&
108
+ item.type === 'button'
109
+ ) {
110
+ processedItem = processedItemCommon;
111
+ } else {
112
+ throw new Error(
113
+ `Invalid item type: ${JSON.stringify(item)}. Valid types are 'button' and 'menu'.`
114
+ );
107
115
  }
108
116
 
109
117
  if (badge) {
110
118
  const badgeBackgroundColor =
111
119
  badge.style?.backgroundColor ?? colors.notification;
112
- const badgeTextColor = Color(badgeBackgroundColor)?.isLight()
113
- ? 'black'
114
- : 'white';
120
+ const badgeTextColor = Color.foreground(badgeBackgroundColor);
115
121
 
116
122
  processedItem = {
117
123
  ...processedItem,
@@ -138,14 +144,30 @@ const processBarButtonItems = (
138
144
  .filter((item) => item != null);
139
145
  };
140
146
 
147
+ const transformIcon = (
148
+ icon: NativeStackHeaderItemButton['icon']
149
+ ):
150
+ | HeaderBarButtonItemWithAction['icon']
151
+ | HeaderBarButtonItemWithMenu['icon'] => {
152
+ if (icon?.type === 'image') {
153
+ return icon.tinted === false
154
+ ? { type: 'imageSource', imageSource: icon.source }
155
+ : { type: 'templateSource', templateSource: icon.source };
156
+ }
157
+
158
+ return icon;
159
+ };
160
+
141
161
  const getMenuItem = (
142
162
  item: NativeStackHeaderItemMenuAction | NativeStackHeaderItemMenuSubmenu
143
163
  ): HeaderBarButtonItemMenuAction | HeaderBarButtonItemSubmenu => {
144
164
  if (item.type === 'submenu') {
145
- const { label, inline, layout, items, multiselectable, ...rest } = item;
165
+ const { label, icon, inline, layout, items, multiselectable, ...rest } =
166
+ item;
146
167
 
147
168
  return {
148
169
  ...rest,
170
+ icon: transformIcon(icon),
149
171
  title: label,
150
172
  displayAsPalette: layout === 'palette',
151
173
  displayInline: inline,
@@ -154,10 +176,11 @@ const getMenuItem = (
154
176
  };
155
177
  }
156
178
 
157
- const { label, description, ...rest } = item;
179
+ const { label, icon, description, ...rest } = item;
158
180
 
159
181
  return {
160
182
  ...rest,
183
+ icon: transformIcon(icon),
161
184
  title: label,
162
185
  subtitle: description,
163
186
  };
@@ -216,6 +239,15 @@ export function useHeaderConfigProps({
216
239
  const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
217
240
  const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
218
241
 
242
+ const headerBackgroundColor =
243
+ headerStyleFlattened.backgroundColor ??
244
+ (headerBackground != null ||
245
+ headerTransparent ||
246
+ // The title becomes invisible if background color is set with large title on iOS 26
247
+ (Platform.OS === 'ios' && headerLargeTitleEnabled)
248
+ ? 'transparent'
249
+ : colors.card);
250
+
219
251
  const backTitleFontSize =
220
252
  'fontSize' in headerBackTitleStyleFlattened
221
253
  ? headerBackTitleStyleFlattened.fontSize
@@ -226,7 +258,12 @@ export function useHeaderConfigProps({
226
258
  const titleColor =
227
259
  'color' in headerTitleStyleFlattened
228
260
  ? headerTitleStyleFlattened.color
229
- : (headerTintColor ?? colors.text);
261
+ : Platform.OS === 'ios' &&
262
+ (headerTransparent || headerBackgroundColor === 'transparent')
263
+ ? // On iOS 26, we want header title to change color based on content underneath
264
+ // So we don't set an explicit color when header is transparent
265
+ undefined
266
+ : (headerTintColor ?? colors.text);
230
267
  const titleFontSize =
231
268
  'fontSize' in headerTitleStyleFlattened
232
269
  ? headerTitleStyleFlattened.fontSize
@@ -260,15 +297,6 @@ export function useHeaderConfigProps({
260
297
  headerTitleStyleSupported.fontWeight = titleFontWeight;
261
298
  }
262
299
 
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
300
  const canGoBack = headerBack != null;
273
301
 
274
302
  const headerLeftElement = headerLeft?.({
@@ -342,6 +370,37 @@ export function useHeaderConfigProps({
342
370
  rightItems = [...rightItems].reverse();
343
371
  }
344
372
 
373
+ const backImageSource = useMemo(() => {
374
+ if (headerBackIcon == null && Platform.OS === 'android') {
375
+ try {
376
+ // Use Material Symbol as default back icon on Android
377
+ // So it's consistent with other material icons
378
+ // Based on the available variant and weight
379
+ return MaterialSymbol.getImageSource({
380
+ name: 'arrow_back',
381
+ color: tintColor,
382
+ size: ICON_SIZE,
383
+ });
384
+ } catch (e) {
385
+ // Fallback to default if symbol is not available
386
+ // This can happen if no font, or multiple fonts are available
387
+ // Or in tests where native module is not available
388
+ }
389
+ } else if (headerBackIcon?.type === 'image') {
390
+ return headerBackIcon.source;
391
+ } else if (headerBackIcon?.type === 'materialSymbol') {
392
+ return MaterialSymbol.getImageSource({
393
+ name: headerBackIcon.name,
394
+ variant: headerBackIcon.variant,
395
+ weight: headerBackIcon.weight,
396
+ color: tintColor,
397
+ size: ICON_SIZE,
398
+ });
399
+ }
400
+
401
+ return undefined;
402
+ }, [headerBackIcon, tintColor]);
403
+
345
404
  const children = (
346
405
  <>
347
406
  {Platform.OS === 'ios' ? (
@@ -417,8 +476,8 @@ export function useHeaderConfigProps({
417
476
  ) : null}
418
477
  </>
419
478
  )}
420
- {headerBackIcon !== undefined ? (
421
- <ScreenStackHeaderBackButtonImage source={headerBackIcon.source} />
479
+ {backImageSource != null ? (
480
+ <ScreenStackHeaderBackButtonImage source={backImageSource} />
422
481
  ) : null}
423
482
  {Platform.OS === 'ios' && rightItems ? (
424
483
  rightItems.map((item, index) => {