@react-navigation/bottom-tabs 8.0.0-alpha.5 → 8.0.0-alpha.50

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.
Files changed (84) hide show
  1. package/lib/module/index.js +1 -1
  2. package/lib/module/index.js.map +1 -1
  3. package/lib/module/navigators/createBottomTabNavigator.js +4 -25
  4. package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
  5. package/lib/module/utils/useBottomTabAnimation.js +1 -1
  6. package/lib/module/utils/useBottomTabAnimation.js.map +1 -1
  7. package/lib/module/utils/useBottomTabBarHeight.js +1 -1
  8. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
  9. package/lib/module/utils/useIsKeyboardShown.js.map +1 -1
  10. package/lib/module/views/BottomTabBar.js +40 -29
  11. package/lib/module/views/BottomTabBar.js.map +1 -1
  12. package/lib/module/views/BottomTabItem.js +3 -5
  13. package/lib/module/views/BottomTabItem.js.map +1 -1
  14. package/lib/module/views/{BottomTabViewCommon.js → BottomTabView.js} +1 -1
  15. package/lib/module/views/BottomTabView.js.map +1 -0
  16. package/lib/module/views/BottomTabViewCustom.js +97 -81
  17. package/lib/module/views/BottomTabViewCustom.js.map +1 -1
  18. package/lib/module/views/BottomTabViewNativeImpl.js +339 -121
  19. package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
  20. package/lib/module/views/Deferred.js +41 -0
  21. package/lib/module/views/Deferred.js.map +1 -0
  22. package/lib/module/views/ScreenContent.js.map +1 -1
  23. package/lib/module/views/TabBarIcon.js +11 -48
  24. package/lib/module/views/TabBarIcon.js.map +1 -1
  25. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +1 -1
  26. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -1
  27. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +1 -1
  28. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  29. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +1 -1
  30. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  31. package/lib/typescript/src/index.d.ts +11 -11
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +9 -15
  34. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  35. package/lib/typescript/src/types.d.ts +66 -104
  36. package/lib/typescript/src/types.d.ts.map +1 -1
  37. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts +1 -1
  38. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts.map +1 -1
  39. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +1 -1
  40. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -1
  41. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts +1 -1
  42. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts.map +1 -1
  43. package/lib/typescript/src/utils/useTabBarPosition.d.ts +1 -1
  44. package/lib/typescript/src/utils/useTabBarPosition.d.ts.map +1 -1
  45. package/lib/typescript/src/views/BottomTabBar.d.ts +3 -2
  46. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  47. package/lib/typescript/src/views/BottomTabItem.d.ts +20 -20
  48. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  49. package/lib/typescript/src/views/{BottomTabViewCommon.d.ts → BottomTabView.d.ts} +3 -3
  50. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -0
  51. package/lib/typescript/src/views/BottomTabViewCustom.d.ts +3 -2
  52. package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
  53. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts +1 -1
  54. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts.map +1 -1
  55. package/lib/typescript/src/views/BottomTabViewNative.d.ts +1 -1
  56. package/lib/typescript/src/views/BottomTabViewNative.d.ts.map +1 -1
  57. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts +1 -1
  58. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts.map +1 -1
  59. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts +3 -2
  60. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
  61. package/lib/typescript/src/views/Deferred.d.ts +31 -0
  62. package/lib/typescript/src/views/Deferred.d.ts.map +1 -0
  63. package/lib/typescript/src/views/ScreenContent.d.ts +4 -4
  64. package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
  65. package/lib/typescript/src/views/TabBarIcon.d.ts +8 -8
  66. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  67. package/package.json +22 -22
  68. package/src/index.tsx +2 -2
  69. package/src/navigators/createBottomTabNavigator.tsx +10 -75
  70. package/src/types.tsx +66 -112
  71. package/src/utils/useAnimatedHashMap.tsx +1 -1
  72. package/src/utils/useBottomTabAnimation.tsx +1 -1
  73. package/src/utils/useBottomTabBarHeight.tsx +1 -1
  74. package/src/utils/useIsKeyboardShown.tsx +2 -2
  75. package/src/views/BottomTabBar.tsx +68 -25
  76. package/src/views/BottomTabItem.tsx +19 -21
  77. package/src/views/BottomTabViewCustom.tsx +172 -143
  78. package/src/views/BottomTabViewNativeImpl.tsx +480 -161
  79. package/src/views/Deferred.tsx +57 -0
  80. package/src/views/ScreenContent.tsx +1 -2
  81. package/src/views/TabBarIcon.tsx +13 -56
  82. package/lib/module/views/BottomTabViewCommon.js.map +0 -1
  83. package/lib/typescript/src/views/BottomTabViewCommon.d.ts.map +0 -1
  84. /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
