kirillzyusko / react-native-keyboard-controller

Keyboard manager which works in identical way on both iOS and Android
https://kirillzyusko.github.io/react-native-keyboard-controller/
MIT License
1.59k stars 64 forks source link

Android side-effect issue with KeyboardProvider #584

Open hyochan opened 5 days ago

hyochan commented 5 days ago

Describe the bug Hello, thank you for creating such a great library. I've been using KeyboardAvoidingView in React Native for a while, but recently needed the keyboard functionality from react-native-keyboard-controller, which allowed me to implement the keyboard behavior I wanted. I really appreciate your work.

However, I encountered an issue where KeyboardAvoidingView from the core react-native package only causes a side-effect on Android. Specifically, when I wrap my app layout with KeyboardProvider, the KeyboardAvoidingView in react-native package stops working, but this issue is only happening on Android.

It seems like the issue was resolved by setting padding instead of leaving the behavior as undefined, and also by providing a keyboardVerticalOffset, but the exact cause and behavior remain unclear, which leaves me feeling a bit uncertain. I would like to better understand how this change impacts screens where KeyboardAvoidingView is already being used. Specifically, I’d like to know what effects this adjustment may have on the existing views, especially in terms of layout shifts or keyboard interaction, and whether this might introduce any potential side effects. Could you explain how this configuration influences the layout and behavior in detail?

Code snippet RootProvider.tsx

function RootProvider({initialThemeType, children}: Props): JSX.Element {
  return (
    <KeyboardProvider>
      <RecoilRoot>
        <DoobooProvider
          themeConfig={{
            initialThemeType: initialThemeType ?? undefined,
            customTheme: theme,
          }}
        >
          <ErrorBoundary
            FallbackComponent={FallbackComponent}
            onError={handleErrorConsole}
          >
            <ActionSheetProvider>{children}</ActionSheetProvider>
          </ErrorBoundary>
        </DoobooProvider>
      </RecoilRoot>
    </KeyboardProvider>
  );
}

_layout.tsx

    <GestureHandlerRootView
      style={css`
        flex: 1;
      `}
    >
      <RootProvider initialThemeType={localThemeType as ColorSchemeName}>
        <>
          <StatusBarBrightness />
          <Layout />
        </>
      </RootProvider>
    </GestureHandlerRootView>

index.tsx

import styled, {css} from '@emotion/native';
import {EditText, IconButton, Typography, useDooboo} from 'dooboo-ui';
import {Stack} from 'expo-router';

import {t} from '../src/STRINGS';
import {KeyboardAvoidingView, Platform, View} from 'react-native';
import {useState} from 'react';

const Container = styled.View`
  background-color: ${({theme}) => theme.bg.basic};

  flex: 1;
  align-self: stretch;
  justify-content: center;
  align-items: center;
`;

export default function Index(): JSX.Element {
  const {theme} = useDooboo();
  const [text, setText] = useState('');

  return (
    <Container>
      <Stack.Screen
        options={{
          title: t('HOME'),
        }}
      />
      <KeyboardAvoidingView
        style={css`
          flex: 1;
          width: 100%;
        `}
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      >
        <View
          style={css`
            flex: 1;
            background-color: ${theme.bg.paper};

            justify-content: center;
            align-items: center;
          `}
        >
          <Typography.Heading5>Hi there!</Typography.Heading5>
        </View>
        <EditText
          onChangeText={setText}
          value={text}
          decoration="boxed"
          style={css`
            background-color: ${theme.bg.basic};
          `}
          endElement={<IconButton icon="Bird" onPress={() => {}} size={24} />}
        />
      </KeyboardAvoidingView>
    </Container>
  );
}

Repo for reproducing I would be highly appreciate if you can provide repository for reproducing your issue. It can significantly reduce the time for discovering and fixing the problem.

To Reproduce Steps to reproduce the behavior:

  1. Go and clone react-native-keyboard-controller-issue repository.
  2. Run bun install or use your package manager.
  3. Checkout initial commit git checkout 16a313a4d8ae1d4a5fb1cdd07529f440527c3dcb.
  4. Run the app and see it if works.
  5. Checkout commit 41f812e9b6edd1b27cf787f74de7600ae20360fe which installed react-native-keyboard-controller and wrapped with KeyboardProvider and test that keyboard is not showing this time.
  6. Checkout commit 61bc6f35ffc30048df04c4a78a8a2a1d29de2d90 and test that it works again with workaround codes.
  7. Also tested using custom hook, useKeyboardAnimation with various options.

