@react-navigation/bottom-tabs 8.0.0-alpha.4 → 8.0.0-alpha.40

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 (82) 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/views/BottomTabBar.js +40 -29
  10. package/lib/module/views/BottomTabBar.js.map +1 -1
  11. package/lib/module/views/BottomTabItem.js +2 -2
  12. package/lib/module/views/BottomTabItem.js.map +1 -1
  13. package/lib/module/views/{BottomTabViewCommon.js → BottomTabView.js} +1 -1
  14. package/lib/module/views/BottomTabView.js.map +1 -0
  15. package/lib/module/views/BottomTabViewCustom.js +90 -71
  16. package/lib/module/views/BottomTabViewCustom.js.map +1 -1
  17. package/lib/module/views/BottomTabViewNativeImpl.js +333 -115
  18. package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
  19. package/lib/module/views/Deferred.js +41 -0
  20. package/lib/module/views/Deferred.js.map +1 -0
  21. package/lib/module/views/ScreenContent.js.map +1 -1
  22. package/lib/module/views/TabBarIcon.js +11 -48
  23. package/lib/module/views/TabBarIcon.js.map +1 -1
  24. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +1 -1
  25. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -1
  26. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +1 -1
  27. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  28. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +1 -1
  29. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +11 -11
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +9 -15
  33. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  34. package/lib/typescript/src/types.d.ts +64 -102
  35. package/lib/typescript/src/types.d.ts.map +1 -1
  36. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts +1 -1
  37. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts.map +1 -1
  38. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +1 -1
  39. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -1
  40. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts +1 -1
  41. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts.map +1 -1
  42. package/lib/typescript/src/utils/useTabBarPosition.d.ts +1 -1
  43. package/lib/typescript/src/utils/useTabBarPosition.d.ts.map +1 -1
  44. package/lib/typescript/src/views/BottomTabBar.d.ts +3 -2
  45. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  46. package/lib/typescript/src/views/BottomTabItem.d.ts +20 -20
  47. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  48. package/lib/typescript/src/views/{BottomTabViewCommon.d.ts → BottomTabView.d.ts} +3 -3
  49. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -0
  50. package/lib/typescript/src/views/BottomTabViewCustom.d.ts +3 -2
  51. package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
  52. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts +1 -1
  53. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts.map +1 -1
  54. package/lib/typescript/src/views/BottomTabViewNative.d.ts +1 -1
  55. package/lib/typescript/src/views/BottomTabViewNative.d.ts.map +1 -1
  56. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts +1 -1
  57. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts.map +1 -1
  58. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts +3 -2
  59. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
  60. package/lib/typescript/src/views/Deferred.d.ts +31 -0
  61. package/lib/typescript/src/views/Deferred.d.ts.map +1 -0
  62. package/lib/typescript/src/views/ScreenContent.d.ts +4 -4
  63. package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
  64. package/lib/typescript/src/views/TabBarIcon.d.ts +8 -8
  65. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  66. package/package.json +19 -22
  67. package/src/index.tsx +2 -2
  68. package/src/navigators/createBottomTabNavigator.tsx +10 -75
  69. package/src/types.tsx +64 -110
  70. package/src/utils/useAnimatedHashMap.tsx +1 -1
  71. package/src/utils/useBottomTabAnimation.tsx +1 -1
  72. package/src/utils/useBottomTabBarHeight.tsx +1 -1
  73. package/src/views/BottomTabBar.tsx +68 -25
  74. package/src/views/BottomTabItem.tsx +18 -20
  75. package/src/views/BottomTabViewCustom.tsx +155 -125
  76. package/src/views/BottomTabViewNativeImpl.tsx +470 -155
  77. package/src/views/Deferred.tsx +57 -0
  78. package/src/views/ScreenContent.tsx +1 -2
  79. package/src/views/TabBarIcon.tsx +13 -56
  80. package/lib/module/views/BottomTabViewCommon.js.map +0 -1
  81. package/lib/typescript/src/views/BottomTabViewCommon.d.ts.map +0 -1
  82. /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(() => {
@@ -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 currentOptions = descriptors[state.routes[state.index].key]?.options;
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 activeTintColor =
101
- currentOptions.tabBarActiveTintColor ?? colors.primary;
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
- const inactiveTintColor =
104
- currentOptions.tabBarInactiveTintColor ??
376
+ inactiveTintColor =
377
+ inactiveTintColor ??
105
378
  Platform.select({ ios: PlatformColor('label'), default: colors.text });
106
379
 
107
- const activeIndicatorColor =
108
- currentOptions?.tabBarActiveIndicatorColor ??
109
- Color(activeTintColor)?.alpha(0.1).string();
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
- 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
- }
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 { options, render, navigation } = descriptors[route.key];
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 = true,
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
- scrollEdgeEffects,
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 tabItemAppearance: TabsScreenItemStateAppearance = {
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 badgeBackgroundColor =
265
- tabBarBadgeStyle?.backgroundColor ?? colors.notification;
266
- const badgeTextColor =
267
- tabBarBadgeStyle?.color ??
268
- (Color(badgeBackgroundColor)?.isLight() ? 'black' : 'white');
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
- const icon = getIcon(false);
301
- const selectedIcon = getIcon(true);
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
- tabKey={route.key}
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
- 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
- }}
587
+ nativeContainerStyle={{ backgroundColor: colors.background }}
354
588
  specialEffects={{
355
589
  repeatedTabSelection: {
356
590
  popToRoot: true,
357
591
  scrollToTop: true,
358
592
  },
359
593
  }}
360
- experimental_userInterfaceStyle={dark ? 'dark' : 'light'}
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
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
363
- <ScreenContent
364
- isFocused={isFocused}
365
- route={route}
366
- navigation={navigation}
367
- options={options}
368
- style={sceneStyle}
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
- <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>
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: BottomTabIcon): PlatformIcon {
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
+ }