@react-navigation/bottom-tabs 7.20.0 → 7.21.0

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 (33) hide show
  1. package/lib/module/unstable/Deferred.js +3 -10
  2. package/lib/module/unstable/Deferred.js.map +1 -1
  3. package/lib/module/unstable/NativeBottomTabView.native.js +288 -196
  4. package/lib/module/unstable/NativeBottomTabView.native.js.map +1 -1
  5. package/lib/module/views/BottomTabBar.js +111 -64
  6. package/lib/module/views/BottomTabBar.js.map +1 -1
  7. package/lib/module/views/BottomTabItem.js +2 -0
  8. package/lib/module/views/BottomTabItem.js.map +1 -1
  9. package/lib/module/views/BottomTabView.js +78 -57
  10. package/lib/module/views/BottomTabView.js.map +1 -1
  11. package/lib/module/views/TabBarIcon.js +6 -2
  12. package/lib/module/views/TabBarIcon.js.map +1 -1
  13. package/lib/typescript/src/types.d.ts +4 -0
  14. package/lib/typescript/src/types.d.ts.map +1 -1
  15. package/lib/typescript/src/unstable/Deferred.d.ts.map +1 -1
  16. package/lib/typescript/src/unstable/NativeBottomTabView.native.d.ts.map +1 -1
  17. package/lib/typescript/src/unstable/types.d.ts +124 -4
  18. package/lib/typescript/src/unstable/types.d.ts.map +1 -1
  19. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  20. package/lib/typescript/src/views/BottomTabItem.d.ts +5 -1
  21. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  22. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  23. package/lib/typescript/src/views/TabBarIcon.d.ts +2 -1
  24. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  25. package/package.json +6 -6
  26. package/src/types.tsx +5 -0
  27. package/src/unstable/Deferred.tsx +3 -11
  28. package/src/unstable/NativeBottomTabView.native.tsx +356 -218
  29. package/src/unstable/types.tsx +133 -4
  30. package/src/views/BottomTabBar.tsx +153 -82
  31. package/src/views/BottomTabItem.tsx +6 -0
  32. package/src/views/BottomTabView.tsx +100 -66
  33. package/src/views/TabBarIcon.tsx +9 -2
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  CommonActions,
8
8
  NavigationMetaContext,
9
+ type NavigationRoute,
9
10
  type ParamListBase,
10
11
  type Route,
11
12
  StackActions,
@@ -19,15 +20,16 @@ import {
19
20
  type NativeSyntheticEvent,
20
21
  Platform,
21
22
  PlatformColor,
23
+ type TextStyle,
22
24
  } from 'react-native';
23
25
  import {
24
- type PlatformIconAndroid,
25
- type PlatformIconIOS,
26
26
  type PlatformIconShared,
27
27
  Tabs,
28
28
  type TabsBottomAccessoryEnvironment,
29
29
  type TabSelectedEvent,
30
30
  type TabSelectionRejectedEvent,
31
+ type TabsScreenIconAndroid,
32
+ type TabsScreenIconIOS,
31
33
  type TabsScreenItemStateAppearanceAndroid,
32
34
  type TabsScreenItemStateAppearanceIOS,
33
35
  } from 'react-native-screens';
