@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.
- package/lib/module/navigators/createNativeStackNavigator.js +1 -1
- package/lib/module/navigators/createNativeStackNavigator.js.map +1 -1
- package/lib/module/utils/useAnimatedHeaderHeight.js +1 -1
- package/lib/module/utils/useAnimatedHeaderHeight.js.map +1 -1
- package/lib/module/views/NativeStackView.js +29 -20
- package/lib/module/views/NativeStackView.js.map +1 -1
- package/lib/module/views/NativeStackView.native.js +72 -60
- package/lib/module/views/NativeStackView.native.js.map +1 -1
- package/lib/module/views/useHeaderConfigProps.js +68 -19
- package/lib/module/views/useHeaderConfigProps.js.map +1 -1
- package/lib/typescript/src/types.d.ts +181 -146
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/views/NativeStackView.d.ts.map +1 -1
- package/lib/typescript/src/views/NativeStackView.native.d.ts.map +1 -1
- package/lib/typescript/src/views/useHeaderConfigProps.d.ts.map +1 -1
- package/package.json +14 -15
- package/src/navigators/createNativeStackNavigator.tsx +1 -1
- package/src/types.tsx +242 -184
- package/src/utils/useAnimatedHeaderHeight.tsx +1 -1
- package/src/views/NativeStackView.native.tsx +112 -79
- package/src/views/NativeStackView.tsx +46 -34
- package/src/views/useHeaderConfigProps.tsx +93 -34
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
98
|
+
...processedItemCommon,
|
|
100
99
|
menu: {
|
|
101
|
-
...
|
|
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)
|
|
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 } =
|
|
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
|
-
:
|
|
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
|
-
{
|
|
421
|
-
<ScreenStackHeaderBackButtonImage source={
|
|
479
|
+
{backImageSource != null ? (
|
|
480
|
+
<ScreenStackHeaderBackButtonImage source={backImageSource} />
|
|
422
481
|
) : null}
|
|
423
482
|
{Platform.OS === 'ios' && rightItems ? (
|
|
424
483
|
rightItems.map((item, index) => {
|