facebook / react-native

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

Duplicated letters when autoCapitalize="characters" on android #11068

Closed joncrocks closed 1 year ago

joncrocks commented 7 years ago

Description

When trying to implement an upper case only input, adding autoCapitalize="characters" seems to work on Android by setting the keyboard to be upper case by default. One can hit the shift key and then type a lower case letter.

To ensure that we only let the user enter (and see) upper case letters, I thought that I might be able to handle it by ensuring that when we update the react state of the component we capture the text in upper case.

By using a toUpperCase on top of what is a pretty standard state update cycle (i.e. a very similar update method ala the examples at https://facebook.github.io/react-native/releases/next/docs/textinput.html ), this saves the input into the state, uppercased, ready for the next render cycle. (I'm not concerned about the dangers of toUpperCase at this point.)

Unfortunately, the behaviour is a bit strange when you start typing both upper and lowercase letters, where you start getting repeated letters, e.g. if I type AbC, I will end up with ABABC, AbcdE I get ABABCDABABCDE.

Reproduction

I created an example app here: https://rnplay.org/apps/t-gBOA Note that the behaviour seems fine on the iOS simulator, but is 'wrong' on the android simulator.

or see the component below:

import React, {Component} from "react";
import {View, TextInput} from "react-native";

export class UpperCaseTextEntry extends Component {

    constructor() {
        super();
        this.state = {
            text: ""
        }
    }

    upperCaseIt(text) {
        var textUpperCase = text.toUpperCase();

        this.setState({text: textUpperCase});
    }

    render() {
        var text = this.state.text;
        return (
            <View>
                <TextInput value={text} autoCapitalize="characters"
                           onChangeText={(text) => {this.upperCaseIt(text)}}
                />
            </View>
        )

    }
}

Solution

I suspect that there's something going awry with the syncing of state between the react state and the state of the underlying components, quite possibly some case-ignoring checks are being in some places, but not others.

Additional Information

I've also noted during that it's more than possible to fire multiple onChangeTexts before a render is triggered, which could lead to unexpected app behaviour. This could be expected unexpected behaviour though :-)

ecrofeg commented 5 years ago

I guess this problem has nothing to do with react-native itself...

Happens to me on all Samsung devices (browser, with "Predictive Text" enabled) when I'm trying to make toUpperCase and then put mutated string in the value of the text input.

shubhamverma27 commented 5 years ago

Appears to be working on a Samsung S7 (React Native 0.57.4) with (suggested by nathvarun):

<TextInput> ... secureTextEntry={true} keyboardType="visible-password" </TextInput>

This fix works but sometimes messes up my Gboard and slows it down and have to restart the phone afterwards/ Restart Gboard by force close. +Keyboardtype visible-password is not compatible with iOS.

nhunzaker commented 5 years ago

I started looking into this, and it looks like this isn't fully related to autocapitalize (though it might have some other impact).

Even if I remove autoCapitalize, the problem persists. For example with this input:

export class UpperCaseTextEntry extends Component {
  state = {
    text: "Test"
  };

  upperCaseIt = text => {
    this.setState({ text: text.toUpperCase() });
  };

  render() {
    const { text } = this.state;

    return (
      <TextInput
        value={text}
        onChangeText={this.upperCaseIt}
      />
    );
  }
}

Typing more than twice produces a duplicate.

jainkunal40 commented 5 years ago

Did you succeed with that solution?

mauricioscotton commented 5 years ago

Did you succeed with that solution?

