Skip to content

Commit d8eb861

Browse files
Bud Rootcausefacebook-github-bot
authored andcommitted
Respect pointerEvents on Android SafeAreaView (#59576)
Summary: Android `SafeAreaView` ignored the `pointerEvents` prop, so a `SafeAreaView` with `pointerEvents="box-none"` still caught touches outside its children instead of passing them to the views underneath. `ReactSafeAreaView` now implements `ReactPointerEventsView` and `ReactSafeAreaViewManager` applies `pointerEvents` in `updateProperties`. Without the prop it stays `AUTO`, so existing behavior is unchanged. Changelog: [Android][Fixed] - SafeAreaView now respects pointerEvents Reviewed By: zeyap Differential Revision: D127611314
1 parent 25b05ac commit d8eb861

3 files changed

Lines changed: 116 additions & 1 deletion

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ import com.facebook.react.bridge.GuardedRunnable
1717
import com.facebook.react.bridge.WritableNativeMap
1818
import com.facebook.react.common.build.ReactBuildConfig
1919
import com.facebook.react.uimanager.PixelUtil.pxToDp
20+
import com.facebook.react.uimanager.PointerEvents
21+
import com.facebook.react.uimanager.ReactPointerEventsView
2022
import com.facebook.react.uimanager.StateWrapper
2123
import com.facebook.react.uimanager.ThemedReactContext
2224

23-
internal class ReactSafeAreaView(val reactContext: ThemedReactContext) : ViewGroup(reactContext) {
25+
internal class ReactSafeAreaView(val reactContext: ThemedReactContext) :
26+
ViewGroup(reactContext), ReactPointerEventsView {
2427
internal var stateWrapper: StateWrapper? = null
2528

29+
override var pointerEvents: PointerEvents = PointerEvents.AUTO
30+
2631
override fun onAttachedToWindow() {
2732
super.onAttachedToWindow()
2833

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
package com.facebook.react.views.safeareaview
99

1010
import com.facebook.react.module.annotations.ReactModule
11+
import com.facebook.react.uimanager.PointerEvents
1112
import com.facebook.react.uimanager.ReactStylesDiffMap
1213
import com.facebook.react.uimanager.StateWrapper
1314
import com.facebook.react.uimanager.ThemedReactContext
1415
import com.facebook.react.uimanager.ViewGroupManager
1516
import com.facebook.react.uimanager.ViewManagerDelegate
17+
import com.facebook.react.uimanager.ViewProps
1618
import com.facebook.react.viewmanagers.SafeAreaViewManagerDelegate
1719
import com.facebook.react.viewmanagers.SafeAreaViewManagerInterface
1820

@@ -30,6 +32,14 @@ internal class ReactSafeAreaViewManager :
3032

3133
override fun getName(): String = REACT_CLASS
3234

35+
override fun updateProperties(viewToUpdate: ReactSafeAreaView, props: ReactStylesDiffMap) {
36+
if (props.hasKey(ViewProps.POINTER_EVENTS)) {
37+
viewToUpdate.pointerEvents =
38+
PointerEvents.parsePointerEvents(props.getString(ViewProps.POINTER_EVENTS))
39+
}
40+
super.updateProperties(viewToUpdate, props)
41+
}
42+
3343
@Suppress("DEPRECATION")
3444
override fun createShadowNodeInstance(): com.facebook.react.uimanager.LayoutShadowNode =
3545
com.facebook.react.uimanager.LayoutShadowNode()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.safeareaview
9+
10+
import android.widget.FrameLayout
11+
import com.facebook.react.bridge.JavaOnlyMap
12+
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
13+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
14+
import com.facebook.react.uimanager.ReactStylesDiffMap
15+
import com.facebook.react.uimanager.ThemedReactContext
16+
import com.facebook.react.uimanager.TouchTargetHelper
17+
import com.facebook.react.views.view.ReactViewGroup
18+
import com.facebook.testutils.shadows.ShadowSoLoader
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.Before
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.robolectric.RobolectricTestRunner
24+
import org.robolectric.RuntimeEnvironment
25+
import org.robolectric.annotation.Config
26+
27+
@RunWith(RobolectricTestRunner::class)
28+
@Config(shadows = [ShadowSoLoader::class])
29+
class ReactSafeAreaViewManagerTest {
30+
31+
private lateinit var themedContext: ThemedReactContext
32+
private lateinit var root: FrameLayout
33+
private lateinit var safeArea: ReactSafeAreaView
34+
private val manager = ReactSafeAreaViewManager()
35+
36+
@Before
37+
fun setUp() {
38+
ReactNativeFeatureFlagsForTests.setUp()
39+
val context = createTestReactApplicationContext(RuntimeEnvironment.getApplication())
40+
themedContext = ThemedReactContext(context, context, null, -1)
41+
42+
root = FrameLayout(themedContext)
43+
root.id = ROOT_TAG
44+
root.measure(500, 500)
45+
root.layout(0, 0, 500, 500)
46+
47+
val underlying = ReactViewGroup(themedContext)
48+
underlying.id = UNDERLYING_TAG
49+
root.addView(underlying)
50+
underlying.layout(0, 0, 500, 500)
51+
52+
safeArea = ReactSafeAreaView(themedContext)
53+
safeArea.id = SAFE_AREA_TAG
54+
root.addView(safeArea)
55+
safeArea.layout(0, 300, 500, 500)
56+
57+
val toast = ReactViewGroup(themedContext)
58+
toast.id = TOAST_TAG
59+
safeArea.addView(toast)
60+
toast.layout(100, 0, 400, 100)
61+
}
62+
63+
@Test
64+
fun boxNone_touchOutsideChildrenReachesViewUnderneath() {
65+
setPointerEvents("box-none")
66+
67+
assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(UNDERLYING_TAG)
68+
}
69+
70+
@Test
71+
fun boxNone_touchOnChildStillReachesChild() {
72+
setPointerEvents("box-none")
73+
74+
assertThat(TouchTargetHelper.findTargetTagForTouch(200f, 350f, root)).isEqualTo(TOAST_TAG)
75+
}
76+
77+
@Test
78+
fun withoutPointerEvents_safeAreaViewReceivesTouchOutsideChildren() {
79+
assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(SAFE_AREA_TAG)
80+
}
81+
82+
@Test
83+
fun resettingPointerEvents_restoresDefaultHitTesting() {
84+
setPointerEvents("box-none")
85+
setPointerEvents(null)
86+
87+
assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(SAFE_AREA_TAG)
88+
}
89+
90+
private fun setPointerEvents(value: String?) {
91+
manager.updateProperties(safeArea, ReactStylesDiffMap(JavaOnlyMap.of("pointerEvents", value)))
92+
}
93+
94+
private companion object {
95+
const val ROOT_TAG = 1
96+
const val UNDERLYING_TAG = 2
97+
const val SAFE_AREA_TAG = 3
98+
const val TOAST_TAG = 4
99+
}
100+
}

0 commit comments

Comments
 (0)