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