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
This commit is contained in:
Devan Buggay
2025-09-23 14:37:56 -07:00
committed by Facebook GitHub Bot
parent 22002bb70c
commit 14dc03e2bd
4 changed files with 23 additions and 6 deletions
@@ -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, {
@@ -21,8 +21,8 @@ const ViewNativeComponent: HostComponent<Props> =
}));
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;
}
@@ -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<HostInstance>();
Fantom.runTask(() => {
root.render(<View ref={nodeRef} />);
root.render(<TextInput ref={nodeRef} />);
});
const node = nullthrows(nodeRef.current);
@@ -73,7 +74,7 @@ describe('ReactFabricPublicInstance', () => {
const ref = createRef<HostInstance>();
Fantom.runTask(() => {
root.render(<View ref={ref} />);
root.render(<TextInput ref={ref} />);
});
const node = nullthrows(ref.current);
@@ -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) {