From 33475d8ebf2779fe6e3338f04ddaa8e86e66bbd3 Mon Sep 17 00:00:00 2001 From: Tomasz Wasilczyk Date: Thu, 23 Jul 2026 08:01:46 -0700 Subject: [PATCH] RN: populate IMPORTANT_FOR_INTERACTION_DONT_CLIP_DESCENDANTS Summary: Use overflow state to set IMPORTANT_FOR_INTERACTION_DONT_CLIP_DESCENDANTS Differential Revision: D112356198 --- .../view/ImportantForInteractionHelper.kt | 12 ++- .../react/views/view/ReactViewGroup.kt | 7 +- .../react/views/view/ReactViewManager.kt | 1 - .../view/ImportantForInteractionHelperTest.kt | 92 +++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ImportantForInteractionHelperTest.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ImportantForInteractionHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ImportantForInteractionHelper.kt index c20972602fa9..70bfb0f2a70a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ImportantForInteractionHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ImportantForInteractionHelper.kt @@ -9,7 +9,9 @@ package com.facebook.react.views.view import android.view.View import com.facebook.react.R +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.style.Overflow /** * Helper class for managing the important_for_interaction view tag. This tag determines how a view @@ -28,6 +30,8 @@ internal object ImportantForInteractionHelper { /** CSS pointer-events: auto heuristics. */ const val IMPORTANT_FOR_INTERACTION_AUTO_CSSPOINTEREVENTSAUTO: Int = 0x8 + const val IMPORTANT_FOR_INTERACTION_DONT_CLIP_DESCENDANTS: Int = 0x10 + /** * Sets the important_for_interaction tag on a view based on the given [PointerEvents] value. * @@ -37,9 +41,11 @@ internal object ImportantForInteractionHelper { * * @param view The view to set the tag on * @param pointerEvents The pointer events value to convert and set + * @param overflow The overflow value; descendants are not clipped when it is [Overflow.VISIBLE] */ @JvmStatic - fun setImportantForInteraction(view: View, pointerEvents: PointerEvents) { + fun setImportantForInteraction(view: View, pointerEvents: PointerEvents, overflow: Overflow) { + if (!ReactNativeFeatureFlags.syncAndroidClipBoundsWithOverflow()) return val value = when (pointerEvents) { PointerEvents.AUTO -> IMPORTANT_FOR_INTERACTION_AUTO_CSSPOINTEREVENTSAUTO @@ -49,7 +55,9 @@ internal object ImportantForInteractionHelper { IMPORTANT_FOR_INTERACTION_AUTO_CSSPOINTEREVENTSAUTO or IMPORTANT_FOR_INTERACTION_EXCLUDE_DESCENDANTS PointerEvents.BOX_NONE -> IMPORTANT_FOR_INTERACTION_NO - } + } or + (if (overflow == Overflow.VISIBLE) IMPORTANT_FOR_INTERACTION_DONT_CLIP_DESCENDANTS + else 0) view.setTag(R.id.important_for_interaction, value) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 864981003d3b..6d155ae9dc4b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -140,6 +140,10 @@ public open class ReactViewGroup public constructor(context: Context?) : public override var hitSlopRect: Rect? = null public override var pointerEvents: PointerEvents = PointerEvents.AUTO + set(value) { + field = value + ImportantForInteractionHelper.setImportantForInteraction(this, value, _overflow) + } public var axOrderList: MutableList? = null @@ -175,9 +179,9 @@ public open class ReactViewGroup public constructor(context: Context?) : allChildrenCount = 0 clippingRect = null hitSlopRect = null + // pointerEvents setter reads _overflow, so _overflow must be assigned first. _overflow = Overflow.VISIBLE pointerEvents = PointerEvents.AUTO - ImportantForInteractionHelper.setImportantForInteraction(this, pointerEvents) childrenLayoutChangeListener = null onInterceptTouchEventListener = null needsOffscreenAlphaCompositing = false @@ -828,6 +832,7 @@ public open class ReactViewGroup public constructor(context: Context?) : } set(overflow) { _overflow = Overflow.fromString(overflow) + ImportantForInteractionHelper.setImportantForInteraction(this, pointerEvents, _overflow) invalidate() } 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 81218b1a93b4..fe3f0b091db0 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 @@ -307,7 +307,6 @@ public open class ReactViewManager : ReactClippingViewManager() @ReactProp(name = ViewProps.POINTER_EVENTS) public open fun setPointerEvents(view: ReactViewGroup, pointerEventsStr: String?) { view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr) - ImportantForInteractionHelper.setImportantForInteraction(view, view.pointerEvents) } @ReactProp(name = "nativeBackgroundAndroid") diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ImportantForInteractionHelperTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ImportantForInteractionHelperTest.kt new file mode 100644 index 000000000000..cb9018cd40d9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ImportantForInteractionHelperTest.kt @@ -0,0 +1,92 @@ +/* + * 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.view + +import android.app.Activity +import android.content.Context +import android.view.View +import com.facebook.react.R +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.style.Overflow +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +/** Tests for [ImportantForInteractionHelper]. */ +@RunWith(RobolectricTestRunner::class) +class ImportantForInteractionHelperTest { + + private lateinit var context: Context + + private val dontClip = + ImportantForInteractionHelper.IMPORTANT_FOR_INTERACTION_DONT_CLIP_DESCENDANTS + private val auto = + ImportantForInteractionHelper.IMPORTANT_FOR_INTERACTION_AUTO_CSSPOINTEREVENTSAUTO + private val no = ImportantForInteractionHelper.IMPORTANT_FOR_INTERACTION_NO + private val exclude = ImportantForInteractionHelper.IMPORTANT_FOR_INTERACTION_EXCLUDE_DESCENDANTS + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun syncAndroidClipBoundsWithOverflow(): Boolean = true + } + ) + context = Robolectric.buildActivity(Activity::class.java).create().get() + } + + @After + fun tearDown() { + ReactNativeFeatureFlags.dangerouslyReset() + } + + private fun tagFor(pointerEvents: PointerEvents, overflow: Overflow): Int { + val view = View(context) + ImportantForInteractionHelper.setImportantForInteraction(view, pointerEvents, overflow) + return view.getTag(R.id.important_for_interaction) as Int + } + + @Test + fun overflowMapping() { + assertThat(tagFor(PointerEvents.AUTO, Overflow.VISIBLE) and dontClip).isEqualTo(dontClip) + assertThat(tagFor(PointerEvents.AUTO, Overflow.HIDDEN) and dontClip).isEqualTo(0) + assertThat(tagFor(PointerEvents.AUTO, Overflow.SCROLL) and dontClip).isEqualTo(0) + } + + @Test + fun pointerEventsMapping() { + assertThat(tagFor(PointerEvents.AUTO, Overflow.HIDDEN)).isEqualTo(auto) + assertThat(tagFor(PointerEvents.NONE, Overflow.HIDDEN)).isEqualTo(no or exclude) + assertThat(tagFor(PointerEvents.BOX_ONLY, Overflow.HIDDEN)).isEqualTo(auto or exclude) + assertThat(tagFor(PointerEvents.BOX_NONE, Overflow.HIDDEN)).isEqualTo(no) + } + + @Test + fun tagRecomputesOnProperties() { + val rvg = ReactViewGroup(context) + fun tag() = rvg.getTag(R.id.important_for_interaction) as Int + + // overflow defaults to VISIBLE + rvg.pointerEvents = PointerEvents.BOX_NONE + assertThat(tag()).isEqualTo(no or dontClip) + + rvg.overflow = "hidden" + assertThat(tag()).isEqualTo(no) + + rvg.pointerEvents = PointerEvents.AUTO + assertThat(tag()).isEqualTo(auto) + } +}