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:
Satyajit Sahoo
2020-11-13 21:23:18 +01:00
parent ddf27bf41a
commit f7ff1adee7
4 changed files with 168 additions and 155 deletions

View File

@@ -24,6 +24,5 @@ export type {
BottomTabNavigationProp,
BottomTabScreenProps,
BottomTabBarProps,
BottomTabBarOptions,
BottomTabBarButtonProps,
} from './types';

View File

@@ -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<

View File

@@ -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>

View File

@@ -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) => {