19

I'm trying to create a passcode protected screen. The screen will uses 4 numeric input as the passcode.

The way I'm doing this is create a TextInput Component and call it 4 times in my main screen.

The problem I'm having is the TextInputs will not focus on the next one as I type the value of the previous TextInput.

I'm using refs for all PasscodeTextInput component (I've been informed that it is a legacy method but I do not know any other way, alas).

Tried this method(without creating my own component), no luck too. METHOD

Image

index.ios.js

import React, { Component } from 'react';
import { AppRegistry, TextInput, View, Text } from 'react-native';
import { PasscodeTextInput } from './common';

export default class ProgressBar extends Component {
  render() {
    const { centerEverything, container, passcodeContainer,  textInputStyle} = styles;
    return (
      <View style={[centerEverything, container]}>
        <View style={[passcodeContainer]}>
          <PasscodeTextInput
            autoFocus={true}
            ref="passcode1"
            onSubmitEditing={(event) => { this.refs.passcode2.focus() }} />
          <PasscodeTextInput
            ref="passcode2"
            onSubmitEditing={(event) => { this.refs.passcode3.focus() }} />
          <PasscodeTextInput
            ref="passcode3"
            onSubmitEditing={(event) => { this.refs.passcode4.focus() }}/>
          <PasscodeTextInput
            ref="passcode4" />
        </View>
      </View>
    );
  }
}

const styles = {
  centerEverything: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  container: {
    flex: 1,
    backgroundColor: '#E7DDD3',
  },
  passcodeContainer: {
    flexDirection: 'row',
  },
}

AppRegistry.registerComponent('ProgressBar', () => ProgressBar);

PasscodeTextInput.js

import React from 'react';
import {
  View,
  Text,
  TextInput,
  Dimensions
} from 'react-native';

const deviceWidth = require('Dimensions').get('window').width;
const deviceHeight = require('Dimensions').get('window').height;

const PasscodeTextInput = ({ ref, autoFocus, onSubmitEditing, onChangeText, value}) => {

  const { inputStyle, underlineStyle } = styles;

  return(
    <View>
      <TextInput
        ref={ref}
        autoFocus={autoFocus}
        onSubmitEditing={onSubmitEditing}
        style={[inputStyle]}
        maxLength={1}
        keyboardType="numeric"
        placeholderTextColor="#212121"
        secureTextEntry={true}
        onChangeText={onChangeText}
        value={value}
      />
      <View style={underlineStyle} />
    </View>
  );
}

const styles = {
  inputStyle: {
    height: 80,
    width: 60,
    fontSize: 50,
    color: '#212121',
    fontSize: 40,
    padding: 18,
    margin: 10,
    marginBottom: 0
  },
  underlineStyle: {
    width: 60,
    height: 4,
    backgroundColor: '#202020',
    marginLeft: 10
  }
}

export { PasscodeTextInput };

Update 1

index.ios.js

import React, { Component } from 'react';
import { AppRegistry, TextInput, View, Text } from 'react-native';
import { PasscodeTextInput } from './common';

export default class ProgressBar extends Component {

  constructor() {
    super()
    this.state = {
      autoFocus1: true,
      autoFocus2: false,
      autoFocus3: false,
      autoFocus4: false,
    }
  }

  onTextChanged(t) { //callback for immediate state change
    if (t == 2) { this.setState({ autoFocus1: false, autoFocus2: true }, () => { console.log(this.state) }) }
    if (t == 3) { this.setState({ autoFocus2: false, autoFocus3: true }, () => { console.log(this.state) }) }
    if (t == 4) { this.setState({ autoFocus3: false, autoFocus4: true }, () => { console.log(this.state) }) }
  }

  render() {
    const { centerEverything, container, passcodeContainer, testShit, textInputStyle } = styles;
    return (
      <View style={[centerEverything, container]}>
        <View style={[passcodeContainer]}>
          <PasscodeTextInput
            autoFocus={this.state.autoFocus1}
            onChangeText={() => this.onTextChanged(2)} />
          <PasscodeTextInput
            autoFocus={this.state.autoFocus2}
            onChangeText={() => this.onTextChanged(3)} />
          <PasscodeTextInput
            autoFocus={this.state.autoFocus3}
            onChangeText={() => this.onTextChanged(4)} />
          <PasscodeTextInput
            autoFocus={this.state.autoFocus4} />
        </View>
      </View>
    );
  }
}

