mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add focus and blur dispatching logic to BaseViewManager (#51724)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51724 Moves focus change listener logic from `ReactViewManager` to `BaseViewManager` so all view managers that extend the class can get focus/blur event dispatching for free. This does so by applying event listeners where `addEventEmitters` is called, so any extending classes must try to call `super.addEventEmitters` or implement it themselves. In the case of TextInput, this logic is re-implemented because the component emits an additional event when the text input is blurred and I wanted to avoid duplicate calls to get the event emitter for the view instance. In addition, I've added logic and a test case to ensure that any preexisting focus change listeners set on the view instance are called. There can only ever be one focus change listener tied to a view instance, so this ensures that ones created during view instantiation are retained. However, this does not guarantee that events are emitted for downstream users who overwrite the focus change listener later in the view's lifecycle (i.e. in response to a prop change or an extending view manager that doesn't call `super.addEventEmitters`). There is no clean way to enforce that the `BaseViewManager` focus change listener is always set without changing the generics and introducing a significant breaking change. Changelog: [Android][Added] - Adds support for onFocus/onBlur event dispatching logic to all native views that implement `BaseViewManager` Reviewed By: NickGerleman Differential Revision: D75579321 fbshipit-source-id: 02e1e6d0e78e9d05e4ec5bb59789f3097b73b3f8
This commit is contained in:
committed by
Facebook GitHub Bot
parent
cd5d74518b
commit
e960a28af7
@@ -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 <init> ()V
|
||||
public fun <init> (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 <init> ()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
|
||||
|
||||
+44
@@ -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<T extends View, C extends LayoutShadowNode
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull T view) {
|
||||
super.addEventEmitters(reactContext, view);
|
||||
|
||||
@Nullable OnFocusChangeListener originalFocusChangeListener = view.getOnFocusChangeListener();
|
||||
view.setOnFocusChangeListener(
|
||||
(v, hasFocus) -> {
|
||||
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<T extends View, C extends LayoutShadowNode
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onClick", "captured", "onClickCapture")))
|
||||
.put(
|
||||
"topBlur",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
|
||||
.put(
|
||||
"topFocus",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture")))
|
||||
.build());
|
||||
return eventTypeConstants;
|
||||
}
|
||||
|
||||
+3
-8
@@ -123,14 +123,6 @@ public open class ReactTextInputManager public constructor() :
|
||||
mapOf(
|
||||
"phasedRegistrationNames" to
|
||||
mapOf("bubbled" to "onEndEditing", "captured" to "onEndEditingCapture")),
|
||||
"topFocus" to
|
||||
mapOf(
|
||||
"phasedRegistrationNames" to
|
||||
mapOf("bubbled" to "onFocus", "captured" to "onFocusCapture")),
|
||||
"topBlur" to
|
||||
mapOf(
|
||||
"phasedRegistrationNames" to
|
||||
mapOf("bubbled" to "onBlur", "captured" to "onBlurCapture")),
|
||||
"topKeyPress" to
|
||||
mapOf(
|
||||
"phasedRegistrationNames" to
|
||||
@@ -894,6 +886,9 @@ public open class ReactTextInputManager public constructor() :
|
||||
override fun addEventEmitters(reactContext: ThemedReactContext, editText: ReactEditText) {
|
||||
editText.setEventDispatcher(getEventDispatcher(reactContext, editText))
|
||||
editText.addTextChangedListener(ReactTextInputTextWatcher(reactContext, editText))
|
||||
|
||||
// Implements focus/blur dispatching on behalf of BaseViewManager since only one focus listener
|
||||
// can be set on a view instance
|
||||
editText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
val surfaceId = reactContext.surfaceId
|
||||
val eventDispatcher = getEventDispatcher(reactContext, editText)
|
||||
|
||||
-37
@@ -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<ReactViewGroup>()
|
||||
public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
|
||||
ReactViewGroup(context)
|
||||
|
||||
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
||||
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<String, Int> =
|
||||
mutableMapOf(HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, "setPressed" to CMD_SET_PRESSED)
|
||||
|
||||
|
||||
+19
-1
@@ -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<ReactViewGroup, *>
|
||||
private lateinit var view: ReactViewGroup
|
||||
private lateinit var arguments: MockedStatic<Arguments>
|
||||
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`<WritableArray> { 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<OnFocusChangeListener>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user