- type PlatformIcon,
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 TabsScreenItemStateAppearance,
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 focusedRouteKey = state.routes[state.index].key;
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 prevRoute = state.routes.find(
161
+ const currentState = navigation.getState();
162
+ const prevRoute = currentState.routes.find(
70
163
  (route) => route.key === previousRouteKey
71
164
  );
72
165
 
73
- if (prevRoute?.state?.type === 'stack' && prevRoute.state.key) {
74
- const popToTopAction = {
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
- const currentOptions = descriptors[state.routes[state.index].key]?.options;
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 activeTintColor =
101
- currentOptions.tabBarActiveTintColor ?? colors.primary;
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
- const inactiveTintColor =
104
- currentOptions.tabBarInactiveTintColor ??
380
+ inactiveTintColor =
381
+ inactiveTintColor ??
105
382
  Platform.select({ ios: PlatformColor('label'), default: colors.text });
106
383
 
107
- const activeIndicatorColor =
108
- currentOptions?.tabBarActiveIndicatorColor ??
109
- Color(activeTintColor)?.alpha(0.1).string();
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
- tabBarHidden={hasCustomTabBar}
164
- bottomAccessory={
165
- bottomAccessory
166
- ? (environment) => bottomAccessory({ placement: environment })
167
- : undefined
168
- }
169
- tabBarItemLabelVisibilityMode={
170
- currentOptions?.tabBarLabelVisibilityMode
171
- }
172
- tabBarControllerMode={tabBarControllerMode}
173
- tabBarMinimizeBehavior={tabBarMinimizeBehavior}
174
- tabBarTintColor={activeTintColor}
175
- tabBarItemIconColor={inactiveTintColor}
176
- tabBarItemIconColorActive={activeTintColor}
177
- tabBarItemTitleFontColor={inactiveTintColor}
178
- tabBarItemTitleFontColorActive={activeTintColor}
179
- tabBarItemTitleFontFamily={fontFamily}
180
- tabBarItemTitleFontWeight={fontWeight}
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
- }
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 { options, render, navigation } = descriptors[route.key];
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 = true,
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
- scrollEdgeEffects,
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 tabItemAppearance: TabsScreenItemStateAppearance = {
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 badgeBackgroundColor =
265
- tabBarBadgeStyle?.backgroundColor ?? colors.notification;
266
- const badgeTextColor =
267
- tabBarBadgeStyle?.color ??
268
- (Color(badgeBackgroundColor)?.isLight() ? 'black' : 'white');
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
- const icon = getIcon(false);
301
- const selectedIcon = getIcon(true);
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
- tabKey={route.key}
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
- 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
- }}
591
+ nativeContainerStyle={{ backgroundColor: colors.background }}
354
592
  specialEffects={{
355
593
  repeatedTabSelection: {
356
594
  popToRoot: true,
357
595
  scrollToTop: true,
358
596
  },
359
597
  }}
360
- experimental_userInterfaceStyle={dark ? 'dark' : 'light'}
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
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
363
- <ScreenContent
364
- isFocused={isFocused}
365
- route={route}
366
- navigation={navigation}
367
- options={options}
368
- style={sceneStyle}
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
- <AnimatedScreenContent isFocused={isFocused}>
371
- <BottomTabBarHeightContext.Provider value={0}>
372
- <NavigationMetaContext.Provider value={meta}>
373
- {render()}
374
- </NavigationMetaContext.Provider>
375
- </BottomTabBarHeightContext.Provider>
376
- </AnimatedScreenContent>
377
- </ScreenContent>
378
- </Lazy>
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: BottomTabIcon): PlatformIcon {
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
+ }