const styles = {
  centerEverything: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  container: {
    flex: 1,
    backgroundColor: '#E7DDD3',
  },
  passcodeContainer: {
    flexDirection: 'row',
  },
}

AppRegistry.registerComponent('ProgressBar', () => ProgressBar);
J.Doe
  • 1,097
  • 1
  • 16
  • 34
  • http://stackoverflow.com/questions/32748718/react-native-how-to-select-the-next-textinput-after-pressing-the-next-keyboar Here is the info. Hope this helps. – Ujjwal Nepal Feb 21 '17 at 15:23
  • @UjjwalNepal The focus() method has deprecated..., and the answer from mitch is not usable after 0.40 http://stackoverflow.com/a/41201939/5809351 – J.Doe Feb 21 '17 at 15:57
  • I suppose you can avoid having state in the parent at all, just do a focus of a 1st input in `componentDidMount` and your `onTextChanged` method can look something like this `if t == 1 or 2 or 3 then focus the t+1'th input` – Igorsvee Feb 21 '17 at 17:12
  • @Igorsvee how to do a focus on componentDidMount? Managing state in componentDidMount is discouraging, mind to share some code? – J.Doe Feb 21 '17 at 17:21
  • 1
    Sure, since the `componentDidMount` is invoked after the `render` method, it means that the elements have already been rendered and we can now access them via `refs`: ```this.refs.passcode1.focus()``` – Igorsvee Feb 21 '17 at 17:29
  • add this autoFocus={true} – Omar bakhsh Dec 22 '20 at 03:41

8 Answers8

21

There is a defaultProp for TextInput where one can focus after component mounted.

autoFocus

If true, focuses the input on componentDidMount, the default value is false. for more information please read the related Docs.

UPDATE

After componentDidUpdate it won't work properly. In that case, one can use ref to focus programmatically.

Community
  • 1
  • 1
Mukundhan
  • 3,284
  • 23
  • 36
16

You cannot forward the ref to <TextInput> using that way because ref is one of the special props. Thus, calling this.refs.passcode2 will return you <PasscodeTextInput> instead.

Try change to the following to get the ref from <TextInput>.

PasscodeTextInput.js

const PasscodeTextInput = ({ inputRef, ... }) => {

  ...

  return (
    <View>
      <TextInput
        ref={(r) => { inputRef && inputRef(r) }}
        ...
      />
    </View>
    ...
  );
}

Then, assign the inputRef from <PasscodeTextInput> to a variable and use focus() to switch focus (it is not deprecated as of RN 0.41.2).

index.ios.js

return (
  <PasscodeTextInput
    autoFocus={true}
    onChangeText={(event) => { event && this.passcode2.focus() }} />

  <PasscodeTextInput
    inputRef={(r) => { this.passcode2 = r }}
    onChangeText={(event) => { event && this.passcode3.focus() }} />

  <PasscodeTextInput
    inputRef={(r) => { this.passcode3 = r }}
    onChangeText={(event) => { event && this.passcode4.focus() }} />

  <PasscodeTextInput
    inputRef={(r) => { this.passcode4 = r }} />
);

P.S: event && this.passcode2.focus() prevents focus is switched when trying to clear the old passcode and enter a new one.

max23_
  • 6,531
  • 4
  • 22
  • 36
  • @J.doe : Thanks for the code. Did you remove on delete key press? – djk Oct 25 '17 at 20:21
  • This saves my life and it's simpler than those techniques such as forwardRef. But I still hope that React Native could handle the focus movement natively in the future. – Jonathan Sep 10 '20 at 01:07
7

we handled this style of screen with a different approach.

Rather than manage 4 individual TextInputs and handle the navigation of focus across each one (and then back again when the user deletes a character), we have a single TextInput on screen but is invisible (ie. 0px x 0px) wide which has the focus, maxLength and keyboard configuration, etc.

This TextInput takes input from the user but can't actually been seen, as each character is typed in we render the entered text as a series simple View/Text elements, styled much similar to your screen above.

This approach worked well for us with no need to manage what the 'next' or 'previous' TextInput to focus next to.

crafterm
  • 1,831
  • 19
  • 17
2

