Skip to content

Reworking keyboardShouldPersistTaps to have a middle ground #10628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from

Conversation

MaxLap
Copy link
Contributor

@MaxLap MaxLap commented Oct 29, 2016

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 know stance on backward compatibility of the project. The default behavior isn't changed and remains at 'always' (true), but in my opinion, the goal should be to change the default to be the new 'handled' behavior since that feels the most intuitive.

Note that true and false are still valid options.

@facebook-github-bot
Copy link
Contributor

By analyzing the blame information on this pull request, we identified @sahrens and @simonayzman to be potential reviewers.

@facebook-github-bot facebook-github-bot added GH Review: review-needed CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. labels Oct 29, 2016
@janicduplessis
Copy link
Contributor

I think we should just make keyboardShouldPersistTaps={false} your new behaviour instead of changing it to 3 values. It will be less of a breaking change in my opinion, it will change the behaviour a little bit but I don't think there are any use cases where you'd want the current behaviour.

Feels more like a bug fix with keyboardShouldPersistTaps={false} than anything else as I'd expect the keyboard to stay opened with touching another input.

cc @mkonicek

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 29, 2016
@MaxLap
Copy link
Contributor Author

MaxLap commented Oct 29, 2016

I absolutely agree, i just didn't know what was the stance on background compat. If the concensus is that it should be changed straight away, I'll happily change the PR.

@janicduplessis
Copy link
Contributor

@MaxLap 👍 Let's wait a bit see what others think

@satya164
Copy link
Contributor

@janicduplessis I think the current behaviour desirable if your screen filled with touchables. you don't wanna trigger a tap while dismissing the keyboard.

@janicduplessis
Copy link
Contributor

Does this actually affects touchables? Looks like it only should only affect TextInputs.

@satya164
Copy link
Contributor

@janicdupless no it doesn't affect touchables. But when you have a screen filled with touchables, you need to tap somewhere to dismiss the keyboard which was triggered due to an text input without triggering a tap on a touchable.

@MaxLap
Copy link
Contributor Author

MaxLap commented Oct 30, 2016

@janicduplessis @satya164 to clarify, the new behavior basically means if a descendant handles the tap, the ScrollView won't, and therefore won't hide the keyboard. If a descendant button wants to also hide the keyboard, then it can do so itself pretty easily.

@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 10, 2016

Is there a way to request feedback from other people? It's not so much about reviewing the code, first about if this is something you want in RN.

I think this change would be helpful to many people. I admit that some people may want the current swallow-every-tap behavior, which is why I left it there. I also do think the default behavior should the new one, maybe with some releases in between to let people specify which behavior they want.

I didn't change the documentation yet as I wanted feedback first.

@janicduplessis
Copy link
Contributor

Ping @mkonicek

Copy link
Contributor

@ericvicenti ericvicenti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this change makes a lot of sense.

Should we set the default prop to 'handled'? Or maybe we can do it later once people have had some time to experiment with the 'handled' behavior in their apps.

