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

KeyboardAvoadingView padding calculation on keyboard type change is not correct #672

Open
davoam opened this issue Nov 1, 2024 · 5 comments
Assignees
Labels
🤖 android Android specific 🐛 bug Something isn't working KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component

Comments

@davoam
Copy link

davoam commented Nov 1, 2024

Describe the bug
There are two inputs with different keyboard types, and only one is rendered at a time. One input has the default keyboard type, while the other uses 'number-pad'. When we unmount the default text input and show the number input, KeyboardAvoidingView first sets padding as if the default keyboard is displayed, and then adjusts it for the number keyboard.

This transition creates a visible jump that only occurs on Android. In the attached GIF file, you can see that when switching to the numeric keyboard, the footer button is initially rendered higher before moving down.

This issue is easily reproducible with an app created using the latest Expo CLI.

Important: the issue is reproducible with a non-default keyboard. In the example, this keyboard is used. With the default keyboard, everything works as expected.

Code snippet

import { Button , View, TextInput, StatusBar} from 'react-native';
import { useState} from 'react';

import { ThemedText } from '@/components/ThemedText';
import { KeyboardAvoidingView, KeyboardStickyView } from 'react-native-keyboard-controller';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function HomeScreen() {
  const [isNumberFieldDisplayed, setIsNumberFieldDisplayed] = useState(true);
  const insets = useSafeAreaInsets()

  return (
    <View style={{flex: 1}}>
      <KeyboardAvoidingView keyboardVerticalOffset={StatusBar.currentHeight} behavior="padding" style={{flex: 1}}>
        <View style={{flex: 1, paddingTop: insets.top}}>
          <View style={{flex: 1}}>
            <ThemedText type="title">Welcome!</ThemedText>
              <View style={{flex: 1, backgroundColor: 'red'}}>
                <Button title="Change focus" onPress={() => {setIsNumberFieldDisplayed((prevValue) => !prevValue)}} />
                {!isNumberFieldDisplayed && <TextInput autoFocus placeholder="Type something here" placeholderTextColor="black"/>}
                {isNumberFieldDisplayed && <TextInput autoFocus placeholder="Type a number here"placeholderTextColor="black" keyboardType="number-pad"/>}
                <View style={{marginTop: 'auto'}}>
                  <Button title="Footer button" onPress={() => {}} />
                </View>
              </View>  
          </View>
        </View>
      </KeyboardAvoidingView>
    </View>
  );
}

Expected behavior
Padding is changed like smoothly like it does on iOS.

Screenshots
untitled
Simulator Screen Recording - iPhone 16 - 2024-11-01 at 12 16 03

Smartphone (please complete the following information):

  • Desktop OS: MacOS 15.0.1
  • Device: Pixel 8 pro (emulator)
  • OS: Android 14
  • RN version: 0.74.5
  • RN architecture: old
  • Library version: 1.14.3
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🤖 android Android specific KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component labels Nov 1, 2024
@kirillzyusko
Copy link
Owner

Thanks @davoam for raising the issue

It's very interesting, that the issue happens only with custom keyboards (i. e. not a stock one). I will have a look on this problem 👀

@kirillzyusko
Copy link
Owner

@davoam I think the issue is reproducible with default keyboard as well? See the video below:

Screen.Recording.2024-11-03.at.13.12.51.mov

I think this problem is located in native code. Because let's say if QWERTY keyboard height is 312 and numeric keyboard height is 268, then when qwerty keyboard gets hidden and numeric keyboard appears, then we are still getting values from 0 to 312 (though we should receive from 0 to 268). And in the end of transition we get "resize" event, where we receive a correct height and we perform instant transition. Because of that you can see such jumps 🤔

For me it looks like it's Android OS bug, but I will see if such problem can be reproduced in native code or can be fixed within this library 👀

@kirillzyusko
Copy link
Owner

kirillzyusko commented Nov 3, 2024

One potentail solution I'm thinking of is to store a map of key value where key is type of TextInput and value is height of the keyboard. In this case when keyboard animation gets interrupted and input type is different we can use a value from our map. But such code will add a lot of complexity 🤯

Is it possible to wait for keyboard hide and then trigger input unmount? Or maybe wait for a keyboard to disappear and only when it's hidden request a focus to a new input? Or you can mount a new input, switch focus, and only when focus gets switched then you will remove previous field? What do you think about these approaches?

@davoam
Copy link
Author

davoam commented Nov 6, 2024

Thanks for the quick response, @kirillzyusko !

I tried closing the keyboard, waiting for keyboardDidHide event and focusing on number input. It does not work as well. There is still this bug. If I add 1-second delay for focus it works correctly (without a jump). If delay is less than 1 second there is still a bug.

Mounting new input is also not a good solution since these two inputs may be on different screens. It makes things complex.

So, as a temporary hack we used this 1-second delay. We close the keyboard, wait for keyboardDidHide event, redirect to another screen, wait one second and focus on input. Everything works correctly, with this sequence.

Here is the code when which works correctly, but if you change 1000 to 300, the problem appears again

import { Button , View, TextInput, StatusBar, Keyboard} from 'react-native';
import { useState, useRef} from 'react';

import { ThemedText } from '@/components/ThemedText';
import { KeyboardAvoidingView, KeyboardStickyView } from 'react-native-keyboard-controller';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function HomeScreen() {
  const [isNumberFieldDisplayed, setIsNumberFieldDisplayed] = useState(true);
  const insets = useSafeAreaInsets()
  const textInputRef = useRef<TextInput | null>(null); 
  const numberInputRef = useRef<TextInput | null>(null); 

  const onChangeFocus = () => {
    function toggleIsNumberFieldDisplayed() {
      setIsNumberFieldDisplayed((prevValue) => {
        const nextIsNumberFieldDisplayed = !prevValue
      
        setTimeout(() => {
          nextIsNumberFieldDisplayed ? numberInputRef.current?.focus() : textInputRef.current?.focus();
        }, 1000); 

        return nextIsNumberFieldDisplayed;
      });
    }

    if (Keyboard.isVisible()) {
        const listener = Keyboard.addListener('keyboardDidHide', () => {
          toggleIsNumberFieldDisplayed();
          listener.remove();
        });
        Keyboard.dismiss();
    } else {
     toggleIsNumberFieldDisplayed();
    }
  }

  return (
    <View style={{flex: 1}}>
      <KeyboardAvoidingView keyboardVerticalOffset={StatusBar.currentHeight} behavior="padding" style={{flex: 1}}>
        <View style={{flex: 1, paddingTop: insets.top}}>
          <View style={{flex: 1}}>
            <ThemedText type="title">Welcome!</ThemedText>
              <View style={{flex: 1, backgroundColor: 'red'}}>
                <Button title="Change focus" onPress={onChangeFocus} />
                {!isNumberFieldDisplayed && <TextInput key="textInput" ref={textInputRef} placeholder="Type something here" placeholderTextColor="black"/>}
                {isNumberFieldDisplayed && <TextInput key="numberInput" ref={numberInputRef} placeholder="Type a number here"placeholderTextColor="black" keyboardType="number-pad"/>}
                <View style={{marginTop: 'auto'}}>
                  <Button title="Footer button" onPress={() => {}} />
                </View>
              </View>  
          </View>
        </View>
      </KeyboardAvoidingView>
    </View>
  );
}

@kirillzyusko
Copy link
Owner

@davoam okay interesting - 300ms is a pretty big time interval, so it should work 🤯 I'll have a look again on this problem! It's weird, that you have to wait 1s!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🤖 android Android specific 🐛 bug Something isn't working KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component
Projects
None yet
Development

No branches or pull requests

2 participants