From 14dc03e2bdbbdeaee974bc7313a843166f10c623 Mon Sep 17 00:00:00 2001 From: Devan Buggay Date: Tue, 23 Sep 2025 14:37:56 -0700 Subject: [PATCH] Genalize focus/blur behavior in JS (#53829) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53829 Forwards along focus and blur calls to their respective native commands, leaving the TextInput path intact for now. There are a few ways places to implement this, all with different trade offs. I decided to leave TextInputState.js intact and instead only go down the original path if the focused/blurred element is a TextInput by using the registered inputs in TextInputState. This revealed a subtle issue where the TextInputs aren't registered in time for the ref callback, meaning you can't focus them there if you rely on the registered list. Changelog: [Internal] Reviewed By: shwanton Differential Revision: D82676628 fbshipit-source-id: d39c92dfc99ec4b5e100203bb43c8a4f9c80a649 --- .../Libraries/Components/TextInput/TextInput.js | 6 ++++++ .../Components/View/ViewNativeComponent.js | 4 ++-- .../__tests__/ReactFabricPublicInstance-itest.js | 5 +++-- .../webapis/dom/nodes/ReactNativeElement.js | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 164c09dc764..5cebe10a3ec 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -449,6 +449,12 @@ function InternalTextInput(props: TextInputProps): React.Node { before we can get to the long term breaking change. */ if (instance != null) { + // Register the input immediately when the ref is set so that focus() + // can be called from ref callbacks + // Double registering during useLayoutEffect is fine, because the underlying + // state is a Set. + TextInputState.registerInput(instance); + // $FlowFixMe[prop-missing] - See the explanation above. // $FlowFixMe[unsafe-object-assign] Object.assign(instance, { diff --git a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js index 192456169e7..600eee6fedb 100644 --- a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js @@ -21,8 +21,8 @@ const ViewNativeComponent: HostComponent = })); interface NativeCommands { - +focus: () => void; - +blur: () => void; + +focus: (viewRef: HostInstance) => void; + +blur: (viewRef: HostInstance) => void; +hotspotUpdate: (viewRef: HostInstance, x: number, y: number) => void; +setPressed: (viewRef: HostInstance, pressed: boolean) => void; } diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-itest.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-itest.js index 1d0543d14c7..dcd1cd1df25 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-itest.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-itest.js @@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement'; +import TextInput from '../../../Components/TextInput/TextInput'; import TextInputState from '../../../Components/TextInput/TextInputState'; import View from '../../../Components/View/View'; import ReactFabricHostComponent from '../ReactFabricHostComponent'; @@ -48,7 +49,7 @@ describe('ReactFabricPublicInstance', () => { const nodeRef = createRef(); Fantom.runTask(() => { - root.render(); + root.render(); }); const node = nullthrows(nodeRef.current); @@ -73,7 +74,7 @@ describe('ReactFabricPublicInstance', () => { const ref = createRef(); Fantom.runTask(() => { - root.render(); + root.render(); }); const node = nullthrows(ref.current); diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index 3da3ed4484a..78ab5696f43 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -25,8 +25,10 @@ import type {InstanceHandle} from './internals/NodeInternals'; import type ReactNativeDocument from './ReactNativeDocument'; import TextInputState from '../../../../../Libraries/Components/TextInput/TextInputState'; +import {Commands as ViewCommands} from '../../../../../Libraries/Components/View/ViewNativeComponent'; import {create as createAttributePayload} from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps'; +import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags'; import { getNativeElementReference, getPublicInstanceFromInstanceHandle, @@ -140,11 +142,19 @@ class ReactNativeElement extends ReadOnlyElement implements NativeMethods { */ blur(): void { - TextInputState.blurTextInput(this); + if (TextInputState.isTextInput(this)) { + TextInputState.blurTextInput(this); + } else if (ReactNativeFeatureFlags.enableImperativeFocus()) { + ViewCommands.blur(this); + } } focus() { - TextInputState.focusTextInput(this); + if (TextInputState.isTextInput(this)) { + TextInputState.focusTextInput(this); + } else if (ReactNativeFeatureFlags.enableImperativeFocus()) { + ViewCommands.focus(this); + } } measure(callback: MeasureOnSuccessCallback) {