You can use focus method onChangeText as Jason stated, in addition to that adding maxLength={1} can make you jump to the next input immediately without checking what's added. (just noticed its deprecated, but still this is how I solved my problem, and should do fine until v0.36, and this link explains how you should update the deprecated function).

  <TextInput
   ref="first"
   style={styles.inputMini}
   maxLength={1}
   keyboardType="numeric"
   returnKeyType='next'
   blurOnSubmit={false}
   placeholderTextColor="gray"
   onChangeText={(val) => {
      this.refs['second'].focus()
   }}
  />
  <TextInput
   ref="second"
   style={styles.inputMini}
   maxLength={1}
   keyboardType="numeric"
   returnKeyType='next'
   blurOnSubmit={false}
   placeholderTextColor="gray"
   onChangeText={(val) => {
     this.refs['third'].focus()
   }}
  />
  ...

Please notice that my use of refs are deprecated too, but I've just copied the code since I can guarantee you that was working back then (hopefully works now too).

Finally, the main issue with this type of implementation is, once you try to remove a number with backspace your focus will jump to next one, causing serious UX issues. However, you can listen for backspace key entry and perform something different instead of focusing to next input. So I'll leave a link here for you to further investigate if you choose to use this type of implementation.

Hacky Solution to Previously Described Issue: If you check what's entered in onChangeText prop before doing anything, you can jump to next input if the value is a number, else (that's a backspace), jump back. (Just came up with this idea, I haven't tried it.)

Community
  • 1
  • 1
eden
  • 5,876
  • 2
  • 28
  • 43
1

I think the issue is that onSubmitEditing is when you hit the "return" or "enter" key on the regular keyboard... there is not one of those buttons on the keypad.

Assuming you want each input to only have one character, you could look at the onChangeText and then check if text has length 1 and call focus if the length is indeed 1.

Jason Gaare
  • 1,511
  • 10
  • 19
1
    <TextInput 
    ref={input => {
          this.nameOrId = input;
        }}
    />



    <TouchableOpacity
      onPress={()=>{
        this.nameOrId.focus()
      }}
     >
      <Text>Click</Text>
    </TouchableOpacity>
noe
  • 194
  • 1
  • 9
0

I solve with this code: const VerifyCode: React.FC = ({ pass, onFinish }) => {

      const inputsRef = useRef<Input[] | null[]>([]);
      const [active, setActive] = useState<number>(0);

      const onKeyPress = ({ nativeEvent }: 
      NativeSyntheticEvent<TextInputKeyPressEventData>) => {
       if (nativeEvent.key === "Backspace") {
        if (active !== 0) {
          inputsRef.current[active - 1]?.focus();
          return setActive(active - 1);
        }
       } else {
          inputsRef.current[active + 1]?.focus();
          return setActive(active + 1);
       }
      return null;
     };

   return (
    <View style={styles.container}>
      <StyledInput
        onKeyPress={onKeyPress}
        autoFocus={active === 0}
        ref={(r) => {
          inputsRef.current[0] = r;
        }}
      />
      <StyledInput
        onKeyPress={onKeyPress}
        autoFocus={active === 1}
        ref={(r) => {
          inputsRef.current[1] = r;
        }}
      />
      <StyledInput
        onKeyPress={onKeyPress}
        autoFocus={active === 2}
        ref={(r) => {
          inputsRef.current[2] = r;
        }}
      />
      <StyledInput
        onKeyPress={onKeyPress}
        autoFocus={active === 3}
        ref={(r) => {
          inputsRef.current[3] = r;
        }}
      />
    </View>
 );
};

export default VerifyCode;

I put one ref in all the inputs, and when the onKeyPress fire, the function verify if have to go back or go to next input

0

Solved it by removing autoFocus={true} and setting timeout. I have a popup as a functional component and using "current.focus()" with Refs like this:

   const Popup = ({   placeholder,   autoFocus,   showStatus, }) => {  const inputRef = useRef(null);   useEffect(() => {
     Platform.OS === 'ios'
        ? inputRef.current.focus()
        : setTimeout(() => inputRef.current.focus(), 40);   }, [showStatus]);   return (
    <View style={styles.inputContainer}>
      <TextInput
        style={styles.inputText}
        defaultValue={placeholder}
        ref={inputRef}
      />
    </View>  };
Deni Al Farizi
  • 1,547
  • 1
  • 8
  • 5