mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-06-19 18:23:52 +08:00
refactor: move tabBarOptions to options for bottom tabs
BREAKING CHANGE: This commit moves options from `tabBarOptions` to regular `options` in order to reduce confusion between the two, as well as to make it more flexible to configure the tab bar based on a per screen basis.
This commit is contained in:
@@ -24,6 +24,5 @@ export type {
|
||||
BottomTabNavigationProp,
|
||||
BottomTabScreenProps,
|
||||
BottomTabBarProps,
|
||||
BottomTabBarOptions,
|
||||
BottomTabBarButtonProps,
|
||||
} from './types';
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
TabActionHelpers,
|
||||
RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import type { EdgeInsets } from 'react-native-safe-area-context';
|
||||
|
||||
export type BottomTabNavigationEventMap = {
|
||||
/**
|
||||
@@ -140,6 +141,73 @@ export type BottomTabNavigationOptions = {
|
||||
*/
|
||||
tabBarButton?: (props: BottomTabBarButtonProps) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Color for the icon and label in the active tab.
|
||||
*/
|
||||
tabBarActiveTintColor?: string;
|
||||
|
||||
/**
|
||||
* Color for the icon and label in the inactive tabs.
|
||||
*/
|
||||
tabBarInactiveTintColor?: string;
|
||||
|
||||
/**
|
||||
* Background color for the active tab.
|
||||
*/
|
||||
tabBarActiveBackgroundColor?: string;
|
||||
|
||||
/**
|
||||
* background color for the inactive tabs.
|
||||
*/
|
||||
tabBarInactiveBackgroundColor?: string;
|
||||
|
||||
/**
|
||||
* Whether label font should scale to respect Text Size accessibility settings.
|
||||
*/
|
||||
tabBarAllowFontScaling?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the tab label should be visible. Defaults to `true`.
|
||||
*/
|
||||
tabBarShowLabel?: boolean;
|
||||
|
||||
/**
|
||||
* Style object for the tab label.
|
||||
*/
|
||||
tabBarLabelStyle?: StyleProp<TextStyle>;
|
||||
|
||||
/**
|
||||
* Style object for the tab icon.
|
||||
*/
|
||||
tabBarIconStyle?: StyleProp<TextStyle>;
|
||||
|
||||
/**
|
||||
* Style object for the tab item container.
|
||||
*/
|
||||
tabBarItemStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
/**
|
||||
* Whether the label is rendered below the icon or beside the icon.
|
||||
* By default, the position is chosen automatically based on device width.
|
||||
* In `below-icon` orientation (typical for iPhones), the label is rendered below and in `beside-icon` orientation, it's rendered beside (typical for iPad).
|
||||
*/
|
||||
tabBarLabelPosition?: LabelPosition;
|
||||
|
||||
/**
|
||||
* Whether the label position should adapt to the orientation.
|
||||
*/
|
||||
tabBarAdaptive?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the tab bar gets hidden when the keyboard is shown. Defaults to `false`.
|
||||
*/
|
||||
tabBarHideOnKeyboard?: boolean;
|
||||
|
||||
/**
|
||||
* Style object for the tab bar container.
|
||||
*/
|
||||
tabBarStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
|
||||
/**
|
||||
* Whether this screen should be unmounted when navigating away from it.
|
||||
* Defaults to `false`.
|
||||
@@ -155,7 +223,7 @@ export type BottomTabDescriptor = Descriptor<
|
||||
|
||||
export type BottomTabDescriptorMap = Record<string, BottomTabDescriptor>;
|
||||
|
||||
export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
|
||||
export type BottomTabNavigationConfig = {
|
||||
/**
|
||||
* Whether the screens should render the first time they are accessed. Defaults to `true`.
|
||||
* Set it to `false` if you want to render all screens on initial render.
|
||||
@@ -164,11 +232,17 @@ export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
|
||||
/**
|
||||
* Function that returns a React element to display as the tab bar.
|
||||
*/
|
||||
tabBar?: (props: BottomTabBarProps<T>) => React.ReactNode;
|
||||
tabBar?: (props: BottomTabBarProps) => React.ReactNode;
|
||||
/**
|
||||
* Options for the tab bar which will be passed as props to the tab bar component.
|
||||
* Safe area insets for the tab bar. This is used to avoid elements like the navigation bar on Android and bottom safe area on iOS.
|
||||
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
|
||||
*/
|
||||
tabBarOptions?: T;
|
||||
safeAreaInsets?: {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
/**
|
||||
* Whether inactive screens should be detached from the view hierarchy to save memory.
|
||||
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
|
||||
@@ -181,77 +255,11 @@ export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
|
||||
sceneContainerStyle?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type BottomTabBarOptions = {
|
||||
/**
|
||||
* Whether the tab bar gets hidden when the keyboard is shown. Defaults to `false`.
|
||||
*/
|
||||
keyboardHidesTabBar?: boolean;
|
||||
/**
|
||||
* Color for the icon and label in the active tab.
|
||||
*/
|
||||
activeTintColor?: string;
|
||||
/**
|
||||
* Color for the icon and label in the inactive tabs.
|
||||
*/
|
||||
inactiveTintColor?: string;
|
||||
/**
|
||||
* Background color for the active tab.
|
||||
*/
|
||||
activeBackgroundColor?: string;
|
||||
/**
|
||||
* background color for the inactive tabs.
|
||||
*/
|
||||
inactiveBackgroundColor?: string;
|
||||
/**
|
||||
* Whether label font should scale to respect Text Size accessibility settings.
|
||||
*/
|
||||
allowFontScaling?: boolean;
|
||||
/**
|
||||
* Whether the tab label should be visible. Defaults to `true`.
|
||||
*/
|
||||
showLabel?: boolean;
|
||||
/**
|
||||
* Style object for the tab label.
|
||||
*/
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
/**
|
||||
* Style object for the tab icon.
|
||||
*/
|
||||
iconStyle?: StyleProp<TextStyle>;
|
||||
/**
|
||||
* Style object for the tab container.
|
||||
*/
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
/**
|
||||
* Whether the label is rendered below the icon or beside the icon.
|
||||
* By default, the position is chosen automatically based on device width.
|
||||
* In `below-icon` orientation (typical for iPhones), the label is rendered below and in `beside-icon` orientation, it's rendered beside (typical for iPad).
|
||||
*/
|
||||
labelPosition?: LabelPosition;
|
||||
/**
|
||||
* Whether the label position should adapt to the orientation.
|
||||
*/
|
||||
adaptive?: boolean;
|
||||
/**
|
||||
* Safe area insets for the tab bar. This is used to avoid elements like the navigation bar on Android and bottom safe area on iOS.
|
||||
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
|
||||
*/
|
||||
safeAreaInsets?: {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
/**
|
||||
* Style object for the tab bar container.
|
||||
*/
|
||||
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
};
|
||||
|
||||
export type BottomTabBarProps<T = BottomTabBarOptions> = T & {
|
||||
export type BottomTabBarProps = {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
descriptors: BottomTabDescriptorMap;
|
||||
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
||||
insets: EdgeInsets;
|
||||
};
|
||||
|
||||
export type BottomTabBarButtonProps = Omit<
|
||||
|
||||
@@ -17,17 +17,16 @@ import {
|
||||
useTheme,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import { useSafeAreaInsets, EdgeInsets } from 'react-native-safe-area-context';
|
||||
import type { EdgeInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import BottomTabItem from './BottomTabItem';
|
||||
import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext';
|
||||
import useWindowDimensions from '../utils/useWindowDimensions';
|
||||
import useIsKeyboardShown from '../utils/useIsKeyboardShown';
|
||||
import type { BottomTabBarProps, LabelPosition } from '../types';
|
||||
import type { BottomTabBarProps, BottomTabDescriptorMap } from '../types';
|
||||
|
||||
type Props = BottomTabBarProps & {
|
||||
activeTintColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
};
|
||||
|
||||
const DEFAULT_TABBAR_HEIGHT = 49;
|
||||
@@ -38,44 +37,47 @@ const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
type Options = {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
descriptors: BottomTabDescriptorMap;
|
||||
layout: { height: number; width: number };
|
||||
dimensions: { height: number; width: number };
|
||||
tabStyle: StyleProp<ViewStyle>;
|
||||
labelPosition: LabelPosition | undefined;
|
||||
adaptive: boolean | undefined;
|
||||
};
|
||||
|
||||
const shouldUseHorizontalLabels = ({
|
||||
state,
|
||||
descriptors,
|
||||
layout,
|
||||
dimensions,
|
||||
adaptive = true,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
}: Options) => {
|
||||
if (labelPosition) {
|
||||
return labelPosition === 'beside-icon';
|
||||
const { tabBarLabelPosition, tabBarAdaptive = true } = descriptors[
|
||||
state.routes[state.index].key
|
||||
].options;
|
||||
|
||||
if (tabBarLabelPosition) {
|
||||
return tabBarLabelPosition === 'beside-icon';
|
||||
}
|
||||
|
||||
if (!adaptive) {
|
||||
if (!tabBarAdaptive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout.width >= 768) {
|
||||
// Screen size matches a tablet
|
||||
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
const maxTabWidth = state.routes.reduce((acc, route) => {
|
||||
const { tabBarItemStyle } = descriptors[route.key].options;
|
||||
const flattenedStyle = StyleSheet.flatten(tabBarItemStyle);
|
||||
|
||||
const flattenedStyle = StyleSheet.flatten(tabStyle);
|
||||
|
||||
if (flattenedStyle) {
|
||||
if (typeof flattenedStyle.width === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.width;
|
||||
} else if (typeof flattenedStyle.maxWidth === 'number') {
|
||||
maxTabItemWidth = flattenedStyle.maxWidth;
|
||||
if (flattenedStyle) {
|
||||
if (typeof flattenedStyle.width === 'number') {
|
||||
return acc + flattenedStyle.width;
|
||||
} else if (typeof flattenedStyle.maxWidth === 'number') {
|
||||
return acc + flattenedStyle.maxWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state.routes.length * maxTabItemWidth <= layout.width;
|
||||
return acc + DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
}, 0);
|
||||
|
||||
return maxTabWidth <= layout.width;
|
||||
} else {
|
||||
return dimensions.width > dimensions.height;
|
||||
}
|
||||
@@ -85,13 +87,15 @@ const getPaddingBottom = (insets: EdgeInsets) =>
|
||||
Math.max(insets.bottom - Platform.select({ ios: 4, default: 0 }), 0);
|
||||
|
||||
export const getTabBarHeight = ({
|
||||
state,
|
||||
descriptors,
|
||||
dimensions,
|
||||
insets,
|
||||
style,
|
||||
...rest
|
||||
}: Options & {
|
||||
insets: EdgeInsets;
|
||||
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>> | undefined;
|
||||
}) => {
|
||||
// @ts-ignore
|
||||
const customHeight = StyleSheet.flatten(style)?.height;
|
||||
@@ -101,7 +105,12 @@ export const getTabBarHeight = ({
|
||||
}
|
||||
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
const horizontalLabels = shouldUseHorizontalLabels({ dimensions, ...rest });
|
||||
const horizontalLabels = shouldUseHorizontalLabels({
|
||||
state,
|
||||
descriptors,
|
||||
dimensions,
|
||||
...rest,
|
||||
});
|
||||
const paddingBottom = getPaddingBottom(insets);
|
||||
|
||||
if (
|
||||
@@ -120,20 +129,8 @@ export default function BottomTabBar({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
activeBackgroundColor,
|
||||
activeTintColor,
|
||||
adaptive,
|
||||
allowFontScaling,
|
||||
inactiveBackgroundColor,
|
||||
inactiveTintColor,
|
||||
keyboardHidesTabBar = false,
|
||||
labelPosition,
|
||||
labelStyle,
|
||||
iconStyle,
|
||||
safeAreaInsets,
|
||||
showLabel,
|
||||
insets,
|
||||
style,
|
||||
tabStyle,
|
||||
}: Props) {
|
||||
const { colors } = useTheme();
|
||||
const buildLink = useLinkBuilder();
|
||||
@@ -142,20 +139,26 @@ export default function BottomTabBar({
|
||||
const focusedDescriptor = descriptors[focusedRoute.key];
|
||||
const focusedOptions = focusedDescriptor.options;
|
||||
|
||||
const {
|
||||
tabBarShowLabel,
|
||||
tabBarHideOnKeyboard = false,
|
||||
tabBarVisibilityAnimationConfig,
|
||||
tabBarStyle,
|
||||
} = focusedOptions;
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const isKeyboardShown = useIsKeyboardShown();
|
||||
|
||||
const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
|
||||
|
||||
const shouldShowTabBar = !(keyboardHidesTabBar && isKeyboardShown);
|
||||
const shouldShowTabBar = !(tabBarHideOnKeyboard && isKeyboardShown);
|
||||
|
||||
const visibilityAnimationConfigRef = React.useRef(
|
||||
focusedOptions.tabBarVisibilityAnimationConfig
|
||||
tabBarVisibilityAnimationConfig
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
visibilityAnimationConfigRef.current =
|
||||
focusedOptions.tabBarVisibilityAnimationConfig;
|
||||
visibilityAnimationConfigRef.current = tabBarVisibilityAnimationConfig;
|
||||
});
|
||||
|
||||
const [isTabBarHidden, setIsTabBarHidden] = React.useState(!shouldShowTabBar);
|
||||
@@ -210,7 +213,7 @@ export default function BottomTabBar({
|
||||
|
||||
const topBorderWidth =
|
||||
// @ts-ignore
|
||||
StyleSheet.flatten([styles.tabBar, style])?.borderTopWidth;
|
||||
StyleSheet.flatten([styles.tabBar, tabBarStyle])?.borderTopWidth;
|
||||
|
||||
onHeightChange?.(
|
||||
height +
|
||||
@@ -232,34 +235,21 @@ export default function BottomTabBar({
|
||||
|
||||
const { routes } = state;
|
||||
|
||||
const defaultInsets = useSafeAreaInsets();
|
||||
|
||||
const insets = {
|
||||
top: safeAreaInsets?.top ?? defaultInsets.top,
|
||||
right: safeAreaInsets?.right ?? defaultInsets.right,
|
||||
bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
|
||||
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||
};
|
||||
|
||||
const paddingBottom = getPaddingBottom(insets);
|
||||
const tabBarHeight = getTabBarHeight({
|
||||
state,
|
||||
descriptors,
|
||||
insets,
|
||||
dimensions,
|
||||
layout,
|
||||
adaptive,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
style,
|
||||
style: [tabBarStyle, style],
|
||||
});
|
||||
|
||||
const hasHorizontalLabels = shouldUseHorizontalLabels({
|
||||
state,
|
||||
descriptors,
|
||||
dimensions,
|
||||
layout,
|
||||
adaptive,
|
||||
labelPosition,
|
||||
tabStyle,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -291,7 +281,7 @@ export default function BottomTabBar({
|
||||
paddingBottom,
|
||||
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||
},
|
||||
style,
|
||||
tabBarStyle,
|
||||
]}
|
||||
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
|
||||
>
|
||||
@@ -351,20 +341,22 @@ export default function BottomTabBar({
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
to={buildLink(route.name, route.params)}
|
||||
testID={options.tabBarTestID}
|
||||
allowFontScaling={allowFontScaling}
|
||||
activeTintColor={activeTintColor}
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
activeBackgroundColor={activeBackgroundColor}
|
||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||
allowFontScaling={options.tabBarAllowFontScaling}
|
||||
activeTintColor={options.tabBarActiveTintColor}
|
||||
inactiveTintColor={options.tabBarInactiveTintColor}
|
||||
activeBackgroundColor={options.tabBarActiveBackgroundColor}
|
||||
inactiveBackgroundColor={
|
||||
options.tabBarInactiveBackgroundColor
|
||||
}
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
badge={options.tabBarBadge}
|
||||
badgeStyle={options.tabBarBadgeStyle}
|
||||
label={label}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
iconStyle={iconStyle}
|
||||
style={tabStyle}
|
||||
showLabel={tabBarShowLabel}
|
||||
labelStyle={options.tabBarLabelStyle}
|
||||
iconStyle={options.tabBarIconStyle}
|
||||
style={options.tabBarItemStyle}
|
||||
/>
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { initialWindowMetrics } from 'react-native-safe-area-context';
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
SafeAreaInsetsContext,
|
||||
} from 'react-native-safe-area-context';
|
||||
|
||||
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
@@ -80,11 +83,12 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { state, tabBarOptions } = this.props;
|
||||
const { state, descriptors } = this.props;
|
||||
|
||||
const dimensions = Dimensions.get('window');
|
||||
const tabBarHeight = getTabBarHeight({
|
||||
state,
|
||||
descriptors,
|
||||
dimensions,
|
||||
layout: { width: dimensions.width, height: 0 },
|
||||
insets: initialWindowMetrics?.insets ?? {
|
||||
@@ -92,11 +96,9 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
...props.safeAreaInsets,
|
||||
},
|
||||
adaptive: tabBarOptions?.adaptive,
|
||||
labelPosition: tabBarOptions?.labelPosition,
|
||||
tabStyle: tabBarOptions?.tabStyle,
|
||||
style: tabBarOptions?.style,
|
||||
style: descriptors[state.routes[state.index].key].options.tabBarStyle,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
@@ -108,17 +110,29 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
private renderTabBar = () => {
|
||||
const {
|
||||
tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
|
||||
tabBarOptions,
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
safeAreaInsets,
|
||||
} = this.props;
|
||||
return tabBar({
|
||||
...tabBarOptions,
|
||||
state: state,
|
||||
descriptors: descriptors,
|
||||
navigation: navigation,
|
||||
});
|
||||
|
||||
return (
|
||||
<SafeAreaInsetsContext.Consumer>
|
||||
{(insets) =>
|
||||
tabBar({
|
||||
state: state,
|
||||
descriptors: descriptors,
|
||||
navigation: navigation,
|
||||
insets: {
|
||||
top: safeAreaInsets?.top ?? insets?.top ?? 0,
|
||||
right: safeAreaInsets?.right ?? insets?.right ?? 0,
|
||||
bottom: safeAreaInsets?.bottom ?? insets?.bottom ?? 0,
|
||||
left: safeAreaInsets?.left ?? insets?.left ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
</SafeAreaInsetsContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
private handleTabBarHeightChange = (height: number) => {
|
||||
|
||||
Reference in New Issue
Block a user