@react-navigation/bottom-tabs 8.0.0-alpha.56 → 8.0.0-alpha.57

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.
@@ -6,11 +6,14 @@ import {
6
6
  } from '@react-navigation/elements/internal';
7
7
  import {
8
8
  CommonActions,
9
+ type LocaleDirection,
9
10
  MaterialSymbol,
11
+ type NavigationRoute,
10
12
  type ParamListBase,
11
13
  type Route,
12
14
  StackActions,
13
15
  type TabNavigationState,
16
+ useLocale,
14
17
  useTheme,
15
18
  } from '@react-navigation/native';
16
19
  import * as React from 'react';
@@ -21,20 +24,22 @@ import {
21
24
  Platform,
22
25
  PlatformColor,
23
26
  StyleSheet,
27
+ type TextStyle,
24
28
  } from 'react-native';
25
29
  import {
26
- type PlatformIconAndroid,
27
- type PlatformIconIOS,
28
30
  type PlatformIconShared,
29
31
  Tabs,
30
32
  type TabsBottomAccessoryEnvironment,
31
33
  type TabSelectedEvent,
32
34
  type TabSelectionRejectedEvent,
35
+ type TabsScreenIconAndroid,
36
+ type TabsScreenIconIOS,
33
37
  type TabsScreenItemStateAppearanceAndroid,
34
38
  type TabsScreenItemStateAppearanceIOS,
35
39
  } from 'react-native-screens';
36
40
 
