12

Imagine simple ScrollView with multiple TextInputs like

  <ScrollView style={styles.container}>
    <TextInput style={styles.input} />
    <TextInput style={styles.input} />
  </ScrollView>

When I enter first input, keyboard opens and I can type text. When I want to change to second input I need to twice tap - first type closes keyboard and only second tap opens the keyboard for second input.

One solution is to use keyboardShouldPersistTaps={true} - switching works fine however then keyboard is not closed at all and the keyboard can cover some of the later inputs (or buttons). I can also use keyboardDismissMode however that just close keyboard on drag.

My question is how to combine those two behaviour - into IMHO the best user experience - when I click another input, the focus is changed immediately without reopening keyboard and when I tap somewhere else the keyboard is closed?

I am using RN0.22 and sample application is available at https://rnplay.org/apps/kagpGw

UPDATE - This problem might have been solved in RN 0.40 - see https://github.com/facebook/react-native/commit/552c60192172f6ec503181c060c08bbc5cbcc5a4

sodik
  • 4,675
  • 2
  • 29
  • 47

7 Answers7

7

I solved my problem with this code

<ScrollView
   keyboardDismissMode='on-drag'
   keyboardShouldPersistTaps={true}
>
Gustavo Rozolin
  • 1,070
  • 2
  • 13
  • 21
3

This SO answer isn't exactly what you're asking for, but will auto-slide the window out from behind the keyboard when a TextInput has focus; resolving your keyboard can cover some of the later inputs (or buttons) issue.

Community
  • 1
  • 1
Josh Buchea
  • 1,366
  • 14
  • 14
  • 1
    thank you. that answer help if you click input and it is then covered by keyboard once it is open. However won't help you if second input (or button) is already covered - since this trick will just scroll first tapped input into "view" but there are no guarantees about other components. – sodik Apr 08 '16 at 08:25
3

At the end I found solution that is not optimal (from coding perspective) but works from user perspective. I made small component that can be used instead of ScrollView:

export class InputScrollView extends React.Component {

  constructor(props, ctx) {
      super(props, ctx);
      this.handleCapture = this.handleCapture.bind(this);
  }

  render() {
    return (
      <ScrollView keyboardShouldPersistTaps={true} keyboardDismissMode='on-drag'>
        <View onStartShouldSetResponderCapture={this.handleCapture}>
          {this.props.children}
        </View>
      </ScrollView>
    );
  }

  handleCapture(e) {
    const focusField = TextInputState.currentlyFocusedField();
    const target = e.nativeEvent.target;
    if (focusField != null && target != focusField){
      const inputs = this.props.inputs;
      if (inputs && inputs.indexOf(target) === -1) {
        dismissKeyboard();
      }
    }
  }
}

InputScrollView.propTypes = {
  inputs : React.PropTypes.array,
}

The only drawback is that I need to collect node handles (as returned by React.findNodeHandle) of all text inputs "manually" and pass it to the component as an array.

sodik
  • 4,675
  • 2
  • 29
  • 47
3

By looking at https://facebook.github.io/react-native/docs/scrollview.html#keyboarddismissmode I find out a method that can do these:

  1. After doing input in TextInput1, if you click Input2, the keyboard persists.
  2. If you click somewhere empty, the keyboard will automatically disappear.

The code is as follows:

<ScrollView
  keyboardDismissMode="none"
  keyboardShouldPersistTaps="handled"
>
  {content}
</ScrollView>
ch271828n
  • 15,854
  • 5
  • 53
  • 88
2

In the new react-native versions you can persist the on scrollview click event like below:

<ScrollView
      keyboardShouldPersistTaps='always'
      keyboardDismissMode={'interactive'}></ScrollView>

The main trick is keyboardShouldPersistTaps='always'

Julfikar
  • 1,353
  • 2
  • 18
  • 35
0

I have updated the above answer because the keyboardShouldPersistTaps={true} is deprecated and now the keyboardShouldPersistTaps='always' is used

export class InputScrollView extends React.Component {

  constructor(props, ctx) {
      super(props, ctx);
      this.handleCapture = this.handleCapture.bind(this);
  }

  render() {
    return (
      <ScrollView keyboardShouldPersistTaps='always' keyboardDismissMode='on-drag'>
        <View onStartShouldSetResponderCapture={this.handleCapture}>
          {this.props.children}
        </View>
      </ScrollView>
    );
  }

  handleCapture(e) {
    const focusField = TextInputState.currentlyFocusedField();
    const target = e.nativeEvent.target;
    if (focusField != null && target != focusField){
      const inputs = this.props.inputs;
      if (inputs && inputs.indexOf(target) === -1) {
        dismissKeyboard();
      }
    }
  }
}

InputScrollView.propTypes = {
  inputs : React.PropTypes.array,
}
Mudassir Khan
  • 1,714
  • 1
  • 20
  • 25
0

If keyboardShouldPersistTaps="always" doesn't work for you, check your button and the action it performs.
Having something like the code below might lead to the two-tap issue irrespective of keyboardShouldPersistTaps value:

const submit = () => {
  const _valid = some_validator();
  setValid(_valid);

  if (valid) {
    // expected action
    // may not fire on first press due to the state update call above
    api.someAction();
  } else {
    // some error thing
  }
};

Instead you could use useEffect to separate the button-state call, and the resulting action so the click event has a single purpose

useEffect(()=>{
  if(valid) {
    // expected action
    api.someAction();
  }
}, [valid])
Jose Ananio
  • 666
  • 6
  • 5