facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.26k stars 24.34k forks source link

iOS Keyboard Autofill/SecureTextEntry makes content jump/glitch when focusing on next TextInput #31722

Open samuelbeaulieu opened 3 years ago

samuelbeaulieu commented 3 years ago

Description

I have a page in my app with two TextInput inside views covering the whole screen.

The two TextInput have a ref ref={refTextInput1}, ref={refTextInput2}. They also have a onSubmitEditing onSubmitEditing={() => refTextInput2().current.focus()}, onSubmitEditing={() => refTextInput1().current.focus()}.

So, when I press "return" on the keyboard, the focus will switch to the desired input but it's also making the whole screen jump/glitch which is really annoying and not wanted.

The focus should switch to the desired input without making the whole screen jump/glitch.

I searched everywhere on StackOverflow and Github Issues but didn't find a lot of issues with this particular problem. At first, I thought I had a problem similar to #30207, but after a while trying to find the problem, I think I found it.

The problem only occurs(I think) when Autofill Passwords is enabled in Settings>Passwords>Autofill Passwords. If I disable Autofill Passwords, the jump/glitch does not occurs.

I also noticed that the keyboard event is always triggered twice on focus with Autofill Passwords enabled, and only once on focus with Autofill Passwords disabled. It may also be related. (See the console when running the snack example.)

I looked at other apps, and they all seems fine in the same type of login screen. Instagram for example doesn't have this problem.

It might also be the way I made my screen, if there's a better way to do the same or similar screen, I would be open to suggestions.

React Native version:

System:
    OS: macOS 11.4
    CPU: (8) x64 Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
    Memory: 137.61 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 14.17.0 - /usr/local/bin/node
    Yarn: Not Found
    npm: 6.14.13 - /usr/local/bin/npm
    Watchman: 4.9.0 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.10.1 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: iOS 14.5, DriverKit 20.4, macOS 11.3, tvOS 14.5, watchOS 7.4
    Android SDK:
      API Levels: 22, 23, 24, 25, 26, 27, 28, 29, 30
      Build Tools: 28.0.3, 29.0.2, 30.0.2
      System Images: android-30 | Google APIs Intel x86 Atom
      Android NDK: Not Found
  IDEs:
    Android Studio: 4.1 AI-201.8743.12.41.6953283
    Xcode: 12.5/12E262 - /usr/bin/xcodebuild
  Languages:
    Java: 1.8.0_271 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.1 => 17.0.1 
    react-native: 0.64.1 => 0.64.1 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps To Reproduce

  1. Make sure Autofill Passwords is enabled in Settings>Passwords>Autofill Passwords.
  2. Have at least two TextInput with a view taking the whole screen space.
  3. Focus on the next TextInput using "return" on the keyboard.

I also noticed that the keyboard event is always triggered twice on focus with Autofill Passwords enabled, and only once on focus with Autofill Passwords disabled. It may also be related. (See the console when running the snack example.)

Expected Results

When pressing "return" on the keyboard, the focus should switch to the desired input without making the whole screen jump/glitch. Like when Autofill Passwords is disabled in Settings>Passwords>Autofill Passwords.

Snack, code example, screenshot, or link to a repository:

Snack 1 is a simpler version of my issue. Snack 1: simple-keyboard-autofill-issue

Snack 2 is closer to what I really have in my app. Snack 2: my-app-keyboard-autofill-issue

Minimal code example: (from simple-keyboard-autofill-issue)

import React, { useState, useRef, useEffect } from 'react';
import { Text, View, KeyboardAvoidingView, TouchableWithoutFeedback, Keyboard, Platform, TextInput } from 'react-native';

export default function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const refUsernameInput = useRef(null);
  const refPasswordInput = useRef(null);

  useEffect(() => {
    Keyboard.addListener('keyboardWillShow', keyboardWillShow);
    Keyboard.addListener('keyboardWillHide', keyboardWillHide);
s
    // cleanup function
    return () => {
      Keyboard.removeListener('keyboardWillShow', keyboardWillShow);
      Keyboard.removeListener('keyboardWillHide', keyboardWillHide);
    };
  }, []);

  const keyboardWillShow = () => {
    console.log('keyboardWillShow');
  };

  const keyboardWillHide = () => {
    console.log('keyboardWillHide');
  };

  return (
    <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View style={{ flex: 1, justifyContent: 'space-around', paddingHorizontal: 20 }}>
            <TextInput
              blurOnSubmit={false}
              keyboardType="email-address"
              placeholder="Username or email"
              textContentType="username" // iOS
              onSubmitEditing={() => refPasswordInput.current.focus()}
              onChangeText={setUsername}
              value={username}
              ref={refUsernameInput}
              style={{
                borderColor: '#000',
                borderWidth: 0.5,
                height: 45,
                paddingHorizontal: 20,
                marginBottom: 5,
              }}
            />
            <TextInput
              blurOnSubmit={false}
              keyboardType="default"
              placeholder="Password"
              secureTextEntry
              textContentType="password" // iOS
              onSubmitEditing={() => refUsernameInput.current.focus()}
              onChangeText={setPassword}
              value={password}
              ref={refPasswordInput}
              style={{
                borderColor: '#000',
                borderWidth: 0.5,
                height: 45,
                paddingHorizontal: 20,
              }}
            />
        </View>
      </TouchableWithoutFeedback>
    </KeyboardAvoidingView>
  );
}

