react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Wrong button position when using KeyboardAvoidingView in combination with SafeAreaView and autofocus

Open SimonVillage opened this issue 5 years ago • 26 comments

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

SimonVillage avatar Jul 27 '20 09:07 SimonVillage

Possibly related to #29467 but the reverted changes which are mentioned there do not help.

SimonVillage avatar Jul 27 '20 10:07 SimonVillage

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.

stale[bot] avatar Dec 25 '20 20:12 stale[bot]

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

gabrieldonadel avatar Jan 28 '21 20:01 gabrieldonadel

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 :(

martin056 avatar Aug 20 '21 13:08 martin056

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

junhohong avatar Aug 27 '21 17:08 junhohong

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

VytautasLozickas avatar Sep 07 '21 15:09 VytautasLozickas

Can confirm this bug still exists.

Manually focusing requires too much delay sadly.

cloudorbush avatar Sep 11 '21 05:09 cloudorbush

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

oliverfrat avatar Sep 20 '21 17:09 oliverfrat

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

cloudorbush avatar Sep 20 '21 23:09 cloudorbush

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

cnazha avatar Oct 30 '21 06:10 cnazha

Still face the issue on react-native 0.66.3

BodaThomas avatar Feb 10 '22 05:02 BodaThomas

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

vadamk avatar Jul 11 '22 14:07 vadamk

This is still a (common?) issue with seemingly no good workaround. Could we have some dev input?

tibbe avatar Aug 08 '22 15:08 tibbe

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 avatar Oct 18 '22 08:10 qardpeet

@qardpeet great that you've found an even better workaround! Happy to see even better solutions :)

oliverfrat avatar Dec 07 '22 23:12 oliverfrat

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.

lyqandy avatar Apr 18 '23 12:04 lyqandy

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.

Maker-Mark avatar May 24 '23 17:05 Maker-Mark

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

rodrigorcs avatar Oct 16 '23 22:10 rodrigorcs

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.

github-actions[bot] avatar Apr 16 '24 05:04 github-actions[bot]

@SimonVillage Can you remove the Stale label? As far as I know, this is still an issue

Maker-Mark avatar Apr 16 '24 13:04 Maker-Mark

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.

GrandChieftain avatar May 26 '24 00:05 GrandChieftain