Have a look on my previous comment (https://github.com/facebook/react-native/issues/11068#issuecomment-462778742), if you keen to edit RN java files you can just swap that line as I've described and it will work as a charm! :D

jainkunal40 commented 5 years ago

Did you succeed with that solution?

Have a look on my previous comment (#11068 (comment)), if you keen to edit RN java files you can just swap that line as I've described and it will work as a charm! :D

I am not able to get proper solution form your comment, can you please add more specific changes step by step to be done, Thanks

mauricioscotton commented 5 years ago

Did you succeed with that solution?

Have a look on my previous comment (#11068 (comment)), if you keen to edit RN java files you can just swap that line as I've described and it will work as a charm! :D

I am not able to get proper solution form your comment, can you please add more specific changes step by step to be done, Thanks

Sure, it is really simple! https://github.com/facebook/react-native/issues/11068#issuecomment-462798961

this is the resolution. You just have to change 1 file in order to fix this issue... image

B4ckes commented 4 years ago

Does anyone know any other way to solve this bug? I've tried all the resolutions mentioned here, but none of them worked for me. I'm trying to set my characters to upper case with textTransform: 'uppercase.

RN Version: 0.60.4

mauricioscotton commented 4 years ago

Does anyone know any other way to solve this bug? I've tried all the resolutions mentioned here, but none of them worked for me. I'm trying to set my characters to upper case with textTransform: 'uppercase.

RN Version: 0.60.4

Mate! I'm also from RS! =D The resolution for android comes by touching RN java files. Give me a shout if you need some help with it.

B4ckes commented 4 years ago

@mauricioscotton Cool dude 😄 , but i tried your resolution and it doesn't work, first we tried to set uppercase using the autoCapitalize prop, but it just work for iOS, when i tried to uppercase this bug started happening, i tried to set upper case with mask too.

mauricioscotton commented 4 years ago

@mauricioscotton Cool dude 😄 , but i tried your resolution and it doesn't work, first we tried to set uppercase using the autoCapitalize prop, but it just work for iOS, when i tried to uppercase this bug started happening, i tried to set upper case with mask too.

I believe that the code you're running is not the same.. Are you able to pop android studio and inject that code on run-time? if you are able to insert that code, it will work (believe me, we had this issue on a production app and I just gave up doing it because I didn't wanted to lock it to a custom RN version.) but if you really need it, you can test it first by debugging those lines as I've mentioned and then if it works you can apply a patch and recompile libs if necessary. But first try to debug and inject those lines (If you havent).

Cheers

B4ckes commented 4 years ago

I don't really need to do that, i just gave up from this issue to focus in the others that are more important for the project, but it was an annoying bug where i was losing too much time, i just let the characters to be lowercased 😊. Thanks for the help.

Soto92 commented 4 years ago

O problema não é de toUpperCase. Qualquer função que obtenha as entradas do input por parâmetro irá duplicar caracteres. A solução é resetar o input. Existe uma lib para isso: react-native-text-input-reset

iagormoraes commented 4 years ago

O problema não é de toUpperCase. Qualquer função que obtenha as entradas do input por parâmetro irá duplicar caracteres. A solução é resetar o input. Existe uma lib para isso: react-native-text-input-reset

this gets the input too slow to type, is a workaround temporarily only unfortunately.

rafalzawadzki commented 4 years ago

Truly shameful how long basic text input issues plague our Android product :) Coming back to this thread since over a year to see nothing has really improved.

The only workaround we found to make the experience bearable was to hide the TextInput behind a normal Text component that does all text rendering. Lost selection/copy/paste etc features, had to create a custom caret, but still works better than the TextInput...

safaiyeh commented 4 years ago

@mauricioscotton Care to make a PR to react-native to close out this issue? Has been open for three years now.

If you cannot find the time, I can debug this and submit a PR so we can close this out and release this fix.

safaiyeh commented 4 years ago

Also, to reproduce it, just drop a TextInput like this:

<TextInput ref={(el) => this.input = el} onChangeText={(val) => { this.input.setNativeProps({text: val.toUpperCase()}); }}/>

and on ReactEditText.java@381, change:

getText().replace(0, length(), spannableStringBuilder);

for:

setText(spannableStringBuilder);

What I tried:

getText().replace(0, length(), spannableStringBuilder, 0, spannableStringBuilder.length());

Same issue persists

setText(spannableStringBuilder);

Sets the cursor to the beginning of every change results in backwards typing.

setText(spannableStringBuilder);
setSelection(spannableStringBuilder.length());

This works however the performance is terrible, some keyboard presses don't get registered. Append should take out the need to set the cursor position.

append(spannableStringBuilder);

The same issue persists

getText().clear();
append(spannableStringBuilder);

This seems to be the only fix; however, performance gets bad after many characters.

Making a PR with this change 🎉 Thank you @mauricioscotton for the initial debugging and narrowing down to that line.

RogerParis commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

rafalzawadzki commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

But also, unfortunately, takes control over crucial keyboard styling away from you.

victorwpbastos commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

Works for me! Thanks, man!

offler9 commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

Works for me! Thanks, man!

works for me too!

iagormoraes commented 4 years ago

This problem persists on RN 62.2

romreed commented 4 years ago

@iagormoraes I got the same issue

timhysniu commented 4 years ago

Does anyone know any other way to solve this bug? I've tried all the resolutions mentioned here, but none of them worked for me. I'm trying to set my characters to upper case with textTransform: 'uppercase.

RN Version: 0.60.4

This produces the same issue.

fabOnReact commented 4 years ago

https://github.com/safaiyeh/react-native/pull/2 and https://github.com/facebook/react-native/pull/27757#discussion_r434400963 fixes issue with pr https://github.com/facebook/react-native/pull/27757

antonello-alfatauristudio commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

this worked for me and saved my life

PedroBern commented 4 years ago

This issue still exists in RN 0.63, but not only with autoCapitalize="characters". The only solution is to not use controlled input, save the value to the state with onChangeText, but don't control it.

levani commented 4 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

This does not work for me...

matthewbal commented 3 years ago
secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

This kinda makes the trick for both platforms

This does not work for me...

This fix worked for us in RN 0.61, but not in RN 0.63 it seems. We got the same duplicating text again.

Removing the autoCapitalize entirely and using toLowerCase(); in an onchange function, and setting the state there did the job for us.

danish-001 commented 3 years ago

Hey @john1jan, I am having the same exact problem. Let me know if there is any fix for this as I am unable to solve this.

Muhammad-Saleet commented 3 years ago

I encounter this issue only when the suggestions bar is activated. For example, when you add too many random characters and thus get no suggestions, the duplication behavior stops. You can further confirm this by disabling suggestion strip in Settings > system > languages & input > on-screen keyboard > Gboard > text correction > show suggestion strip

This worked for me:

secureTextEntry={Platform.OS === 'ios' ? false : true} keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}

