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