@react-navigation/bottom-tabs 8.0.0-alpha.3 → 8.0.0-alpha.31
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/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigators/createBottomTabNavigator.js +4 -25
- package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
- package/lib/module/utils/useBottomTabAnimation.js +1 -1
- package/lib/module/utils/useBottomTabAnimation.js.map +1 -1
- package/lib/module/utils/useBottomTabBarHeight.js +1 -1
- package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
- package/lib/module/views/BottomTabBar.js +14 -11
- package/lib/module/views/BottomTabBar.js.map +1 -1
- package/lib/module/views/BottomTabItem.js +1 -1
- package/lib/module/views/BottomTabItem.js.map +1 -1
- package/lib/module/views/{BottomTabViewCommon.js → BottomTabView.js} +1 -1
- package/lib/module/views/BottomTabView.js.map +1 -0
- package/lib/module/views/BottomTabViewCustom.js +64 -66
- package/lib/module/views/BottomTabViewCustom.js.map +1 -1
- package/lib/module/views/BottomTabViewNativeImpl.js +299 -100
- package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
- package/lib/module/views/ScreenContent.js.map +1 -1
- package/lib/module/views/TabBarIcon.js +5 -14
- package/lib/module/views/TabBarIcon.js.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +8 -14
- package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +56 -108
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabItem.d.ts +16 -16
- package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
- package/lib/typescript/src/views/{BottomTabViewCommon.d.ts → BottomTabView.d.ts} +1 -1
- package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -0
- package/lib/typescript/src/views/BottomTabViewCustom.d.ts +1 -1
- package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
- package/lib/typescript/src/views/ScreenContent.d.ts +2 -2
- package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
- package/lib/typescript/src/views/TabBarIcon.d.ts +6 -6
- package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
- package/package.json +18 -21
- package/src/index.tsx +2 -2
- package/src/navigators/createBottomTabNavigator.tsx +10 -75
- package/src/types.tsx +56 -116
- package/src/utils/useBottomTabAnimation.tsx +1 -1
- package/src/utils/useBottomTabBarHeight.tsx +1 -1
- package/src/views/BottomTabBar.tsx +24 -12
- package/src/views/BottomTabItem.tsx +17 -19
- package/src/views/BottomTabViewCustom.tsx +119 -117
- package/src/views/BottomTabViewNativeImpl.tsx +447 -151
- package/src/views/ScreenContent.tsx +1 -2
- package/src/views/TabBarIcon.tsx +17 -24
- package/lib/module/views/BottomTabViewCommon.js.map +0 -1
- package/lib/typescript/src/views/BottomTabViewCommon.d.ts.map +0 -1
- /package/src/views/{BottomTabViewCommon.tsx → BottomTabView.tsx} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getLabel } from '@react-navigation/elements';
|
|
1
|
+
import { getLabel, type Icon } from '@react-navigation/elements';
|
|
2
2
|
import {
|
|
3
|
+
ActivityView,
|
|
3
4
|
Color,
|
|
4
|
-
Lazy,
|
|
5
5
|
SafeAreaProviderCompat,
|
|
6
6
|
} from '@react-navigation/elements/internal';
|
|
7
7
|
import {
|
|
@@ -15,16 +15,28 @@ import {
|
|
|
15
15
|
useTheme,
|
|
16
16
|
} from '@react-navigation/native';
|
|
17
17
|
import * as React from 'react';
|
|
18
|
-
import { Animated, Platform, PlatformColor } from 'react-native';
|
|
19
18
|
import {
|
|
20
|
-
|
|
19
|
+
Animated,
|
|
20
|
+
type ColorValue,
|
|
21
|
+
type NativeSyntheticEvent,
|
|
22
|
+
Platform,
|
|
23
|
+
PlatformColor,
|
|
24
|
+
StyleSheet,
|
|
25
|
+
} from 'react-native';
|
|
26
|
+
import {
|
|
27
|
+
type PlatformIconAndroid,
|
|
28
|
+
type PlatformIconIOS,
|
|
29
|
+
type PlatformIconShared,
|
|
21
30
|
Tabs,
|
|
22
|
-
type
|
|
31
|
+
type TabsBottomAccessoryEnvironment,
|
|
32
|
+
type TabSelectedEvent,
|
|
33
|
+
type TabSelectionRejectedEvent,
|
|
34
|
+
type TabsScreenItemStateAppearanceAndroid,
|
|
35
|
+
type TabsScreenItemStateAppearanceIOS,
|
|
23
36
|
} from 'react-native-screens';
|
|
24
37
|
|
|
25
38
|
import type {
|
|
26
39
|
BottomTabDescriptorMap,
|
|
27
|
-
BottomTabIcon,
|
|
28
40
|
BottomTabNavigationConfig,
|
|
29
41
|
BottomTabNavigationHelpers,
|
|
30
42
|
} from '../types';
|
|
@@ -39,6 +51,37 @@ type Props = BottomTabNavigationConfig & {
|
|
|
39
51
|
descriptors: BottomTabDescriptorMap;
|
|
40
52
|
};
|
|
41
53
|
|
|
54
|
+
type TabSelectionPreventedEvent = {
|
|
55
|
+
selectedScreenKey: string;
|
|
56
|
+
provenance: number;
|
|
57
|
+
preventedScreenKey: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type PlatformIcon = {
|
|
61
|
+
ios?: PlatformIconIOS;
|
|
62
|
+
android?: PlatformIconAndroid;
|
|
63
|
+
shared?: PlatformIconShared;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ConfirmedState = {
|
|
67
|
+
routeKey: string;
|
|
68
|
+
provenance: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type NativeState = {
|
|
72
|
+
lastTransition: { from: string; to: string } | null;
|
|
73
|
+
confirmed: ConfirmedState;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type NativeAction =
|
|
77
|
+
| { type: 'CLEAR_TRANSITION'; to: string }
|
|
78
|
+
| {
|
|
79
|
+
type: 'TRACK_TRANSITION';
|
|
80
|
+
confirmed: ConfirmedState;
|
|
81
|
+
lastTransition: NonNullable<NativeState['lastTransition']>;
|
|
82
|
+
}
|
|
83
|
+
| { type: 'CONFIRM_STATE'; confirmed: ConfirmedState };
|
|
84
|
+
|
|
42
85
|
const ICON_SIZE = Platform.select({
|
|
43
86
|
ios: 25,
|
|
44
87
|
default: 24,
|
|
@@ -48,6 +91,32 @@ const meta = {
|
|
|
48
91
|
type: 'native-tabs',
|
|
49
92
|
};
|
|
50
93
|
|
|
94
|
+
function reducer(state: NativeState, action: NativeAction): NativeState {
|
|
95
|
+
switch (action.type) {
|
|
96
|
+
case 'TRACK_TRANSITION':
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
lastTransition: action.lastTransition,
|
|
100
|
+
confirmed: action.confirmed,
|
|
101
|
+
};
|
|
102
|
+
case 'CLEAR_TRANSITION':
|
|
103
|
+
return state.lastTransition?.to === action.to
|
|
104
|
+
? {
|
|
105
|
+
...state,
|
|
106
|
+
lastTransition: null,
|
|
107
|
+
}
|
|
108
|
+
: state;
|
|
109
|
+
case 'CONFIRM_STATE':
|
|
110
|
+
return state.confirmed.routeKey === action.confirmed.routeKey &&
|
|
111
|
+
state.confirmed.provenance === action.confirmed.provenance
|
|
112
|
+
? state
|
|
113
|
+
: {
|
|
114
|
+
...state,
|
|
115
|
+
confirmed: action.confirmed,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
51
120
|
export function BottomTabViewNative({
|
|
52
121
|
state,
|
|
53
122
|
navigation,
|
|
@@ -57,6 +126,21 @@ export function BottomTabViewNative({
|
|
|
57
126
|
const { dark, colors, fonts } = useTheme();
|
|
58
127
|
|
|
59
128
|
const focusedRouteKey = state.routes[state.index].key;
|
|
129
|
+
|
|
130
|
+
const [loaded, setLoaded] = React.useState([focusedRouteKey]);
|
|
131
|
+
|
|
132
|
+
if (!loaded.includes(focusedRouteKey)) {
|
|
133
|
+
setLoaded([...loaded, focusedRouteKey]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const [nativeState, dispatch] = React.useReducer(reducer, {
|
|
137
|
+
lastTransition: null,
|
|
138
|
+
confirmed: {
|
|
139
|
+
routeKey: focusedRouteKey,
|
|
140
|
+
provenance: 0,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
60
144
|
const previousRouteKeyRef = React.useRef(focusedRouteKey);
|
|
61
145
|
|
|
62
146
|
React.useEffect(() => {
|
|
@@ -75,13 +159,149 @@ export function BottomTabViewNative({
|
|
|
75
159
|
...StackActions.popToTop(),
|
|
76
160
|
target: prevRoute.state.key,
|
|
77
161
|
};
|
|
162
|
+
|
|
78
163
|
navigation.dispatch(popToTopAction);
|
|
79
164
|
}
|
|
80
165
|
}
|
|
81
166
|
|
|
82
167
|
previousRouteKeyRef.current = focusedRouteKey;
|
|
168
|
+
|
|
169
|
+
// We dispatch `popToTop` for unfocused tabs when `popToTopOnBlur` is true
|
|
170
|
+
// So we delay clearing `lastTransition` to keep the screen active for longer
|
|
171
|
+
// This gives time for the action to be handled before the screen is paused,
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
dispatch({
|
|
174
|
+
type: 'CLEAR_TRANSITION',
|
|
175
|
+
to: focusedRouteKey,
|
|
176
|
+
});
|
|
177
|
+
}, 32);
|
|
178
|
+
|
|
179
|
+
return () => clearTimeout(timer);
|
|
83
180
|
}, [descriptors, focusedRouteKey, navigation, state.index, state.routes]);
|
|
84
181
|
|
|
182
|
+
const navigate = (
|
|
183
|
+
route: (typeof state.routes)[number],
|
|
184
|
+
confirmed: ConfirmedState
|
|
185
|
+
) => {
|
|
186
|
+
dispatch({
|
|
187
|
+
type: 'TRACK_TRANSITION',
|
|
188
|
+
confirmed,
|
|
189
|
+
lastTransition: {
|
|
190
|
+
from: previousRouteKeyRef.current,
|
|
191
|
+
to: route.key,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
navigation.dispatch({
|
|
196
|
+
...CommonActions.navigate(route.name, route.params),
|
|
197
|
+
target: state.key,
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Native tabs are the source of truth for the selected tab.
|
|
202
|
+
// JS sends a requested tab with the native provenance it was based on.
|
|
203
|
+
// Native replies with the selected tab and its new provenance.
|
|
204
|
+
const onTabSelected = (event: NativeSyntheticEvent<TabSelectedEvent>) => {
|
|
205
|
+
const { selectedScreenKey, provenance, actionOrigin } = event.nativeEvent;
|
|
206
|
+
|
|
207
|
+
const confirmed = {
|
|
208
|
+
routeKey: selectedScreenKey,
|
|
209
|
+
provenance,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const route = state.routes.find((route) => route.key === selectedScreenKey);
|
|
213
|
+
|
|
214
|
+
if (!route) {
|
|
215
|
+
console.error(
|
|
216
|
+
`Received 'tabSelected' for route that doesn't exist: ${selectedScreenKey}`
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (actionOrigin === 'user') {
|
|
223
|
+
const event = navigation.emit({
|
|
224
|
+
type: 'tabPress',
|
|
225
|
+
target: route.key,
|
|
226
|
+
canPreventDefault: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (event.defaultPrevented) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Preventing default for 'tabPress' is not supported with native tab bar. Use the 'tabBarSelectionEnabled: false' option instead."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (actionOrigin === 'programmatic-js' || focusedRouteKey === route.key) {
|
|
237
|
+
dispatch({
|
|
238
|
+
type: 'CONFIRM_STATE',
|
|
239
|
+
confirmed,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
navigate(route, confirmed);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// If native rejects a JS request, keep native as the source of truth
|
|
249
|
+
// and move JS back to the tab that native says is selected.
|
|
250
|
+
const onTabSelectionRejected = (
|
|
251
|
+
event: NativeSyntheticEvent<TabSelectionRejectedEvent>
|
|
252
|
+
) => {
|
|
253
|
+
const { selectedScreenKey, provenance } = event.nativeEvent;
|
|
254
|
+
|
|
255
|
+
const confirmed = {
|
|
256
|
+
routeKey: selectedScreenKey,
|
|
257
|
+
provenance,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const route = state.routes.find((route) => route.key === selectedScreenKey);
|
|
261
|
+
|
|
262
|
+
if (!route) {
|
|
263
|
+
console.error(
|
|
264
|
+
`Received 'tabSelectionRejected' for route that doesn't exist: ${selectedScreenKey}`
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (focusedRouteKey === route.key) {
|
|
271
|
+
dispatch({
|
|
272
|
+
type: 'CONFIRM_STATE',
|
|
273
|
+
confirmed,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
navigate(route, confirmed);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const onTabSelectionPrevented = (
|
|
283
|
+
event: NativeSyntheticEvent<TabSelectionPreventedEvent>
|
|
284
|
+
) => {
|
|
285
|
+
const { selectedScreenKey, provenance, preventedScreenKey } =
|
|
286
|
+
event.nativeEvent;
|
|
287
|
+
|
|
288
|
+
const confirmed = {
|
|
289
|
+
routeKey: selectedScreenKey,
|
|
290
|
+
provenance,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
dispatch({
|
|
294
|
+
type: 'CONFIRM_STATE',
|
|
295
|
+
confirmed,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
navigation.emit({
|
|
299
|
+
type: 'tabPress',
|
|
300
|
+
target: preventedScreenKey,
|
|
301
|
+
canPreventDefault: true,
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
85
305
|
const currentOptions = descriptors[state.routes[state.index].key]?.options;
|
|
86
306
|
|
|
87
307
|
const {
|
|
@@ -95,18 +315,67 @@ export function BottomTabViewNative({
|
|
|
95
315
|
}),
|
|
96
316
|
fontSize,
|
|
97
317
|
fontStyle,
|
|
318
|
+
color: fontColor,
|
|
98
319
|
} = currentOptions.tabBarLabelStyle || {};
|
|
99
320
|
|
|
100
|
-
const
|
|
101
|
-
currentOptions.
|
|
321
|
+
const backgroundColor =
|
|
322
|
+
currentOptions.tabBarStyle?.backgroundColor ?? colors.background;
|
|
323
|
+
|
|
324
|
+
const shouldHideTabBar = currentOptions.tabBarStyle?.display === 'none';
|
|
325
|
+
|
|
326
|
+
let activeIndicatorColor = currentOptions?.tabBarActiveIndicatorColor;
|
|
327
|
+
let activeTintColor = currentOptions.tabBarActiveTintColor;
|
|
328
|
+
let inactiveTintColor = currentOptions.tabBarInactiveTintColor;
|
|
329
|
+
|
|
330
|
+
// Derive colors based on Material Design guidelines
|
|
331
|
+
// https://m3.material.io/components/navigation-bar/specs
|
|
332
|
+
if (Platform.OS === 'android') {
|
|
333
|
+
switch (getAndroidColorName(backgroundColor)) {
|
|
334
|
+
case 'system_surface_container_light':
|
|
335
|
+
case 'system_surface_container_high_light':
|
|
336
|
+
case 'system_surface_container_highest_light':
|
|
337
|
+
case 'system_surface_container_low_light':
|
|
338
|
+
case 'system_surface_container_lowest_light':
|
|
339
|
+
inactiveTintColor =
|
|
340
|
+
inactiveTintColor ??
|
|
341
|
+
PlatformColor('@android:color/system_on_surface_variant_light');
|
|
342
|
+
activeTintColor =
|
|
343
|
+
activeTintColor ??
|
|
344
|
+
PlatformColor('@android:color/system_on_secondary_container_light');
|
|
345
|
+
activeIndicatorColor =
|
|
346
|
+
activeIndicatorColor ??
|
|
347
|
+
PlatformColor('@android:color/system_secondary_container_light');
|
|
348
|
+
break;
|
|
349
|
+
case 'system_surface_container_dark':
|
|
350
|
+
case 'system_surface_container_high_dark':
|
|
351
|
+
case 'system_surface_container_highest_dark':
|
|
352
|
+
case 'system_surface_container_low_dark':
|
|
353
|
+
case 'system_surface_container_lowest_dark':
|
|
354
|
+
inactiveTintColor =
|
|
355
|
+
inactiveTintColor ??
|
|
356
|
+
PlatformColor('@android:color/system_on_surface_variant_dark');
|
|
357
|
+
activeTintColor =
|
|
358
|
+
activeTintColor ??
|
|
359
|
+
PlatformColor('@android:color/system_on_secondary_container_dark');
|
|
360
|
+
activeIndicatorColor =
|
|
361
|
+
activeIndicatorColor ??
|
|
362
|
+
PlatformColor('@android:color/system_secondary_container_dark');
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
102
366
|
|
|
103
|
-
|
|
104
|
-
|
|
367
|
+
inactiveTintColor =
|
|
368
|
+
inactiveTintColor ??
|
|
105
369
|
Platform.select({ ios: PlatformColor('label'), default: colors.text });
|
|
106
370
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
371
|
+
activeTintColor = activeTintColor ?? colors.primary;
|
|
372
|
+
|
|
373
|
+
activeIndicatorColor =
|
|
374
|
+
activeIndicatorColor ??
|
|
375
|
+
Platform.select({
|
|
376
|
+
android: Color(activeTintColor)?.alpha(0.075).string(),
|
|
377
|
+
default: undefined,
|
|
378
|
+
});
|
|
110
379
|
|
|
111
380
|
const onTransitionStart = ({ route }: { route: Route<string> }) => {
|
|
112
381
|
navigation.emit({
|
|
@@ -160,65 +429,24 @@ export function BottomTabViewNative({
|
|
|
160
429
|
? tabBarElement
|
|
161
430
|
: null}
|
|
162
431
|
<Tabs.Host
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
tabBarItemTitleFontSize={fontSize}
|
|
182
|
-
tabBarItemTitleFontSizeActive={fontSize}
|
|
183
|
-
tabBarItemTitleFontStyle={fontStyle}
|
|
184
|
-
tabBarBackgroundColor={
|
|
185
|
-
currentOptions.tabBarStyle?.backgroundColor ?? colors.card
|
|
186
|
-
}
|
|
187
|
-
tabBarItemActiveIndicatorColor={activeIndicatorColor}
|
|
188
|
-
tabBarItemActiveIndicatorEnabled={
|
|
189
|
-
currentOptions?.tabBarActiveIndicatorEnabled
|
|
190
|
-
}
|
|
191
|
-
tabBarItemRippleColor={currentOptions?.tabBarRippleColor}
|
|
192
|
-
experimentalControlNavigationStateInJS={false}
|
|
193
|
-
onNativeFocusChange={(e) => {
|
|
194
|
-
const route = state.routes.find(
|
|
195
|
-
(route) => route.key === e.nativeEvent.tabKey
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (route) {
|
|
199
|
-
const event = navigation.emit({
|
|
200
|
-
type: 'tabPress',
|
|
201
|
-
target: route.key,
|
|
202
|
-
canPreventDefault: true,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
if (event.defaultPrevented) {
|
|
206
|
-
throw new Error(
|
|
207
|
-
"Preventing default for 'tabPress' is not supported with native tab bar."
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const isFocused =
|
|
212
|
-
state.index ===
|
|
213
|
-
state.routes.findIndex((r) => r.key === route.key);
|
|
214
|
-
|
|
215
|
-
if (!isFocused) {
|
|
216
|
-
navigation.dispatch({
|
|
217
|
-
...CommonActions.navigate(route.name, route.params),
|
|
218
|
-
target: state.key,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
432
|
+
navStateRequest={{
|
|
433
|
+
selectedScreenKey: focusedRouteKey,
|
|
434
|
+
baseProvenance: nativeState.confirmed.provenance,
|
|
435
|
+
}}
|
|
436
|
+
rejectStaleNavStateUpdates
|
|
437
|
+
onTabSelected={onTabSelected}
|
|
438
|
+
onTabSelectionRejected={onTabSelectionRejected}
|
|
439
|
+
onTabSelectionPrevented={onTabSelectionPrevented}
|
|
440
|
+
tabBarHidden={hasCustomTabBar || shouldHideTabBar}
|
|
441
|
+
colorScheme={dark ? 'dark' : 'light'}
|
|
442
|
+
ios={{
|
|
443
|
+
bottomAccessory: bottomAccessory
|
|
444
|
+
? (environment: TabsBottomAccessoryEnvironment) =>
|
|
445
|
+
bottomAccessory({ placement: environment })
|
|
446
|
+
: undefined,
|
|
447
|
+
tabBarControllerMode,
|
|
448
|
+
tabBarMinimizeBehavior,
|
|
449
|
+
tabBarTintColor: activeTintColor,
|
|
222
450
|
}}
|
|
223
451
|
>
|
|
224
452
|
{state.routes.map((route, index) => {
|
|
@@ -229,7 +457,9 @@ export function BottomTabViewNative({
|
|
|
229
457
|
const {
|
|
230
458
|
title,
|
|
231
459
|
lazy = true,
|
|
460
|
+
inactiveBehavior = 'pause',
|
|
232
461
|
tabBarLabel,
|
|
462
|
+
tabBarSelectionEnabled,
|
|
233
463
|
tabBarBadgeStyle,
|
|
234
464
|
tabBarIcon,
|
|
235
465
|
tabBarBadge,
|
|
@@ -239,7 +469,7 @@ export function BottomTabViewNative({
|
|
|
239
469
|
tabBarAccessibilityLabel,
|
|
240
470
|
tabBarButtonTestID,
|
|
241
471
|
sceneStyle,
|
|
242
|
-
|
|
472
|
+
overrideScrollViewContentInsetAdjustmentBehavior,
|
|
243
473
|
} = options;
|
|
244
474
|
|
|
245
475
|
const {
|
|
@@ -254,18 +484,32 @@ export function BottomTabViewNative({
|
|
|
254
484
|
? tabBarLabel
|
|
255
485
|
: getLabel({ label: tabBarLabel, title }, route.name);
|
|
256
486
|
|
|
257
|
-
const
|
|
487
|
+
const badgeBackgroundColor =
|
|
488
|
+
tabBarBadgeStyle?.backgroundColor ?? colors.notification;
|
|
489
|
+
const badgeTextColor =
|
|
490
|
+
tabBarBadgeStyle?.color ?? Color.foreground(badgeBackgroundColor);
|
|
491
|
+
|
|
492
|
+
const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
|
|
258
493
|
tabBarItemTitleFontFamily: fontFamily,
|
|
259
494
|
tabBarItemTitleFontSize: fontSize,
|
|
260
495
|
tabBarItemTitleFontWeight: fontWeight,
|
|
261
496
|
tabBarItemTitleFontStyle: fontStyle,
|
|
497
|
+
tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
|
|
498
|
+
tabBarItemIconColor: inactiveTintColor,
|
|
499
|
+
tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
|
|
262
500
|
};
|
|
263
501
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
502
|
+
const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
|
|
503
|
+
{
|
|
504
|
+
tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
|
|
505
|
+
tabBarItemIconColor: inactiveTintColor,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
|
|
509
|
+
{
|
|
510
|
+
tabBarItemTitleFontColor: activeTintColor,
|
|
511
|
+
tabBarItemIconColor: activeTintColor,
|
|
512
|
+
};
|
|
269
513
|
|
|
270
514
|
const getIcon = (selected: boolean) => {
|
|
271
515
|
if (typeof tabBarIcon === 'function') {
|
|
@@ -297,85 +541,127 @@ export function BottomTabViewNative({
|
|
|
297
541
|
return undefined;
|
|
298
542
|
};
|
|
299
543
|
|
|
300
|
-
|
|
301
|
-
|
|
544
|
+
// An error is thrown for React Elements in `getIcon`
|
|
545
|
+
// So we only call it when we actually render a native tab bar
|
|
546
|
+
const icon = hasCustomTabBar ? undefined : getIcon(false);
|
|
547
|
+
const selectedIcon = hasCustomTabBar ? undefined : getIcon(true);
|
|
548
|
+
|
|
549
|
+
// For preloaded screens and if lazy is false,
|
|
550
|
+
// Keep them active so that the effects can run
|
|
551
|
+
const isActive =
|
|
552
|
+
inactiveBehavior === 'none' ||
|
|
553
|
+
isPreloaded ||
|
|
554
|
+
nativeState.lastTransition?.from === route.key ||
|
|
555
|
+
nativeState.lastTransition?.to === route.key ||
|
|
556
|
+
(lazy === false && !loaded.includes(route.key));
|
|
302
557
|
|
|
303
558
|
return (
|
|
304
559
|
<Tabs.Screen
|
|
305
560
|
onWillAppear={() => onTransitionStart({ route })}
|
|
306
561
|
onDidAppear={() => onTransitionEnd({ route })}
|
|
307
562
|
key={route.key}
|
|
308
|
-
|
|
309
|
-
icon={icon}
|
|
310
|
-
selectedIcon={selectedIcon?.ios ?? selectedIcon?.shared}
|
|
311
|
-
tabBarItemBadgeBackgroundColor={badgeBackgroundColor}
|
|
312
|
-
tabBarItemBadgeTextColor={badgeTextColor}
|
|
563
|
+
screenKey={route.key}
|
|
313
564
|
tabBarItemAccessibilityLabel={tabBarAccessibilityLabel}
|
|
314
565
|
tabBarItemTestID={tabBarButtonTestID}
|
|
566
|
+
preventNativeSelection={tabBarSelectionEnabled === false}
|
|
315
567
|
badgeValue={tabBarBadge?.toString()}
|
|
316
|
-
systemItem={tabBarSystemItem}
|
|
317
|
-
isFocused={isFocused}
|
|
318
568
|
title={tabTitle}
|
|
319
|
-
scrollEdgeEffects={{
|
|
320
|
-
top:
|
|
321
|
-
scrollEdgeEffects?.top === 'auto'
|
|
322
|
-
? 'automatic'
|
|
323
|
-
: scrollEdgeEffects?.top,
|
|
324
|
-
bottom:
|
|
325
|
-
scrollEdgeEffects?.bottom === 'auto'
|
|
326
|
-
? 'automatic'
|
|
327
|
-
: scrollEdgeEffects?.bottom,
|
|
328
|
-
left:
|
|
329
|
-
scrollEdgeEffects?.left === 'auto'
|
|
330
|
-
? 'automatic'
|
|
331
|
-
: scrollEdgeEffects?.left,
|
|
332
|
-
right:
|
|
333
|
-
scrollEdgeEffects?.right === 'auto'
|
|
334
|
-
? 'automatic'
|
|
335
|
-
: scrollEdgeEffects?.right,
|
|
336
|
-
}}
|
|
337
|
-
// FIXME: if this is not provided, ScrollView on lazy tabs glitches on iOS 18
|
|
338
|
-
// For now we provide an empty object before adding proper support
|
|
339
|
-
scrollEdgeAppearance={{}}
|
|
340
|
-
standardAppearance={{
|
|
341
|
-
tabBarBackgroundColor,
|
|
342
|
-
tabBarShadowColor,
|
|
343
|
-
tabBarBlurEffect,
|
|
344
|
-
stacked: {
|
|
345
|
-
normal: tabItemAppearance,
|
|
346
|
-
},
|
|
347
|
-
inline: {
|
|
348
|
-
normal: tabItemAppearance,
|
|
349
|
-
},
|
|
350
|
-
compactInline: {
|
|
351
|
-
normal: tabItemAppearance,
|
|
352
|
-
},
|
|
353
|
-
}}
|
|
354
569
|
specialEffects={{
|
|
355
570
|
repeatedTabSelection: {
|
|
356
571
|
popToRoot: true,
|
|
357
572
|
scrollToTop: true,
|
|
358
573
|
},
|
|
359
574
|
}}
|
|
360
|
-
|
|
575
|
+
android={{
|
|
576
|
+
icon: icon?.android ?? icon?.shared,
|
|
577
|
+
selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
|
|
578
|
+
standardAppearance: {
|
|
579
|
+
tabBarBackgroundColor:
|
|
580
|
+
tabBarBackgroundColor ?? backgroundColor,
|
|
581
|
+
tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
|
|
582
|
+
tabBarItemLabelVisibilityMode:
|
|
583
|
+
currentOptions?.tabBarLabelVisibilityMode,
|
|
584
|
+
normal: normalTabItemAppearance,
|
|
585
|
+
selected: selectedTabItemAppearance,
|
|
586
|
+
tabBarItemActiveIndicatorColor: activeIndicatorColor,
|
|
587
|
+
tabBarItemActiveIndicatorEnabled:
|
|
588
|
+
currentOptions?.tabBarActiveIndicatorEnabled,
|
|
589
|
+
tabBarItemTitleFontFamily: fontFamily,
|
|
590
|
+
tabBarItemTitleFontWeight: fontWeight,
|
|
591
|
+
tabBarItemTitleSmallLabelFontSize: fontSize,
|
|
592
|
+
tabBarItemTitleLargeLabelFontSize: fontSize,
|
|
593
|
+
tabBarItemTitleFontStyle: fontStyle,
|
|
594
|
+
tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
|
|
595
|
+
tabBarItemBadgeTextColor: badgeTextColor,
|
|
596
|
+
},
|
|
597
|
+
}}
|
|
598
|
+
ios={{
|
|
599
|
+
icon: icon?.ios ?? icon?.shared,
|
|
600
|
+
selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
|
|
601
|
+
systemItem: tabBarSystemItem,
|
|
602
|
+
scrollEdgeAppearance: {
|
|
603
|
+
tabBarBackgroundColor,
|
|
604
|
+
tabBarShadowColor,
|
|
605
|
+
tabBarBlurEffect,
|
|
606
|
+
stacked: {
|
|
607
|
+
normal: tabItemAppearance,
|
|
608
|
+
},
|
|
609
|
+
inline: {
|
|
610
|
+
normal: tabItemAppearance,
|
|
611
|
+
},
|
|
612
|
+
compactInline: {
|
|
613
|
+
normal: tabItemAppearance,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
standardAppearance: {
|
|
617
|
+
tabBarBackgroundColor,
|
|
618
|
+
tabBarShadowColor,
|
|
619
|
+
tabBarBlurEffect,
|
|
620
|
+
stacked: {
|
|
621
|
+
normal: tabItemAppearance,
|
|
622
|
+
},
|
|
623
|
+
inline: {
|
|
624
|
+
normal: tabItemAppearance,
|
|
625
|
+
},
|
|
626
|
+
compactInline: {
|
|
627
|
+
normal: tabItemAppearance,
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
overrideScrollViewContentInsetAdjustmentBehavior,
|
|
631
|
+
}}
|
|
361
632
|
>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
633
|
+
{lazy &&
|
|
634
|
+
!loaded.includes(route.key) &&
|
|
635
|
+
!isFocused &&
|
|
636
|
+
!isPreloaded ? null : (
|
|
637
|
+
<ActivityView
|
|
638
|
+
key={route.key}
|
|
639
|
+
mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
|
|
640
|
+
visible={
|
|
641
|
+
// We don't need to hide the content since it's handled natively
|
|
642
|
+
// Hiding may also cause flash due to lag after native tab switch
|
|
643
|
+
// So we leave it always visible
|
|
644
|
+
true
|
|
645
|
+
}
|
|
646
|
+
style={StyleSheet.absoluteFill}
|
|
369
647
|
>
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
648
|
+
<ScreenContent
|
|
649
|
+
isFocused={isFocused}
|
|
650
|
+
route={route}
|
|
651
|
+
navigation={navigation}
|
|
652
|
+
options={options}
|
|
653
|
+
style={sceneStyle}
|
|
654
|
+
>
|
|
655
|
+
<AnimatedScreenContent isFocused={isFocused}>
|
|
656
|
+
<BottomTabBarHeightContext.Provider value={0}>
|
|
657
|
+
<NavigationMetaContext.Provider value={meta}>
|
|
658
|
+
{render()}
|
|
659
|
+
</NavigationMetaContext.Provider>
|
|
660
|
+
</BottomTabBarHeightContext.Provider>
|
|
661
|
+
</AnimatedScreenContent>
|
|
662
|
+
</ScreenContent>
|
|
663
|
+
</ActivityView>
|
|
664
|
+
)}
|
|
379
665
|
</Tabs.Screen>
|
|
380
666
|
);
|
|
381
667
|
})}
|
|
@@ -386,6 +672,7 @@ export function BottomTabViewNative({
|
|
|
386
672
|
</SafeAreaProviderCompat>
|
|
387
673
|
);
|
|
388
674
|
}
|
|
675
|
+
|
|
389
676
|
function AnimatedScreenContent({
|
|
390
677
|
isFocused,
|
|
391
678
|
children,
|
|
@@ -419,7 +706,7 @@ function AnimatedScreenContent({
|
|
|
419
706
|
);
|
|
420
707
|
}
|
|
421
708
|
|
|
422
|
-
function getPlatformIcon(icon:
|
|
709
|
+
function getPlatformIcon(icon: Icon): PlatformIcon {
|
|
423
710
|
switch (icon.type) {
|
|
424
711
|
case 'sfSymbol':
|
|
425
712
|
return {
|
|
@@ -434,6 +721,9 @@ function getPlatformIcon(icon: BottomTabIcon): PlatformIcon {
|
|
|
434
721
|
type: 'imageSource',
|
|
435
722
|
imageSource: MaterialSymbol.getImageSource({
|
|
436
723
|
name: icon.name,
|
|
724
|
+
variant: icon.variant,
|
|
725
|
+
weight: icon.weight,
|
|
726
|
+
size: ICON_SIZE,
|
|
437
727
|
}),
|
|
438
728
|
},
|
|
439
729
|
shared: undefined,
|
|
@@ -456,16 +746,6 @@ function getPlatformIcon(icon: BottomTabIcon): PlatformIcon {
|
|
|
456
746
|
imageSource: icon.source,
|
|
457
747
|
},
|
|
458
748
|
};
|
|
459
|
-
case 'resource':
|
|
460
|
-
return {
|
|
461
|
-
// TODO: support xcasset once new version of react-native-screens is released
|
|
462
|
-
ios: undefined,
|
|
463
|
-
android: {
|
|
464
|
-
type: 'drawableResource',
|
|
465
|
-
name: icon.name,
|
|
466
|
-
},
|
|
467
|
-
shared: undefined,
|
|
468
|
-
};
|
|
469
749
|
default: {
|
|
470
750
|
const _exhaustiveCheck: never = icon;
|
|
471
751
|
|
|
@@ -473,3 +753,19 @@ function getPlatformIcon(icon: BottomTabIcon): PlatformIcon {
|
|
|
473
753
|
}
|
|
474
754
|
}
|
|
475
755
|
}
|
|
756
|
+
|
|
757
|
+
function getAndroidColorName(color: ColorValue) {
|
|
758
|
+
const value = color as unknown;
|
|
759
|
+
|
|
760
|
+
if (
|
|
761
|
+
typeof value === 'object' &&
|
|
762
|
+
value != null &&
|
|
763
|
+
'resource_paths' in value &&
|
|
764
|
+
Array.isArray(value.resource_paths) &&
|
|
765
|
+
typeof value.resource_paths[0] === 'string'
|
|
766
|
+
) {
|
|
767
|
+
return value.resource_paths[0].replace('@android:color/', '');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return null;
|
|
771
|
+
}
|