It works because, for a password input, the suggestion strip is disabled.

fabOnReact commented 3 years ago

Hello ReactNative Developers! :smiley:

I just prepared Pull Request https://github.com/facebook/react-native/pull/29070 that seems to solve this issue

PREVIEWS OF THE BUGFIX

| **BEFORE** | **AFTER** | |:-------------------------:|:-------------------------:| | | |

You can contact me by email at fabrizio.developer@gmail.com

Please head over to the Pull Request https://github.com/facebook/react-native/pull/29070 and thumbs up if you like it. If you want to get this fix you can follow the instructions for building ReactAndroid or checkout my video introduction on forking react-native (patch-package does not work).

If you don't like the pr please feel free to leave a code review or comment, I'll be happy to add improvements and changes.

Thanks a lot :pray: :peace_symbol: :beach_umbrella:

fabOnReact commented 3 years ago

Please, is it possible to merge this PR in the next version ?

To answer the question of when the pr https://github.com/facebook/react-native/pull/29070 which fixes this issue will be released in the next version of React Native, I suggest you to follow the discussions at https://github.com/react-native-community/releases/issues by subscribing to those threads. The process of releasing a new version with a bug fix is the following:

safaiyeh commented 3 years ago

PR for this: https://github.com/facebook/react-native/pull/29070

JoaoPedroli commented 3 years ago

This partially solves the problem

autoCapitalize="characters"

The downside is that this only capitalizes the keyboard letters but the user may as well make them all lowercase

SrBrahma commented 2 years ago

Where is RN team?

NorthBlue333 commented 2 years ago

Hello, I ran into this issue while testing my app. I think the replace function in some cases also triggers this bug (using letters, i.e. replace('a', 'b')). Is there any update about this ?

MarcoScabbiolo commented 2 years ago

How is this still a thing 5 years later?

NorthBlue333 commented 2 years ago

