Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SimonVillage opened this issue Jul 27, 2020 · 24 comments

Comments

@SimonVillage
Copy link

SimonVillage commented Jul 27, 2020

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
Copy link
Author

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

@stale
Copy link

stale bot commented Dec 25, 2020

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 stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Dec 25, 2020
@gabrieldonadel
Copy link
Collaborator

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

@stale stale bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Jan 28, 2021
@martin056
Copy link

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

@junhohong
Copy link

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

@VytautasLozickas
Copy link

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

@vbylen
Copy link

vbylen commented Sep 11, 2021

Can confirm this bug still exists.

Manually focusing requires too much delay sadly.

@oliverfrat
Copy link

oliverfrat commented Sep 20, 2021

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

@vbylen
Copy link

vbylen commented Sep 20, 2021

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

@cnazha
Copy link

cnazha commented Oct 30, 2021

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

@BodaThomas
Copy link

Still face the issue on react-native 0.66.3

@vadamk
Copy link

vadamk commented Jul 11, 2022

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

@tibbe
Copy link

tibbe commented Aug 8, 2022

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

@qardpeet
Copy link

qardpeet commented Oct 18, 2022

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.

@oliverfrat
Copy link

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

@lyqandy
Copy link
Contributor

lyqandy commented Apr 18, 2023

react-navigation/react-navigation#10681
software-mansion/react-native-screens#1504

same issue. I think it is caused by react-navigation, the frame height will be changed twice.

facebook-github-bot pushed a commit that referenced this issue Apr 20, 2023
…ed (#36970)

Summary:
Fix this issue: #29499

- We should change the bottom height if the frame height of KeyboardAvoidingView is changed

In some scenarios, the height of `KeyboardAvoidingView` would be changed because its container is re-layouted. So `onLayout` of `KeyboardAvoidingView` may be triggered more than once.

https://github.com/facebook/react-native/blob/bbc3657ff4efd0218e02ad9a3c73725a7f8a366c/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L114-L125

But at line 122 above, `_updateBottomIfNecessary ` would be called only for the first trigger of `onLayout`. That means, if the height of `KeyboardAvoidingView` is changed, the bottom height can't be updated.

#### See the videos below:

##### before

In this video, `KeyboardAvoidingView ` is rendered twice, and the height is changed from 844 to 753, `bottomHeight ` is not updated when the height changed, so there is a white gap below the `continue` button. Once I re-open the keyboard, `_updateBottomIfNecessary` is called again, then the white gap disappeared.

https://user-images.githubusercontent.com/25719782/232962924-c69adc11-deb9-4426-9b5c-4e990a0470db.mp4

##### after

https://user-images.githubusercontent.com/25719782/232962956-a163020f-5f40-4d82-9f6c-5ee67416c489.mp4

## Changelog:

[GENERAL] [CHANGED] - change `_onLayout` to update bottom height when frame height is changed

Pull Request resolved: #36970

Reviewed By: rshest

Differential Revision: D45138176

Pulled By: NickGerleman

fbshipit-source-id: b7ce6d75622ed6e8f104ae0d8441e1cb97cfa15b
jeongshin pushed a commit to jeongshin/react-native that referenced this issue May 7, 2023
…ed (facebook#36970)

Summary:
Fix this issue: facebook#29499

- We should change the bottom height if the frame height of KeyboardAvoidingView is changed

In some scenarios, the height of `KeyboardAvoidingView` would be changed because its container is re-layouted. So `onLayout` of `KeyboardAvoidingView` may be triggered more than once.

https://github.com/facebook/react-native/blob/bbc3657ff4efd0218e02ad9a3c73725a7f8a366c/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L114-L125

But at line 122 above, `_updateBottomIfNecessary ` would be called only for the first trigger of `onLayout`. That means, if the height of `KeyboardAvoidingView` is changed, the bottom height can't be updated.

#### See the videos below:

##### before

In this video, `KeyboardAvoidingView ` is rendered twice, and the height is changed from 844 to 753, `bottomHeight ` is not updated when the height changed, so there is a white gap below the `continue` button. Once I re-open the keyboard, `_updateBottomIfNecessary` is called again, then the white gap disappeared.

https://user-images.githubusercontent.com/25719782/232962924-c69adc11-deb9-4426-9b5c-4e990a0470db.mp4

##### after

https://user-images.githubusercontent.com/25719782/232962956-a163020f-5f40-4d82-9f6c-5ee67416c489.mp4

## Changelog:

[GENERAL] [CHANGED] - change `_onLayout` to update bottom height when frame height is changed

Pull Request resolved: facebook#36970

Reviewed By: rshest

Differential Revision: D45138176

Pulled By: NickGerleman

fbshipit-source-id: b7ce6d75622ed6e8f104ae0d8441e1cb97cfa15b
OlimpiaZurek pushed a commit to OlimpiaZurek/react-native that referenced this issue May 22, 2023
…ed (facebook#36970)

Summary:
Fix this issue: facebook#29499

- We should change the bottom height if the frame height of KeyboardAvoidingView is changed

In some scenarios, the height of `KeyboardAvoidingView` would be changed because its container is re-layouted. So `onLayout` of `KeyboardAvoidingView` may be triggered more than once.

https://github.com/facebook/react-native/blob/bbc3657ff4efd0218e02ad9a3c73725a7f8a366c/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L114-L125

But at line 122 above, `_updateBottomIfNecessary ` would be called only for the first trigger of `onLayout`. That means, if the height of `KeyboardAvoidingView` is changed, the bottom height can't be updated.

#### See the videos below:

##### before

In this video, `KeyboardAvoidingView ` is rendered twice, and the height is changed from 844 to 753, `bottomHeight ` is not updated when the height changed, so there is a white gap below the `continue` button. Once I re-open the keyboard, `_updateBottomIfNecessary` is called again, then the white gap disappeared.

https://user-images.githubusercontent.com/25719782/232962924-c69adc11-deb9-4426-9b5c-4e990a0470db.mp4

##### after

https://user-images.githubusercontent.com/25719782/232962956-a163020f-5f40-4d82-9f6c-5ee67416c489.mp4

## Changelog:

[GENERAL] [CHANGED] - change `_onLayout` to update bottom height when frame height is changed

Pull Request resolved: facebook#36970

Reviewed By: rshest

Differential Revision: D45138176

Pulled By: NickGerleman

fbshipit-source-id: b7ce6d75622ed6e8f104ae0d8441e1cb97cfa15b
@Maker-Mark
Copy link

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.

@rodrigorcs
Copy link

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

Copy link

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 github-actions bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 16, 2024
@Maker-Mark
Copy link

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

@github-actions github-actions bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 17, 2024
@GrandChieftain
Copy link

GrandChieftain commented May 26, 2024

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.

@react-native-bot
Copy link
Collaborator

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.

@react-native-bot react-native-bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Nov 22, 2024
@rodrigorcs
Copy link

rodrigorcs commented Nov 22, 2024

Please remove the stale label. As far as I know, this is still an issue.

@migueldaipre
Copy link
Collaborator

Can anyone create an updated repro with only the react-native core?

ex: v0.76.3+ and with the new architecture enabled.

@react-native-bot react-native-bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Nov 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests