Reworking keyboardShouldPersistTaps to have a middle ground
Summary: Right now, the ScrollView's keyboard hiding behavior is either all or nothing: Hide the keyboard on any tap, or do nothing ever. This PR introduces a third mode to keyboardShouldPersistTaps which is much closer to what I consider should be the default. In the new behavior, the tap responding is done in the bubbling phase (instead of the capture phase like =true). As a result, a child can handle the tap. If no child does, then the ScrollView will receive the tap and will hide the keyboard. As a result, changing TextInput focus works as a user expects, with a single tap and without keyboard hiding. But taping on Text or on the empty part of the ScrollView hides the keyboard and removes the focus. You can view the behavior in a monkey patched ScrollView demo on rnplay: https://rnplay.org/apps/E90UYw https://rnplay.org/apps/UGzhKA In order to have a uniform props set, i added 3 values to the keyboardShouldPersistTaps: 'never' and 'always' are the same as false and true. 'handled' is the new behavior. I don't Closes https://github.com/facebook/react-native/pull/10628 Differential Revision: D4294945 Pulled By: ericvicenti fbshipit-source-id: 1a753014156cac1a23fabfa8e1faa9a768868ef2
This commit is contained in:
parent
4d5ef76b1a
commit
d56530d7d5
|
@ -299,7 +299,7 @@ var SearchScreen = React.createClass({
|
||||||
onEndReached={this.onEndReached}
|
onEndReached={this.onEndReached}
|
||||||
automaticallyAdjustContentInsets={false}
|
automaticallyAdjustContentInsets={false}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardDismissMode="on-drag"
|
||||||
keyboardShouldPersistTaps={true}
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ class UIExplorerExampleList extends React.Component {
|
||||||
renderRow={this._renderExampleRow.bind(this)}
|
renderRow={this._renderExampleRow.bind(this)}
|
||||||
renderSectionHeader={this._renderSectionHeader}
|
renderSectionHeader={this._renderSectionHeader}
|
||||||
enableEmptySections={true}
|
enableEmptySections={true}
|
||||||
keyboardShouldPersistTaps={true}
|
keyboardShouldPersistTaps="handled"
|
||||||
automaticallyAdjustContentInsets={false}
|
automaticallyAdjustContentInsets={false}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardDismissMode="on-drag"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -35,13 +35,11 @@ var UIExplorerTitle = require('./UIExplorerTitle');
|
||||||
|
|
||||||
class UIExplorerPage extends React.Component {
|
class UIExplorerPage extends React.Component {
|
||||||
props: {
|
props: {
|
||||||
keyboardShouldPersistTaps?: boolean,
|
|
||||||
noScroll?: boolean,
|
noScroll?: boolean,
|
||||||
noSpacer?: boolean,
|
noSpacer?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
keyboardShouldPersistTaps: React.PropTypes.bool,
|
|
||||||
noScroll: React.PropTypes.bool,
|
noScroll: React.PropTypes.bool,
|
||||||
noSpacer: React.PropTypes.bool,
|
noSpacer: React.PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -55,7 +53,7 @@ class UIExplorerPage extends React.Component {
|
||||||
ContentWrapper = (ScrollView: ReactClass<any>);
|
ContentWrapper = (ScrollView: ReactClass<any>);
|
||||||
// $FlowFixMe found when converting React.createClass to ES6
|
// $FlowFixMe found when converting React.createClass to ES6
|
||||||
wrapperProps.automaticallyAdjustContentInsets = !this.props.title;
|
wrapperProps.automaticallyAdjustContentInsets = !this.props.title;
|
||||||
wrapperProps.keyboardShouldPersistTaps = true;
|
wrapperProps.keyboardShouldPersistTaps = 'handled';
|
||||||
wrapperProps.keyboardDismissMode = 'interactive';
|
wrapperProps.keyboardDismissMode = 'interactive';
|
||||||
}
|
}
|
||||||
var title = this.props.title ?
|
var title = this.props.title ?
|
||||||
|
|
|
@ -18,6 +18,7 @@ var ReactNative = require('ReactNative');
|
||||||
var Subscribable = require('Subscribable');
|
var Subscribable = require('Subscribable');
|
||||||
var TextInputState = require('TextInputState');
|
var TextInputState = require('TextInputState');
|
||||||
var UIManager = require('UIManager');
|
var UIManager = require('UIManager');
|
||||||
|
var warning = require('fbjs/lib/warning');
|
||||||
|
|
||||||
var { getInstanceFromNode } = require('ReactNativeComponentTree');
|
var { getInstanceFromNode } = require('ReactNativeComponentTree');
|
||||||
var { ScrollViewManager } = require('NativeModules');
|
var { ScrollViewManager } = require('NativeModules');
|
||||||
|
@ -134,7 +135,7 @@ var ScrollResponderMixin = {
|
||||||
// - Determine if the scroll view has been scrolled and therefore should
|
// - Determine if the scroll view has been scrolled and therefore should
|
||||||
// refuse to give up its responder lock.
|
// refuse to give up its responder lock.
|
||||||
// - Determine if releasing should dismiss the keyboard when we are in
|
// - Determine if releasing should dismiss the keyboard when we are in
|
||||||
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
|
// tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
|
||||||
observedScrollSinceBecomingResponder: false,
|
observedScrollSinceBecomingResponder: false,
|
||||||
becameResponderWhileAnimating: false,
|
becameResponderWhileAnimating: false,
|
||||||
};
|
};
|
||||||
|
@ -172,7 +173,14 @@ var ScrollResponderMixin = {
|
||||||
* true.
|
* true.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
scrollResponderHandleStartShouldSetResponder: function(): boolean {
|
scrollResponderHandleStartShouldSetResponder: function(e: Event): boolean {
|
||||||
|
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||||
|
|
||||||
|
if (this.props.keyboardShouldPersistTaps === 'handled' &&
|
||||||
|
currentlyFocusedTextInput != null &&
|
||||||
|
e.target !== currentlyFocusedTextInput) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -190,7 +198,10 @@ var ScrollResponderMixin = {
|
||||||
scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
|
scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
|
||||||
// First see if we want to eat taps while the keyboard is up
|
// First see if we want to eat taps while the keyboard is up
|
||||||
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||||
if (!this.props.keyboardShouldPersistTaps &&
|
var {keyboardShouldPersistTaps} = this.props;
|
||||||
|
var keyboardNeverPersistTaps = !keyboardShouldPersistTaps ||
|
||||||
|
keyboardShouldPersistTaps === 'never';
|
||||||
|
if (keyboardNeverPersistTaps &&
|
||||||
currentlyFocusedTextInput != null &&
|
currentlyFocusedTextInput != null &&
|
||||||
!isTagInstanceOfTextInput(e.target)) {
|
!isTagInstanceOfTextInput(e.target)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -250,7 +261,8 @@ var ScrollResponderMixin = {
|
||||||
// By default scroll views will unfocus a textField
|
// By default scroll views will unfocus a textField
|
||||||
// if another touch occurs outside of it
|
// if another touch occurs outside of it
|
||||||
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||||
if (!this.props.keyboardShouldPersistTaps &&
|
if (this.props.keyboardShouldPersistTaps !== true &&
|
||||||
|
this.props.keyboardShouldPersistTaps !== 'always' &&
|
||||||
currentlyFocusedTextInput != null &&
|
currentlyFocusedTextInput != null &&
|
||||||
e.target !== currentlyFocusedTextInput &&
|
e.target !== currentlyFocusedTextInput &&
|
||||||
!this.state.observedScrollSinceBecomingResponder &&
|
!this.state.observedScrollSinceBecomingResponder &&
|
||||||
|
@ -481,6 +493,13 @@ var ScrollResponderMixin = {
|
||||||
* The `keyboardWillShow` is called before input focus.
|
* The `keyboardWillShow` is called before input focus.
|
||||||
*/
|
*/
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
var {keyboardShouldPersistTaps} = this.props;
|
||||||
|
warning(
|
||||||
|
typeof keyboardShouldPersistTaps !== 'boolean',
|
||||||
|
`'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. `
|
||||||
|
+ `Use 'keyboardShouldPersistTaps="${keyboardShouldPersistTaps ? "always" : "never"}"' instead`
|
||||||
|
);
|
||||||
|
|
||||||
this.keyboardWillOpenTo = null;
|
this.keyboardWillOpenTo = null;
|
||||||
this.additionalScrollOffset = 0;
|
this.additionalScrollOffset = 0;
|
||||||
this.addListenerOn(Keyboard, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
|
this.addListenerOn(Keyboard, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
|
||||||
|
|
|
@ -192,12 +192,18 @@ const ScrollView = React.createClass({
|
||||||
'on-drag',
|
'on-drag',
|
||||||
]),
|
]),
|
||||||
/**
|
/**
|
||||||
* When false, tapping outside of the focused text input when the keyboard
|
* Determines when the keyboard should stay visible after a tap.
|
||||||
* is up dismisses the keyboard. When true, the keyboard will not dismiss
|
*
|
||||||
* automatically, and the scroll view will not catch taps, but children of
|
* - 'never' (the default), tapping outside of the focused text input when the keyboard
|
||||||
* the scroll view can catch taps. The default value is false.
|
* is up dismisses the keyboard. When this happens, children won't receive the tap.
|
||||||
|
* - 'always', the keyboard will not dismiss automatically, and the scroll view will not
|
||||||
|
* catch taps, but children of the scroll view can catch taps.
|
||||||
|
* - 'handled', the keyboard will not dismiss automatically when the tap was handled by
|
||||||
|
* a children, (or captured by an ancestor).
|
||||||
|
* - false, deprecated, use 'never' instead
|
||||||
|
* - true, deprecated, use 'always' instead
|
||||||
*/
|
*/
|
||||||
keyboardShouldPersistTaps: PropTypes.bool,
|
keyboardShouldPersistTaps: PropTypes.oneOf(['always', 'never', 'handled', false, true]),
|
||||||
/**
|
/**
|
||||||
* The maximum allowed zoom scale. The default value is 1.0.
|
* The maximum allowed zoom scale. The default value is 1.0.
|
||||||
* @platform ios
|
* @platform ios
|
||||||
|
|
Loading…
Reference in New Issue