Hi @MarcoScabbiolo I think it might be because the team relies on the community to resolve these bugs. See : https://github.com/facebook/react-native/issues/29815

Yup still ongoing. Most issues won't get personal attention. We rely on the community to solve specific bugs like this with a PR

omishe commented 2 years ago

I have same problem, nobody belives me this so ancient,!!

lucassouza16 commented 2 years ago

This worked for me:

import React from "react";
import { useRef, useState } from "react";
import { StyleSheet, TextInput, View, Platform } from "react-native";

const Index = () => {

    const [value, setValue] = useState();
    const textInputRef = useRef();

    return (
        <View style={styles.container}>
           <TextInput
               ref={textInputRef}
               value={Platform.OS === 'ios' ? undefined : ''}
               style={styles.input}
               onChangeText={val => {
                   const text = val.toUpperCase();
                   textInputRef.current && textInputRef.current.setNativeProps({ text });
                   setValue(text);
               }}
               placeholder={'Value here'}
            />
        </View>
    );
}

const styles = StyleSheet.create({
    input: {
        borderWidth: 1,
        height: 40
    },
    container: {
        flex: 1,
        padding: 10,
        justifyContent: 'center'
    }
});

export default Index;
fabOnReact commented 2 years ago
video testing the solution above

https://github.com/facebook/react-native/issues/11068#issuecomment-1072564012

setNativeProp is not available in Fabric

https://github.com/facebook/react/issues/18499#issuecomment-650686427

I don't have the time to verify, but I believe setNativeProp({ text }) skips the line of code causing this issue.

https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L6081-L6109 "AOSP method TextView setText" https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L2336-L2338 "AOSP method TextView getText"

I'm testing now with setTextKeepState to avoid changing the cursor position.

Remtosan commented 2 years ago

Hello, hope this could help anyone

I use react-native-elements Input component so basically i did this

I created a hook, then in my Input component I use onChangeText function, so I put this to clear the text previosly saved, and now set the new text again in uppercase.

const [textInput, setTextInput] = useState('');

<Input value={textInput} onChangeText={async text => { await setTextInput(''); await setTextInput(text.toUpperCase()); }} />

yylingsj commented 2 years ago

Hello, hope this could help anyone

I use react-native-elements Input component so basically i did this

I created a hook, then in my Input component I use onChangeText function, so I put this to clear the text previosly saved, and now set the new text again in uppercase.

const [textInput, setTextInput] = useState('');

<Input value={textInput} onChangeText={async text => { await setTextInput(''); await setTextInput(text.toUpperCase()); }} />

It works correctly.

lucassouza16 commented 2 years ago

Hello, hope this could help anyone

I use react-native-elements Input component so basically i did this

I created a hook, then in my Input component I use onChangeText function, so I put this to clear the text previosly saved, and now set the new text again in uppercase.

const [textInput, setTextInput] = useState('');

<Input value={textInput} onChangeText={async text => { await setTextInput(''); await setTextInput(text.toUpperCase()); }} />

This solution worked very well, I encapsulated it in a custom InputText component, in the tests I did it worked very well, both for capitalization of letters and for masking numerical values, a context where I had a similar problem:

import { forwardRef, useEffect, useState, useRef, useLayoutEffect } from "react";
import { TextInput as DefaultTextInput, TextInputProps } from "react-native";

function useAsyncState<T = undefined>(defaultValue: T | (() => T)): [T, ((value: T) => Promise<void>)] {

    const [value, setValue] = useState(defaultValue);
    const resolvesRef = useRef<(() => void)[]>([]).current;

    const setValueAsync = (newValue: T): Promise<void> => {
        return new Promise(resolve => {

            if (value === newValue) return resolve();

            resolvesRef.push(resolve);
            setValue(newValue);
        });
    };

    useEffect(() => {
        resolvesRef.forEach(resolve => resolve());
        resolvesRef.splice(0, resolvesRef.length);
    }, [value]);

    return [value, setValueAsync];
};

const TextInput = forwardRef<
    DefaultTextInput,
    TextInputProps