Expected behavior Even when using react-native-keyboard-controller and wrapping the app with KeyboardProvider, the existing KeyboardAvoidingView from react-native should continue to function without issues on Android.

Screenshots

Video Previews

  1. Initial Commit

    https://github.com/user-attachments/assets/44d4d630-d2eb-4a23-80bc-b2e59719f064

  2. Install and wrap with KeyboardProvider

    https://github.com/user-attachments/assets/2789ad4d-8c3e-4348-9c05-fedf377f7431

  3. Naive workaround

    https://github.com/user-attachments/assets/a4ba161c-eee7-4e17-9a6f-efed71499b25

  4. Test other options with useKeyboardAnimation

    https://github.com/user-attachments/assets/b9b4a76c-bddf-44f9-b1e0-4ae2d6e5a55b

Smartphone (please complete the following information):

Additional context N/A

kirillzyusko commented 5 days ago

Hello @hyochan

When you wrap the app in KeyboardProvider it automatically moves the app in edge-to-edge mode and with adjustResize mode the automatic window resize will not work.

So in your case:

The ways how you can solve your problem:

Let me know if you want me to explain any aspect in more details 🙏

Even when using react-native-keyboard-controller and wrapping the app with KeyboardProvider, the existing KeyboardAvoidingView from react-native should continue to function without issues on Android.

Unfortunately it's not achievable in a current reality. The best thing that you can do is to use KeyboardAvoidingView from react-native-keyboard-controller.

hyochan commented 4 days ago

@kirillzyusko Thank you so much for the detailed and excellent response. Thanks to your explanation, I have gained a better understanding of your library.

Screenshot 2024-09-14 at 12 54 51 AM

As per your suggestion, I removed the platform-specific code and used the KeyboardAvoidingView from react-native-keyboard-controller in this commit: https://github.com/hyochan/react-native-keyboard-controller-issue/commit/01eb988a7543c8f579771020c4b14346add7cbba. I also removed keyboardOffset. However, as shown in the video below, the input does not rise above the keyboard like it does when using the native React Native KeyboardAvoidingView. This also needs to be explained.

https://github.com/user-attachments/assets/cd6a8030-cc75-48d8-a10d-c279d92aec9b

Additionally, if you have more references regarding the statement "the useWindowDimensions hook from React Native, used inside KeyboardAvoidingView, is buggy when it comes to edge-to-edge," it would be very helpful to me.

Thank you.

kirillzyusko commented 4 days ago

Additionally, if you have more references regarding the statement "the useWindowDimensions hook from React Native, used inside KeyboardAvoidingView, is buggy when it comes to edge-to-edge," it would be very helpful to me.

You can read about that for example here - https://github.com/facebook/react-native/issues/41918

As per your suggestion, I removed the platform-specific code and used the KeyboardAvoidingView from react-native-keyboard-controller in this commit: https://github.com/hyochan/react-native-keyboard-controller-issue/commit/01eb988a7543c8f579771020c4b14346add7cbba. I also removed keyboardOffset. However, as shown in the video below, the input does not rise above the keyboard like it does when using the native React Native KeyboardAvoidingView. This also needs to be explained.

I'll check why it happens in nearest time!

kirillzyusko commented 4 days ago

Okay @hyochan

Sorry for initial misleading - in your case you should have verticalKeyboardOffset. Let me slightly dive in the implementation of KeyboardAvoidingView:

image