scrollResponderHandleStartShouldSetResponder: function(e: Event): boolean {
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();

if (this.props.keyboardShouldPersistTaps == 'handled' &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use === in cases like this

@@ -180,7 +187,9 @@ var ScrollResponderMixin = {
scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
// First see if we want to eat taps while the keyboard is up
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
if (!this.props.keyboardShouldPersistTaps &&
var keyboardNeverPersistTaps = !this.props.keyboardShouldPersistTaps ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be more consise if you do this on a previous line:

const {keyboardShouldPersistTaps} = this.props;

@janicduplessis
Copy link
Contributor

@MaxLap @satya164 I tested this out more in an app and it does affect the interaction with Touchables. If you have a touchable in the screen it will handle the event and thus the keyboard won't be close. Because of this I think we should not change it to the default behavior.

After thinking about it there are use cases for all 3 behaviors

  • Persist taps 'always': Never dismiss the keyboard
  • Persist taps 'handled': Dismiss the keyboard only when tapping non-touchable views, useful let's say you have a form that include buttons and want to be able to click then without dismissing keyboard beforehand.
  • Persist taps 'never': Always dismiss the keyboard even when tapping touchables, useful if you have something like a search bar with a bunch of touchable rows that fill the rest of the screen, you don't want it to trigger the onPress and dismiss the keyboard.

However there is still an issue with the current 'never' behavior (which is I was mostly referring to in my earlier comments) where tapping a different text input while the keyboard is open dismisses the keyboard instead of selecting it. I'll open a new PR to fix this case separately as it doesn't depend on this.

@janicduplessis
Copy link
Contributor

janicduplessis commented Nov 11, 2016

With that said I'm not the biggest fan of the string enum + boolean prop, what about we add a deprecation warning for the boolean values or create a new prop altogether and deprecate keyboardShouldPersistTaps?

@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 13, 2016

I did the changes requested in the review.

I agree that have both the true/false and the "always"/"never" is probably not the best thing. A deprecation is probably a good idea? Is that the final call?

Making the "never" sometimes still persist the keyboard may make the name of that value confusing.

@satya164
Copy link
Contributor

Persist taps 'never': Always dismiss the keyboard even when tapping touchables, useful if you have something like a search bar with a bunch of touchable rows that fill the rest of the screen, you don't want it to trigger the onPress and dismiss the keyboard.

What happens when I tap non-touchable? Shouldn't the keyboard dismiss then too? Why limit this to touchables only?

what about we add a deprecation warning for the boolean values

Yeah, we should deprecate the boolean values and show a warning specifying which value to change to.

@janicduplessis
Copy link
Contributor

@satya164 Maybe that wasnt clear but yea it would dismiss the keyboard when tapping non-touchables too.

@satya164
Copy link
Contributor

@janicduplessis great!

@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 14, 2016

Okay, so for the deprecation. Do I make a new prop or I keep using keyboardShouldPersistTaps? If I keep using it, is there a better way of doing the deprecation than this? It works, but the message starts by saying keyboardShouldPersistTaps is deprecated. Is there an alternative / should I code one or that's good enough?

keyboardShouldPersistTaps: PropTypes.oneOfType(
  [PropTypes.oneOf(['always', 'never', 'handled']),
   deprecatedPropType(PropTypes.bool, 
                      'Values true & false for keyboardShouldPersistTaps ' + 
                      'are deprecated. Use "always" & "never" respectively ' + 
                      'instead or the new "handled".')]),

The resulting message is: keyboardShouldPersistTaps supplied to ScrollView has been deprecated. Values true & false for keyboardShouldPersistTaps are deprecated. Use "always" & "never" respectively instead or the new "handled".

@satya164
Copy link
Contributor

satya164 commented Nov 14, 2016

Do I make a new prop or I keep using keyboardShouldPersistTaps

keyboardShouldPersistTaps sounds fine to me @janicduplessis?

Is there an alternative

I think better to check it in componentDidMount,

const { keyboardShouldPersistTaps } = this.props;
if (typeof keyboardShouldPersistTaps === 'boolean') {
  console.warn(`'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. Use 'keyboardShouldPersistTaps=${keyboardShouldPersistTaps ? "always" : "never"}' instead`);
}

@ericvicenti
Copy link
Contributor

ericvicenti commented Nov 14, 2016

@satya164, fbjs has a warning module, which is considered best practice inside RN:

const warning = require('warning');
...
warning(
  typeof keyboardShouldPersistTaps !== 'boolean',
  `'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. Use 'keyboardShouldPersistTaps=${keyboardShouldPersistTaps ? "always" : "never"}' instead`
);

@janicduplessis
Copy link
Contributor

@ericvicenti Well it wont actually open / close it, it's just my mental model of it and why I think that never is fine even if it doesnt close it on text inputs.

* 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', they keyboard will not dismiss automatically when the tap was handled by
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: they -> the

@@ -474,6 +486,13 @@ var ScrollResponderMixin = {
* The `keyboardWillShow` is called before input focus.
*/
componentWillMount: function() {
var keyboardShouldPersistTaps = this.props.keyboardShouldPersistTaps;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: lets use destructuring here too for consistency

This adds new possible values to keyboardShouldPersistTaps.
'never' and 'always' are the same as false and true.
'handled' is similar to 'never' (false), but the behavior is done in the bubbling phase of the event instead of the capture phase. So if a descendant component decides to respond to the tap, the ScrollResponder won't receive the event.
The default behavior isn't changed and remains at 'always' (true), but in my opinion, the goal should be to change the default to be the new 'handled' behavior since that feels the most intuitive.
Note that true and false are still valid options.
The exact change would be to 'always', but handled most likely make more sense and helps spread awareness of that option.
@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 16, 2016

Did the requested tweaks from the review. Thank you @janicduplessis

I edited the examples to use string values. I replaced the true by 'handled' since that's probably a more sane behavior for them. I have no idea how to test the examples, is anyone setup to do it relatively painlessly?

As a side note, am I wrong in thinking that the props and propTypes that refer to keyboardShouldPersistTaps are useless in UIExplorerPage? (only wrapperProps.keyboardShouldPersistTaps = ... makes sense) If you agree, I'll remove those useless references.

@janicduplessis's reasoning makes sense to me, so I will stick with 'never'.

@janicduplessis
Copy link
Contributor

janicduplessis commented Nov 16, 2016

You can run the UIExplorer app from the xcode project in Examples/UIExplorer on iOS or with ./gradlew Examples:UIExplorer:android:app:installDebug with a simulator opened on Android. You will also need to start the packager with ./packager/packager.sh.

Yea, looks like the prop keyboardShouldPersistTaps declared in the propTypes is never used we can remove it.

@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 16, 2016

Ok, went easier than expected (I'm on windows with android).

The UIExample works great.

I can't get the Movies example to work. It starts, but it can't seem to do its request to rotten tomatoes: undefined is not an object (evaluating 'responseData.total') at line 136 of SearchScreen.js. Because of this, the ListView is never displayed. I doubt there is an issue with my change since it's a single prop.

@janicduplessis
Copy link
Contributor

Ok good, as long as uiexplorer works thats fine, anyway your changes don't affect the others.

@ericvicenti Do you want to take care of shipping this since you'll probably want to update fb internal codebase as well to avoid the warning. We should also make sure to communicate this with the release team so they can add something in the release notes about it (not really a breaking change but still).

facebook-github-bot pushed a commit that referenced this pull request Nov 25, 2016
Summary:
When using text inputs inside a ScrollView with `keyboardShouldPersistTaps=false` (default behavior) tapping another text input dismisses the keyboard instead of keeping it open and focusing the new text input which I think is the better and expected behavior.

See #10628 for more discussion about that. Note that this affects nothing but the behavior with text inputs unlike #10628.

cc satya164 MaxLap ericvicenti
Closes #10887

Differential Revision: D4178474

Pulled By: ericvicenti

fbshipit-source-id: 0c62ea2fac0017d559d1f8674b0a686a5e1b3d2d
@MaxLap
Copy link
Contributor Author

MaxLap commented Dec 7, 2016

Anybody willing to make the review on this? Thank you

@janicduplessis
Copy link
Contributor

Sorry about the delay, I'll ping Eric about it.

@ericvicenti
Copy link
Contributor

Sorry for the delay on the re-review. This looks great and I'm landing it now!

@facebook-github-bot facebook-github-bot added GH Review: accepted Import Started This pull request has been imported. This does not imply the PR has been approved. and removed GH Review: review-needed labels Dec 7, 2016
@facebook-github-bot
Copy link
Contributor

@ericvicenti has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@MaxLap
Copy link
Contributor Author

MaxLap commented Dec 8, 2016

Thank you everyone

mkonicek pushed a commit that referenced this pull request Dec 12, 2016
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
DanielMSchmidt pushed a commit to DanielMSchmidt/react-native that referenced this pull request Jan 4, 2017
Summary:
When using text inputs inside a ScrollView with `keyboardShouldPersistTaps=false` (default behavior) tapping another text input dismisses the keyboard instead of keeping it open and focusing the new text input which I think is the better and expected behavior.

See facebook#10628 for more discussion about that. Note that this affects nothing but the behavior with text inputs unlike facebook#10628.

cc satya164 MaxLap ericvicenti
Closes facebook#10887

Differential Revision: D4178474

Pulled By: ericvicenti

fbshipit-source-id: 0c62ea2fac0017d559d1f8674b0a686a5e1b3d2d
DanielMSchmidt pushed a commit to DanielMSchmidt/react-native that referenced this pull request Jan 4, 2017
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 facebook#10628

Differential Revision: D4294945

Pulled By: ericvicenti

fbshipit-source-id: 1a753014156cac1a23fabfa8e1faa9a768868ef2
rozele pushed a commit to microsoft/react-native-windows that referenced this pull request Feb 7, 2017
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 facebook/react-native#10628

Differential Revision: D4294945

Pulled By: ericvicenti

fbshipit-source-id: 1a753014156cac1a23fabfa8e1faa9a768868ef2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Import Started This pull request has been imported. This does not imply the PR has been approved.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants