Skip to content

Commit e9b4928

Browse files
elicwhitefacebook-github-bot
authored andcommitted
TextInput: Don't do an extra round trip to native on focus/blur
Summary: I wrote up a bunch of context for this in response to #27038 by fat. That comment is reproduced here in this commit message. You can see it in it's original contxt here: #27038 Okay, here is what I think is happening. For context, here is a diagram I have of how focus and blur propagates through the system. This might be interesting to refer back to as you go through the rest of my explanation. ![graphviz (12)](https://user-images.githubusercontent.com/249164/67992345-982c9d80-fbf9-11e9-96ea-b091210dddbe.png) ScrollView's scrollResponder is responsible for blurring text inputs when a touch occurs in the ScrollView but outside of the currently focused TextInput. The code for that is here: https://github.com/facebook/react-native/blob/6ba2769f0f92ca75fb0eb60ccb8337920a9c31eb/Libraries/Components/ScrollResponder.js#L301-L314 This happens on `scrollResponderHandleResponderRelease` aka, touch up. It checks for what the currently focused textinput is by calling `TextInputState.currentlyFocusedField()`. That function is a JS variable that is being updated by calls to `TextInputState.focusTextInput` and `TextInputState.blurTextInput`: https://github.com/facebook/react-native/blob/6ba2769f0f92ca75fb0eb60ccb8337920a9c31eb/Libraries/Components/TextInput/TextInputState.js#L36-L71 I added some console logs to those methods to see which ones are being called when running your repro (thanks for the repro!). **This is without your fix** Click on and off: ``` // Click on input 1 focusTextInput input1 TextInput's _onFocus called // Click on blank space scrollResponderHandleResponderRelease blur input1 blurTextInput input1 TextInput's _onBlur called ``` Click on input1, then input 2, then off ``` // Click on input 1 focusTextInput input1 TextInput's _onFocus called for input1 // Click on input 2 focusTextInput input2 TextInput's _onBlur called for input1 TextInput's _onFocus called for input2 // Click on blank space scrollResponderHandleResponderRelease blur input2 blurTextInput input2 TextInput's _onBlur called for input2 ``` And now for the bug. Click on input 1, tab to 2, then off ``` // Click on input 1 focusTextInput input1 TextInput's _onFocus called for input1 // Tab to input 2 TextInput's _onBlur called for input1 TextInput's _onFocus called for input2 // Click on blank space scrollResponderHandleResponderRelease blur input1 blurTextInput input1 ``` Notice how `focusTextInput` was never called with input2 in the last example. Since this is the function that sets the `currentlyFocusedField` when we click on the blank space RN is trying to blur the first input instead of the second. # The root cause We are tracking the state of which field is focused in JS which has to stay in sync with what native knows is focused. We [listen to _onPress](https://github.com/facebook/react-native/blob/6ba2769f0f92ca75fb0eb60ccb8337920a9c31eb/Libraries/Components/TextInput/TextInput.js#L1103-L1107) and call `TextInputState.focusTextInput` in that handler. However, we don't currently have anything listening to other ways for an input to become focused (like tabbing) so it doesn't end up updating the `currentlyFocusedField`. We have the same problem with blur that we actually fixed the same way you did here in this PR: https://github.com/facebook/react-native/blob/6ba2769f0f92ca75fb0eb60ccb8337920a9c31eb/Libraries/Components/TextInput/TextInput.js#L1182-L1189 If you look back at my diagram at the beginning of this post, you'll notice the missing edge from `TextInput._onFocus` to `TextInputState.focusTextInput`. That's the problem. :) The reason this solution works is because this function **is** the notification from native that an input was focused or blurred. This solution is *fine* because this updates the `currentlyFocusedID` but isn't great because it both sets that value and **calls the native code to focus or blur again**. Luckily the native code doesn't send an event back to JS if you try to blur an already blurred TextInput otherwise we'd have an infinite loop. # The correct solution The correct thing would probably be to have all of this tracking in native code and not in JavaScript code. That's a pretty big change though and very out of scope. Something for our team to keep in mind for the future. A short term term solution would be to refactor `focusTextInput` and `blurTextInput` to pull out the part that sets the `currentlyFocusedID` that we could call from `TextInput` directly from `_onFocus` and `_onBlur`. # ^This short term term solution is what this commit is doing. Changelog: [General][Changed] TextInput no longer does an extra round trip to native on focus/blur Reviewed By: RSNara Differential Revision: D18278359 fbshipit-source-id: 417566f25075a847b0f4bac2888f92fbac934096
1 parent dfba312 commit e9b4928