So as you can see you had a difference which is equal to headerHeight + statusBarHeight (depends on whether it's translucent or not).

And this space is roughly equal to the size of your text input - that's why it gets hidden behind the keyboard. In your case you should artificially increase the area of a blue rectangle and you can do that by adding verticalKeyboardOffset. You can get headerSize using useHeaderHeight hook. If this space is not enough then you can add + StatusBar.currentHeight

I hope I explained why it doesn't work as you expect 😅

Thank you for providing such a detailed and good quality reproduction example ❤️ Let me know if you have any additional questions - I'll be happy to answer on all of them 😊

beshoo commented 4 days ago

@kirillzyusko

Thank you for your efforts. Unfortunately, we are still encountering the same issue on Android. The field animates to the top of the upper side of the keyboard, meaning it remains covered.

We have tested all your solutions, but none of them have worked.

I am quite surprised that this bug exists in React Native. I am considering migrating to Flutter, although it is not an easy decision. Some bugs force you to consider alternatives.

If you have any solution that can be applied with clear steps, we would be very grateful. Eg demo which we can understand your sulution since your last reply is very complicated. Demo code is appreciate.

All our app forms are covered by this bug, and the React Native team remains silent.

hyochan commented 4 days ago

@kirillzyusko Thanks for the great explanation 👍👍

No Header With Header
No Header With Header

However, I am still curious why react-native-keyboard-controller's KeyboardAvoidingView needs extra headerSize to keyboardOffset. If you could spot the suspicious sections of the code, I would also like to take a look.

This issue needs to be clearly understood to prevent side effects in other views that use KeyboardAvoidingView. This could be critical for large applications, as it would require testing every view before integrating react-native-keyboard-controller. (I understand that we can safely adopt this library by disabling it with <KeyboardProvider enabled={false}> and selectively enabling it where needed. However, I’m trying to understand the root issue. 🤔)

@beshoo I am not getting your statement.

The field animates to the top of the upper side of the keyboard, meaning it remains covered.

I think this should be explained in more details. Providing some code examples and recordings would be very helpful.

kirillzyusko commented 3 days ago

Eg demo which we can understand your sulution since your last reply is very complicated. Demo code is appreciate.

@beshoo you can clone the example app https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example and see how everything is handled there. This app has a plenty of use cases and covers almost all basic interactions with the keyboard. If you run the code in example app and it works there, but the same code produces different output in your app - it means that something is misconfigured, and it's easier to fix a misconfiguration rather than switching to a new framework/technology 🤷‍♂️

If you can replace the code in example app with your own code and the bug persist in example application, then feel to free to open a new issue and I'll be glad to help to resolve the problem.

The field animates to the top of the upper side of the keyboard, meaning it remains covered.

It's very hard to say what exactly causes the problem, because I don't see the code, don't see the output (including view hierarchy) and can not test/interact with this code. If you think it's a problem in this library - feel free to open a new issue with reproduction example and I'll try to do all my best to help you.

kirillzyusko commented 3 days ago

However, I am still curious why react-native-keyboard-controller's KeyboardAvoidingView needs extra headerSize to keyboardOffset. If you could spot the suspicious sections of the code, I would also like to take a look.

It's all handled in https://github.com/kirillzyusko/react-native-keyboard-controller/blob/e2198afcf71e9bef79e43bf7a1b2a2f19a8a42a8/src/components/KeyboardAvoidingView/index.tsx#L78-L79

This issue needs to be clearly understood to prevent side effects in other views that use KeyboardAvoidingView. This could be critical for large applications, as it would require testing every view before integrating react-native-keyboard-controller. (I understand that we can safely adopt this library by disabling it with and selectively enabling it where needed. However, I’m trying to understand the root issue. 🤔)

Well, feel free to correct me here, but I think that verticalOffset is needed for KeyboardAvoidingView from RN as well. Basically how KeyboardAvoidingView works - it measures its dimensions (width/height), then when keyboard is open it understands which part of view is covered by keyboard and changes padding to the same size, so that all the content is visible.

But if some elements are placed above the KeyboardAvoidingView, then dimensions will be gathered incorrectly and it may lead to a situation, when part of the content is obscured by the keyboard.

In the end I implemented test example and covered it by e2e tests to assure that KeyboardAvoidingView satisfies to default implementation:

Default KAV KAV from RNKC

On iOS if you don't specify keyboardVerticalOffset (i. e. header size) you also will end up in the same situation, when bottom elements will be overlapped by keyboard:

RN RNKC
image image

So I think that a component from react-native-keyboard-controller works in the same manner as a default KeyboardAvoidingView on iOS (and on Android it just re-uses all the logic).

I didn't find time to run your project on iOS - but if you run it with keyboardVerticalOffset={0} will the input be overlapped by keyboard?


If you want to fully match default keyboard handling from Android, then you can wrap your entire app in KAV:

    <GestureHandlerRootView
      style={css`
        flex: 1;
      `}
    >
      <RootProvider initialThemeType={localThemeType as ColorSchemeName}>
        <KeyboardAvoidingView
          behavior="padding"
          style={{flex: 1, width: '100%'}}
        >
          <StatusBarBrightness />
          <Layout />
        </KeyboardAvoidingView>
      </RootProvider>
    </GestureHandlerRootView>

Then layout will be measured correctly and you don't need to think about headers and other elements (because all of them will be rendered inside KAV - KAV will match window dimensions).

But sometimes having just KeyboardAvoidingView is not always enough to handle the keyboard in all scenarios, but when you have something on a global level it may be hard to disable that (or it can make your code slightly more complicated). Anyway - feel free to chose what exactly best suitable for your project, but I think wrapping an app in KeyboardAvoidingView is kind of anti-pattern.

Let me know if you have any other questions - will be happy to answer on them as well! 😊 🙌