react-native
react-native copied to clipboard
Wrong button position when using KeyboardAvoidingView in combination with SafeAreaView and autofocus
Description
I have an input with autoFocus, a SafeAreaView, and a KeyboardAvoidingView. However the Button which should have his position exactly above the keyboard gets some margin when using SafeAreaView in combination with autoFocus.
It is to mention that if I add a delay to autoFocus of around 250ms it is working as expected.
I've build an expo snack here: https://snack.expo.io/@simbob/keyboardavoidingview-bug

React Native version:
react: ~16.11.0 => 16.11.0
react-native: https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz => 0.62.2
Expected Results
Button should have the same position all time.
Steps To Reproduce
import React from "react";
import {
StyleSheet,
Text,
View,
TextInput,
KeyboardAvoidingView,
TouchableOpacity,
Keyboard,
SafeAreaView,
} from "react-native";
export default function App() {
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView style={styles.container} behavior="padding">
<View style={styles.top}>
<Text>Open up App.js to start working on your app!</Text>
<TextInput style={{ borderWidth: 1 }} autoFocus={true} />
</View>
<View style={styles.bottom}>
<TouchableOpacity
style={styles.loginScreenButton}
onPress={Keyboard.dismiss}
>
<Text style={styles.loginText}>Blur</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: 16,
},
top: {
flex: 0.7,
},
bottom: {
flex: 0.3,
justifyContent: "flex-end",
},
loginScreenButton: {
paddingTop: 10,
paddingBottom: 10,
backgroundColor: "#1E6738",
borderWidth: 1,
},
loginText: {
color: "#fff",
textAlign: "center",
},
});
Possibly related to #29467 but the reverted changes which are mentioned there do not help.
Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.
I'm on react-native 0.63.3 and still face this issue
I'm on react-native 0.63.3 and still face this issue
I experience the same issue.
I can say that I see it on iOS only. It does not appear on most of the devices that I've tried. It appears on iPhone Xr.
Removing the autoFocus from the input solves the issue with the padding but introduces bad UX in my case :(
this is a really common use case. are there any workaround people have found other than removing autoFocus?
this is a really common use case. are there any workaround people have found other than removing
autoFocus?
Sort-of workaround is to wrap the TextInput component and focus the input manually (based on autofocus prop) with a little timeout. This breaks the screen transition animation if navigating (with react-navigation) to the next screen which also has autofocus on an input though. Unless you increase that timeout quite a bit (like 500ms), but then the keyboard lags for a while to open after the screen transition...
Can confirm this bug still exists.
Manually focusing requires too much delay sadly.
+1
In my case it's because of react-navigation.
My workaround:
// With ref
const insets = useSafeAreaInsets();
const headerHeight = useHeaderHeight();
const ref = useRef(null);
useEffect(() => {
const unsubscribe = navigation.addListener('transitionEnd', () => {
ref.current?.focus();
});
return unsubscribe;
}, [navigation]);
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight + insets.bottom}>
<View>
<TextInput ref={ref} />
</View>
</KeyboardAvoidingView>
)
// Without ref (In case you can't set ref)
const insets = useSafeAreaInsets();
const headerHeight = useHeaderHeight();
const [isNavMounted, setNavMounted] = useState(false);
useEffect(() => {
const unsubscribe = navigation.addListener('transitionEnd', () => {
setNavMounted(true);
});
return unsubscribe;
}, [navigation]);
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight + insets.bottom}>
<View>
{isNavMounted && <Component autoFocus/>}
</View>
</KeyboardAvoidingView>
)
+1
In my case it's because of react-navigation.
My workaround:
// With ref const insets = useSafeAreaInsets(); const headerHeight = useHeaderHeight(); const ref = useRef(null); useEffect(() => { const unsubscribe = navigation.addListener('transitionEnd', () => { ref.current?.focus(); }); return unsubscribe; }, [navigation]); return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight + insets.bottom}> <View> <TextInput ref={ref} /> </View> </KeyboardAvoidingView> ) // Without ref (In case you can't set ref) const insets = useSafeAreaInsets(); const headerHeight = useHeaderHeight(); const [isNavMounted, setNavMounted] = useState(false); useEffect(() => { const unsubscribe = navigation.addListener('transitionEnd', () => { setNavMounted(true); }); return unsubscribe; }, [navigation]); return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight + insets.bottom}> <View> {isNavMounted && <Component autoFocus/>} </View> </KeyboardAvoidingView> )
Thank you, works like a charm!
Refactored the above to the following 2 hooks:
Hook to return transition complete based on react navigation
// Returns finished as true when transition is complete
const useScreenTransitionEnded = () => {
const navigation = useNavigation();
const [finished, setTransitionFinished] = useState(false);
useEffect(() => {
const unsubscribe = navigation.addListener('transitionEnd', () => {
setTransitionFinished(true);
});
return unsubscribe;
}, [navigation]);
return {finished};
};
Hook to focus an input when the above returns finished
// focuses an input ref when transition is completed
const useInputFocusAfterTransition = inputRef => {
const {finished} = useScreenTransitionEnded();
useEffect(() => {
if (finished && inputRef.current) {
inputRef.current?.focus();
}
}, [finished]);
};
Usage
const nameRef = useRef();
useInputFocusAfterTransition(nameRef);
...
<TextInput ref={nameRef} />
Just pass the hook the input ref, it will handle all the logic and autofocus when a transition is done
Thanks @oliverfrat
Still face the issue on react-native 0.66.3
Still face the issue on react-native 0.68.2
Here is the best way (from my side) to handle this issue:
export const useScreenTransitionEnded = () => {
const navigation = useNavigation();
const [isTransitionFinished, setTransitionFinished] = React.useState(false);
React.useEffect(() => {
const unsubscribe = navigation.addListener("transitionEnd", () => {
setTransitionFinished(true);
});
return unsubscribe;
}, [navigation]);
return isTransitionFinished;
};
export const useKeyboardVerticalOffset = (offset = 0) => {
const isTransitionFinished = useScreenTransitionEnded();
const inset = useSafeAreaInsets();
return isTransitionFinished
? offset + (inset.top + inset.bottom)
: offset;
};
Usage:
const Component = () => {
const keyboardVerticalOffset = useKeyboardVerticalOffset(64);
return (
<KeyboardAvoidingView keyboardVerticalOffset={keyboardVerticalOffset}>
{/* ... */}
</KeyboardAvoidingView>
)
}
I've tried useInputFocusAfterTransition but there were little delay before autoFocus. In my case it was unacceptable.
Thank you guys oliverfrat cnazha
This is still a (common?) issue with seemingly no good workaround. Could we have some dev input?
While the useEffect with the transitionEnded listener works, its a bit too large for my liking. I managed to solve this issue by firing the focus function using the onLayout prop. Also this error only happened with "email-address" keyboard type text inputs in my case.
<TextInput
onLayout={() => emailInputRef.current?.focus()}
textContentType="emailAddress"
keyboardType="email-address"
autoCorrect={false}
autoCapitalize="none"
ref={emailInputRef}
/>
Bonus: the focus happens a lot faster when navigating, giving you the behavior you were looking for.
@qardpeet great that you've found an even better workaround! Happy to see even better solutions :)
https://github.com/react-navigation/react-navigation/issues/10681 https://github.com/software-mansion/react-native-screens/issues/1504
same issue. I think it is caused by react-navigation, the frame height will be changed twice.
While the useEffect with the transitionEnded listener works, its a bit too large for my liking. I managed to solve this issue by firing the focus function using the onLayout prop. Also this error only happened with "email-address" keyboard type text inputs in my case.
<TextInput onLayout={() => emailInputRef.current?.focus()} textContentType="emailAddress" keyboardType="email-address" autoCorrect={false} autoCapitalize="none" ref={emailInputRef} />Bonus: the focus happens a lot faster when navigating, giving you the behavior you were looking for.
This does not consistent work from my testing. Looks like the react-navigation underlying issue makes the other workaround that uses ontransition end a more reliable solution.
Hey guys, in my specific case, I was able to fix it by using router.push() instead of <Redirect/>.
Note 1: It is an workaround, not a proper fix
Note 2: I am using expo-router, there might be an equivalent to other routers. Not sure what changed at render-level but hope to help other devs in the same circunstances.
Before
import { Redirect, usePathname } from 'expo-router'
// ...
const currentRoute = usePathname()
// ...
if (user && currentRoute !== '/name') {
if (!user.name) return <Redirect href="/name" />
return <Redirect href="/(app)" />
}
After
import { router, usePathname } from 'expo-router'
// ...
const currentRoute = usePathname()
// ...
if (user && currentRoute !== '/name') {
if (!user.name) return router.push('/name')
return router.push('/app')
}
This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.
@SimonVillage Can you remove the Stale label? As far as I know, this is still an issue
I was having the same problem. Using onLayout seemed to work for the most part, but I did experience the inconsistent behavior that @Maker-Mark was talking about, so I sought to investigate the cause behind the matter.
For iOS, the solution for me was removing padding from my KeyboardAvoidingView styles and putting it in a direct child View. The KeyboardAvoidingView's behavior is typically 'padding' on iOS, which probably applies some padding on it already, so I guessed that the two could possibly be interfering, performed that fix, and it works in my app now.
For Android, using behavior='padding' seemed to cause bad flickering and using behavior='height' ironically miscalculated the appropriate height for one Stack.Screen. So a couple of Stack Overflow posts told me that Android doesn't really need to have a KeyboardAvoidingView. So you can just set behavior='undefined' for Android and it should function just like a regular View. Here's the new prop: behavior={Platform.OS === 'ios' ? 'padding' : undefined} // [Platform.select(...) works too].
In either case, there was no need to use keyboardVerticalOffset, which I'm very happy about. Screen sizes vary so much that I don't like its absolute nature. I haven't tested these exhaustively (only iPhone 12 and Pixel 3a simulator), but hopefully this helps.