>(function ({ value, ...rest }: TextInputProps, ref: any) {

    const [stateValue, setStateValue] = useAsyncState(value);

    useLayoutEffect(() => {

        (async () => {
            await setStateValue('');
            await setStateValue(value);
        })();

    }, [value]);

    return (
        <DefaultTextInput
            {...rest}
            ref={ref}
            value={stateValue}
        />
    );
});

export default TextInput;

Then just import it and use it as the default TextInput of react:

<TextInput
    value={value}
    onChangeText={value => {
        setValue(value.toUpperCase())
   }}
   placeholder={'Value here'}
   style={styles.input}
 />
hendpraz commented 2 years ago

const [textInput, setTextInput] = useState('');

<Input value={textInput} onChangeText={async text => { await setTextInput(''); await setTextInput(text.toUpperCase()); }} />

This solution worked well if Keyboard capital is on. If user deactivate capital on their keyboard, duplicate letters inputted.

This solution worked very well, I encapsulated it in a custom InputText component, in the tests I did it worked very well, both for capitalization of letters and for masking numerical values, a context where I had a similar problem:

import { forwardRef, useEffect, useState, useRef, useLayoutEffect } from "react"; import { TextInput as DefaultTextInput, TextInputProps } from "react-native";

function useAsyncState<T = undefined>(defaultValue: T | (() => T)): [T, ((value: T) => Promise)] {

This solution worked for me. But there is a drawback, typing becomes laggy. If I typed fast, some characters are not inputted

secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

Now this solution working perfectly for me... react-native version 0.67.2

Thanks!!

lucassouza16 commented 2 years ago

const [textInput, setTextInput] = useState(''); <Input value={textInput} onChangeText={async text => { await setTextInput(''); await setTextInput(text.toUpperCase()); }} />

This solution worked well if Keyboard capital is on. If user deactivate capital on their keyboard, duplicate letters inputted.

This solution worked very well, I encapsulated it in a custom InputText component, in the tests I did it worked very well, both for capitalization of letters and for masking numerical values, a context where I had a similar problem: import { forwardRef, useEffect, useState, useRef, useLayoutEffect } from "react"; import { TextInput as DefaultTextInput, TextInputProps } from "react-native"; function useAsyncState<T = undefined>(defaultValue: T | (() => T)): [T, ((value: T) => Promise)] {

This solution worked for me. But there is a drawback, typing becomes laggy. If I typed fast, some characters are not inputted

secureTextEntry={Platform.OS === 'ios' ? false : true}
keyboardType={Platform.OS === 'ios' ? null : 'visible-password'}
autoCapitalize="characters"

Now this solution working perfectly for me... react-native version 0.67.2

Thanks!!

This last solution really solves the problem with no side effects, but it only solves it in the context of capitalizing the text, and this problem of doubling characters is also a problem that occurs when any value change is made at typing time, either to mask numeric values or something similar, I think the only definitive solution will only occur when react developers pay attention to the problem, or using a standard parallel input lib from react, until then there are only these quick solutions, but such a simple problem is frustrating It's still there after so long.

w4t3r-45 commented 2 years ago

still happens guys in 2022

Planetfunky commented 1 year ago

This worked for me:

import React from "react";
import { useRef, useState } from "react";
import { StyleSheet, TextInput, View, Platform } from "react-native";

const Index = () => {

    const [value, setValue] = useState();
    const textInputRef = useRef();

    return (
        <View style={styles.container}>
           <TextInput
               ref={textInputRef}
               value={Platform.OS === 'ios' ? undefined : ''}
               style={styles.input}
               onChangeText={val => {
                   const text = val.toUpperCase();
                   textInputRef.current && textInputRef.current.setNativeProps({ text });
                   setValue(text);
               }}
               placeholder={'Value here'}
            />
        </View>
    );
}

const styles = StyleSheet.create({
    input: {
        borderWidth: 1,
        height: 40
    },
    container: {
        flex: 1,
        padding: 10,
        justifyContent: 'center'
    }
});

export default Index;

This works pretty well but for some reason the user is not able to earase the whole text by holding the delete button on a mobile keyboard. It's weird but it is 😓

cleytonchagasbr commented 1 year ago

Hello ?