diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 6e087d2273d..30fd9846728 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3388,6 +3388,7 @@ public final class com/facebook/react/uimanager/BackgroundStyleApplicator { public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener { public fun ()V public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V + protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V public fun getExportedCustomBubblingEventTypeConstants ()Ljava/util/Map; public fun getExportedCustomDirectEventTypeConstants ()Ljava/util/Map; protected fun onAfterUpdateTransaction (Landroid/view/View;)V @@ -6784,12 +6785,9 @@ 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/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 97501e341c9..5b9ce2188f1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -12,6 +12,7 @@ import android.graphics.Paint; import android.os.Build; import android.text.TextUtils; import android.view.View; +import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; @@ -35,6 +36,9 @@ import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.annotations.ReactProp; 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.EventDispatcher; +import com.facebook.react.uimanager.events.FocusEvent; import com.facebook.react.uimanager.events.PointerEventHelper; import com.facebook.react.uimanager.style.OutlineStyle; import com.facebook.react.uimanager.util.ReactFindViewUtil; @@ -165,6 +169,36 @@ public abstract class BaseViewManager { + if (originalFocusChangeListener != null) { + originalFocusChangeListener.onFocusChange(v, hasFocus); + } + int surfaceId = UIManagerHelper.getSurfaceId(v.getContext()); + if (surfaceId == View.NO_ID) { + return; + } + if (view.getContext() instanceof ThemedReactContext) { + ThemedReactContext themedReactContext = (ThemedReactContext) v.getContext(); + @Nullable + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(themedReactContext, view.getId()); + if (eventDispatcher != null) { + if (hasFocus) { + eventDispatcher.dispatchEvent(new FocusEvent(surfaceId, view.getId())); + } else { + eventDispatcher.dispatchEvent(new BlurEvent(surfaceId, view.getId())); + } + } + } + }); + } + // Currently, layout listener is only attached when transform or transformOrigin is set. @Override public void onLayoutChange( @@ -778,6 +812,16 @@ public abstract class BaseViewManager val surfaceId = reactContext.surfaceId val eventDispatcher = getEventDispatcher(reactContext, editText) 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 c40e222946b..7f16d4e4c32 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,7 +9,6 @@ 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 @@ -34,8 +33,6 @@ 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 @@ -345,40 +342,6 @@ 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/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt index 24ccffee48a..f99ded815f2 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt @@ -7,8 +7,10 @@ package com.facebook.react.uimanager +import android.view.View.OnFocusChangeListener import com.facebook.react.R import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.BridgeReactContext import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.WritableArray @@ -23,6 +25,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockedStatic import org.mockito.Mockito +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @@ -31,12 +36,15 @@ class BaseViewManagerTest { private lateinit var viewManager: BaseViewManager private lateinit var view: ReactViewGroup private lateinit var arguments: MockedStatic + private lateinit var themedReactContext: ThemedReactContext @Before fun setUp() { ReactNativeFeatureFlagsForTests.setUp() viewManager = ReactViewManager() - view = ReactViewGroup(RuntimeEnvironment.getApplication()) + val context = BridgeReactContext(RuntimeEnvironment.getApplication()) + themedReactContext = ThemedReactContext(context, context, null, -1) + view = ReactViewGroup(themedReactContext) arguments = Mockito.mockStatic(Arguments::class.java) arguments.`when` { Arguments.createMap() }.thenAnswer { JavaOnlyArray() } } @@ -75,4 +83,14 @@ class BaseViewManagerTest { viewManager.setRole(view, "list") Assertions.assertThat(view.getTag(R.id.role)).isEqualTo(ReactAccessibilityDelegate.Role.LIST) } + + @Test + fun testAddEventEmittersDoesNotOverrideExistingEventEmitters() { + val originalFocusListener = mock() + view.onFocusChangeListener = originalFocusListener + viewManager.addEventEmitters(themedReactContext, view) + Assertions.assertThat(view.onFocusChangeListener).isNotEqualTo(originalFocusListener) + view.onFocusChangeListener.onFocusChange(view, true) + verify(originalFocusListener, times(1)).onFocusChange(view, true) + } }