diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a5fa388742a7..9f62f0c9b496 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5126,6 +5126,7 @@ public class com/facebook/react/viewmanagers/SafeAreaViewManagerDelegate : com/f } public abstract interface class com/facebook/react/viewmanagers/SafeAreaViewManagerInterface : com/facebook/react/uimanager/ViewManagerWithGeneratedInterface { + public abstract fun setPointerEvents (Landroid/view/View;Ljava/lang/String;)V } public class com/facebook/react/viewmanagers/UnimplementedNativeViewManagerDelegate : com/facebook/react/uimanager/BaseViewManagerDelegate { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt index 034879ca96e7..7c9223ca2aa0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt @@ -17,12 +17,17 @@ import com.facebook.react.bridge.GuardedRunnable import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.uimanager.PixelUtil.pxToDp +import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.ReactPointerEventsView import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext -internal class ReactSafeAreaView(val reactContext: ThemedReactContext) : ViewGroup(reactContext) { +internal class ReactSafeAreaView(val reactContext: ThemedReactContext) : + ViewGroup(reactContext), ReactPointerEventsView { internal var stateWrapper: StateWrapper? = null + override var pointerEvents: PointerEvents = PointerEvents.AUTO + override fun onAttachedToWindow() { super.onAttachedToWindow() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt index 4e0c7154b88e..da06598105c9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt @@ -8,11 +8,14 @@ package com.facebook.react.views.safeareaview import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.PointerEvents import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.ViewProps +import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.SafeAreaViewManagerDelegate import com.facebook.react.viewmanagers.SafeAreaViewManagerInterface @@ -30,6 +33,11 @@ internal class ReactSafeAreaViewManager : override fun getName(): String = REACT_CLASS + @ReactProp(name = ViewProps.POINTER_EVENTS) + override fun setPointerEvents(view: ReactSafeAreaView, value: String?) { + view.pointerEvents = PointerEvents.parsePointerEvents(value) + } + @Suppress("DEPRECATION") override fun createShadowNodeInstance(): com.facebook.react.uimanager.LayoutShadowNode = com.facebook.react.uimanager.LayoutShadowNode() diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManagerTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManagerTest.kt new file mode 100644 index 000000000000..48ea24a83b54 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManagerTest.kt @@ -0,0 +1,100 @@ +/* + * 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.safeareaview + +import android.widget.FrameLayout +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.TouchTargetHelper +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.testutils.shadows.ShadowSoLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowSoLoader::class]) +class ReactSafeAreaViewManagerTest { + + private lateinit var themedContext: ThemedReactContext + private lateinit var root: FrameLayout + private lateinit var safeArea: ReactSafeAreaView + private val manager = ReactSafeAreaViewManager() + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + val context = createTestReactApplicationContext(RuntimeEnvironment.getApplication()) + themedContext = ThemedReactContext(context, context, null, -1) + + root = FrameLayout(themedContext) + root.id = ROOT_TAG + root.measure(500, 500) + root.layout(0, 0, 500, 500) + + val underlying = ReactViewGroup(themedContext) + underlying.id = UNDERLYING_TAG + root.addView(underlying) + underlying.layout(0, 0, 500, 500) + + safeArea = ReactSafeAreaView(themedContext) + safeArea.id = SAFE_AREA_TAG + root.addView(safeArea) + safeArea.layout(0, 300, 500, 500) + + val toast = ReactViewGroup(themedContext) + toast.id = TOAST_TAG + safeArea.addView(toast) + toast.layout(100, 0, 400, 100) + } + + @Test + fun boxNone_touchOutsideChildrenReachesViewUnderneath() { + setPointerEvents("box-none") + + assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(UNDERLYING_TAG) + } + + @Test + fun boxNone_touchOnChildStillReachesChild() { + setPointerEvents("box-none") + + assertThat(TouchTargetHelper.findTargetTagForTouch(200f, 350f, root)).isEqualTo(TOAST_TAG) + } + + @Test + fun withoutPointerEvents_safeAreaViewReceivesTouchOutsideChildren() { + assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(SAFE_AREA_TAG) + } + + @Test + fun resettingPointerEvents_restoresDefaultHitTesting() { + setPointerEvents("box-none") + setPointerEvents(null) + + assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 380f, root)).isEqualTo(SAFE_AREA_TAG) + } + + private fun setPointerEvents(value: String?) { + manager.updateProperties(safeArea, ReactStylesDiffMap(JavaOnlyMap.of("pointerEvents", value))) + } + + private companion object { + const val ROOT_TAG = 1 + const val UNDERLYING_TAG = 2 + const val SAFE_AREA_TAG = 3 + const val TOAST_TAG = 4 + } +} diff --git a/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js b/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js index 0e5ef68ef9ed..3aec515d25ba 100644 --- a/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js +++ b/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js @@ -9,6 +9,7 @@ */ import type {ViewProps} from '../../../../../Libraries/Components/View/ViewPropTypes'; +import type {WithDefault} from '../../../../../Libraries/Types/CodegenTypes'; import type {HostComponent} from '../../../types/HostComponent'; import codegenNativeComponent from '../../../../../Libraries/Utilities/codegenNativeComponent'; @@ -16,7 +17,11 @@ import codegenNativeComponent from '../../../../../Libraries/Utilities/codegenNa type RCTSafeAreaViewNativeProps = Readonly<{ ...ViewProps, - // No props + // Declared so the generated Android delegate applies it to the native view. + pointerEvents?: WithDefault< + 'auto' | 'box-none' | 'box-only' | 'none', + 'auto', + >, }>; export default codegenNativeComponent(