From 6286270e4cb10b40cfd7c8193e31d965f6815150 Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 19 Feb 2020 15:26:09 -0800 Subject: [PATCH] Migrate TextInputState to take a ref instead of a reactTag Summary: Changelog: [General][Breaking] Multiple deprecations and breaking changes to TextInputState. Use native component refs instead of react tags Reviewed By: JoshuaGross Differential Revision: D19458214 fbshipit-source-id: f67649657fa44b6c707a0ac91f07d2ceb433bc70 --- Libraries/Components/ScrollResponder.js | 37 ++++-- Libraries/Components/TextInput/TextInput.js | 21 +-- .../Components/TextInput/TextInputState.js | 122 +++++++++++++++--- .../TextInput/__tests__/TextInput-test.js | 26 +++- Libraries/Utilities/dismissKeyboard.js | 2 +- 5 files changed, 163 insertions(+), 45 deletions(-) diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 9353c90beb16bf..a520d17d2875fd 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -183,12 +183,12 @@ const ScrollResponderMixin = { return false; } - const currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); + const currentlyFocusedInput = TextInputState.currentlyFocusedInput(); if ( this.props.keyboardShouldPersistTaps === 'handled' && - currentlyFocusedTextInput != null && - ReactNative.findNodeHandle(e.target) !== currentlyFocusedTextInput + currentlyFocusedInput != null && + e.target !== currentlyFocusedInput ) { return true; } @@ -224,17 +224,26 @@ const ScrollResponderMixin = { // and a new touch starts with a non-textinput target (in which case the // first tap should be sent to the scroll view and dismiss the keyboard, // then the second tap goes to the actual interior view) - const currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); + const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); const {keyboardShouldPersistTaps} = this.props; const keyboardNeverPersistTaps = !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; - const reactTag = ReactNative.findNodeHandle(e.target); + if (typeof e.target === 'number') { + if (__DEV__) { + console.error( + 'Did not expect event target to be a number. Should have been a native component', + ); + } + + return false; + } + if ( keyboardNeverPersistTaps && currentlyFocusedTextInput != null && - reactTag && - !TextInputState.isTextInput(reactTag) + e.target != null && + !TextInputState.isTextInput(e.target) ) { return true; } @@ -300,14 +309,24 @@ const ScrollResponderMixin = { scrollResponderHandleResponderRelease: function(e: PressEvent) { this.props.onResponderRelease && this.props.onResponderRelease(e); + if (typeof e.target === 'number') { + if (__DEV__) { + console.error( + 'Did not expect event target to be a number. Should have been a native component', + ); + } + + return; + } + // By default scroll views will unfocus a textField // if another touch occurs outside of it - const currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); + const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); if ( this.props.keyboardShouldPersistTaps !== true && this.props.keyboardShouldPersistTaps !== 'always' && currentlyFocusedTextInput != null && - ReactNative.findNodeHandle(e.target) !== currentlyFocusedTextInput && + e.target !== currentlyFocusedTextInput && !this.state.observedScrollSinceBecomingResponder && !this.state.becameResponderWhileAnimating ) { diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index d5cc30cbe2f59b..38dd945a620b3d 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -888,12 +888,13 @@ function InternalTextInput(props: Props): React.Node { useFocusOnMount(props.autoFocus, inputRef); useEffect(() => { - const tag = ReactNative.findNodeHandle(inputRef.current); - if (tag != null) { - TextInputState.registerInput(tag); + const inputRefValue = inputRef.current; + + if (inputRefValue != null) { + TextInputState.registerInput(inputRefValue); return () => { - TextInputState.unregisterInput(tag); + TextInputState.unregisterInput(inputRefValue); }; } }, [inputRef]); @@ -915,10 +916,7 @@ function InternalTextInput(props: Props): React.Node { // TODO: Fix this returning true on null === null, when no input is focused function isFocused(): boolean { - return ( - TextInputState.currentlyFocusedField() === - ReactNative.findNodeHandle(inputRef.current) - ); + return TextInputState.currentlyFocusedInput() === inputRef.current; } function getNativeRef(): ?React.ElementRef> { @@ -1009,14 +1007,14 @@ function InternalTextInput(props: Props): React.Node { }; const _onFocus = (event: FocusEvent) => { - TextInputState.focusField(ReactNative.findNodeHandle(inputRef.current)); + TextInputState.focusInput(inputRef.current); if (props.onFocus) { props.onFocus(event); } }; const _onBlur = (event: BlurEvent) => { - TextInputState.blurField(ReactNative.findNodeHandle(inputRef.current)); + TextInputState.blurInput(inputRef.current); if (props.onBlur) { props.onBlur(event); } @@ -1143,6 +1141,8 @@ ExportedForwardRef.propTypes = DeprecatedTextInputPropTypes; // $FlowFixMe ExportedForwardRef.State = { + currentlyFocusedInput: TextInputState.currentlyFocusedInput, + currentlyFocusedField: TextInputState.currentlyFocusedField, focusTextInput: TextInputState.focusTextInput, blurTextInput: TextInputState.blurTextInput, @@ -1150,6 +1150,7 @@ ExportedForwardRef.State = { type TextInputComponentStatics = $ReadOnly<{| State: $ReadOnly<{| + currentlyFocusedInput: typeof TextInputState.currentlyFocusedInput, currentlyFocusedField: typeof TextInputState.currentlyFocusedField, focusTextInput: typeof TextInputState.focusTextInput, blurTextInput: typeof TextInputState.blurTextInput, diff --git a/Libraries/Components/TextInput/TextInputState.js b/Libraries/Components/TextInput/TextInputState.js index c57ebbc611a440..50ddea585eca0e 100644 --- a/Libraries/Components/TextInput/TextInputState.js +++ b/Libraries/Components/TextInput/TextInputState.js @@ -14,30 +14,61 @@ 'use strict'; +const React = require('react'); const Platform = require('../../Utilities/Platform'); const UIManager = require('../../ReactNative/UIManager'); +const {findNodeHandle} = require('../../Renderer/shims/ReactNative'); -let currentlyFocusedID: ?number = null; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +type ComponentRef = React.ElementRef>; + +let currentlyFocusedInputRef: ?ComponentRef = null; const inputs = new Set(); +function currentlyFocusedInput(): ?ComponentRef { + return currentlyFocusedInputRef; +} + /** * Returns the ID of the currently focused text field, if one exists * If no text field is focused it returns null */ function currentlyFocusedField(): ?number { - return currentlyFocusedID; + if (__DEV__) { + console.error( + 'currentlyFocusedField is deprecated and will be removed in a future release. Use currentlyFocusedInput', + ); + } + + return findNodeHandle(currentlyFocusedInputRef); +} + +function focusInput(textField: ?ComponentRef): void { + if (currentlyFocusedInputRef !== textField && textField != null) { + currentlyFocusedInputRef = textField; + } +} + +function blurInput(textField: ?ComponentRef): void { + if (currentlyFocusedInputRef === textField && textField != null) { + currentlyFocusedInputRef = null; + } } function focusField(textFieldID: ?number): void { - if (currentlyFocusedID !== textFieldID && textFieldID != null) { - currentlyFocusedID = textFieldID; + if (__DEV__) { + console.error('focusField no longer works. Use focusInput'); } + + return; } function blurField(textFieldID: ?number) { - if (currentlyFocusedID === textFieldID && textFieldID != null) { - currentlyFocusedID = null; + if (__DEV__) { + console.error('blurField no longer works. Use blurInput'); } + + return; } /** @@ -45,9 +76,20 @@ function blurField(textFieldID: ?number) { * Focuses the specified text field * noop if the text field was already focused */ -function focusTextInput(textFieldID: ?number) { - if (currentlyFocusedID !== textFieldID && textFieldID != null) { - focusField(textFieldID); +function focusTextInput(textField: ?ComponentRef) { + if (typeof textField === 'number') { + if (__DEV__) { + console.error( + 'focusTextInput must be called with a host component. Passing a react tag is deprecated.', + ); + } + + return; + } + + if (currentlyFocusedInputRef !== textField && textField != null) { + const textFieldID = findNodeHandle(textField); + focusInput(textField); if (Platform.OS === 'ios') { UIManager.focus(textFieldID); } else if (Platform.OS === 'android') { @@ -66,9 +108,20 @@ function focusTextInput(textFieldID: ?number) { * Unfocuses the specified text field * noop if it wasn't focused */ -function blurTextInput(textFieldID: ?number) { - if (currentlyFocusedID === textFieldID && textFieldID != null) { - blurField(textFieldID); +function blurTextInput(textField: ?ComponentRef) { + if (typeof textField === 'number') { + if (__DEV__) { + console.error( + 'focusTextInput must be called with a host component. Passing a react tag is deprecated.', + ); + } + + return; + } + + if (currentlyFocusedInputRef === textField && textField != null) { + const textFieldID = findNodeHandle(textField); + blurInput(textField); if (Platform.OS === 'ios') { UIManager.blur(textFieldID); } else if (Platform.OS === 'android') { @@ -82,19 +135,52 @@ function blurTextInput(textFieldID: ?number) { } } -function registerInput(textFieldID: number) { - inputs.add(textFieldID); +function registerInput(textField: ComponentRef) { + if (typeof textField === 'number') { + if (__DEV__) { + console.error( + 'registerInput must be called with a host component. Passing a react tag is deprecated.', + ); + } + + return; + } + + inputs.add(textField); } -function unregisterInput(textFieldID: number) { - inputs.delete(textFieldID); +function unregisterInput(textField: ComponentRef) { + if (typeof textField === 'number') { + if (__DEV__) { + console.error( + 'unregisterInput must be called with a host component. Passing a react tag is deprecated.', + ); + } + + return; + } + inputs.delete(textField); } -function isTextInput(textFieldID: number): boolean { - return inputs.has(textFieldID); +function isTextInput(textField: ComponentRef): boolean { + if (typeof textField === 'number') { + if (__DEV__) { + console.error( + 'isTextInput must be called with a host component. Passing a react tag is deprecated.', + ); + } + + return false; + } + + return inputs.has(textField); } module.exports = { + currentlyFocusedInput, + focusInput, + blurInput, + currentlyFocusedField, focusField, blurField, diff --git a/Libraries/Components/TextInput/__tests__/TextInput-test.js b/Libraries/Components/TextInput/__tests__/TextInput-test.js index 8eb8a477185dc8..38a64dccb6936c 100644 --- a/Libraries/Components/TextInput/__tests__/TextInput-test.js +++ b/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -87,6 +87,10 @@ describe('TextInput tests', () => { expect(textInputRef.current.isFocused()).toBe(false); ReactNative.findNodeHandle = jest.fn().mockImplementation(ref => { + if (ref == null) { + return null; + } + if ( ref === textInputRef.current || ref === textInputRef.current.getNativeRef() @@ -97,13 +101,17 @@ describe('TextInput tests', () => { return 2; }); - const inputTag = ReactNative.findNodeHandle(textInputRef.current); - - TextInput.State.focusTextInput(inputTag); + TextInput.State.focusTextInput(textInputRef.current); expect(textInputRef.current.isFocused()).toBe(true); - expect(TextInput.State.currentlyFocusedField()).toBe(inputTag); - TextInput.State.blurTextInput(inputTag); + expect(TextInput.State.currentlyFocusedInput()).toBe(textInputRef.current); + // This function is currently deprecated and will be removed in the future + expect(TextInput.State.currentlyFocusedField()).toBe( + ReactNative.findNodeHandle(textInputRef.current), + ); + TextInput.State.blurTextInput(textInputRef.current); expect(textInputRef.current.isFocused()).toBe(false); + expect(TextInput.State.currentlyFocusedInput()).toBe(null); + // This function is currently deprecated and will be removed in the future expect(TextInput.State.currentlyFocusedField()).toBe(null); }); @@ -141,16 +149,20 @@ describe('TextInput tests', () => { const inputTag1 = ReactNative.findNodeHandle(textInputRe1.current); const inputTag2 = ReactNative.findNodeHandle(textInputRe2.current); - TextInput.State.focusTextInput(inputTag1); + TextInput.State.focusTextInput(textInputRe1.current); expect(textInputRe1.current.isFocused()).toBe(true); expect(textInputRe2.current.isFocused()).toBe(false); + expect(TextInput.State.currentlyFocusedInput()).toBe(textInputRe1.current); + // This function is currently deprecated and will be removed in the future expect(TextInput.State.currentlyFocusedField()).toBe(inputTag1); - TextInput.State.focusTextInput(inputTag2); + TextInput.State.focusTextInput(textInputRe2.current); expect(textInputRe1.current.isFocused()).toBe(false); expect(textInputRe2.current.isFocused()).toBe(true); + expect(TextInput.State.currentlyFocusedInput()).toBe(textInputRe2.current); + // This function is currently deprecated and will be removed in the future expect(TextInput.State.currentlyFocusedField()).toBe(inputTag2); }); diff --git a/Libraries/Utilities/dismissKeyboard.js b/Libraries/Utilities/dismissKeyboard.js index 98a5cda003ab63..16625a98894d65 100644 --- a/Libraries/Utilities/dismissKeyboard.js +++ b/Libraries/Utilities/dismissKeyboard.js @@ -15,7 +15,7 @@ const TextInputState = require('../Components/TextInput/TextInputState'); function dismissKeyboard() { - TextInputState.blurTextInput(TextInputState.currentlyFocusedField()); + TextInputState.blurTextInput(TextInputState.currentlyFocusedInput()); } module.exports = dismissKeyboard;