From a06f87cee7177c07a79dc13c5487fbd85f417f97 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Tue, 27 May 2025 11:10:03 -0700 Subject: [PATCH 1/2] Add support for blur and focus on View (#51570) Summary: As the title suggests: adds support strictly to `View` components on Android for `onFocus` and `onBlur` events. This is especially helpful for apps that respond to controller or remote inputs and aligns with existing support for the `focusable` prop. In order to make this change cross-compatible with text inputs, `TextInputFocusEvent` has been deprecated in favor of the `BlurEvent`/`FocusEvent` types now available from core. Their type signatures are identical but `BlurEvent`/`FocusEvent` should be the type going forward for all views that intend to support focus/blur. Text inputs intentionally do not forward information about their state upon focus/blur and docs specifically call out `onEndEditing` as a means of reading state synchronously when blurring. Therefore, the changes to the native side to remove the event type specifically for text inputs is not breaking. Changelog: [Android][Added] - Support for `onFocus` and `onBlur` function calls in `View` components Reviewed By: mdvacca Differential Revision: D75238291 --- .../Components/TextInput/TextInput.d.ts | 12 ++++-- .../Components/TextInput/TextInput.flow.js | 20 +++++----- .../Components/TextInput/TextInput.js | 8 +++- .../Components/View/ViewPropTypes.d.ts | 21 ++++++++++- .../NativeComponent/BaseViewConfig.android.js | 12 ++++++ .../Libraries/Types/CoreEventTypes.d.ts | 4 ++ .../__snapshots__/public-api-test.js.snap | 7 ++-- .../ReactAndroid/api/ReactAndroid.api | 3 ++ .../react/uimanager/events/BlurEvent.kt | 27 ++++++++++++++ .../react/uimanager/events/FocusEvent.kt | 27 ++++++++++++++ .../textinput/ReactTextInputBlurEvent.kt | 34 ----------------- .../textinput/ReactTextInputFocusEvent.kt | 34 ----------------- .../views/textinput/ReactTextInputManager.kt | 6 ++- .../react/views/view/ReactViewManager.kt | 37 +++++++++++++++++++ .../types/__typetests__/index.tsx | 6 ++- 15 files changed, 168 insertions(+), 90 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.kt delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.kt diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 7204efc93ab521..b3ca1560c224f5 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -17,6 +17,8 @@ import { import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet'; import {TextStyle} from '../../StyleSheet/StyleSheetTypes'; import { + BlurEvent, + FocusEvent, NativeSyntheticEvent, NativeTouchEvent, TargetedEvent, @@ -455,7 +457,7 @@ export interface TextInputAndroidProps { } /** - * @deprecated Use `TextInputFocusEvent` instead + * @deprecated Use `FocusEvent` instead */ export interface TextInputFocusEventData extends TargetedEvent { text: string; @@ -464,6 +466,7 @@ export interface TextInputFocusEventData extends TargetedEvent { /** * @see TextInputProps.onFocus + * @deprecated Use `FocusEvent` instead */ export type TextInputFocusEvent = NativeSyntheticEvent; @@ -809,8 +812,11 @@ export interface TextInputProps /** * Callback that is called when the text input is blurred + * + * Note: If you are trying to find the last value of TextInput, you can use the `onEndEditing` + * event, which is fired upon completion of editing. */ - onBlur?: ((e: TextInputFocusEvent) => void) | undefined; + onBlur?: ((e: BlurEvent) => void) | undefined; /** * Callback that is called when the text input's text changes. @@ -859,7 +865,7 @@ export interface TextInputProps /** * Callback that is called when the text input is focused */ - onFocus?: ((e: TextInputFocusEvent) => void) | undefined; + onFocus?: ((e: FocusEvent) => void) | undefined; /** * Callback that is called when the text input selection is changed. diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index d949de7af342af..acefa1ffc9525a 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -10,6 +10,8 @@ import type {HostInstance} from '../../../src/private/types/HostInstance'; import type { + BlurEvent, + FocusEvent, GestureResponderEvent, NativeSyntheticEvent, ScrollEvent, @@ -58,22 +60,22 @@ type TextInputContentSizeChangeEventData = $ReadOnly<{ export type TextInputContentSizeChangeEvent = NativeSyntheticEvent; -type TargetEvent = $ReadOnly<{ - target: number, - ... -}>; - -type TextInputFocusEventData = TargetEvent; - /** * @see TextInputProps.onBlur + * @deprecated Use `BlurEvent` instead. */ -export type TextInputBlurEvent = NativeSyntheticEvent; +export type TextInputBlurEvent = BlurEvent; /** * @see TextInputProps.onFocus + * @deprecated Use `FocusEvent` instead. */ -export type TextInputFocusEvent = NativeSyntheticEvent; +export type TextInputFocusEvent = FocusEvent; + +type TargetEvent = $ReadOnly<{ + target: number, + ... +}>; export type Selection = $ReadOnly<{ start: number, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index f7e495aff399c7..88513bf4f0bea0 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -11,6 +11,8 @@ import type {HostInstance} from '../../../src/private/types/HostInstance'; import type {____TextStyle_Internal as TextStyleInternal} from '../../StyleSheet/StyleSheetTypes'; import type { + BlurEvent, + FocusEvent, GestureResponderEvent, ScrollEvent, } from '../../Types/CoreEventTypes'; @@ -86,10 +88,12 @@ if (Platform.OS === 'android') { export type { AutoCapitalize, + BlurEvent, EnterKeyHintType, EnterKeyHintTypeAndroid, EnterKeyHintTypeIOS, EnterKeyHintTypeOptions, + FocusEvent, InputModeOptions, KeyboardType, KeyboardTypeAndroid, @@ -520,14 +524,14 @@ function InternalTextInput(props: TextInputProps): React.Node { }); }; - const _onFocus = (event: TextInputFocusEvent) => { + const _onFocus = (event: FocusEvent) => { TextInputState.focusInput(inputRef.current); if (props.onFocus) { props.onFocus(event); } }; - const _onBlur = (event: TextInputBlurEvent) => { + const _onBlur = (event: BlurEvent) => { TextInputState.blurInput(inputRef.current); if (props.onBlur) { props.onBlur(event); diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index eb44b3adea28bb..b52feb743bee3a 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -12,7 +12,12 @@ import {Insets} from '../../../types/public/Insets'; import {GestureResponderHandlers} from '../../../types/public/ReactNativeRenderer'; import {StyleProp} from '../../StyleSheet/StyleSheet'; import {ViewStyle} from '../../StyleSheet/StyleSheetTypes'; -import {LayoutChangeEvent, PointerEvents} from '../../Types/CoreEventTypes'; +import { + BlurEvent, + FocusEvent, + LayoutChangeEvent, + PointerEvents, +} from '../../Types/CoreEventTypes'; import {Touchable} from '../Touchable/Touchable'; import {AccessibilityProps} from './ViewAccessibility'; @@ -76,6 +81,20 @@ export interface ViewPropsIOS extends TVViewPropsIOS { } export interface ViewPropsAndroid { + /** + * Callback that is called when the view is blurred. + * + * Note: This will only be called if the view is focusable. + */ + onBlur?: ((e: BlurEvent) => void) | null | undefined; + + /** + * Callback that is called when the view is focused. + * + * Note: This will only be called if the view is focusable. + */ + onFocus?: ((e: FocusEvent) => void) | null | undefined; + /** * Whether this view should render itself (and all of its children) into a single hardware texture on the GPU. * diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index fb4c4a30e460d9..bdcd439ab4721a 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -111,6 +111,18 @@ const bubblingEventTypes = { bubbled: 'onClick', }, }, + topBlur: { + phasedRegistrationNames: { + captured: 'onBlurCapture', + bubbled: 'onBlur', + }, + }, + topFocus: { + phasedRegistrationNames: { + captured: 'onFocusCapture', + bubbled: 'onFocus', + }, + }, }; const directEventTypes = { diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts index 281195f3d90c78..1325e1d635d524 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts +++ b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts @@ -248,6 +248,10 @@ export interface TargetedEvent { target: number; } +export type BlurEvent = NativeSyntheticEvent; + +export type FocusEvent = NativeSyntheticEvent; + export interface PointerEvents { onPointerEnter?: ((event: PointerEvent) => void) | undefined; onPointerEnterCapture?: ((event: PointerEvent) => void) | undefined; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index b8d5903830c787..f87764b9cd15b8 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -2672,13 +2672,12 @@ type TextInputContentSizeChangeEventData = $ReadOnly<{ }>; export type TextInputContentSizeChangeEvent = NativeSyntheticEvent; +export type TextInputBlurEvent = BlurEvent; +export type TextInputFocusEvent = FocusEvent; type TargetEvent = $ReadOnly<{ target: number, ... }>; -type TextInputFocusEventData = TargetEvent; -export type TextInputBlurEvent = NativeSyntheticEvent; -export type TextInputFocusEvent = NativeSyntheticEvent; export type Selection = $ReadOnly<{ start: number, end: number, @@ -3010,10 +3009,12 @@ export type TextInputType = InternalTextInput & TextInputComponentStatics; exports[`public API should not change unintentionally Libraries/Components/TextInput/TextInput.js 1`] = ` "export type { AutoCapitalize, + BlurEvent, EnterKeyHintType, EnterKeyHintTypeAndroid, EnterKeyHintTypeIOS, EnterKeyHintTypeOptions, + FocusEvent, InputModeOptions, KeyboardType, KeyboardTypeAndroid, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 52cb13ccd4da8f..8520202731b81e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6777,9 +6777,12 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public static final field Companion Lcom/facebook/react/views/view/ReactViewManager$Companion; public static final field REACT_CLASS Ljava/lang/String; public fun ()V + public synthetic fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V + protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)V public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/view/ReactViewGroup; public fun getCommandsMap ()Ljava/util/Map; + public fun getExportedCustomBubblingEventTypeConstants ()Ljava/util/Map; public fun getName ()Ljava/lang/String; public fun nextFocusDown (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusForward (Lcom/facebook/react/views/view/ReactViewGroup;I)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt new file mode 100644 index 00000000000000..c40ab1bf706a2c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap + +/** Represents a View losing focus */ +internal class BlurEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + protected override fun getEventData(): WritableMap { + return Arguments.createMap().apply { putInt("target", viewTag) } + } + + internal companion object { + internal const val EVENT_NAME: String = "topBlur" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt new file mode 100644 index 00000000000000..4ee67a899aabf9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap + +/** Represents a View gaining focus */ +internal class FocusEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + protected override fun getEventData(): WritableMap { + return Arguments.createMap().apply { putInt("target", viewTag) } + } + + internal companion object { + internal const val EVENT_NAME: String = "topFocus" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.kt deleted file mode 100644 index b6ed75e71e78d2..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.textinput - -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.WritableMap -import com.facebook.react.uimanager.common.ViewUtil -import com.facebook.react.uimanager.events.Event - -/** Event emitted by EditText native view when it loses focus. */ -internal class ReactTextInputBlurEvent(surfaceId: Int, viewId: Int) : - Event(surfaceId, viewId) { - @Deprecated( - "Use the constructor with surfaceId instead", - ReplaceWith("ReactTextInputBlurEvent(surfaceId, viewId)")) - constructor(viewId: Int) : this(ViewUtil.NO_SURFACE_ID, viewId) - - override fun getEventName(): String = EVENT_NAME - - override fun canCoalesce(): Boolean = false - - override fun getEventData(): WritableMap { - return Arguments.createMap().apply { putInt("target", viewTag) } - } - - companion object { - private const val EVENT_NAME = "topBlur" - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.kt deleted file mode 100644 index c07657c0867a3f..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.textinput - -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.WritableMap -import com.facebook.react.uimanager.common.ViewUtil -import com.facebook.react.uimanager.events.Event - -/** Event emitted by EditText native view when it receives focus. */ -internal class ReactTextInputFocusEvent(surfaceId: Int, viewId: Int) : - Event(surfaceId, viewId) { - @Deprecated( - "Use the constructor with surfaceId instead", - ReplaceWith("ReactTextInputFocusEvent(surfaceId, viewId)")) - constructor(viewId: Int) : this(ViewUtil.NO_SURFACE_ID, viewId) - - override fun getEventName(): String = EVENT_NAME - - override fun getEventData(): WritableMap { - return Arguments.createMap().apply { putInt("target", viewTag) } - } - - override fun canCoalesce(): Boolean = false - - companion object { - private const val EVENT_NAME = "topFocus" - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index 38464155d8209f..daa8ccbd5fadb9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -52,7 +52,9 @@ import com.facebook.react.uimanager.ViewDefaults import com.facebook.react.uimanager.ViewProps import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactPropGroup +import com.facebook.react.uimanager.events.BlurEvent import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.FocusEvent import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderStyle.Companion.fromString import com.facebook.react.uimanager.style.LogicalEdge @@ -896,9 +898,9 @@ public open class ReactTextInputManager public constructor() : val surfaceId = reactContext.surfaceId val eventDispatcher = getEventDispatcher(reactContext, editText) if (hasFocus) { - eventDispatcher?.dispatchEvent(ReactTextInputFocusEvent(surfaceId, editText.id)) + eventDispatcher?.dispatchEvent(FocusEvent(surfaceId, editText.id)) } else { - eventDispatcher?.dispatchEvent(ReactTextInputBlurEvent(surfaceId, editText.id)) + eventDispatcher?.dispatchEvent(BlurEvent(surfaceId, editText.id)) eventDispatcher?.dispatchEvent( ReactTextInputEndEditingEvent(surfaceId, editText.id, editText.text.toString())) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index b7afbf717eaceb..baae64b04e9069 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -9,6 +9,7 @@ package com.facebook.react.views.view import android.graphics.Rect import android.view.View +import android.view.View.OnFocusChangeListener import com.facebook.common.logging.FLog import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.DynamicFromObject @@ -33,6 +34,8 @@ import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactPropGroup import com.facebook.react.uimanager.common.UIManagerType import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.events.BlurEvent +import com.facebook.react.uimanager.events.FocusEvent import com.facebook.react.uimanager.style.BackgroundImageLayer import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderStyle @@ -342,6 +345,40 @@ public open class ReactViewManager : ReactClippingViewManager() public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = ReactViewGroup(context) + override fun getExportedCustomBubblingEventTypeConstants(): Map { + val baseEventTypeConstants = super.getExportedCustomBubblingEventTypeConstants() + val eventTypeConstants = baseEventTypeConstants ?: mutableMapOf() + eventTypeConstants.putAll( + mapOf( + FocusEvent.EVENT_NAME to + mapOf( + "phasedRegistrationNames" to + mapOf("bubbled" to "onFocus", "captured" to "onFocusCapture")), + BlurEvent.EVENT_NAME to + mapOf( + "phasedRegistrationNames" to + mapOf("bubbled" to "onBlur", "captured" to "onBlurCapture")), + )) + return eventTypeConstants + } + + override fun addEventEmitters(reactContext: ThemedReactContext, view: ReactViewGroup) { + view.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> + val surfaceId = UIManagerHelper.getSurfaceId(view.context) + if (surfaceId == View.NO_ID) { + return@OnFocusChangeListener + } + val eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag((view.context as ReactContext), view.id) + ?: return@OnFocusChangeListener + if (hasFocus) { + eventDispatcher.dispatchEvent(FocusEvent(surfaceId, view.id)) + } else { + eventDispatcher.dispatchEvent(BlurEvent(surfaceId, view.id)) + } + } + } + override fun getCommandsMap(): MutableMap = mutableMapOf(HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, "setPressed" to CMD_SET_PRESSED) diff --git a/packages/react-native/types/__typetests__/index.tsx b/packages/react-native/types/__typetests__/index.tsx index 8172f0b21bb71d..47f720b21946a7 100644 --- a/packages/react-native/types/__typetests__/index.tsx +++ b/packages/react-native/types/__typetests__/index.tsx @@ -30,6 +30,7 @@ import { AppStateStatus, Appearance, BackHandler, + BlurEvent, Button, ColorValue, DevSettings, @@ -42,6 +43,7 @@ import { EventSubscription, FlatList, FlatListProps, + FocusEvent, GestureResponderEvent, HostComponent, I18nManager, @@ -1193,11 +1195,11 @@ class TextInputTest extends React.Component<{}, {username: string}> { console.log(`y: ${e.nativeEvent.contentOffset.y}`); }; - handleOnBlur = (e: TextInputFocusEvent) => { + handleOnBlur = (e: BlurEvent) => { testNativeSyntheticEvent(e); }; - handleOnFocus = (e: TextInputFocusEvent) => { + handleOnFocus = (e: FocusEvent) => { testNativeSyntheticEvent(e); }; From 02dba64dd4f3e493f9119b770b53653b52e7851e Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Tue, 27 May 2025 11:10:03 -0700 Subject: [PATCH 2/2] Add focus/blur example to RNTester Summary: Creates an example in the ViewExample setup for how onBlur/onFocus behave when using keyboard navigation Changelog: [Internal] Differential Revision: D75238317 --- .../rn-tester/js/examples/View/ViewExample.js | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index 3014fb1d040b36..cbddb935dc3ac9 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -14,6 +14,7 @@ import type {RNTesterModule} from '../../types/RNTesterTypes'; import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; +import {useState} from 'react'; import { Platform, PlatformColor, @@ -699,6 +700,74 @@ function BoxSizingExample(): React.Node { ); } +function FocusableInnerRow({focusable}: {focusable: boolean}) { + const styles = StyleSheet.create({ + focused: { + borderColor: 'blue', + borderWidth: 2, + }, + innerBox: { + backgroundColor: 'red', + width: '100%', + height: 50, + borderColor: 'transparent', + borderWidth: 2, + }, + innerBoxTextColor: { + color: 'white', + }, + }); + const [focused, setFocused] = useState(false); + return ( + setFocused(false)} + onFocus={() => setFocused(true)} + style={[styles.innerBox, focused && styles.focused]}> + + Focusable: {focusable ? 'true' : 'false'} + + + Focused: {focused ? 'true' : 'false'} + + + ); +} + +function FocusBlurExample(): React.Node { + const styles = StyleSheet.create({ + focused: { + borderColor: 'blue', + borderWidth: 2, + }, + outerBox: { + backgroundColor: 'green', + borderColor: 'transparent', + borderWidth: 2, + padding: 10, + }, + outerBoxTextColor: { + color: 'white', + }, + }); + const [outerFocused, setOuterFocused] = useState(false); + return ( + setOuterFocused(false)} + onFocus={() => setOuterFocused(true)} + style={[styles.outerBox, outerFocused && styles.focused]}> + + Focused: {outerFocused ? 'true' : 'false'} + + + + + + + ); +} + export default ({ title: 'View', documentationURL: 'https://reactnative.dev/docs/view', @@ -1363,5 +1432,10 @@ export default ({ name: 'box-sizing', render: BoxSizingExample, }, + { + title: 'Focus/Blur', + name: 'focus-blur', + render: FocusBlurExample, + }, ], }: RNTesterModule);