Skip to content

Commit 276485b

Browse files
MaxLapMartin Konicek
authored and
Martin Konicek
committed
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 #10628 Differential Revision: D4294945 Pulled By: ericvicenti fbshipit-source-id: 1a753014156cac1a23fabfa8e1faa9a768868ef2
1 parent ee92b5c commit 276485b

File tree

5 files changed

+38
-15
lines changed

5 files changed

+38
-15
lines changed

Examples/Movies/SearchScreen.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ var SearchScreen = React.createClass({
299299
onEndReached={this.onEndReached}
300300
automaticallyAdjustContentInsets={false}
301301
keyboardDismissMode="on-drag"
302-
keyboardShouldPersistTaps={true}
302+
keyboardShouldPersistTaps="handled"
303303
showsVerticalScrollIndicator={false}
304304
/>;
305305

Examples/UIExplorer/js/UIExplorerExampleList.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class UIExplorerExampleList extends React.Component {
8282
renderRow={this._renderExampleRow.bind(this)}
8383
renderSectionHeader={this._renderSectionHeader}
8484
enableEmptySections={true}
85-
keyboardShouldPersistTaps={true}
85+
keyboardShouldPersistTaps="handled"
8686
automaticallyAdjustContentInsets={false}
8787
keyboardDismissMode="on-drag"
8888
/>

Examples/UIExplorer/js/UIExplorerPage.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@ var UIExplorerTitle = require('./UIExplorerTitle');
3535

3636
class UIExplorerPage extends React.Component {
3737
props: {
38-
keyboardShouldPersistTaps?: boolean,
3938
noScroll?: boolean,
4039
noSpacer?: boolean,
4140
};
4241

4342
static propTypes = {
44-
keyboardShouldPersistTaps: React.PropTypes.bool,
4543
noScroll: React.PropTypes.bool,
4644
noSpacer: React.PropTypes.bool,
4745
};
@@ -55,7 +53,7 @@ class UIExplorerPage extends React.Component {
5553
ContentWrapper = (ScrollView: ReactClass<any>);
5654
// $FlowFixMe found when converting React.createClass to ES6
5755
wrapperProps.automaticallyAdjustContentInsets = !this.props.title;
58-
wrapperProps.keyboardShouldPersistTaps = true;
56+
wrapperProps.keyboardShouldPersistTaps = 'handled';
5957
wrapperProps.keyboardDismissMode = 'interactive';
6058
}
6159
var title = this.props.title ?

Libraries/Components/ScrollResponder.js

+23-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var ReactNative = require('ReactNative');
1818
var Subscribable = require('Subscribable');
1919
var TextInputState = require('TextInputState');
2020
var UIManager = require('UIManager');
21+
var warning = require('fbjs/lib/warning');
2122

2223
var { getInstanceFromNode } = require('ReactNativeComponentTree');
2324
var { ScrollViewManager } = require('NativeModules');
@@ -134,7 +135,7 @@ var ScrollResponderMixin = {
134135
// - Determine if the scroll view has been scrolled and therefore should
135136
// refuse to give up its responder lock.
136137
// - Determine if releasing should dismiss the keyboard when we are in
137-
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
138+
// tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
138139
observedScrollSinceBecomingResponder: false,
139140
becameResponderWhileAnimating: false,
140141
};
@@ -172,7 +173,14 @@ var ScrollResponderMixin = {
172173
* true.
173174
*
174175
*/
175-
scrollResponderHandleStartShouldSetResponder: function(): boolean {
176+
scrollResponderHandleStartShouldSetResponder: function(e: Event): boolean {
177+
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
178+
179+
if (this.props.keyboardShouldPersistTaps === 'handled' &&
180+
currentlyFocusedTextInput != null &&
181+
e.target !== currentlyFocusedTextInput) {
182+
return true;
183+
}
176184
return false;
177185
},
178186

@@ -190,7 +198,10 @@ var ScrollResponderMixin = {
190198
scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
191199
// First see if we want to eat taps while the keyboard is up
192200
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
193-
if (!this.props.keyboardShouldPersistTaps &&
201+
var {keyboardShouldPersistTaps} = this.props;
202+
var keyboardNeverPersistTaps = !keyboardShouldPersistTaps ||
203+
keyboardShouldPersistTaps === 'never';
204+
if (keyboardNeverPersistTaps &&
194205
currentlyFocusedTextInput != null &&
195206
!isTagInstanceOfTextInput(e.target)) {
196207
return true;
@@ -250,7 +261,8 @@ var ScrollResponderMixin = {
250261
// By default scroll views will unfocus a textField
251262
// if another touch occurs outside of it
252263
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
253-
if (!this.props.keyboardShouldPersistTaps &&
264+
if (this.props.keyboardShouldPersistTaps !== true &&
265+
this.props.keyboardShouldPersistTaps !== 'always' &&
254266
currentlyFocusedTextInput != null &&
255267
e.target !== currentlyFocusedTextInput &&
256268
!this.state.observedScrollSinceBecomingResponder &&
@@ -481,6 +493,13 @@ var ScrollResponderMixin = {
481493
* The `keyboardWillShow` is called before input focus.
482494
*/
483495
componentWillMount: function() {
496+
var {keyboardShouldPersistTaps} = this.props;
497+
warning(
498+
typeof keyboardShouldPersistTaps !== 'boolean',
499+
`'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. `
500+
+ `Use 'keyboardShouldPersistTaps="${keyboardShouldPersistTaps ? "always" : "never"}"' instead`
501+
);
502+
484503
this.keyboardWillOpenTo = null;
485504
this.additionalScrollOffset = 0;
486505
this.addListenerOn(Keyboard, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);

Libraries/Components/ScrollView/ScrollView.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,18 @@ const ScrollView = React.createClass({
192192
'on-drag',
193193
]),
194194
/**
195-
* When false, tapping outside of the focused text input when the keyboard
196-
* is up dismisses the keyboard. When true, the keyboard will not dismiss
197-
* automatically, and the scroll view will not catch taps, but children of
198-
* the scroll view can catch taps. The default value is false.
199-
*/
200-
keyboardShouldPersistTaps: PropTypes.bool,
195+
* Determines when the keyboard should stay visible after a tap.
196+
*
197+
* - 'never' (the default), tapping outside of the focused text input when the keyboard
198+
* is up dismisses the keyboard. When this happens, children won't receive the tap.
199+
* - 'always', the keyboard will not dismiss automatically, and the scroll view will not
200+
* catch taps, but children of the scroll view can catch taps.
201+
* - 'handled', the keyboard will not dismiss automatically when the tap was handled by
202+
* a children, (or captured by an ancestor).
203+
* - false, deprecated, use 'never' instead
204+
* - true, deprecated, use 'always' instead
205+
*/
206+
keyboardShouldPersistTaps: PropTypes.oneOf(['always', 'never', 'handled', false, true]),
201207
/**
202208
* The maximum allowed zoom scale. The default value is 1.0.
203209
* @platform ios

0 commit comments

Comments
 (0)