import { getDefaultSidebarWidth, getLabel, MissingIcon, useFrameSize, } from '@react-navigation/elements'; import { CommonActions, NavigationContext, NavigationRouteContext, type ParamListBase, type TabNavigationState, useLinkBuilder, useLocale, useTheme, } from '@react-navigation/native'; import React from 'react'; import { Animated, type LayoutChangeEvent, Platform, type StyleProp, StyleSheet, View, type ViewStyle, } from 'react-native'; import { type EdgeInsets } from 'react-native-safe-area-context'; import type { BottomTabBarProps, BottomTabDescriptorMap } from '../types'; import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext'; import { useIsKeyboardShown } from '../utils/useIsKeyboardShown'; import { BottomTabItem } from './BottomTabItem'; type Props = BottomTabBarProps & { style?: Animated.WithAnimatedValue>; }; const TABBAR_HEIGHT_UIKIT = 49; const TABBAR_HEIGHT_UIKIT_COMPACT = 32; const SPACING_UIKIT = 15; const SPACING_MATERIAL = 12; const DEFAULT_MAX_TAB_ITEM_WIDTH = 125; const useNativeDriver = Platform.OS !== 'web'; type Options = { state: TabNavigationState; descriptors: BottomTabDescriptorMap; dimensions: { height: number; width: number }; }; const shouldUseHorizontalLabels = ({ state, descriptors, dimensions, }: Options) => { const { tabBarLabelPosition } = descriptors[state.routes[state.index].key].options; if (tabBarLabelPosition) { switch (tabBarLabelPosition) { case 'beside-icon': return true; case 'below-icon': return false; } } if (dimensions.width >= 768) { // Screen size matches a tablet const maxTabWidth = state.routes.reduce((acc, route) => { const { tabBarItemStyle } = descriptors[route.key].options; const flattenedStyle = StyleSheet.flatten(tabBarItemStyle); if (flattenedStyle) { if (typeof flattenedStyle.width === 'number') { return acc + flattenedStyle.width; } else if (typeof flattenedStyle.maxWidth === 'number') { return acc + flattenedStyle.maxWidth; } } return acc + DEFAULT_MAX_TAB_ITEM_WIDTH; }, 0); return maxTabWidth <= dimensions.width; } else { return dimensions.width > dimensions.height; } }; const isCompact = ({ state, descriptors, dimensions }: Options): boolean => { const { tabBarPosition, tabBarVariant } = descriptors[state.routes[state.index].key].options; if ( tabBarPosition === 'left' || tabBarPosition === 'right' || tabBarVariant === 'material' ) { return false; } const isLandscape = dimensions.width > dimensions.height; const horizontalLabels = shouldUseHorizontalLabels({ state, descriptors, dimensions, }); if ( Platform.OS === 'ios' && !Platform.isPad && isLandscape && horizontalLabels ) { return true; } return false; }; export const getTabBarHeight = ({ state, descriptors, dimensions, insets, style, }: Options & { insets: EdgeInsets; style: Animated.WithAnimatedValue> | undefined; }) => { const { tabBarPosition } = descriptors[state.routes[state.index].key].options; const flattenedStyle = StyleSheet.flatten(style); const customHeight = flattenedStyle && 'height' in flattenedStyle ? flattenedStyle.height : undefined; if (typeof customHeight === 'number') { return customHeight; } const inset = insets[tabBarPosition === 'top' ? 'top' : 'bottom']; if (isCompact({ state, descriptors, dimensions })) { return TABBAR_HEIGHT_UIKIT_COMPACT + inset; } return TABBAR_HEIGHT_UIKIT + inset; }; export function BottomTabBar({ state, navigation, descriptors, insets, style, }: Props) { const { colors } = useTheme(); const { direction } = useLocale(); const { buildHref } = useLinkBuilder(); const focusedRoute = state.routes[state.index]; const focusedDescriptor = descriptors[focusedRoute.key]; const focusedOptions = focusedDescriptor.options; const { tabBarPosition = 'bottom', tabBarShowLabel, tabBarLabelPosition, tabBarHideOnKeyboard = false, tabBarVisibilityAnimationConfig, tabBarVariant = 'uikit', tabBarStyle, tabBarBackground, tabBarActiveTintColor, tabBarInactiveTintColor, tabBarActiveBackgroundColor, tabBarInactiveBackgroundColor, } = focusedOptions; if ( tabBarVariant === 'material' && tabBarPosition !== 'left' && tabBarPosition !== 'right' ) { throw new Error( "The 'material' variant for tab bar is only supported when 'tabBarPosition' is set to 'left' or 'right'." ); } if ( tabBarLabelPosition === 'below-icon' && tabBarVariant === 'uikit' && (tabBarPosition === 'left' || tabBarPosition === 'right') ) { throw new Error( "The 'below-icon' label position for tab bar is only supported when 'tabBarPosition' is set to 'top' or 'bottom' when using the 'uikit' variant." ); } const isKeyboardShown = useIsKeyboardShown(); const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext); const shouldShowTabBar = !(tabBarHideOnKeyboard && isKeyboardShown); const visibilityAnimationConfigRef = React.useRef( tabBarVisibilityAnimationConfig ); React.useEffect(() => { visibilityAnimationConfigRef.current = tabBarVisibilityAnimationConfig; }); const [isTabBarHidden, setIsTabBarHidden] = React.useState(!shouldShowTabBar); const [visible] = React.useState( () => new Animated.Value(shouldShowTabBar ? 1 : 0) ); React.useEffect(() => { const visibilityAnimationConfig = visibilityAnimationConfigRef.current; if (shouldShowTabBar) { const animation = visibilityAnimationConfig?.show?.animation === 'spring' ? Animated.spring : Animated.timing; animation(visible, { toValue: 1, useNativeDriver, duration: 250, ...visibilityAnimationConfig?.show?.config, }).start(({ finished }) => { if (finished) { setIsTabBarHidden(false); } }); } else { // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setIsTabBarHidden(true); const animation = visibilityAnimationConfig?.hide?.animation === 'spring' ? Animated.spring : Animated.timing; animation(visible, { toValue: 0, useNativeDriver, duration: 200, ...visibilityAnimationConfig?.hide?.config, }).start(); } return () => visible.stopAnimation(); }, [visible, shouldShowTabBar]); const [layout, setLayout] = React.useState({ height: 0, }); const handleLayout = (e: LayoutChangeEvent) => { const { height } = e.nativeEvent.layout; onHeightChange?.(height); setLayout((layout) => { if (height === layout.height) { return layout; } else { return { height }; } }); }; const { routes } = state; const tabBarHeight = useFrameSize((dimensions) => getTabBarHeight({ state, descriptors, insets, dimensions, style: [tabBarStyle, style], }) ); const hasHorizontalLabels = useFrameSize((dimensions) => shouldUseHorizontalLabels({ state, descriptors, dimensions, }) ); const compact = useFrameSize((dimensions) => isCompact({ state, descriptors, dimensions }) ); const sidebar = tabBarPosition === 'left' || tabBarPosition === 'right'; const spacing = tabBarVariant === 'material' ? SPACING_MATERIAL : SPACING_UIKIT; const minSidebarWidth = useFrameSize((size) => sidebar && hasHorizontalLabels ? getDefaultSidebarWidth(size) : 0 ); const tabBarBackgroundElement = tabBarBackground?.(); return ( ); } const styles = StyleSheet.create({ start: { top: 0, bottom: 0, start: 0, }, end: { top: 0, bottom: 0, end: 0, }, bottom: { start: 0, end: 0, bottom: 0, elevation: 8, }, bottomContent: { flex: 1, flexDirection: 'row', }, sideContent: { flex: 1, flexDirection: 'column', }, bottomItem: { flex: 1, }, });