37
41
  import type {
42
+ BottomTabDescriptor,
38
43
  BottomTabDescriptorMap,
39
44
  BottomTabNavigationConfig,
40
45
  BottomTabNavigationHelpers,
@@ -59,8 +64,8 @@ type TabSelectionPreventedEvent = {
59
64
  };
60
65
 
61
66
  type PlatformIcon = {
62
- ios?: PlatformIconIOS | undefined;
63
- android?: PlatformIconAndroid | undefined;
67
+ ios?: TabsScreenIconIOS | undefined;
68
+ android?: TabsScreenIconAndroid | undefined;
64
69
  shared?: PlatformIconShared | undefined;
65
70
  };
66
71
 
@@ -88,6 +93,9 @@ const ICON_SIZE = Platform.select({
88
93
  default: 24,
89
94
  });
90
95
 
96
+ const SUPPORTS_PROMINENT_TAB =
97
+ Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 27;
98
+
91
99
  function reducer(state: NativeState, action: NativeAction): NativeState {
92
100
  switch (action.type) {
93
101
  case 'TRACK_TRANSITION':
@@ -351,73 +359,75 @@ export function BottomTabViewNative({
351
359
 
352
360
  const shouldHideTabBar = currentOptions.tabBarStyle?.display === 'none';
353
361
 
354
- let activeIndicatorColor = currentOptions?.tabBarActiveIndicatorColor;
355
- let activeTintColor = currentOptions.tabBarActiveTintColor;
356
- let inactiveTintColor = currentOptions.tabBarInactiveTintColor;
357
-
358
- // Derive colors based on Material Design guidelines
359
- // https://m3.material.io/components/navigation-bar/specs
360
- if (Platform.OS === 'android') {
361
- switch (getAndroidColorName(backgroundColor)) {
362
- case 'system_surface_container_light':
363
- case 'system_surface_container_high_light':
364
- case 'system_surface_container_highest_light':
365
- case 'system_surface_container_low_light':
366
- case 'system_surface_container_lowest_light':
367
- inactiveTintColor =
368
- inactiveTintColor ??
369
- PlatformColor('@android:color/system_on_surface_variant_light');
370
- activeTintColor =
371
- activeTintColor ??
372
- PlatformColor('@android:color/system_on_secondary_container_light');
373
- activeIndicatorColor =
374
- activeIndicatorColor ??
375
- PlatformColor('@android:color/system_secondary_container_light');
376
- break;
377
- case 'system_surface_container_dark':
378
- case 'system_surface_container_high_dark':
379
- case 'system_surface_container_highest_dark':
380
- case 'system_surface_container_low_dark':
381
- case 'system_surface_container_lowest_dark':
382
- inactiveTintColor =
383
- inactiveTintColor ??
384
- PlatformColor('@android:color/system_on_surface_variant_dark');
385
- activeTintColor =
386
- activeTintColor ??
387
- PlatformColor('@android:color/system_on_secondary_container_dark');
388
- activeIndicatorColor =
389
- activeIndicatorColor ??
390
- PlatformColor('@android:color/system_secondary_container_dark');
391
- break;
392
- }
393
- }
394
-
395
- inactiveTintColor =
396
- inactiveTintColor ??
397
- Platform.select({ ios: PlatformColor('label'), default: colors.text });
362
+ const { activeIndicatorColor, activeTintColor, inactiveTintColor } =
363
+ React.useMemo(() => {
364
+ let activeIndicatorColor = currentOptions?.tabBarActiveIndicatorColor;
365
+ let activeTintColor = currentOptions.tabBarActiveTintColor;
366
+ let inactiveTintColor = currentOptions.tabBarInactiveTintColor;
367
+
368
+ // Derive colors based on Material Design guidelines
369
+ // https://m3.material.io/components/navigation-bar/specs
370
+ if (Platform.OS === 'android') {
371
+ switch (getAndroidColorName(backgroundColor)) {
372
+ case 'system_surface_container_light':
373
+ case 'system_surface_container_high_light':
374
+ case 'system_surface_container_highest_light':
375
+ case 'system_surface_container_low_light':
376
+ case 'system_surface_container_lowest_light':
377
+ inactiveTintColor =
378
+ inactiveTintColor ??
379
+ PlatformColor('@android:color/system_on_surface_variant_light');
380
+ activeTintColor =
381
+ activeTintColor ??
382
+ PlatformColor(
383
+ '@android:color/system_on_secondary_container_light'
384
+ );
385
+ activeIndicatorColor =
386
+ activeIndicatorColor ??
387
+ PlatformColor('@android:color/system_secondary_container_light');
388
+ break;
389
+ case 'system_surface_container_dark':
390
+ case 'system_surface_container_high_dark':
391
+ case 'system_surface_container_highest_dark':
392
+ case 'system_surface_container_low_dark':
393
+ case 'system_surface_container_lowest_dark':
394
+ inactiveTintColor =
395
+ inactiveTintColor ??
396
+ PlatformColor('@android:color/system_on_surface_variant_dark');
397
+ activeTintColor =
398
+ activeTintColor ??
399
+ PlatformColor(
400
+ '@android:color/system_on_secondary_container_dark'
401
+ );
402
+ activeIndicatorColor =
403
+ activeIndicatorColor ??
404
+ PlatformColor('@android:color/system_secondary_container_dark');
405
+ break;
406
+ }
407
+ }
398
408
 
399
- activeTintColor = activeTintColor ?? colors.primary;
409
+ inactiveTintColor =
410
+ inactiveTintColor ??
411
+ Platform.select({ ios: PlatformColor('label'), default: colors.text });
400
412
 
401
- activeIndicatorColor =
402
- activeIndicatorColor ??
403
- Platform.select({
404
- android: Color(activeTintColor)?.alpha(0.075).string(),
405
- default: undefined,
406
- });
413
+ activeTintColor = activeTintColor ?? colors.primary;
407
414
 
408
- const onTransitionStart = ({ route }: { route: Route<string> }) => {
409
- navigation.emit({
410
- type: 'transitionStart',
411
- target: route.key,
412
- });
413
- };
415
+ activeIndicatorColor =
416
+ activeIndicatorColor ??
417
+ Platform.select({
418
+ android: Color(activeTintColor)?.alpha(0.075).string(),
419
+ default: undefined,
420
+ });
414
421
 
415
- const onTransitionEnd = ({ route }: { route: Route<string> }) => {
416
- navigation.emit({
417
- type: 'transitionEnd',
418
- target: route.key,
419
- });
420
- };
422
+ return { activeIndicatorColor, activeTintColor, inactiveTintColor };
423
+ }, [
424
+ currentOptions?.tabBarActiveIndicatorColor,
425
+ currentOptions.tabBarActiveTintColor,
426
+ currentOptions.tabBarInactiveTintColor,
427
+ backgroundColor,
428
+ colors.primary,
429
+ colors.text,
430
+ ]);
421
431
 
422
432
  const tabBarPosition = useTabBarPosition(currentOptions);
423
433
 
@@ -442,6 +452,17 @@ export function BottomTabViewNative({
442
452
  ? 'never'
443
453
  : currentOptions.tabBarMinimizeBehavior;
444
454
 
455
+ const tabBarSidebarPreferredPlacement =
456
+ currentOptions.tabBarSidebarPreferredPlacement === 'auto'
457
+ ? 'automatic'
458
+ : currentOptions.tabBarSidebarPreferredPlacement;
459
+
460
+ const prominentScreenKey = SUPPORTS_PROMINENT_TAB
461
+ ? state.routes.find(
462
+ (route) => descriptors[route.key]?.options.tabBarRole === 'prominent'
463
+ )?.key
464
+ : undefined;
465
+
445
466
  const bottomAccessory = currentOptions.bottomAccessory;
446
467
 
447
468
  return (
@@ -469,11 +490,15 @@ export function BottomTabViewNative({
469
490
  colorScheme={dark ? 'dark' : 'light'}
470
491
  nativeContainerStyle={{ backgroundColor: colors.background }}
471
492
  ios={{
493
+ prominentScreenKey,
472
494
  bottomAccessory: bottomAccessory
473
495
  ? (environment: TabsBottomAccessoryEnvironment) =>
474
496
  bottomAccessory({ placement: environment })
475
497
  : undefined,
498
+ tabBarHiddenAnimationEnabled:
499
+ currentOptions.tabBarVisibilityAnimationEnabled,
476
500
  tabBarControllerMode,
501
+ tabBarSidebarPreferredPlacement,
477
502
  tabBarMinimizeBehavior,
478
503
  tabBarTintColor: activeTintColor,
479
504
  }}
@@ -487,105 +512,11 @@ export function BottomTabViewNative({
487
512
  );
488
513
  }
489
514
 
490
- const { options, render, navigation } = descriptor;
491
515
  const isFocused = state.index === index;
492
516
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
493
517
 
494
- const {
495
- title,
496
- lazy = false,
497
- inactiveBehavior = 'pause',
498
- tabBarLabel,
499
- tabBarSelectionEnabled,
500
- tabBarRepeatedPressBehavior,
501
- tabBarBadgeStyle,
502
- tabBarIcon,
503
- tabBarBadge,
504
- tabBarSystemItem,
505
- tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
506
- tabBarStyle,
507
- tabBarAccessibilityLabel,
508
- tabBarButtonTestID,
509
- sceneStyle,
510
- overrideScrollViewContentInsetAdjustmentBehavior,
511
- } = options;
512
-
513
- const {
514
- backgroundColor: tabBarBackgroundColor,
515
- shadowColor: tabBarShadowColor,
516
- } = tabBarStyle || {};
517
-
518
- const tabTitle =
519
- // On iOS, `systemItem` already provides a localized label
520
- // So we should only use `tabBarLabel` if explicitly provided
521
- Platform.OS === 'ios' && tabBarSystemItem != null
522
- ? tabBarLabel
523
- : getLabel({ label: tabBarLabel, title }, route.name);
524
-
525
- const badgeBackgroundColor =
526
- tabBarBadgeStyle?.backgroundColor ?? colors.notification;
527
- const badgeTextColor =
528
- tabBarBadgeStyle?.color ?? Color.foreground(badgeBackgroundColor);
529
-
530
- const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
531
- tabBarItemTitleFontFamily: fontFamily,
532
- tabBarItemTitleFontSize: fontSize,
533
- tabBarItemTitleFontWeight: fontWeight,
534
- tabBarItemTitleFontStyle: fontStyle,
535
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
536
- tabBarItemIconColor: inactiveTintColor,
537
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
538
- };
539
-
540
- const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
541
- {
542
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
543
- tabBarItemIconColor: inactiveTintColor,
544
- };
545
-
546
- const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
547
- {
548
- tabBarItemTitleFontColor: activeTintColor,
549
- tabBarItemIconColor: activeTintColor,
550
- };
551
-
552
- const getIcon = (selected: boolean) => {
553
- if (typeof tabBarIcon === 'function') {
554
- const result = tabBarIcon({
555
- focused: selected,
556
- size: ICON_SIZE!,
557
- color: selected ? activeTintColor : inactiveTintColor,
558
- });
559
-
560
- if (React.isValidElement(result)) {
561
- throw new Error(
562
- `Returning a React element from 'tabBarIcon' is not supported with native tab bar.`
563
- );
564
- } else if (
565
- result &&
566
- typeof result === 'object' &&
567
- 'type' in result
568
- ) {
569
- return getPlatformIcon(result);
570
- } else {
571
- throw new Error(
572
- `The 'tabBarIcon' function must return an icon object (got ${typeof result}).`
573
- );
574
- }
575
- } else if (tabBarIcon != null) {
576
- return getPlatformIcon(tabBarIcon);
577
- }
578
-
579
- return undefined;
580
- };
581
-
582
- // An error is thrown for React Elements in `getIcon`
583
- // So we only call it when we actually render a native tab bar
584
- const icon = hasCustomTabBar ? undefined : getIcon(false);
585
- const selectedIcon =
586
- !hasCustomTabBar && typeof tabBarIcon === 'function'
587
- ? getIcon(true)
588
- : icon;
518
+ const { lazy = false, inactiveBehavior = 'pause' } =
519
+ descriptor.options;
589
520
 
590
521
  // For preloaded screens and if lazy is false,
591
522
  // Keep them active so that the effects can run
@@ -597,113 +528,31 @@ export function BottomTabViewNative({
597
528
  (lazy === false && !loaded.includes(route.key));
598
529
 
599
530
  return (
600
- <Tabs.Screen
601
- onWillAppear={() => onTransitionStart({ route })}
602
- onDidAppear={() => onTransitionEnd({ route })}
531
+ <TabScene
603
532
  key={route.key}
604
- screenKey={route.key}
605
- tabBarItemAccessibilityLabel={tabBarAccessibilityLabel}
606
- tabBarItemTestID={tabBarButtonTestID}
607
- preventNativeSelection={tabBarSelectionEnabled === false}
608
- badgeValue={tabBarBadge?.toString()}
609
- title={tabTitle}
610
- nativeContainerStyle={{ backgroundColor: colors.background }}
611
- specialEffects={{
612
- repeatedTabSelection: {
613
- popToRoot: tabBarRepeatedPressBehavior?.popToTop !== false,
614
- scrollToTop:
615
- tabBarRepeatedPressBehavior?.scrollToTop !== false,
616
- },
617
- }}
618
- android={{
619
- icon: icon?.android ?? icon?.shared,
620
- selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
621
- standardAppearance: {
622
- tabBarBackgroundColor:
623
- tabBarBackgroundColor ?? backgroundColor,
624
- tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
625
- tabBarItemLabelVisibilityMode:
626
- currentOptions?.tabBarLabelVisibilityMode,
627
- normal: normalTabItemAppearance,
628
- selected: selectedTabItemAppearance,
629
- tabBarItemActiveIndicatorColor: activeIndicatorColor,
630
- tabBarItemActiveIndicatorEnabled:
631
- currentOptions?.tabBarActiveIndicatorEnabled,
632
- tabBarItemTitleFontFamily: fontFamily,
633
- tabBarItemTitleFontWeight: fontWeight,
634
- tabBarItemTitleSmallLabelFontSize: fontSize,
635
- tabBarItemTitleLargeLabelFontSize: fontSize,
636
- tabBarItemTitleFontStyle: fontStyle,
637
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
638
- tabBarItemBadgeTextColor: badgeTextColor,
639
- },
640
- }}
641
- ios={{
642
- icon: icon?.ios ?? icon?.shared,
643
- selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
644
- systemItem: tabBarSystemItem,
645
- scrollEdgeAppearance: {
646
- tabBarBackgroundColor,
647
- tabBarShadowColor,
648
- tabBarBlurEffect,
649
- stacked: {
650
- normal: tabItemAppearance,
651
- },
652
- inline: {
653
- normal: tabItemAppearance,
654
- },
655
- compactInline: {
656
- normal: tabItemAppearance,
657
- },
658
- },
659
- standardAppearance: {
660
- tabBarBackgroundColor,
661
- tabBarShadowColor,
662
- tabBarBlurEffect,
663
- stacked: {
664
- normal: tabItemAppearance,
665
- },
666
- inline: {
667
- normal: tabItemAppearance,
668
- },
669
- compactInline: {
670
- normal: tabItemAppearance,
671
- },
672
- },
673
- overrideScrollViewContentInsetAdjustmentBehavior,
674
- }}
675
- >
676
- <Deferred
677
- lazy={lazy}
678
- visible={isFocused || isPreloaded || loaded.includes(route.key)}
679
- >
680
- <ActivityView
681
- key={route.key}
682
- mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
683
- visible={
684
- // We don't need to hide the content since it's handled natively
685
- // Hiding may also cause flash due to lag after native tab switch
686
- // So we leave it always visible
687
- true
688
- }
689
- style={StyleSheet.absoluteFill}
690
- >
691
- <ScreenContent
692
- isFocused={isFocused}
693
- route={route}
694
- navigation={navigation}
695
- options={options}
696
- style={sceneStyle}
697
- >
698
- <AnimatedScreenContent isFocused={isFocused}>
699
- <BottomTabBarHeightContext.Provider value={0}>
700
- {render()}
701
- </BottomTabBarHeightContext.Provider>
702
- </AnimatedScreenContent>
703
- </ScreenContent>
704
- </ActivityView>
705
- </Deferred>
706
- </Tabs.Screen>
533
+ route={route}
534
+ descriptor={descriptor}
535
+ navigation={navigation}
536
+ isFocused={isFocused}
537
+ isActive={isActive}
538
+ lazy={lazy}
539
+ visible={isFocused || isPreloaded || loaded.includes(route.key)}
540
+ hasCustomTabBar={hasCustomTabBar}
541
+ fontFamily={fontFamily}
542
+ fontWeight={fontWeight}
543
+ fontSize={fontSize}
544
+ fontStyle={fontStyle}
545
+ fontColor={fontColor}
546
+ backgroundColor={backgroundColor}
547
+ activeTintColor={activeTintColor}
548
+ inactiveTintColor={inactiveTintColor}
549
+ activeIndicatorColor={activeIndicatorColor}
550
+ rippleColor={currentOptions.tabBarRippleColor}
551
+ labelVisibilityMode={currentOptions.tabBarLabelVisibilityMode}
552
+ activeIndicatorEnabled={
553
+ currentOptions.tabBarActiveIndicatorEnabled
554
+ }
555
+ />
707
556
  );
708
557
  })}
709
558
  </Tabs.Host>
@@ -714,6 +563,287 @@ export function BottomTabViewNative({
714
563
  );
715
564
  }
716
565
 
566
+ type TabSceneProps = {
567
+ route: NavigationRoute<ParamListBase, string>;
568
+ descriptor: BottomTabDescriptor;
569
+ navigation: BottomTabNavigationHelpers;
570
+ isFocused: boolean;
571
+ isActive: boolean;
572
+ lazy: boolean;
573
+ visible: boolean;
574
+ hasCustomTabBar: boolean;
575
+ fontFamily: TextStyle['fontFamily'];
576
+ fontWeight: TextStyle['fontWeight'];
577
+ fontSize: TextStyle['fontSize'];
578
+ fontStyle: TextStyle['fontStyle'];
579
+ fontColor: TextStyle['color'];
580
+ backgroundColor: ColorValue;
581
+ activeTintColor: ColorValue;
582
+ inactiveTintColor: ColorValue;
583
+ activeIndicatorColor: ColorValue | undefined;
584
+ rippleColor: ColorValue | undefined;
585
+ labelVisibilityMode: BottomTabNavigationOptions['tabBarLabelVisibilityMode'];
586
+ activeIndicatorEnabled: boolean | undefined;
587
+ };
588
+
589
+ const TabScene = React.memo(function TabScene({
590
+ route,
591
+ descriptor,
592
+ navigation,
593
+ isFocused,
594
+ isActive,
595
+ lazy,
596
+ visible,
597
+ hasCustomTabBar,
598
+ fontFamily,
599
+ fontWeight,
600
+ fontSize,
601
+ fontStyle,
602
+ fontColor,
603
+ backgroundColor,
604
+ activeTintColor,
605
+ inactiveTintColor,
606
+ activeIndicatorColor,
607
+ rippleColor,
608
+ labelVisibilityMode,
609
+ activeIndicatorEnabled,
610
+ }: TabSceneProps) {
611
+ const { dark, colors } = useTheme();
612
+ const { direction } = useLocale();
613
+
614
+ const onTransitionStart = ({ route }: { route: Route<string> }) => {
615
+ navigation.emit({
616
+ type: 'transitionStart',
617
+ target: route.key,
618
+ });
619
+ };
620
+
621
+ const onTransitionEnd = ({ route }: { route: Route<string> }) => {
622
+ navigation.emit({
623
+ type: 'transitionEnd',
624
+ target: route.key,
625
+ });
626
+ };
627
+
628
+ const { options, render } = descriptor;
629
+
630
+ const {
631
+ title,
632
+ tabBarLabel,
633
+ tabBarLabelPositionAdjustment,
634
+ tabBarSelectionEnabled,
635
+ tabBarRepeatedPressBehavior,
636
+ tabBarBadgeStyle,
637
+ tabBarIcon,
638
+ tabBarIconSize,
639
+ tabBarActiveIndicatorWidth,
640
+ tabBarActiveIndicatorHeight,
641
+ tabBarBadge,
642
+ tabBarSystemItem,
643
+ tabBarRole,
644
+ tabBarSearchAutoFocus,
645
+ tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
646
+ tabBarStyle,
647
+ tabBarAccessibilityLabel,
648
+ tabBarButtonTestID,
649
+ sceneStyle,
650
+ overrideScrollViewContentInsetAdjustmentBehavior,
651
+ } = options;
652
+
653
+ const systemItem =
654
+ tabBarSystemItem ??
655
+ // Setting the `role` to 'search' doesn't add title and icon automatically
656
+ // So we also set `tabBarSystemItem` to 'search' to match SwiftUI's behavior
657
+ (tabBarRole === 'search' ? 'search' : undefined);
658
+
659
+ const {
660
+ backgroundColor: tabBarBackgroundColor,
661
+ shadowColor: tabBarShadowColor,
662
+ } = tabBarStyle || {};
663
+
664
+ const tabTitle =
665
+ // On iOS, `systemItem` already provides a localized label
666
+ // So we should only use `tabBarLabel` if explicitly provided
667
+ Platform.OS === 'ios' && systemItem != null
668
+ ? tabBarLabel
669
+ : getLabel({ label: tabBarLabel, title }, route.name);
670
+
671
+ const badgeBackgroundColor =
672
+ tabBarBadgeStyle?.backgroundColor ?? colors.notification;
673
+ const badgeTextColor =
674
+ tabBarBadgeStyle?.color ?? Color.foreground(badgeBackgroundColor);
675
+
676
+ const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
677
+ tabBarItemTitleFontFamily: fontFamily,
678
+ tabBarItemTitleFontSize: fontSize,
679
+ tabBarItemTitleFontWeight: fontWeight,
680
+ tabBarItemTitleFontStyle: fontStyle,
681
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
682
+ tabBarItemIconColor: inactiveTintColor,
683
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
684
+ };
685
+
686
+ const tabItemLayoutAppearance = {
687
+ stacked: {
688
+ normal: {
689
+ ...tabItemAppearance,
690
+ tabBarItemTitlePositionAdjustment:
691
+ tabBarLabelPositionAdjustment?.stacked,
692
+ },
693
+ },
694
+ inline: {
695
+ normal: {
696
+ ...tabItemAppearance,
697
+ tabBarItemTitlePositionAdjustment:
698
+ tabBarLabelPositionAdjustment?.inline,
699
+ },
700
+ },
701
+ compactInline: {
702
+ normal: {
703
+ ...tabItemAppearance,
704
+ tabBarItemTitlePositionAdjustment:
705
+ tabBarLabelPositionAdjustment?.compactInline,
706
+ },
707
+ },
708
+ };
709
+
710
+ const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
711
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
712
+ tabBarItemIconColor: inactiveTintColor,
713
+ };
714
+
715
+ const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
716
+ tabBarItemTitleFontColor: activeTintColor,
717
+ tabBarItemIconColor: activeTintColor,
718
+ };
719
+
720
+ const iconSize =
721
+ Platform.OS === 'android' ? (tabBarIconSize ?? ICON_SIZE!) : ICON_SIZE!;
722
+
723
+ const getIcon = (selected: boolean) => {
724
+ if (typeof tabBarIcon === 'function') {
725
+ const result = tabBarIcon({
726
+ focused: selected,
727
+ size: iconSize,
728
+ color: selected ? activeTintColor : inactiveTintColor,
729
+ });
730
+
731
+ if (React.isValidElement(result)) {
732
+ throw new Error(
733
+ `Returning a React element from 'tabBarIcon' is not supported with native tab bar.`
734
+ );
735
+ } else if (result && typeof result === 'object' && 'type' in result) {
736
+ return getPlatformIcon(result, direction, iconSize);
737
+ } else {
738
+ throw new Error(
739
+ `The 'tabBarIcon' function must return an icon object (got ${typeof result}).`
740
+ );
741
+ }
742
+ } else if (tabBarIcon != null) {
743
+ return getPlatformIcon(tabBarIcon, direction, iconSize);
744
+ }
745
+
746
+ return undefined;
747
+ };
748
+
749
+ // An error is thrown for React Elements in `getIcon`
750
+ // So we only call it when we actually render a native tab bar
751
+ const icon = hasCustomTabBar ? undefined : getIcon(false);
752
+ const selectedIcon =
753
+ !hasCustomTabBar && typeof tabBarIcon === 'function' ? getIcon(true) : icon;
754
+
755
+ return (
756
+ <Tabs.Screen
757
+ onWillAppear={() => onTransitionStart({ route })}
758
+ onDidAppear={() => onTransitionEnd({ route })}
759
+ screenKey={route.key}
760
+ tabBarItemAccessibilityLabel={tabBarAccessibilityLabel}
761
+ tabBarItemTestID={tabBarButtonTestID}
762
+ preventNativeSelection={tabBarSelectionEnabled === false}
763
+ badgeValue={tabBarBadge?.toString()}
764
+ title={tabTitle}
765
+ style={{ backgroundColor: colors.background }}
766
+ specialEffects={{
767
+ repeatedTabSelection: {
768
+ popToRoot: tabBarRepeatedPressBehavior?.popToTop !== false,
769
+ scrollToTop: tabBarRepeatedPressBehavior?.scrollToTop !== false,
770
+ },
771
+ }}
772
+ android={{
773
+ icon: icon?.android ?? icon?.shared,
774
+ selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
775
+ standardAppearance: {
776
+ tabBarBackgroundColor: tabBarBackgroundColor ?? backgroundColor,
777
+ tabBarItemRippleColor: rippleColor,
778
+ tabBarItemLabelVisibilityMode: labelVisibilityMode,
779
+ normal: normalTabItemAppearance,
780
+ selected: selectedTabItemAppearance,
781
+ tabBarItemIconSize: tabBarIconSize,
782
+ tabBarItemActiveIndicatorColor: activeIndicatorColor,
783
+ tabBarItemActiveIndicatorEnabled: activeIndicatorEnabled,
784
+ tabBarItemActiveIndicatorWidth: tabBarActiveIndicatorWidth,
785
+ tabBarItemActiveIndicatorHeight: tabBarActiveIndicatorHeight,
786
+ tabBarItemTitleFontFamily: fontFamily,
787
+ tabBarItemTitleFontWeight: fontWeight,
788
+ tabBarItemTitleSmallLabelFontSize: fontSize,
789
+ tabBarItemTitleLargeLabelFontSize: fontSize,
790
+ tabBarItemTitleFontStyle: fontStyle,
791
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
792
+ tabBarItemBadgeTextColor: badgeTextColor,
793
+ },
794
+ }}
795
+ ios={{
796
+ icon: icon?.ios ?? icon?.shared,
797
+ selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
798
+ systemItem,
799
+ role: tabBarRole === 'search' ? tabBarRole : undefined,
800
+ automaticallyActivatesSearch: tabBarSearchAutoFocus,
801
+ scrollEdgeAppearance: {
802
+ tabBarBackgroundColor,
803
+ tabBarShadowColor,
804
+ tabBarBlurEffect,
805
+ ...tabItemLayoutAppearance,
806
+ },
807
+ standardAppearance: {
808
+ tabBarBackgroundColor,
809
+ tabBarShadowColor,
810
+ tabBarBlurEffect,
811
+ ...tabItemLayoutAppearance,
812
+ },
813
+ overrideScrollViewContentInsetAdjustmentBehavior,
814
+ }}
815
+ >
816
+ <Deferred lazy={lazy} visible={visible}>
817
+ <ActivityView
818
+ key={route.key}
819
+ mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
820
+ visible={
821
+ // We don't need to hide the content since it's handled natively
822
+ // Hiding may also cause flash due to lag after native tab switch
823
+ // So we leave it always visible
824
+ true
825
+ }
826
+ style={StyleSheet.absoluteFill}
827
+ >
828
+ <ScreenContent
829
+ isFocused={isFocused}
830
+ route={route}
831
+ navigation={descriptor.navigation}
832
+ options={options}
833
+ style={sceneStyle}
834
+ >
835
+ <AnimatedScreenContent isFocused={isFocused}>
836
+ <BottomTabBarHeightContext.Provider value={0}>
837
+ {render()}
838
+ </BottomTabBarHeightContext.Provider>
839
+ </AnimatedScreenContent>
840
+ </ScreenContent>
841
+ </ActivityView>
842
+ </Deferred>
843
+ </Tabs.Screen>
844
+ );
845
+ });
846
+
717
847
  function AnimatedScreenContent({
718
848
  isFocused,
719
849
  children,
@@ -747,11 +877,22 @@ function AnimatedScreenContent({
747
877
  );
748
878
  }
749
879
 
750
- function getPlatformIcon(icon: Icon): PlatformIcon {
880
+ function getPlatformIcon(
881
+ icon: Icon,
882
+ direction: LocaleDirection,
883
+ size: number
884
+ ): PlatformIcon {
751
885
  switch (icon.type) {
752
886
  case 'sfSymbol':
753
887
  return {
754
- ios: icon,
888
+ ios: {
889
+ type: 'sfSymbol',
890
+ name: icon.name,
891
+ renderingMode:
892
+ icon.renderingMode === 'multicolor'
893
+ ? 'original'
894
+ : icon.renderingMode,
895
+ },
755
896
  android: undefined,
756
897
  shared: undefined,
757
898
  };
@@ -764,29 +905,43 @@ function getPlatformIcon(icon: Icon): PlatformIcon {
764
905
  name: icon.name,
765
906
  variant: icon.variant,
766
907
  weight: icon.weight,
767
- size: ICON_SIZE,
908
+ fill: icon.fill,
909
+ size,
910
+ direction,
768
911
  }),
769
912
  },
770
913
  shared: undefined,
771
914
  };
772
- case 'image':
915
+ case 'image': {
916
+ // A `{ uri: 'name' }` source maps to an Android drawable by name.
917
+ const drawableName =
918
+ typeof icon.source === 'object' &&
919
+ 'uri' in icon.source &&
920
+ icon.source.uri?.includes(':') === false
921
+ ? icon.source.uri
922
+ : undefined;
923
+
773
924
  return {
774
- ios:
775
- icon.tinted === false
925
+ ios: {
926
+ type: 'imageSource',
927
+ imageSource: icon.source,
928
+ renderingMode: icon.tinted === false ? 'original' : 'template',
929
+ },
930
+ android:
931
+ drawableName != null
776
932
  ? {
777
- type: 'imageSource',
778
- imageSource: icon.source,
933
+ type: 'drawableResource',
934
+ name: drawableName,
935
+ tinting: icon.tinted === false ? 'original' : 'tinted',
779
936
  }
780
937
  : {
781
- type: 'templateSource',
782
- templateSource: icon.source,
938
+ type: 'imageSource',
939
+ imageSource: icon.source,
940
+ tinting: icon.tinted === false ? 'original' : 'tinted',
783
941
  },
784
- android: undefined,
785
- shared: {
786
- type: 'imageSource',
787
- imageSource: icon.source,
788
- },
942
+ shared: undefined,
789
943
  };
944
+ }
790
945
  default: {
791
946
  const _exhaustiveCheck: never = icon;
792
947