react-native: 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

safeareavie 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",
  },
});

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 24
  • Comments: 20 (2 by maintainers)

Commits related to this issue

Most upvoted comments

I’m on react-native 0.63.3 and still face this issue

+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>
)

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 😦

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

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.

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…

this is a really common use case. are there any workaround people have found other than removing autoFocus?

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')
}

@qardpeet great that you’ve found an even better workaround! Happy to see even better solutions 😃