@@ -35,6 +37,7 @@ import {
35
37
  import { Deferred } from './Deferred';
36
38
  import { NativeScreen } from './NativeScreen/NativeScreen';
37
39
  import type {
40
+ NativeBottomTabDescriptor,
38
41
  NativeBottomTabDescriptorMap,
39
42
  NativeBottomTabIcon,
40
43
  NativeBottomTabNavigationConfig,
@@ -50,8 +53,8 @@ type Props = NativeBottomTabNavigationConfig & {
50
53
  };
51
54
 
52
55
  type PlatformIcon = {
53
- ios?: PlatformIconIOS;
54
- android?: PlatformIconAndroid;
56
+ ios?: TabsScreenIconIOS;
57
+ android?: TabsScreenIconAndroid;
55
58
  shared?: PlatformIconShared;
56
59
  };
57
60
 
@@ -76,6 +79,9 @@ const meta = {
76
79
  type: 'native-tabs',
77
80
  };
78
81
 
82
+ const SUPPORTS_PROMINENT_TAB =
83
+ Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 27;
84
+
79
85
  function reducer(state: NativeState, action: NativeAction): NativeState {
80
86
  switch (action.type) {
81
87
  case 'CONFIRM_STATE':
@@ -274,49 +280,32 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
274
280
  color: fontColor,
275
281
  } = currentOptions.tabBarLabelStyle || {};
276
282
 
277
- const activeTintColor =
278
- currentOptions.tabBarActiveTintColor ?? colors.primary;
283
+ const { activeIndicatorColor, activeTintColor, inactiveTintColor } =
284
+ React.useMemo(() => {
285
+ const activeTintColor =
286
+ currentOptions.tabBarActiveTintColor ?? colors.primary;
279
287
 
280
- const inactiveTintColor =
281
- currentOptions.tabBarInactiveTintColor ??
282
- Platform.select<ColorValue | string>({
283
- ios: PlatformColor('label'),
284
- default: colors.text,
285
- });
288
+ const inactiveTintColor =
289
+ currentOptions.tabBarInactiveTintColor ??
290
+ Platform.select<ColorValue | string>({
291
+ ios: PlatformColor('label'),
292
+ default: colors.text,
293
+ });
286
294
 
287
- const activeIndicatorColor =
288
- (currentOptions?.tabBarActiveIndicatorColor ??
289
- typeof activeTintColor === 'string')
290
- ? Color(activeTintColor)?.alpha(0.1).string()
291
- : undefined;
292
-
293
- const onTransitionStart = ({
294
- closing,
295
- route,
296
- }: {
297
- closing: boolean;
298
- route: Route<string>;
299
- }) => {
300
- navigation.emit({
301
- type: 'transitionStart',
302
- data: { closing },
303
- target: route.key,
304
- });
305
- };
306
-
307
- const onTransitionEnd = ({
308
- closing,
309
- route,
310
- }: {
311
- closing: boolean;
312
- route: Route<string>;
313
- }) => {
314
- navigation.emit({
315
- type: 'transitionEnd',
316
- data: { closing },
317
- target: route.key,
318
- });
319
- };
295
+ const activeIndicatorColor =
296
+ currentOptions?.tabBarActiveIndicatorColor ??
297
+ (typeof activeTintColor === 'string'
298
+ ? Color(activeTintColor)?.alpha(0.1).string()
299
+ : undefined);
300
+
301
+ return { activeIndicatorColor, activeTintColor, inactiveTintColor };
302
+ }, [
303
+ currentOptions?.tabBarActiveIndicatorColor,
304
+ currentOptions.tabBarActiveTintColor,
305
+ currentOptions.tabBarInactiveTintColor,
306
+ colors.primary,
307
+ colors.text,
308
+ ]);
320
309
 
321
310
  const tabBarControllerMode =
322
311
  currentOptions.tabBarControllerMode === 'auto'
@@ -330,6 +319,17 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
330
319
  ? 'never'
331
320
  : currentOptions.tabBarMinimizeBehavior;
332
321
 
322
+ const tabBarSidebarPreferredPlacement =
323
+ currentOptions.tabBarSidebarPreferredPlacement === 'auto'
324
+ ? 'automatic'
325
+ : currentOptions.tabBarSidebarPreferredPlacement;
326
+
327
+ const prominentScreenKey = SUPPORTS_PROMINENT_TAB
328
+ ? state.routes.find(
329
+ (route) => descriptors[route.key]?.options.tabBarRole === 'prominent'
330
+ )?.key
331
+ : undefined;
332
+
333
333
  const shouldHideTabBar = currentOptions.tabBarStyle?.display === 'none';
334
334
 
335
335
  const bottomAccessory = currentOptions.bottomAccessory;
@@ -349,182 +349,49 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
349
349
  colorScheme={dark ? 'dark' : 'light'}
350
350
  nativeContainerStyle={{ backgroundColor: colors.background }}
351
351
  ios={{
352
+ prominentScreenKey,
352
353
  bottomAccessory: bottomAccessory
353
354
  ? (environment: TabsBottomAccessoryEnvironment) =>
354
355
  bottomAccessory({ placement: environment })
355
356
  : undefined,
357
+ tabBarHiddenAnimationEnabled:
358
+ currentOptions.tabBarVisibilityAnimationEnabled,
356
359
  tabBarControllerMode,
360
+ tabBarSidebarPreferredPlacement,
357
361
  tabBarMinimizeBehavior,
358
362
  tabBarTintColor: activeTintColor,
359
363
  }}
360
364
  >
361
365
  {state.routes.map((route, index) => {
362
- const { options, render, navigation } = descriptors[route.key];
366
+ const descriptor = descriptors[route.key];
363
367
  const isFocused = state.index === index;
364
368
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
365
369
 
366
- const {
367
- title,
368
- lazy = false,
369
- tabBarLabel,
370
- tabBarSelectionEnabled,
371
- tabBarRepeatedPressBehavior,
372
- tabBarBadgeStyle,
373
- tabBarIcon,
374
- tabBarBadge,
375
- tabBarSystemItem,
376
- tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
377
- tabBarStyle,
378
- overrideScrollViewContentInsetAdjustmentBehavior,
379
- } = options;
380
-
381
- const {
382
- backgroundColor: tabBarBackgroundColor,
383
- shadowColor: tabBarShadowColor,
384
- } = tabBarStyle || {};
385
-
386
- const tabTitle =
387
- // On iOS, `systemItem` already provides a localized label
388
- // So we should only use `tabBarLabel` if explicitly provided
389
- Platform.OS === 'ios' && tabBarSystemItem != null
390
- ? tabBarLabel
391
- : getLabel({ label: tabBarLabel, title }, route.name);
392
-
393
- const badgeBackgroundColor =
394
- tabBarBadgeStyle?.backgroundColor ?? colors.notification;
395
- const badgeTextColor =
396
- tabBarBadgeStyle?.color ??
397
- (typeof badgeBackgroundColor === 'string'
398
- ? Color(badgeBackgroundColor).isLight()
399
- ? 'black'
400
- : 'white'
401
- : undefined);
402
-
403
- const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
404
- tabBarItemTitleFontFamily: fontFamily,
405
- tabBarItemTitleFontSize: fontSize,
406
- tabBarItemTitleFontWeight: fontWeight,
407
- tabBarItemTitleFontStyle: fontStyle,
408
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
409
- tabBarItemIconColor: inactiveTintColor,
410
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
411
- };
412
-
413
- const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
414
- {
415
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
416
- tabBarItemIconColor: inactiveTintColor,
417
- };
418
-
419
- const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
420
- {
421
- tabBarItemTitleFontColor: activeTintColor,
422
- tabBarItemIconColor: activeTintColor,
423
- };
424
-
425
- const icon =
426
- typeof tabBarIcon === 'function'
427
- ? getPlatformIcon(tabBarIcon({ focused: false }))
428
- : tabBarIcon != null
429
- ? getPlatformIcon(tabBarIcon)
430
- : undefined;
431
-
432
- const selectedIcon =
433
- typeof tabBarIcon === 'function'
434
- ? getPlatformIcon(tabBarIcon({ focused: true }))
435
- : undefined;
370
+ const { lazy = false } = descriptor.options;
436
371
 
437
372
  return (
438
- <Tabs.Screen
439
- onWillDisappear={() =>
440
- onTransitionStart({ closing: true, route })
441
- }
442
- onWillAppear={() => onTransitionStart({ closing: false, route })}
443
- onDidAppear={() => onTransitionEnd({ closing: false, route })}
444
- onDidDisappear={() => onTransitionEnd({ closing: true, route })}
373
+ <TabScene
445
374
  key={route.key}
446
- screenKey={route.key}
447
- preventNativeSelection={tabBarSelectionEnabled === false}
448
- badgeValue={tabBarBadge?.toString()}
449
- title={tabTitle}
450
- nativeContainerStyle={{ backgroundColor: colors.background }}
451
- specialEffects={{
452
- repeatedTabSelection: {
453
- popToRoot: tabBarRepeatedPressBehavior?.popToTop !== false,
454
- scrollToTop:
455
- tabBarRepeatedPressBehavior?.scrollToTop !== false,
456
- },
457
- }}
458
- android={{
459
- icon: icon?.android ?? icon?.shared,
460
- selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
461
- standardAppearance: {
462
- tabBarBackgroundColor: tabBarBackgroundColor ?? colors.card,
463
- tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
464
- tabBarItemLabelVisibilityMode:
465
- currentOptions?.tabBarLabelVisibilityMode,
466
- normal: normalTabItemAppearance,
467
- selected: selectedTabItemAppearance,
468
- tabBarItemActiveIndicatorColor: activeIndicatorColor,
469
- tabBarItemActiveIndicatorEnabled:
470
- currentOptions?.tabBarActiveIndicatorEnabled,
471
- tabBarItemTitleFontFamily: fontFamily,
472
- tabBarItemTitleFontWeight: fontWeight,
473
- tabBarItemTitleSmallLabelFontSize: fontSize,
474
- tabBarItemTitleLargeLabelFontSize: fontSize,
475
- tabBarItemTitleFontStyle: fontStyle,
476
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
477
- tabBarItemBadgeTextColor: badgeTextColor,
478
- },
479
- }}
480
- ios={{
481
- icon: icon?.ios ?? icon?.shared,
482
- selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
483
- systemItem: tabBarSystemItem,
484
- scrollEdgeAppearance: {
485
- tabBarBackgroundColor,
486
- tabBarShadowColor,
487
- tabBarBlurEffect,
488
- stacked: {
489
- normal: tabItemAppearance,
490
- },
491
- inline: {
492
- normal: tabItemAppearance,
493
- },
494
- compactInline: {
495
- normal: tabItemAppearance,
496
- },
497
- },
498
- standardAppearance: {
499
- tabBarBackgroundColor,
500
- tabBarShadowColor,
501
- tabBarBlurEffect,
502
- stacked: {
503
- normal: tabItemAppearance,
504
- },
505
- inline: {
506
- normal: tabItemAppearance,
507
- },
508
- compactInline: {
509
- normal: tabItemAppearance,
510
- },
511
- },
512
- overrideScrollViewContentInsetAdjustmentBehavior,
513
- }}
514
- >
515
- <Deferred lazy={lazy} visible={isFocused || isPreloaded}>
516
- <ScreenWithHeader
517
- isFocused={isFocused}
518
- route={route}
519
- navigation={navigation}
520
- options={options}
521
- >
522
- <NavigationMetaContext.Provider value={meta}>
523
- {render()}
524
- </NavigationMetaContext.Provider>
525
- </ScreenWithHeader>
526
- </Deferred>
527
- </Tabs.Screen>
375
+ route={route}
376
+ descriptor={descriptor}
377
+ navigation={navigation}
378
+ isFocused={isFocused}
379
+ lazy={lazy}
380
+ visible={isFocused || isPreloaded}
381
+ fontFamily={fontFamily}
382
+ fontWeight={fontWeight}
383
+ fontSize={fontSize}
384
+ fontStyle={fontStyle}
385
+ fontColor={fontColor}
386
+ activeTintColor={activeTintColor}
387
+ inactiveTintColor={inactiveTintColor}
388
+ activeIndicatorColor={activeIndicatorColor}
389
+ rippleColor={currentOptions.tabBarRippleColor}
390
+ labelVisibilityMode={currentOptions.tabBarLabelVisibilityMode}
391
+ activeIndicatorEnabled={
392
+ currentOptions.tabBarActiveIndicatorEnabled
393
+ }
394
+ />
528
395
  );
529
396
  })}
530
397
  </Tabs.Host>
@@ -532,6 +399,258 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
532
399
  );
533
400
  }
