@react-navigation/bottom-tabs 8.0.0-alpha.3 → 8.0.0-alpha.31

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