@react-navigation/bottom-tabs
Version:
Bottom tab navigator following iOS design guidelines
369 lines (368 loc) • 12.8 kB
JavaScript
"use strict";
import { getDefaultSidebarWidth, getLabel, MissingIcon } from '@react-navigation/elements';
import { CommonActions, NavigationContext, NavigationRouteContext, useLinkBuilder, useLocale, useTheme } from '@react-navigation/native';
import React from 'react';
import { Animated, Platform, StyleSheet, View } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { BottomTabBarHeightCallbackContext } from "../utils/BottomTabBarHeightCallbackContext.js";
import { useIsKeyboardShown } from "../utils/useIsKeyboardShown.js";
import { BottomTabItem } from "./BottomTabItem.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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';
const shouldUseHorizontalLabels = ({
state,
descriptors,
dimensions
}) => {
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
}) => {
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
}) => {
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
}) {
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 dimensions = useSafeAreaFrame();
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 => {
const {
height
} = e.nativeEvent.layout;
onHeightChange?.(height);
setLayout(layout => {
if (height === layout.height) {
return layout;
} else {
return {
height
};
}
});
};
const {
routes
} = state;
const tabBarHeight = getTabBarHeight({
state,
descriptors,
insets,
dimensions,
style: [tabBarStyle, style]
});
const hasHorizontalLabels = shouldUseHorizontalLabels({
state,
descriptors,
dimensions
});
const compact = isCompact({
state,
descriptors,
dimensions
});
const sidebar = tabBarPosition === 'left' || tabBarPosition === 'right';
const spacing = tabBarVariant === 'material' ? SPACING_MATERIAL : SPACING_UIKIT;
const tabBarBackgroundElement = tabBarBackground?.();
return /*#__PURE__*/_jsxs(Animated.View, {
style: [tabBarPosition === 'left' ? styles.start : tabBarPosition === 'right' ? styles.end : styles.bottom, (Platform.OS === 'web' ? tabBarPosition === 'right' : direction === 'rtl' && tabBarPosition === 'left' || direction !== 'rtl' && tabBarPosition === 'right') ? {
borderLeftWidth: StyleSheet.hairlineWidth
} : (Platform.OS === 'web' ? tabBarPosition === 'left' : direction === 'rtl' && tabBarPosition === 'right' || direction !== 'rtl' && tabBarPosition === 'left') ? {
borderRightWidth: StyleSheet.hairlineWidth
} : tabBarPosition === 'top' ? {
borderBottomWidth: StyleSheet.hairlineWidth
} : {
borderTopWidth: StyleSheet.hairlineWidth
}, {
backgroundColor: tabBarBackgroundElement != null ? 'transparent' : colors.card,
borderColor: colors.border
}, sidebar ? {
paddingTop: (hasHorizontalLabels ? spacing : spacing / 2) + insets.top,
paddingBottom: (hasHorizontalLabels ? spacing : spacing / 2) + insets.bottom,
paddingStart: spacing + (tabBarPosition === 'left' ? insets.left : 0),
paddingEnd: spacing + (tabBarPosition === 'right' ? insets.right : 0),
minWidth: hasHorizontalLabels ? getDefaultSidebarWidth(dimensions) : 0
} : [{
transform: [{
translateY: visible.interpolate({
inputRange: [0, 1],
outputRange: [layout.height + insets[tabBarPosition === 'top' ? 'top' : 'bottom'] + StyleSheet.hairlineWidth, 0]
})
}],
// Absolutely position the tab bar so that the content is below it
// This is needed to avoid gap at bottom when the tab bar is hidden
position: isTabBarHidden ? 'absolute' : undefined
}, {
height: tabBarHeight,
paddingBottom: tabBarPosition === 'bottom' ? insets.bottom : 0,
paddingTop: tabBarPosition === 'top' ? insets.top : 0,
paddingHorizontal: Math.max(insets.left, insets.right)
}], tabBarStyle],
pointerEvents: isTabBarHidden ? 'none' : 'auto',
onLayout: sidebar ? undefined : handleLayout,
children: [/*#__PURE__*/_jsx(View, {
pointerEvents: "none",
style: StyleSheet.absoluteFill,
children: tabBarBackgroundElement
}), /*#__PURE__*/_jsx(View, {
accessibilityRole: "tablist",
style: sidebar ? styles.sideContent : styles.bottomContent,
children: routes.map((route, index) => {
const focused = index === state.index;
const {
options
} = descriptors[route.key];
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true
});
if (!focused && !event.defaultPrevented) {
navigation.dispatch({
...CommonActions.navigate(route),
target: state.key
});
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key
});
};
const label = typeof options.tabBarLabel === 'function' ? options.tabBarLabel : getLabel({
label: options.tabBarLabel,
title: options.title
}, route.name);
const accessibilityLabel = options.tabBarAccessibilityLabel !== undefined ? options.tabBarAccessibilityLabel : typeof label === 'string' && Platform.OS === 'ios' ? `${label}, tab, ${index + 1} of ${routes.length}` : undefined;
return /*#__PURE__*/_jsx(NavigationContext.Provider, {
value: descriptors[route.key].navigation,
children: /*#__PURE__*/_jsx(NavigationRouteContext.Provider, {
value: route,
children: /*#__PURE__*/_jsx(BottomTabItem, {
href: buildHref(route.name, route.params),
route: route,
descriptor: descriptors[route.key],
focused: focused,
horizontal: hasHorizontalLabels,
compact: compact,
sidebar: sidebar,
variant: tabBarVariant,
onPress: onPress,
onLongPress: onLongPress,
accessibilityLabel: accessibilityLabel,
testID: options.tabBarButtonTestID,
allowFontScaling: options.tabBarAllowFontScaling,
activeTintColor: tabBarActiveTintColor,
inactiveTintColor: tabBarInactiveTintColor,
activeBackgroundColor: tabBarActiveBackgroundColor,
inactiveBackgroundColor: tabBarInactiveBackgroundColor,
button: options.tabBarButton,
icon: options.tabBarIcon ?? (({
color,
size
}) => /*#__PURE__*/_jsx(MissingIcon, {
color: color,
size: size
})),
badge: options.tabBarBadge,
badgeStyle: options.tabBarBadgeStyle,
label: label,
showLabel: tabBarShowLabel,
labelStyle: options.tabBarLabelStyle,
iconStyle: options.tabBarIconStyle,
style: [sidebar ? {
marginVertical: hasHorizontalLabels ? tabBarVariant === 'material' ? 0 : 1 : spacing / 2
} : styles.bottomItem, options.tabBarItemStyle]
})
})
}, route.key);
})
})]
});
}
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
}
});
//# sourceMappingURL=BottomTabBar.js.map