The problem I am having is while using a react-navigation
stack navigator nested inside a tab navigator for a basic chat UI appearance, the keyboard was hiding the chat message input field at the bottom. So I tried KeyboardAvoidingView
to bring the keyboard up to a visible position, but the keyboard wasn't showing. I have tried a solution that involves adding headerHeight
to the keyboardVerticalOffset
prop, but it seems to be about 50px off. For example, if I add headerHeight + 50
to keyboardVerticalOffset
everything looks great, but if I switched devices to an iPhone 5 or something, with a smaller screen, different SafeArea insets, etc, the keyboard would be in the wrong position again.
I am not sure what the culprit is exactly, but I am now thinking it's the SafeArea padding on top and/or bottom, which I have learned are the "insets". I am trying to use useSafeAreaInsets
, but all the values return 0! I want to use those insets to add to the keyboardVerticalOffset
prop so the avoiding view works properly.
I like the style of the tab bar right now, so I'd like to keep it with an increased height and padding and font size, but maybe I am doing it wrong with react native navigation? Maybe I cannot have this tab bar and stack navigator styling as I want with React Native? Regardless, I believe the insets should be returning a value, so I think that's where a problem lies.
Notice if I move the <SafeAreaView>
block to surround the <TouchableWithoutFeedback>
, instead of around the <NavigationContainer>
block, and remove the 50 extra pixels added to the keyboardVerticalOffset
, then the keyboard pushes the input field up properly, but the tab bar icons on iPhone 11 are squished. As I am writing this out, I am noticing that this change now has the bottom/topPadding variables returning values? If I then remove the tabBarOptions
, I get a basic appearance of the tabs that work, but I like the design of my initial tabs much more.
How do I maintain my current styling of the tab bar and have the keyboard avoid the chat input field on every device?
(Note: The useEffect
usage below is something I tried using the solution outlined in this issue: https://github.com/th3rdwave/react-native-safe-area-context/issues/54)
App.js:
import React, { Component, useEffect, useState } from 'react';
import { View, KeyboardAvoidingView, TextInput, Text, Platform, TouchableWithoutFeedback, Keyboard, ActivityIndicator, SafeAreaView, ScrollView, Button, StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStackNavigator, useHeaderHeight } from '@react-navigation/stack';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { useSafeAreaInsets, useSafeAreaFrame } from 'react-native-safe-area-context';
import { Dimensions } from 'react-native';
const Stack = createStackNavigator();
function TicketStack() {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "dodgerblue",
elevation: 0, // remove shadow on Android
shadowOpacity: 0, // remove shadow on iOS
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "900",
fontSize: 26,
},
}}>
<Stack.Screen name="Ticket">
{(props) => <TicketScreen {...props} />}
</Stack.Screen>
<Stack.Screen name="Chat">
{(props) => <ChatScreen {...props} />}
</Stack.Screen>
</Stack.Navigator>
);
}
function HomeScreen() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text><FontAwesome5 name={"home"} size={20} color={"dodgerblue"} /> Home screen!</Text>
</View>
);
}
function TicketScreen(props){
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Ticket screen :)!</Text>
<Button title="Go to Chat" onPress={() => props.navigation.navigate('Chat')} />
</View>
);
}
function CustomKeyboardAvoidingView({ children, style }) {
const headerHeight = useHeaderHeight();
console.log("headerHeight: " + headerHeight)
console.log("StatusBar.currentHeight: " + StatusBar.currentHeight)
const insets = useSafeAreaInsets();
console.log("insets.top: " + topPadding)
console.log("insets.bottom: " + bottomPadding)
const [bottomPadding, setBottomPadding] = useState(insets.bottom)
const [topPadding, setTopPadding] = useState(insets.top)
useEffect(() => {
setBottomPadding(insets.bottom)
setTopPadding(insets.top)
console.log("topPadding: " + topPadding)
console.log("bottomPadding: " + bottomPadding)
}, [insets.bottom, insets.top])
// const frame = useSafeAreaFrame();
// const windowHeight = Dimensions.get('window').height
// console.log("frame.height: " + frame.height)
// console.log("windowHeight: " + windowHeight)
// const safeAreaHeight = windowHeight - frame.height
// console.log("safeAreaHeight: " + safeAreaHeight)
// safeAreaHeight is too much, needs to just be bottom or top padding from safearea
return (
<KeyboardAvoidingView
style={style}
behavior={Platform.OS == "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight + 50}
>
{children}
</KeyboardAvoidingView>
);
}
function ChatScreen(){
return(
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<CustomKeyboardAvoidingView style={{backgroundColor: "#fff", flex: 1, flexDirection: "column", justifyContent: "space-between" }}>
<View style={{backgroundColor: "dodgerblue", paddingVertical: 15}}>
<View style={{ margin: 10, marginBottom: 15}}>
<ActivityIndicator size="large" style={{marginBottom: 10}}/>
<Text>Waiting for more info here....</Text>
</View>
</View>
<ScrollView style={{backgroundColor: "tomato", paddingVertical: 15}}>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
</ScrollView>
<View style={{backgroundColor: "yellow", paddingVertical: 15}}>
<TextInput placeholder="Type your message here..." />
</View>
</CustomKeyboardAvoidingView>
</TouchableWithoutFeedback>
)
}
const Tab = createBottomTabNavigator();
// TODO:
// - removing safeareaview makes tabs squished and icon nearly invisible, but chat message input fields avoids keyboard properly (squished can be fixed by removing tabBarOptions)
// - having safeareaview makes tabs look good, but chat message input field is hidden by keyboard
// - safeareainsets? why are they 0? i would be adding the bottom or top padding of insets to the vertical offset of the keyboard avoiding view.
export default class App extends Component {
render(){
return (
<SafeAreaView style={{flex: 1, backgroundColor: "dodgerblue"}}>
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = 'home';
} else if (route.name === 'Ticket') {
iconName = 'question';
}
return <FontAwesome5 name={iconName} size={size} color={color} />;
},
})}
tabBarOptions={{
style: {
height: 70
},
activeTintColor: "#fff",
inactiveTintColor: "dodgerblue",
inactiveBackgroundColor: "#fff",
activeBackgroundColor: "dodgerblue",
tabStyle: {
paddingTop: 10,
paddingBottom: 10
},
labelStyle: {
fontSize: 14
},
}}>
<Tab.Screen name="Home">
{(props) => <HomeScreen {...props} />}
</Tab.Screen>
<Tab.Screen name="Ticket">
{(props) => <TicketStack {...props} />}
</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
</SafeAreaView>
);
}
}
package.json:
{
"name": "ReactNativeTest",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"@react-native-community/masked-view": "^0.1.10",
"@react-navigation/bottom-tabs": "^5.9.2",
"@react-navigation/native": "^5.7.6",
"@react-navigation/stack": "^5.9.3",
"react": "16.13.1",
"react-native": "0.63.3",
"react-native-gesture-handler": "^1.8.0",
"react-native-reanimated": "^1.13.1",
"react-native-safe-area-context": "^3.1.8",
"react-native-screens": "^2.11.0",
"react-native-vector-icons": "^7.1.0"
},
"devDependencies": {
"@babel/core": "7.11.6",
"@babel/runtime": "7.11.2",
"@react-native-community/eslint-config": "1.1.0",
"babel-jest": "25.5.1",
"eslint": "6.8.0",
"jest": "25.5.4",
"metro-react-native-babel-preset": "0.59.0",
"react-test-renderer": "16.13.1"
},
"jest": {
"preset": "react-native"
}
}
So as Alex over at this Github issue pointed out, wrapping the app in SafeAreaProvider
fixes the insets not returning values properly.
The Tab bar was still squished with SafeAreaView
wrapping the NavigationContainer
and I fixed that by adding a height of 70 to tabStyle
prop of Tab.Navigator
. Then another issue arose, where the Stack Header of the TicketScreen was way too large and was fixed from this Github issue by adding headerStatusBarHeight: 0
to Stack.Navigator
screenOptions
prop. The insets were only returning properly inside useEffect
with a setState
usage, which I then used the topPadding
value to add to the keyboardVerticalOffset
prop and the keyboard was popping up without hiding the input field on an iPhone 11 and iPhone 6s!
Update note: Make sure to also set keyboardHidesTabBar: (Platform.OS == "ios" ? false : true)
as a prop on Tab.Navigator
.
Here's the full working code:
import React, { Component, useEffect, useState } from 'react';
import { View, KeyboardAvoidingView, TextInput, Text, Platform, TouchableWithoutFeedback, Keyboard, ActivityIndicator, SafeAreaView, ScrollView, Button, StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStackNavigator, useHeaderHeight } from '@react-navigation/stack';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { useSafeAreaInsets, SafeAreaProvider } from 'react-native-safe-area-context';
const Stack = createStackNavigator();
function TicketStack() {
return (
<Stack.Navigator
screenOptions={{
headerStatusBarHeight: 0, // Header had increased size with SafeArea for some reason (https://github.com/react-navigation/react-navigation/issues/5936)
headerStyle: {
backgroundColor: "dodgerblue",
elevation: 0, // remove shadow on Android
shadowOpacity: 0, // remove shadow on iOS
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "900",
fontSize: 26,
},
}}>
<Stack.Screen name="Ticket">
{(props) => <TicketScreen {...props} />}
</Stack.Screen>
<Stack.Screen name="Chat">
{(props) => <ChatScreen {...props} />}
</Stack.Screen>
</Stack.Navigator>
);
}
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center' }}>
<Text><FontAwesome5 name={"home"} size={20} color={"dodgerblue"} />Home screen</Text>
</View>
);
}
function TicketScreen(props){
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Ticket screen</Text>
<Button title="Go to Chat" onPress={() => props.navigation.navigate('Chat')} />
</View>
);
}
function CustomKeyboardAvoidingView({ children, style }) {
const headerHeight = useHeaderHeight();
console.log("headerHeight: " + headerHeight)
console.log("StatusBar.currentHeight: " + StatusBar.currentHeight)
const insets = useSafeAreaInsets();
const [bottomPadding, setBottomPadding] = useState(insets.bottom)
const [topPadding, setTopPadding] = useState(insets.top)
useEffect(() => {
// This useEffect is needed because insets are undefined at first for some reason
// https://github.com/th3rdwave/react-native-safe-area-context/issues/54
setBottomPadding(insets.bottom)
setTopPadding(insets.top)
console.log("topPadding: " + topPadding)
console.log("bottomPadding: " + bottomPadding)
}, [insets.bottom, insets.top])
return (
<KeyboardAvoidingView
style={style}
behavior={Platform.OS == "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight + topPadding + StatusBar.currentHeight}
>
{children}
</KeyboardAvoidingView>
);
}
function ChatScreen(){
return(
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<CustomKeyboardAvoidingView style={{backgroundColor: "#fff", flex: 1, flexDirection: "column", justifyContent: "space-between" }}>
<View style={{backgroundColor: "dodgerblue", paddingVertical: 15}}>
<View style={{ margin: 10, marginBottom: 15}}>
<ActivityIndicator size="large" style={{marginBottom: 10}}/>
<Text>Waiting for more info here....</Text>
</View>
</View>
<ScrollView style={{backgroundColor: "tomato", paddingVertical: 15}}>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
<Text>Chat messages</Text>
</ScrollView>
<View style={{backgroundColor: "yellow", paddingVertical: 15}}>
<TextInput placeholder="Type your message here..." />
</View>
</CustomKeyboardAvoidingView>
</TouchableWithoutFeedback>
)
}
const Tab = createBottomTabNavigator();
export default class App extends Component {
render(){
return (
<SafeAreaProvider>
<SafeAreaView style={{flex: 1, backgroundColor: "dodgerblue"}}>
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = 'home';
} else if (route.name === 'Ticket') {
iconName = 'question';
}
return <FontAwesome5 name={iconName} size={size} color={color} />;
},
})}
tabBarOptions={{
style: {
height: 70
},
activeTintColor: "#fff",
inactiveTintColor: "dodgerblue",
inactiveBackgroundColor: "#fff",
activeBackgroundColor: "dodgerblue",
tabStyle: {
paddingTop: 10,
paddingBottom: 10,
height: 70
},
labelStyle: {
fontSize: 14
},
keyboardHidesTabBar: (Platform.OS == "ios" ? false : true)
}}>
<Tab.Screen name="Home">
{(props) => <HomeScreen {...props} />}
</Tab.Screen>
<Tab.Screen name="Ticket">
{(props) => <TicketStack {...props} />}
</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
</SafeAreaView>
</SafeAreaProvider>
);
}
}