Skip to content

Commit ccb2d38

Browse files
committed
feat: hide tab bar when keyboard is shown (facebook#112)
Closes facebook#16 When the statusbar is not translucent, the view resizes when the keyboard is shown on Android. The tab bar stays above the keyboard. This PR makes the tab bar hide automatically when the keyboard is shown. The behaviour is enabled by default and can be disabled with `keyboardHidesTabBar: false` in `tabBarOptions`
1 parent 73e9b4c commit ccb2d38

File tree

1 file changed

+142
-57
lines changed

1 file changed

+142
-57
lines changed

packages/bottom-tabs/src/views/BottomTabBar.js

Lines changed: 142 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import React from 'react';
44
import {
5+
Animated,
56
TouchableWithoutFeedback,
67
StyleSheet,
78
View,
9+
Keyboard,
810
Platform,
911
} from 'react-native';
1012
import { SafeAreaView } from '@react-navigation/native';
11-
import Animated from 'react-native-reanimated';
1213

1314
import CrossFadeIcon from './CrossFadeIcon';
1415
import withDimensions from '../utils/withDimensions';
1516

1617
export type TabBarOptions = {
18+
keyboardHidesTabBar: boolean,
1719
activeTintColor?: string,
1820
inactiveTintColor?: string,
1921
activeBackgroundColor?: string,
@@ -45,6 +47,12 @@ type Props = TabBarOptions & {
4547
safeAreaInset: { top: string, right: string, bottom: string, left: string },
4648
};
4749

50+
type State = {
51+
layout: { height: number, width: number },
52+
keyboard: boolean,
53+
visible: Animated.Value,
54+
};
55+
4856
const majorVersion = parseInt(Platform.Version, 10);
4957
const isIos = Platform.OS === 'ios';
5058
const isIOS11 = majorVersion >= 11 && isIos;
@@ -79,8 +87,9 @@ class TouchableWithoutFeedbackWrapper extends React.Component<*> {
7987
}
8088
}
8189

82-
class TabBarBottom extends React.Component<Props> {
90+
class TabBarBottom extends React.Component<Props, State> {
8391
static defaultProps = {
92+
keyboardHidesTabBar: true,
8493
activeTintColor: '#007AFF',
8594
activeBackgroundColor: 'transparent',
8695
inactiveTintColor: '#8E8E93',
@@ -92,6 +101,66 @@ class TabBarBottom extends React.Component<Props> {
92101
safeAreaInset: { bottom: 'always', top: 'never' },
93102
};
94103

104+
state = {
105+
layout: { height: 0, width: 0 },
106+
keyboard: false,
107+
visible: new Animated.Value(1),
108+
};
109+
110+
componentDidMount() {
111+
if (Platform.OS === 'ios') {
112+
Keyboard.addListener('keyboardWillShow', this._handleKeyboardShow);
113+
Keyboard.addListener('keyboardWillHide', this._handleKeyboardHide);
114+
} else {
115+
Keyboard.addListener('keyboardDidShow', this._handleKeyboardShow);
116+
Keyboard.addListener('keyboardDidHide', this._handleKeyboardHide);
117+
}
118+
}
119+
120+
componentWillUnmount() {
121+
if (Platform.OS === 'ios') {
122+
Keyboard.removeListener('keyboardWillShow', this._handleKeyboardShow);
123+
Keyboard.removeListener('keyboardWillHide', this._handleKeyboardHide);
124+
} else {
125+
Keyboard.removeListener('keyboardDidShow', this._handleKeyboardShow);
126+
Keyboard.removeListener('keyboardDidHide', this._handleKeyboardHide);
127+
}
128+
}
129+
130+
_handleKeyboardShow = () =>
131+
this.setState({ keyboard: true }, () =>
132+
Animated.timing(this.state.visible, {
133+
toValue: 0,
134+
duration: 150,
135+
useNativeDriver: true,
136+
}).start()
137+
);
138+
139+
_handleKeyboardHide = () =>
140+
Animated.timing(this.state.visible, {
141+
toValue: 1,
142+
duration: 100,
143+
useNativeDriver: true,
144+
}).start(() => {
145+
this.setState({ keyboard: false });
146+
});
147+
148+
_handleLayout = e => {
149+
const { layout } = this.state;
150+
const { height, width } = e.nativeEvent.layout;
151+
152+
if (height === layout.height && width === layout.width) {
153+
return;
154+
}
155+
156+
this.setState({
157+
layout: {
158+
height,
159+
width,
160+
},
161+
});
162+
};
163+
95164
_renderLabel = ({ route, focused }) => {
96165
const {
97166
activeTintColor,
@@ -202,6 +271,7 @@ class TabBarBottom extends React.Component<Props> {
202271
render() {
203272
const {
204273
navigation,
274+
keyboardHidesTabBar,
205275
activeBackgroundColor,
206276
inactiveBackgroundColor,
207277
onTabPress,
@@ -222,62 +292,71 @@ class TabBarBottom extends React.Component<Props> {
222292
];
223293

224294
return (
225-
<SafeAreaView style={tabBarStyle} forceInset={safeAreaInset}>
226-
{routes.map((route, index) => {
227-
const focused = index === navigation.state.index;
228-
const scene = { route, focused };
229-
230-
const accessibilityLabel = this.props.getAccessibilityLabel({
231-
route,
232-
});
233-
234-
const accessibilityRole =
235-
this.props.getAccessibilityRole({
295+
<Animated.View
296+
style={[
297+
styles.container,
298+
keyboardHidesTabBar
299+
? {
300+
// When the keyboard is shown, slide down the tab bar
301+
transform: [
302+
{
303+
translateY: this.state.visible.interpolate({
304+
inputRange: [0, 1],
305+
outputRange: [this.state.layout.height, 0],
306+
}),
307+
},
308+
],
309+
// Absolutely position the tab bar so that the content is below it
310+
// This is needed to avoid gap at bottom when the tab bar is hidden
311+
position: this.state.keyboard ? 'absolute' : null,
312+
}
313+
: null,
314+
]}
315+
pointerEvents={
316+
keyboardHidesTabBar && this.state.keyboard ? 'none' : 'auto'
317+
}
318+
onLayout={this._handleLayout}
319+
>
320+
<SafeAreaView style={tabBarStyle} forceInset={safeAreaInset}>
321+
{routes.map((route, index) => {
322+
const focused = index === navigation.state.index;
323+
const scene = { route, focused };
324+
const accessibilityLabel = this.props.getAccessibilityLabel({
236325
route,
237-
}) || 'button';
238-
239-
let accessibilityStates = this.props.getAccessibilityStates({
240-
route,
241-
});
242-
243-
if (!accessibilityStates) {
244-
accessibilityStates = focused ? ['selected'] : [];
245-
}
246-
247-
const testID = this.props.getTestID({ route });
248-
249-
const backgroundColor = focused
250-
? activeBackgroundColor
251-
: inactiveBackgroundColor;
252-
253-
const ButtonComponent =
254-
this.props.getButtonComponent({ route }) ||
255-
TouchableWithoutFeedbackWrapper;
256-
257-
return (
258-
<ButtonComponent
259-
key={route.key}
260-
onPress={() => onTabPress({ route })}
261-
onLongPress={() => onTabLongPress({ route })}
262-
testID={testID}
263-
accessibilityLabel={accessibilityLabel}
264-
accessibilityRole={accessibilityRole}
265-
accessibilityStates={accessibilityStates}
266-
style={[
267-
styles.tab,
268-
{ backgroundColor },
269-
this._shouldUseHorizontalLabels()
270-
? styles.tabLandscape
271-
: styles.tabPortrait,
272-
tabStyle,
273-
]}
274-
>
275-
{this._renderIcon(scene)}
276-
{this._renderLabel(scene)}
277-
</ButtonComponent>
278-
);
279-
})}
280-
</SafeAreaView>
326+
});
327+
const testID = this.props.getTestID({ route });
328+
329+
const backgroundColor = focused
330+
? activeBackgroundColor
331+
: inactiveBackgroundColor;
332+
333+
const ButtonComponent =
334+
this.props.getButtonComponent({ route }) ||
335+
TouchableWithoutFeedbackWrapper;
336+
337+
return (
338+
<ButtonComponent
339+
key={route.key}
340+
onPress={() => onTabPress({ route })}
341+
onLongPress={() => onTabLongPress({ route })}
342+
testID={testID}
343+
accessibilityLabel={accessibilityLabel}
344+
style={[
345+
styles.tab,
346+
{ backgroundColor },
347+
this._shouldUseHorizontalLabels()
348+
? styles.tabLandscape
349+
: styles.tabPortrait,
350+
tabStyle,
351+
]}
352+
>
353+
{this._renderIcon(scene)}
354+
{this._renderLabel(scene)}
355+
</ButtonComponent>
356+
);
357+
})}
358+
</SafeAreaView>
359+
</Animated.View>
281360
);
282361
}
283362
}
@@ -292,6 +371,12 @@ const styles = StyleSheet.create({
292371
borderTopColor: 'rgba(0, 0, 0, .3)',
293372
flexDirection: 'row',
294373
},
374+
container: {
375+
left: 0,
376+
right: 0,
377+
bottom: 0,
378+
elevation: 8,
379+
},
295380
tabBarCompact: {
296381
height: COMPACT_HEIGHT,
297382
},

0 commit comments

Comments
 (0)