Here you can see the screen recording taken from an iPhone 11 Pro with iOS 14.6: (Note that secureTextEntry was temporarily disabled while screen recording, the problem is still present)

https://user-images.githubusercontent.com/64789082/121977389-e7363080-cd53-11eb-9a4c-e26fc364bd03.MP4

samuelbeaulieu commented 3 years ago

UPDATE: I made another example: simple-keyboard-autofill-issue-2

In this example, I have 4 TextInput with only blurOnSubmit, onSubmitEditing and ref. Only the last TextInput has secureTextEntry.

The first two TextInput are focusing each other on "return" press, no problem with those two, like it should be.

The last two TextInput are also focusing each other on "return" press, the bug/jump/glitch happens with both TextInput. If I remove the secureTextEntry prop from the last input(line 60), the bug/jump/glitch no longer occurs.

I really think the bug is caused by the Autofill Passwords feature on iOS.

Here's a screen recording of the new snack example, taken from an iPhone 11 Pro with iOS 14.6:

https://user-images.githubusercontent.com/64789082/122135053-cdf1ba80-ce0d-11eb-895a-cfd17f8919bb.MP4

samuelbeaulieu commented 3 years ago

UPDATE 2: I made yet another example: keyboardavoidingview

This time I used the example provided right in the KeyboardAvoidingView documentation on reactnative.dev, added the same props from my other examples; focus on onSubmitEditing, blurOnSubmit and ref.

Once again, If I remove the secureTextEntry prop from the last input(line 27), the bug/jump/glitch no longer occurs.

Here's a screen recording of the this snack example, taken from an iPhone 11 Pro with iOS 14.6:

https://user-images.githubusercontent.com/64789082/122144597-d0f6a600-ce21-11eb-9257-5ac1f9d95235.MP4

vvdodiya commented 3 years ago

@samuelbeaulieu did you find any solution? I am facing the same issue.

samuelbeaulieu commented 3 years ago

@vvdodiya I did not. I'll keep you/this issue updated if I do. If you find something, please keep me updated too.

Cmonorail commented 3 years ago

same issue...

mykytabatrak commented 2 years ago

Maybe it'll help somebody, but I decided to use IQKeyboardManager for iOS, and this for android, to be exact I've set android:windowSoftInputMode="stateVisible". Works like a charm now.

CDBridger commented 2 years ago

this seems like a regression right? I'm getting the same issue but I used to use this pattern in older versions of react native without this occurring.

TereshchenkoVlad commented 11 months ago

I have the same problem.

ashirkhan94 commented 6 months ago

Hi This TextInput prop fix my problem textContentType='oneTimeCode' pls refer below link https://github.com/facebook/react-native/issues/39411#issuecomment-1746484565

jurmadani commented 6 months ago

Hi This TextInput prop fix my problem textContentType='oneTimeCode' pls refer below link https://github.com/facebook/react-native/issues/39411#issuecomment-1746484565

I can confirm this solved the issue.

Pixelatex commented 5 months ago

I worked around this in a creative way...


export function Password({ onChange, value, placeholder }: Props) {
  const [hideValue, setHideValue] = useState(true)
  const [pwd, setPwd] = useState<string>(value || '')

  const censored = pwd.replace(/./g, '*')
  return (
    <TextInput
      right={
        <TextInput.Icon onPress={() => setHideValue(!hideValue)} icon="eye" />
      }
      value={hideValue ? censored : pwd}
      placeholder={placeholder}
      onChangeText={text => {
        let newPwd = pwd
        if (text.length < pwd.length) {
          newPwd = pwd.substring(0, pwd.length - 1)
        } else {
          const newEntry = text.slice(-1)
          newPwd = pwd + newEntry
        }

        onChange(newPwd)
        setPwd(newPwd)
      }}
  )
}

It keeps the password in a state hook and censors it manually to ***. It might not look super clean but it's one way to solve it