/* @flow */
import React from 'react';
import {
Animated,
TouchableWithoutFeedback,
StyleSheet,
View,
Keyboard,
Platform,
} from 'react-native';
import { ThemeColors, ThemeContext } from '@react-navigation/core';
import { SafeAreaView } from '@react-navigation/native';
import CrossFadeIcon from './CrossFadeIcon';
import withDimensions from '../utils/withDimensions';
export type ThemedColor = {
light: string,
dark: string,
};
export type TabBarOptions = {
keyboardHidesTabBar: boolean,
activeTintColor?: string | ThemedColor,
inactiveTintColor?: string | ThemedColor,
activeBackgroundColor?: string | ThemedColor,
inactiveBackgroundColor?: string | ThemedColor,
allowFontScaling: boolean,
showLabel: boolean,
showIcon: boolean,
labelStyle: any,
tabStyle: any,
adaptive?: boolean,
style: any,
};
type Props = TabBarOptions & {
navigation: any,
descriptors: any,
jumpTo: any,
onTabPress: any,
onTabLongPress: any,
getAccessibilityLabel: (props: { route: any }) => string,
getAccessibilityRole: (props: { route: any }) => string,
getAccessibilityStates: (props: { route: any }) => string[],
getButtonComponent: ({ route: any }) => any,
getLabelText: ({ route: any }) => any,
getTestID: (props: { route: any }) => string,
renderIcon: any,
dimensions: { width: number, height: number },
isLandscape: boolean,
safeAreaInset: { top: string, right: string, bottom: string, left: string },
};
type State = {
layout: { height: number, width: number },
keyboard: boolean,
visible: Animated.Value,
};
const majorVersion = parseInt(Platform.Version, 10);
const isIos = Platform.OS === 'ios';
const isIOS11 = majorVersion >= 11 && isIos;
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
class TouchableWithoutFeedbackWrapper extends React.Component<*> {
render() {
const {
onPress,
onLongPress,
testID,
accessibilityLabel,
accessibilityRole,
accessibilityStates,
...props
} = this.props;
return (
<TouchableWithoutFeedback
onPress={onPress}
onLongPress={onLongPress}
testID={testID}
hitSlop={{ left: 15, right: 15, top: 0, bottom: 5 }}
accessibilityLabel={accessibilityLabel}
accessibilityRole={accessibilityRole}
accessibilityStates={accessibilityStates}
>
<View {...props} />
</TouchableWithoutFeedback>
);
}
}
class TabBarBottom extends React.Component<Props, State> {
static contextType = ThemeContext;
static defaultProps = {
keyboardHidesTabBar: false,
activeTintColor: {
light: '#007AFF',
dark: '#fff',
},
inactiveTintColor: {
light: '#8e8e93',
dark: '#7f7f7f',
},
activeBackgroundColor: 'transparent',
inactiveBackgroundColor: 'transparent',
showLabel: true,
showIcon: true,
allowFontScaling: true,
adaptive: isIOS11,
safeAreaInset: { bottom: 'always', top: 'never' },
};
state = {
layout: { height: 0, width: 0 },
keyboard: false,
visible: new Animated.Value(1),
};
componentDidMount() {
if (Platform.OS === 'ios') {
Keyboard.addListener('keyboardWillShow', this._handleKeyboardShow);
Keyboard.addListener('keyboardWillHide', this._handleKeyboardHide);
} else {
Keyboard.addListener('keyboardDidShow', this._handleKeyboardShow);
Keyboard.addListener('keyboardDidHide', this._handleKeyboardHide);
}
}
componentWillUnmount() {
if (Platform.OS === 'ios') {
Keyboard.removeListener('keyboardWillShow', this._handleKeyboardShow);
Keyboard.removeListener('keyboardWillHide', this._handleKeyboardHide);
} else {
Keyboard.removeListener('keyboardDidShow', this._handleKeyboardShow);
Keyboard.removeListener('keyboardDidHide', this._handleKeyboardHide);
}
}
_handleKeyboardShow = () =>
this.setState({ keyboard: true }, () =>
Animated.timing(this.state.visible, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}).start()
);
_handleKeyboardHide = () =>
Animated.timing(this.state.visible, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}).start(() => {
this.setState({ keyboard: false });
});
_handleLayout = e => {
const { layout } = this.state;
const { height, width } = e.nativeEvent.layout;
if (height === layout.height && width === layout.width) {
return;
}
this.setState({
layout: {
height,
width,
},
});
};
_getActiveTintColor = () => {
let { activeTintColor } = this.props;
if (!activeTintColor) {
return;
} else if (typeof activeTintColor === 'string') {
return activeTintColor;
}
return activeTintColor[this.context];
};
_getInactiveTintColor = () => {
let { inactiveTintColor } = this.props;
if (!inactiveTintColor) {
return;
} else if (typeof inactiveTintColor === 'string') {
return inactiveTintColor;
}
return inactiveTintColor[this.context];
};
_getActiveBackgroundColor = () => {
let { activeBackgroundColor } = this.props;
if (!activeBackgroundColor) {
return;
} else if (typeof activeBackgroundColor === 'string') {
return activeBackgroundColor;
}
return activeBackgroundColor[this.context];
};
_getInactiveBackgroundColor = () => {
let { inactiveBackgroundColor } = this.props;
if (!inactiveBackgroundColor) {
return;
} else if (typeof inactiveBackgroundColor === 'string') {
return inactiveBackgroundColor;
}
return inactiveBackgroundColor[this.context];
};
_renderLabel = ({ route, focused }) => {
const { labelStyle, showLabel, showIcon, allowFontScaling } = this.props;
if (showLabel === false) {
return null;
}
const activeTintColor = this._getActiveTintColor();
const inactiveTintColor = this._getInactiveTintColor();
const label = this.props.getLabelText({ route });
const tintColor = focused ? activeTintColor : inactiveTintColor;
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color: tintColor },
showIcon && this._shouldUseHorizontalLabels()
? styles.labelBeside
: styles.labelBeneath,
labelStyle,
]}
allowFontScaling={allowFontScaling}
>
{label}
</Animated.Text>
);
}
if (typeof label === 'function') {
return label({ route, focused, tintColor });
}
return label;
};
_renderIcon = ({ route, focused }) => {
const { navigation, renderIcon, showIcon, showLabel } = this.props;
if (showIcon === false) {
return null;
}
const horizontal = this._shouldUseHorizontalLabels();
const activeTintColor = this._getActiveTintColor();
const inactiveTintColor = this._getInactiveTintColor();
const activeOpacity = focused ? 1 : 0;
const inactiveOpacity = focused ? 0 : 1;
return (
<CrossFadeIcon
route={route}
horizontal={horizontal}
navigation={navigation}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
style={[
styles.iconWithExplicitHeight,
showLabel === false && !horizontal && styles.iconWithoutLabel,
showLabel !== false && !horizontal && styles.iconWithLabel,
]}
/>
);
};
_shouldUseHorizontalLabels = () => {
const { routes } = this.props.navigation.state;
const { isLandscape, dimensions, adaptive, tabStyle } = this.props;
if (!adaptive) {
return false;
}
if (Platform.isPad) {
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
const flattenedStyle = StyleSheet.flatten(tabStyle);
if (flattenedStyle) {
if (typeof flattenedStyle.width === 'number') {
maxTabItemWidth = flattenedStyle.width;
} else if (typeof flattenedStyle.maxWidth === 'number') {
maxTabItemWidth = flattenedStyle.maxWidth;
}
}
return routes.length * maxTabItemWidth <= dimensions.width;
} else {
return isLandscape;
}
};
render() {
const {
navigation,
keyboardHidesTabBar,
onTabPress,
onTabLongPress,
safeAreaInset,
style,
tabStyle,
} = this.props;
const { routes } = navigation.state;
const isDark = this.context === 'dark';
const activeBackgroundColor = this._getActiveBackgroundColor();
const inactiveBackgroundColor = this._getInactiveBackgroundColor();
const tabBarStyle = [
styles.tabBar,
isDark ? styles.tabBarDark : styles.tabBarLight,
this._shouldUseHorizontalLabels() && !Platform.isPad
? styles.tabBarCompact
: styles.tabBarRegular,
style,
];
return (
<Animated.View
style={[
styles.container,
keyboardHidesTabBar
? {
// When the keyboard is shown, slide down the tab bar
transform: [
{
translateY: this.state.visible.interpolate({
inputRange: [0, 1],
outputRange: [this.state.layout.height, 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: this.state.keyboard ? 'absolute' : null,
}
: null,
]}
pointerEvents={
keyboardHidesTabBar && this.state.keyboard ? 'none' : 'auto'
}
onLayout={this._handleLayout}
>
<SafeAreaView style={tabBarStyle} forceInset={safeAreaInset}>
{routes.map((route, index) => {
const focused = index === navigation.state.index;
const scene = { route, focused };
const accessibilityLabel = this.props.getAccessibilityLabel({
route,
});
const accessibilityRole = this.props.getAccessibilityRole({
route,
});
const accessibilityStates = this.props.getAccessibilityStates(
scene
);
const testID = this.props.getTestID({ route });
const backgroundColor = focused
? activeBackgroundColor
: inactiveBackgroundColor;
const ButtonComponent =
this.props.getButtonComponent({ route }) ||
TouchableWithoutFeedbackWrapper;
return (
<ButtonComponent
key={route.key}
onPress={() => onTabPress({ route })}
onLongPress={() => onTabLongPress({ route })}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessibilityRole={accessibilityRole}
accessibilityStates={accessibilityStates}
style={[
styles.tab,
{ backgroundColor },
this._shouldUseHorizontalLabels()
? styles.tabLandscape
: styles.tabPortrait,
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
</ButtonComponent>
);
})}
</SafeAreaView>
</Animated.View>
);
}
}
const DEFAULT_HEIGHT = 49;
const COMPACT_HEIGHT = 29;
const styles = StyleSheet.create({
tabBar: {
borderTopWidth: StyleSheet.hairlineWidth,
flexDirection: 'row',
},
tabBarLight: {
backgroundColor: ThemeColors.light.header,
borderTopColor: ThemeColors.light.headerBorder,
},
tabBarDark: {
backgroundColor: ThemeColors.dark.header,
borderTopColor: ThemeColors.dark.headerBorder,
},
container: {
left: 0,
right: 0,
bottom: 0,
elevation: 8,
},
tabBarCompact: {
height: COMPACT_HEIGHT,
},
tabBarRegular: {
height: DEFAULT_HEIGHT,
},
tab: {
flex: 1,
alignItems: isIos ? 'center' : 'stretch',
},
tabPortrait: {
justifyContent: 'flex-end',
flexDirection: 'column',
},
tabLandscape: {
justifyContent: 'center',
flexDirection: 'row',
},
iconWithoutLabel: {
flex: 1,
},
iconWithLabel: {
flex: 1,
},
iconWithExplicitHeight: {
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 11,
marginBottom: 1.5,
},
labelBeside: {
fontSize: 12,
marginLeft: 15,
},
});
export default withDimensions(TabBarBottom);