534
401
 
402
+ type TabSceneProps = {
403
+ route: NavigationRoute<ParamListBase, string>;
404
+ descriptor: NativeBottomTabDescriptor;
405
+ navigation: NativeBottomTabNavigationHelpers;
406
+ isFocused: boolean;
407
+ lazy: boolean;
408
+ visible: boolean;
409
+ fontFamily: TextStyle['fontFamily'];
410
+ fontWeight: TextStyle['fontWeight'];
411
+ fontSize: TextStyle['fontSize'];
412
+ fontStyle: TextStyle['fontStyle'];
413
+ fontColor: TextStyle['color'];
414
+ activeTintColor: ColorValue;
415
+ inactiveTintColor: ColorValue;
416
+ activeIndicatorColor: ColorValue | undefined;
417
+ rippleColor: ColorValue | undefined;
418
+ labelVisibilityMode: NativeBottomTabNavigationOptions['tabBarLabelVisibilityMode'];
419
+ activeIndicatorEnabled: boolean | undefined;
420
+ };
421
+
422
+ const TabScene = React.memo(function TabScene({
423
+ route,
424
+ descriptor,
425
+ navigation,
426
+ isFocused,
427
+ lazy,
428
+ visible,
429
+ fontFamily,
430
+ fontWeight,
431
+ fontSize,
432
+ fontStyle,
433
+ fontColor,
434
+ activeTintColor,
435
+ inactiveTintColor,
436
+ activeIndicatorColor,
437
+ rippleColor,
438
+ labelVisibilityMode,
439
+ activeIndicatorEnabled,
440
+ }: TabSceneProps) {
441
+ const { dark, colors } = useTheme();
442
+
443
+ const onTransitionStart = ({
444
+ closing,
445
+ route,
446
+ }: {
447
+ closing: boolean;
448
+ route: Route<string>;
449
+ }) => {
450
+ navigation.emit({
451
+ type: 'transitionStart',
452
+ data: { closing },
453
+ target: route.key,
454
+ });
455
+ };
456
+
457
+ const onTransitionEnd = ({
458
+ closing,
459
+ route,
460
+ }: {
461
+ closing: boolean;
462
+ route: Route<string>;
463
+ }) => {
464
+ navigation.emit({
465
+ type: 'transitionEnd',
466
+ data: { closing },
467
+ target: route.key,
468
+ });
469
+ };
470
+
471
+ const { options, render } = descriptor;
472
+
473
+ const {
474
+ title,
475
+ tabBarLabel,
476
+ tabBarLabelPositionAdjustment,
477
+ tabBarSelectionEnabled,
478
+ tabBarRepeatedPressBehavior,
479
+ tabBarBadgeStyle,
480
+ tabBarIcon,
481
+ tabBarIconSize,
482
+ tabBarActiveIndicatorWidth,
483
+ tabBarActiveIndicatorHeight,
484
+ tabBarBadge,
485
+ tabBarSystemItem,
486
+ tabBarRole,
487
+ tabBarSearchAutoFocus,
488
+ tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
489
+ tabBarStyle,
490
+ overrideScrollViewContentInsetAdjustmentBehavior,
491
+ } = options;
492
+
493
+ const systemItem =
494
+ tabBarSystemItem ??
495
+ // Setting the `role` to 'search' doesn't add title and icon automatically
496
+ // So we also set `tabBarSystemItem` to 'search' to match SwiftUI's behavior
497
+ (tabBarRole === 'search' ? 'search' : undefined);
498
+
499
+ const {
500
+ backgroundColor: tabBarBackgroundColor,
501
+ shadowColor: tabBarShadowColor,
502
+ } = tabBarStyle || {};
503
+
504
+ const tabTitle =
505
+ // On iOS, `systemItem` already provides a localized label
506
+ // So we should only use `tabBarLabel` if explicitly provided
507
+ Platform.OS === 'ios' && systemItem != null
508
+ ? tabBarLabel
509
+ : getLabel({ label: tabBarLabel, title }, route.name);
510
+
511
+ const badgeBackgroundColor =
512
+ tabBarBadgeStyle?.backgroundColor ?? colors.notification;
513
+ const badgeTextColor =
514
+ tabBarBadgeStyle?.color ??
515
+ (typeof badgeBackgroundColor === 'string'
516
+ ? Color(badgeBackgroundColor).isLight()
517
+ ? 'black'
518
+ : 'white'
519
+ : undefined);
520
+
521
+ const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
522
+ tabBarItemTitleFontFamily: fontFamily,
523
+ tabBarItemTitleFontSize: fontSize,
524
+ tabBarItemTitleFontWeight: fontWeight,
525
+ tabBarItemTitleFontStyle: fontStyle,
526
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
527
+ tabBarItemIconColor: inactiveTintColor,
528
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
529
+ };
530
+
531
+ const tabItemLayoutAppearance = {
532
+ stacked: {
533
+ normal: {
534
+ ...tabItemAppearance,
535
+ tabBarItemTitlePositionAdjustment:
536
+ tabBarLabelPositionAdjustment?.stacked,
537
+ },
538
+ },
539
+ inline: {
540
+ normal: {
541
+ ...tabItemAppearance,
542
+ tabBarItemTitlePositionAdjustment:
543
+ tabBarLabelPositionAdjustment?.inline,
544
+ },
545
+ },
546
+ compactInline: {
547
+ normal: {
548
+ ...tabItemAppearance,
549
+ tabBarItemTitlePositionAdjustment:
550
+ tabBarLabelPositionAdjustment?.compactInline,
551
+ },
552
+ },
553
+ };
554
+
555
+ const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
556
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
557
+ tabBarItemIconColor: inactiveTintColor,
558
+ };
559
+
560
+ const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
561
+ tabBarItemTitleFontColor: activeTintColor,
562
+ tabBarItemIconColor: activeTintColor,
563
+ };
564
+
565
+ const icon =
566
+ typeof tabBarIcon === 'function'
567
+ ? getPlatformIcon(tabBarIcon({ focused: false }))
568
+ : tabBarIcon != null
569
+ ? getPlatformIcon(tabBarIcon)
570
+ : undefined;
571
+
572
+ const selectedIcon =
573
+ typeof tabBarIcon === 'function'
574
+ ? getPlatformIcon(tabBarIcon({ focused: true }))
575
+ : undefined;
576
+
577
+ return (
578
+ <Tabs.Screen
579
+ onWillDisappear={() => onTransitionStart({ closing: true, route })}
580
+ onWillAppear={() => onTransitionStart({ closing: false, route })}
581
+ onDidAppear={() => onTransitionEnd({ closing: false, route })}
582
+ onDidDisappear={() => onTransitionEnd({ closing: true, route })}
583
+ screenKey={route.key}
584
+ preventNativeSelection={tabBarSelectionEnabled === false}
585
+ badgeValue={tabBarBadge?.toString()}
586
+ title={tabTitle}
587
+ style={{ backgroundColor: colors.background }}
588
+ specialEffects={{
589
+ repeatedTabSelection: {
590
+ popToRoot: tabBarRepeatedPressBehavior?.popToTop !== false,
591
+ scrollToTop: tabBarRepeatedPressBehavior?.scrollToTop !== false,
592
+ },
593
+ }}
594
+ android={{
595
+ icon: icon?.android ?? icon?.shared,
596
+ selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
597
+ standardAppearance: {
598
+ tabBarBackgroundColor: tabBarBackgroundColor ?? colors.card,
599
+ tabBarItemRippleColor: rippleColor,
600
+ tabBarItemLabelVisibilityMode: labelVisibilityMode,
601
+ normal: normalTabItemAppearance,
602
+ selected: selectedTabItemAppearance,
603
+ tabBarItemIconSize: tabBarIconSize,
604
+ tabBarItemActiveIndicatorColor: activeIndicatorColor,
605
+ tabBarItemActiveIndicatorEnabled: activeIndicatorEnabled,
606
+ tabBarItemActiveIndicatorWidth: tabBarActiveIndicatorWidth,
607
+ tabBarItemActiveIndicatorHeight: tabBarActiveIndicatorHeight,
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,
621
+ role: tabBarRole === 'search' ? tabBarRole : undefined,
622
+ automaticallyActivatesSearch: tabBarSearchAutoFocus,
623
+ scrollEdgeAppearance: {
624
+ tabBarBackgroundColor,
625
+ tabBarShadowColor,
626
+ tabBarBlurEffect,
627
+ ...tabItemLayoutAppearance,
628
+ },
629
+ standardAppearance: {
630
+ tabBarBackgroundColor,
631
+ tabBarShadowColor,
632
+ tabBarBlurEffect,
633
+ ...tabItemLayoutAppearance,
634
+ },
635
+ overrideScrollViewContentInsetAdjustmentBehavior,
636
+ }}
637
+ >
638
+ <Deferred lazy={lazy} visible={visible}>
639
+ <ScreenWithHeader
640
+ isFocused={isFocused}
641
+ route={route}
642
+ navigation={descriptor.navigation}
643
+ options={options}
644
+ >
645
+ <NavigationMetaContext.Provider value={meta}>
646
+ {render()}
647
+ </NavigationMetaContext.Provider>
648
+ </ScreenWithHeader>
649
+ </Deferred>
650
+ </Tabs.Screen>
651
+ );
652
+ });
653
+
535
654
  function ScreenWithHeader({
536
655
  isFocused,
537
656
  route,
@@ -593,28 +712,47 @@ function getPlatformIcon(icon: NativeBottomTabIcon): PlatformIcon {
593
712
  switch (icon.type) {
594
713
  case 'sfSymbol':
595
714
  return {
596
- ios: icon,
715
+ ios: {
716
+ type: 'sfSymbol',
717
+ name: icon.name,
718
+ renderingMode:
719
+ icon.renderingMode === 'multicolor'
720
+ ? 'original'
721
+ : icon.renderingMode,
722
+ },
597
723
  android: undefined,
598
724
  shared: undefined,
599
725
  };
600
- case 'image':
726
+ case 'image': {
727
+ // A `{ uri: 'name' }` source maps to an Android drawable by name.
728
+ const drawableName =
729
+ typeof icon.source === 'object' &&
730
+ 'uri' in icon.source &&
731
+ icon.source.uri?.includes(':') === false
732
+ ? icon.source.uri
733
+ : undefined;
734
+
601
735
  return {
602
- ios:
603
- icon.tinted === false
736
+ ios: {
737
+ type: 'imageSource',
738
+ imageSource: icon.source,
739
+ renderingMode: icon.tinted === false ? 'original' : 'template',
740
+ },
741
+ android:
742
+ drawableName != null
604
743
  ? {
605
- type: 'imageSource',
606
- imageSource: icon.source,
744
+ type: 'drawableResource',
745
+ name: drawableName,
746
+ tinting: icon.tinted === false ? 'original' : 'tinted',
607
747
  }
608
748
  : {
609
- type: 'templateSource',
610
- templateSource: icon.source,
749
+ type: 'imageSource',
750
+ imageSource: icon.source,
751
+ tinting: icon.tinted === false ? 'original' : 'tinted',
611
752
  },
612
- android: undefined,
613
- shared: {
614
- type: 'imageSource',
615
- imageSource: icon.source,
616
- },
753
+ shared: undefined,
617
754
  };
755
+ }
618
756
  default: {
619
757
  const _exhaustiveCheck: never = icon;
620
758