Skip to content

Commit e960a28

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Add focus and blur dispatching logic to BaseViewManager (#51724)
Summary: Pull Request resolved: #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
1 parent cd5d745 commit e960a28

5 files changed

Lines changed: 67 additions & 49 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,6 +3388,7 @@ public final class com/facebook/react/uimanager/BackgroundStyleApplicator {
33883388
public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener {
33893389
public fun <init> ()V
33903390
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
3391+
protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V
33913392
public fun getExportedCustomBubblingEventTypeConstants ()Ljava/util/Map;
33923393
public fun getExportedCustomDirectEventTypeConstants ()Ljava/util/Map;
33933394
protected fun onAfterUpdateTransaction (Landroid/view/View;)V
@@ -6784,12 +6785,9 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
67846785
public static final field Companion Lcom/facebook/react/views/view/ReactViewManager$Companion;
67856786
public static final field REACT_CLASS Ljava/lang/String;
67866787
public fun <init> ()V
6787-
public synthetic fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V
6788-
protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)V
67896788
public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View;
67906789
public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/view/ReactViewGroup;
67916790
public fun getCommandsMap ()Ljava/util/Map;
6792-
public fun getExportedCustomBubblingEventTypeConstants ()Ljava/util/Map;
67936791
public fun getName ()Ljava/lang/String;
67946792
public fun nextFocusDown (Lcom/facebook/react/views/view/ReactViewGroup;I)V
67956793
public fun nextFocusForward (Lcom/facebook/react/views/view/ReactViewGroup;I)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import android.os.Build;
1313
import android.text.TextUtils;
1414
import android.view.View;
15+
import android.view.View.OnFocusChangeListener;
1516
import android.view.ViewGroup;
1617
import android.view.ViewParent;
1718
import android.view.accessibility.AccessibilityEvent;
@@ -35,6 +36,9 @@
3536
import com.facebook.react.uimanager.annotations.ReactProp;
3637
import com.facebook.react.uimanager.common.UIManagerType;
3738
import com.facebook.react.uimanager.common.ViewUtil;
39+
import com.facebook.react.uimanager.events.BlurEvent;
40+
import com.facebook.react.uimanager.events.EventDispatcher;
41+
import com.facebook.react.uimanager.events.FocusEvent;
3842
import com.facebook.react.uimanager.events.PointerEventHelper;
3943
import com.facebook.react.uimanager.style.OutlineStyle;
4044
import com.facebook.react.uimanager.util.ReactFindViewUtil;
@@ -165,6 +169,36 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
165169
return view;
166170
}
167171

172+
@Override
173+
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull T view) {
174+
super.addEventEmitters(reactContext, view);
175+
176+
@Nullable OnFocusChangeListener originalFocusChangeListener = view.getOnFocusChangeListener();
177+
view.setOnFocusChangeListener(
178+
(v, hasFocus) -> {
179+
if (originalFocusChangeListener != null) {
180+
originalFocusChangeListener.onFocusChange(v, hasFocus);
181+
}
182+
int surfaceId = UIManagerHelper.getSurfaceId(v.getContext());
183+
if (surfaceId == View.NO_ID) {
184+
return;
185+
}
186+
if (view.getContext() instanceof ThemedReactContext) {
187+
ThemedReactContext themedReactContext = (ThemedReactContext) v.getContext();
188+
@Nullable
189+
EventDispatcher eventDispatcher =
190+
UIManagerHelper.getEventDispatcherForReactTag(themedReactContext, view.getId());
191+
if (eventDispatcher != null) {
192+
if (hasFocus) {
193+
eventDispatcher.dispatchEvent(new FocusEvent(surfaceId, view.getId()));
194+
} else {
195+
eventDispatcher.dispatchEvent(new BlurEvent(surfaceId, view.getId()));
196+
}
197+
}
198+
}
199+
});
200+
}
201+
168202
// Currently, layout listener is only attached when transform or transformOrigin is set.
169203
@Override
170204
public void onLayoutChange(
@@ -778,6 +812,16 @@ protected void onAfterUpdateTransaction(@NonNull T view) {
778812
MapBuilder.of(
779813
"phasedRegistrationNames",
780814
MapBuilder.of("bubbled", "onClick", "captured", "onClickCapture")))
815+
.put(
816+
"topBlur",
817+
MapBuilder.of(
818+
"phasedRegistrationNames",
819+
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
820+
.put(
821+
"topFocus",
822+
MapBuilder.of(
823+
"phasedRegistrationNames",
824+
MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture")))
781825
.build());
782826
return eventTypeConstants;
783827
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,6 @@ public open class ReactTextInputManager public constructor() :
123123
mapOf(
124124
"phasedRegistrationNames" to
125125
mapOf("bubbled" to "onEndEditing", "captured" to "onEndEditingCapture")),
126-
"topFocus" to
127-
mapOf(
128-
"phasedRegistrationNames" to
129-
mapOf("bubbled" to "onFocus", "captured" to "onFocusCapture")),
130-
"topBlur" to
131-
mapOf(
132-
"phasedRegistrationNames" to
133-
mapOf("bubbled" to "onBlur", "captured" to "onBlurCapture")),
134126
"topKeyPress" to
135127
mapOf(
136128
"phasedRegistrationNames" to
@@ -894,6 +886,9 @@ public open class ReactTextInputManager public constructor() :
894886
override fun addEventEmitters(reactContext: ThemedReactContext, editText: ReactEditText) {
895887
editText.setEventDispatcher(getEventDispatcher(reactContext, editText))
896888
editText.addTextChangedListener(ReactTextInputTextWatcher(reactContext, editText))
889+
890+
// Implements focus/blur dispatching on behalf of BaseViewManager since only one focus listener
891+
// can be set on a view instance
897892
editText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
898893
val surfaceId = reactContext.surfaceId
899894
val eventDispatcher = getEventDispatcher(reactContext, editText)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package com.facebook.react.views.view
99

1010
import android.graphics.Rect
1111
import android.view.View
12-
import android.view.View.OnFocusChangeListener
1312
import com.facebook.common.logging.FLog
1413
import com.facebook.react.bridge.Dynamic
1514
import com.facebook.react.bridge.DynamicFromObject
@@ -34,8 +33,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
3433
import com.facebook.react.uimanager.annotations.ReactPropGroup
3534
import com.facebook.react.uimanager.common.UIManagerType
3635
import com.facebook.react.uimanager.common.ViewUtil
37-
import com.facebook.react.uimanager.events.BlurEvent
38-
import com.facebook.react.uimanager.events.FocusEvent
3936
import com.facebook.react.uimanager.style.BackgroundImageLayer
4037
import com.facebook.react.uimanager.style.BorderRadiusProp
4138
import com.facebook.react.uimanager.style.BorderStyle
@@ -345,40 +342,6 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
345342
public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
346343
ReactViewGroup(context)
347344

348-
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
349-
val baseEventTypeConstants = super.getExportedCustomBubblingEventTypeConstants()
350-
val eventTypeConstants = baseEventTypeConstants ?: mutableMapOf()
351-
eventTypeConstants.putAll(
352-
mapOf(
353-
FocusEvent.EVENT_NAME to
354-
mapOf(
355-
"phasedRegistrationNames" to
356-
mapOf("bubbled" to "onFocus", "captured" to "onFocusCapture")),
357-
BlurEvent.EVENT_NAME to
358-
mapOf(
359-
"phasedRegistrationNames" to
360-
mapOf("bubbled" to "onBlur", "captured" to "onBlurCapture")),
361-
))
362-
return eventTypeConstants
363-
}
364-
365-
override fun addEventEmitters(reactContext: ThemedReactContext, view: ReactViewGroup) {
366-
view.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
367-
val surfaceId = UIManagerHelper.getSurfaceId(view.context)
368-
if (surfaceId == View.NO_ID) {
369-
return@OnFocusChangeListener
370-
}
371-
val eventDispatcher =
372-
UIManagerHelper.getEventDispatcherForReactTag((view.context as ReactContext), view.id)
373-
?: return@OnFocusChangeListener
374-
if (hasFocus) {
375-
eventDispatcher.dispatchEvent(FocusEvent(surfaceId, view.id))
376-
} else {
377-
eventDispatcher.dispatchEvent(BlurEvent(surfaceId, view.id))
378-
}
379-
}
380-
}
381-
382345
override fun getCommandsMap(): MutableMap<String, Int> =
383346
mutableMapOf(HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, "setPressed" to CMD_SET_PRESSED)
384347

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
package com.facebook.react.uimanager
99

10+
import android.view.View.OnFocusChangeListener
1011
import com.facebook.react.R
1112
import com.facebook.react.bridge.Arguments
13+
import com.facebook.react.bridge.BridgeReactContext
1214
import com.facebook.react.bridge.JavaOnlyArray
1315
import com.facebook.react.bridge.JavaOnlyMap
1416
import com.facebook.react.bridge.WritableArray
@@ -23,6 +25,9 @@ import org.junit.Test
2325
import org.junit.runner.RunWith
2426
import org.mockito.MockedStatic
2527
import org.mockito.Mockito
28+
import org.mockito.kotlin.mock
29+
import org.mockito.kotlin.times
30+
import org.mockito.kotlin.verify
2631
import org.robolectric.RobolectricTestRunner
2732
import org.robolectric.RuntimeEnvironment
2833

@@ -31,12 +36,15 @@ class BaseViewManagerTest {
3136
private lateinit var viewManager: BaseViewManager<ReactViewGroup, *>
3237
private lateinit var view: ReactViewGroup
3338
private lateinit var arguments: MockedStatic<Arguments>
39+
private lateinit var themedReactContext: ThemedReactContext
3440

3541
@Before
3642
fun setUp() {
3743
ReactNativeFeatureFlagsForTests.setUp()
3844
viewManager = ReactViewManager()
39-
view = ReactViewGroup(RuntimeEnvironment.getApplication())
45+
val context = BridgeReactContext(RuntimeEnvironment.getApplication())
46+
themedReactContext = ThemedReactContext(context, context, null, -1)
47+
view = ReactViewGroup(themedReactContext)
4048
arguments = Mockito.mockStatic(Arguments::class.java)
4149
arguments.`when`<WritableArray> { Arguments.createMap() }.thenAnswer { JavaOnlyArray() }
4250
}
@@ -75,4 +83,14 @@ class BaseViewManagerTest {
7583
viewManager.setRole(view, "list")
7684
Assertions.assertThat(view.getTag(R.id.role)).isEqualTo(ReactAccessibilityDelegate.Role.LIST)
7785
}
86+
87+
@Test
88+
fun testAddEventEmittersDoesNotOverrideExistingEventEmitters() {
89+
val originalFocusListener = mock<OnFocusChangeListener>()
90+
view.onFocusChangeListener = originalFocusListener
91+
viewManager.addEventEmitters(themedReactContext, view)
92+
Assertions.assertThat(view.onFocusChangeListener).isNotEqualTo(originalFocusListener)
93+
view.onFocusChangeListener.onFocusChange(view, true)
94+
verify(originalFocusListener, times(1)).onFocusChange(view, true)
95+
}
7896
}

0 commit comments

Comments
 (0)