File tree

3 files changed

+32
-8
lines changed

3 files changed

+32
-8
lines changed

Libraries/Components/TextInput/TextInput.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,7 @@ const TextInput = createReactClass({
993993
},
994994

995995
_onFocus: function(event: FocusEvent) {
996-
this.focus();
996+
TextInputState.focusField(ReactNative.findNodeHandle(this._inputRef));
997997
if (this.props.onFocus) {
998998
this.props.onFocus(event);
999999
}
@@ -1079,9 +1079,7 @@ const TextInput = createReactClass({
10791079
},
10801080

10811081
_onBlur: function(event: BlurEvent) {
1082-
// This is a hack to fix https://fburl.com/toehyir8
1083-
// @todo(rsnara) Figure out why this is necessary.
1084-
this.blur();
1082+
TextInputState.blurField(ReactNative.findNodeHandle(this._inputRef));
10851083
if (this.props.onBlur) {
10861084
this.props.onBlur(event);
10871085
}

Libraries/Components/TextInput/TextInputState.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,26 @@ function currentlyFocusedField(): ?number {
2828
return currentlyFocusedID;
2929
}
3030

31+
function focusField(textFieldID: ?number): void {
32+
if (currentlyFocusedID !== textFieldID && textFieldID != null) {
33+
currentlyFocusedID = textFieldID;
34+
}
35+
}
36+
37+
function blurField(textFieldID: ?number) {
38+
if (currentlyFocusedID === textFieldID && textFieldID != null) {
39+
currentlyFocusedID = null;
40+
}
41+
}
42+
3143
/**
3244
* @param {number} TextInputID id of the text field to focus
3345
* Focuses the specified text field
3446
* noop if the text field was already focused
3547
*/
3648
function focusTextInput(textFieldID: ?number) {
37-
if (currentlyFocusedID !== textFieldID && textFieldID !== null) {
38-
currentlyFocusedID = textFieldID;
49+
if (currentlyFocusedID !== textFieldID && textFieldID != null) {
50+
focusField(textFieldID);
3951
if (Platform.OS === 'ios') {
4052
UIManager.focus(textFieldID);
4153
} else if (Platform.OS === 'android') {
@@ -55,8 +67,8 @@ function focusTextInput(textFieldID: ?number) {
5567
* noop if it wasn't focused
5668
*/
5769
function blurTextInput(textFieldID: ?number) {
58-
if (currentlyFocusedID === textFieldID && textFieldID !== null) {
59-
currentlyFocusedID = null;
70+
if (currentlyFocusedID === textFieldID && textFieldID != null) {
71+
blurField(textFieldID);
6072
if (Platform.OS === 'ios') {
6173
UIManager.blur(textFieldID);
6274
} else if (Platform.OS === 'android') {
@@ -84,6 +96,8 @@ function isTextInput(textFieldID: number): boolean {
8496

8597
module.exports = {
8698
currentlyFocusedField,
99+
focusField,
100+
blurField,
87101
focusTextInput,
88102
blurTextInput,
89103
registerInput,

Libraries/Components/TextInput/__tests__/TextInput-test.js

+12
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,19 @@ describe('TextInput tests', () => {
7777
ReactTestRenderer.create(<TextInput ref={textInputRef} value="value1" />);
7878

7979
expect(textInputRef.current.isFocused()).toBe(false);
80+
ReactNative.findNodeHandle = jest.fn().mockImplementation(ref => {
81+
if (
82+
ref === textInputRef.current ||
83+
ref === textInputRef.current.getNativeRef()
84+
) {
85+
return 1;
86+
}
87+
88+
return 2;
89+
});
90+
8091
const inputTag = ReactNative.findNodeHandle(textInputRef.current);
92+
8193
TextInput.State.focusTextInput(inputTag);
8294
expect(textInputRef.current.isFocused()).toBe(true);
8395
expect(TextInput.State.currentlyFocusedField()).toBe(inputTag);

0 commit comments

Comments
 (0)