From 8de43b5834396391ee2c9f445673f814a81c6d61 Mon Sep 17 00:00:00 2001 From: Fabrizio Cucci Date: Mon, 27 Apr 2026 03:46:42 -0700 Subject: [PATCH 1/2] Add syncAndroidClipBoundsWithOverflow feature flag Summary: Add a new feature flag to gate overriding getClipBounds on Android views to return the padding box when overflow is hidden. This communicates the view's clipping region to the Android framework for correct getGlobalVisibleRect calculations (T253147322, T263753590). Changelog: [Internal] Reviewed By: twasilczyk Differential Revision: D102353841 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 ++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 ++- .../featureflags/ReactNativeFeatureFlags.h | 7 ++- .../ReactNativeFeatureFlagsAccessor.cpp | 52 +++++++++++++------ .../ReactNativeFeatureFlagsAccessor.h | 6 ++- .../ReactNativeFeatureFlagsDefaults.h | 6 ++- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 ++- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 10 ++++ .../featureflags/ReactNativeFeatureFlags.js | 7 ++- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 20 files changed, 152 insertions(+), 36 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 5a9736feef9..66c05902b4e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -480,6 +480,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun skipActivityIdentityAssertionOnHostPause(): Boolean = accessor.skipActivityIdentityAssertionOnHostPause() + /** + * Override getClipBounds on Android views to return the padding box when overflow is hidden + */ + @JvmStatic + public fun syncAndroidClipBoundsWithOverflow(): Boolean = accessor.syncAndroidClipBoundsWithOverflow() + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 31e7ff51beb..f5c895d05ab 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4f9f5c1c46217ed6802abd5f786aac19>> */ /** @@ -95,6 +95,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null @@ -787,6 +788,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { + var cached = syncAndroidClipBoundsWithOverflowCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.syncAndroidClipBoundsWithOverflow() + syncAndroidClipBoundsWithOverflowCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 345b7649dfd..02842143771 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<62d1f0fcdf8e3165480da12575d62826>> + * @generated SignedSource<<8916e9f4a938a69ff175c806db9835d4>> */ /** @@ -178,6 +178,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip @JvmStatic public external fun syncAndroidClipBoundsWithOverflow(): Boolean + @DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip @JvmStatic public external fun updateRuntimeShadowNodeReferencesOnCommit(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index e9d57e7423b..a9e1a1e0cb7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<84ac5b80585f9185c879c81822718d86>> + * @generated SignedSource<> */ /** @@ -173,6 +173,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun skipActivityIdentityAssertionOnHostPause(): Boolean = false + override fun syncAndroidClipBoundsWithOverflow(): Boolean = false + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false override fun updateRuntimeShadowNodeReferencesOnCommit(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index d076ea34d2f..698723ec7ef 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3574e23fe8f846e408964af26cf0dd4c>> + * @generated SignedSource<> */ /** @@ -99,6 +99,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null @@ -866,6 +867,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { + var cached = syncAndroidClipBoundsWithOverflowCache + if (cached == null) { + cached = currentProvider.syncAndroidClipBoundsWithOverflow() + accessedFeatureFlags.add("syncAndroidClipBoundsWithOverflow") + syncAndroidClipBoundsWithOverflowCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 6edb264c3c9..3bcc2f82ed2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9222fd90d191a91893e2c58770d83259>> + * @generated SignedSource<<536a5156deea17740dd24782bf79feb4>> */ /** @@ -173,6 +173,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip public fun syncAndroidClipBoundsWithOverflow(): Boolean + @DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip public fun updateRuntimeShadowNodeReferencesOnCommit(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 365b7f6392a..67a3144ccd0 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -489,6 +489,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool syncAndroidClipBoundsWithOverflow() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("syncAndroidClipBoundsWithOverflow"); + return method(javaProvider_); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("traceTurboModulePromiseRejectionsOnAndroid"); @@ -964,6 +970,11 @@ bool JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPaus return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); +} + bool JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); @@ -1300,6 +1311,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "skipActivityIdentityAssertionOnHostPause", JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPause), + makeNativeMethod( + "syncAndroidClipBoundsWithOverflow", + JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow), makeNativeMethod( "traceTurboModulePromiseRejectionsOnAndroid", JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 27d515ea516..aff905ed7d7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<773741594e911047dac28904e9fc9544>> + * @generated SignedSource<<4be32bad403baeca1a28f19ad181c42c>> */ /** @@ -255,6 +255,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool skipActivityIdentityAssertionOnHostPause( facebook::jni::alias_ref); + static bool syncAndroidClipBoundsWithOverflow( + facebook::jni::alias_ref); + static bool traceTurboModulePromiseRejectionsOnAndroid( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 071660787b0..c2a07d57023 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6a15a6444767342acf4da0c0c8aa6208>> + * @generated SignedSource<> */ /** @@ -326,6 +326,10 @@ bool ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause() { return getAccessor().skipActivityIdentityAssertionOnHostPause(); } +bool ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow() { + return getAccessor().syncAndroidClipBoundsWithOverflow(); +} + bool ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid() { return getAccessor().traceTurboModulePromiseRejectionsOnAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index dab48f3e43c..423e7765879 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<62d1840f9a39eaceaa800542b7351a41>> + * @generated SignedSource<<8e9b09843bf4a0312b254559a975f612>> */ /** @@ -414,6 +414,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool skipActivityIdentityAssertionOnHostPause(); + /** + * Override getClipBounds on Android views to return the padding box when overflow is hidden + */ + RN_EXPORT static bool syncAndroidClipBoundsWithOverflow(); + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index d4f6761af0e..948a6241e53 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -1379,6 +1379,24 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { + auto flagValue = syncAndroidClipBoundsWithOverflow_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(75, "syncAndroidClipBoundsWithOverflow"); + + flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); + syncAndroidClipBoundsWithOverflow_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid() { auto flagValue = traceTurboModulePromiseRejectionsOnAndroid_.load(); @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(76, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(77, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(78, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(79, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useFabricInterop"); + markFlagAsAccessed(80, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useLISAlgorithmInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useLISAlgorithmInDifferentiator"); + markFlagAsAccessed(81, "useLISAlgorithmInDifferentiator"); flagValue = currentProvider_->useLISAlgorithmInDifferentiator(); useLISAlgorithmInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(82, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useNestedScrollViewAndroid"); + markFlagAsAccessed(83, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useSharedAnimatedBackend"); + markFlagAsAccessed(84, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(85, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useTurboModuleInterop"); + markFlagAsAccessed(86, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useTurboModules"); + markFlagAsAccessed(87, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(88, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = flagValue; @@ -1622,7 +1640,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "viewCullingOutsetRatio"); + markFlagAsAccessed(89, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "viewTransitionEnabled"); + markFlagAsAccessed(90, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1658,7 +1676,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "virtualViewPrerenderRatio"); + markFlagAsAccessed(91, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 9529d8a5406..cf2ca1f872e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<11a1936e637ed40b79a1aedd02932e7b>> + * @generated SignedSource<<9334675799ea378311b8c675ed419b1d>> */ /** @@ -107,6 +107,7 @@ class ReactNativeFeatureFlagsAccessor { bool shouldPressibilityUseW3CPointerEventsForHover(); bool shouldTriggerResponderTransferOnScrollAndroid(); bool skipActivityIdentityAssertionOnHostPause(); + bool syncAndroidClipBoundsWithOverflow(); bool traceTurboModulePromiseRejectionsOnAndroid(); bool updateRuntimeShadowNodeReferencesOnCommit(); bool updateRuntimeShadowNodeReferencesOnCommitThread(); @@ -134,7 +135,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 91> accessedFeatureFlags_; + std::array, 92> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -211,6 +212,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> shouldPressibilityUseW3CPointerEventsForHover_; std::atomic> shouldTriggerResponderTransferOnScrollAndroid_; std::atomic> skipActivityIdentityAssertionOnHostPause_; + std::atomic> syncAndroidClipBoundsWithOverflow_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; std::atomic> updateRuntimeShadowNodeReferencesOnCommit_; std::atomic> updateRuntimeShadowNodeReferencesOnCommitThread_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index e37c310a2b4..bfbe407374a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6aa42056b9acfeb38e3ea612b6421a15>> + * @generated SignedSource<<4a2fd61cbcdb28042f09ccb03c970674>> */ /** @@ -327,6 +327,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool syncAndroidClipBoundsWithOverflow() override { + return false; + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 5269d2c64c8..1f9a7c1307f 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2a4b0d699f7e7e701defd972cd79b116>> + * @generated SignedSource<<7853bedb8a11b20a633eb6579257b4bc>> */ /** @@ -720,6 +720,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::skipActivityIdentityAssertionOnHostPause(); } + bool syncAndroidClipBoundsWithOverflow() override { + auto value = values_["syncAndroidClipBoundsWithOverflow"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::syncAndroidClipBoundsWithOverflow(); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { auto value = values_["traceTurboModulePromiseRejectionsOnAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 9eef5b56f5e..2acdef33604 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -100,6 +100,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool shouldPressibilityUseW3CPointerEventsForHover() = 0; virtual bool shouldTriggerResponderTransferOnScrollAndroid() = 0; virtual bool skipActivityIdentityAssertionOnHostPause() = 0; + virtual bool syncAndroidClipBoundsWithOverflow() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; virtual bool updateRuntimeShadowNodeReferencesOnCommit() = 0; virtual bool updateRuntimeShadowNodeReferencesOnCommitThread() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 5451b8c68cf..816c333b141 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4b821513e808ca071104f413e15c2fd9>> + * @generated SignedSource<> */ /** @@ -419,6 +419,11 @@ bool NativeReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause( return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool NativeReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); +} + bool NativeReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 844e1602154..9097a2a90c4 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<55da460bc2f8c915552eeae11f0b2e3e>> + * @generated SignedSource<<3f44b628a681f0d005827f51f4ad885e>> */ /** @@ -186,6 +186,8 @@ class NativeReactNativeFeatureFlags bool skipActivityIdentityAssertionOnHostPause(jsi::Runtime& runtime); + bool syncAndroidClipBoundsWithOverflow(jsi::Runtime& runtime); + bool traceTurboModulePromiseRejectionsOnAndroid(jsi::Runtime& runtime); bool updateRuntimeShadowNodeReferencesOnCommit(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 9c4788f6789..8f5856661e9 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -850,6 +850,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + syncAndroidClipBoundsWithOverflow: { + defaultValue: false, + metadata: { + description: + 'Override getClipBounds on Android views to return the padding box when overflow is hidden', + expectedReleaseValue: true, + purpose: 'release', + }, + ossReleaseStage: 'none', + }, traceTurboModulePromiseRejectionsOnAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 0ab8c557bff..ad14fb3138a 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1d648095164f22868bbdf3500668bb14>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -122,6 +122,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ shouldPressibilityUseW3CPointerEventsForHover: Getter, shouldTriggerResponderTransferOnScrollAndroid: Getter, skipActivityIdentityAssertionOnHostPause: Getter, + syncAndroidClipBoundsWithOverflow: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, updateRuntimeShadowNodeReferencesOnCommit: Getter, updateRuntimeShadowNodeReferencesOnCommitThread: Getter, @@ -504,6 +505,10 @@ export const shouldTriggerResponderTransferOnScrollAndroid: Getter = cr * Skip activity identity assertion in ReactHostImpl::onHostPause() */ export const skipActivityIdentityAssertionOnHostPause: Getter = createNativeFlagGetter('skipActivityIdentityAssertionOnHostPause', false); +/** + * Override getClipBounds on Android views to return the padding box when overflow is hidden + */ +export const syncAndroidClipBoundsWithOverflow: Getter = createNativeFlagGetter('syncAndroidClipBoundsWithOverflow', false); /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index a268a1c5144..c9116e01e89 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5dea8e41a09f4ed658ce248121a2e981>> + * @generated SignedSource<<5ab72b9af228bc7e591bc0addaf6150e>> * @flow strict * @noformat */ @@ -100,6 +100,7 @@ export interface Spec extends TurboModule { +shouldPressibilityUseW3CPointerEventsForHover?: () => boolean; +shouldTriggerResponderTransferOnScrollAndroid?: () => boolean; +skipActivityIdentityAssertionOnHostPause?: () => boolean; + +syncAndroidClipBoundsWithOverflow?: () => boolean; +traceTurboModulePromiseRejectionsOnAndroid?: () => boolean; +updateRuntimeShadowNodeReferencesOnCommit?: () => boolean; +updateRuntimeShadowNodeReferencesOnCommitThread?: () => boolean; From 7083a5760b56aa7953b15c114c29a6759fab91b1 Mon Sep 17 00:00:00 2001 From: Fabrizio Cucci Date: Mon, 27 Apr 2026 03:46:42 -0700 Subject: [PATCH 2/2] Override getClipBounds to expose overflow clipping to the framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Override `getClipBounds()` on ReactViewGroup to return the padding box rect when `overflow` is hidden or scroll, gated behind the `syncAndroidClipBoundsWithOverflow` feature flag. This exposes the view's clipping behavior to the Android framework via the standard `View.getClipBounds()` API: - `getClipBounds(Rect): Boolean` returns `true` and populates the rect with the padding box when overflow is hidden/scroll, or `false` when overflow is visible - `getClipBounds(): Rect?` returns the padding box rect or `null` respectively This allows framework-level systems to inspect a view and determine whether it clips its children by checking `getClipBounds() != null`, and to obtain the actual clipping region for `getGlobalVisibleRect()` calculations. ## Why not `setClipBounds()`? An alternative approach would be to call `View.setClipBounds(Rect)` which sets the `mClipBounds` field directly. This has the advantage of being picked up by AOSP's `ViewGroup.getChildVisibleRect()` (which reads `mClipBounds` via direct field access rather than calling `getClipBounds()`). However, `setClipBounds()` also propagates the clip rect to the RenderNode via `mRenderNode.setClipRect()`, which clips the view's own rendering — including its borders. It also requires maintaining a Rect that must be kept in sync across size changes, border width changes, and overflow changes. For now, the `getClipBounds()` override is sufficient because harvesting systems query the method rather than the field. If we find that AOSP's field-based path is also needed, we can switch to `setClipBounds()` in the future. Adds `BackgroundStyleApplicator.getPaddingBoxRect()` (internal via buck friend_paths) which computes the padding box (view bounds minus border insets) reusing the same border inset resolution logic as `clipToPaddingBox()`. Changelog: [Internal] Reviewed By: twasilczyk, javache Differential Revision: D102353840 --- .../ReactAndroid/api/ReactAndroid.api | 2 ++ .../uimanager/BackgroundStyleApplicator.kt | 29 ++++++++++++++++ .../react/views/view/ReactViewGroup.kt | 34 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 7239ad550ee..024da3c221b 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6457,6 +6457,8 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z public fun endViewTransition (Landroid/view/View;)V public final fun getAxOrderList ()Ljava/util/List; + public fun getClipBounds ()Landroid/graphics/Rect; + public fun getClipBounds (Landroid/graphics/Rect;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getHitSlopRect ()Landroid/graphics/Rect; public fun getOverflow ()Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index f7e7fade8c4..5582ad102d8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -465,6 +465,35 @@ public object BackgroundStyleApplicator { clipToPaddingBoxWithAntiAliasing(view, canvas, null) } + /** + * Populates [outRect] with the padding box rect of the view. + * + * The padding box is the area within the borders of the view. For views without a + * [CompositeBackgroundDrawable] or without borders, this returns the full view bounds. + * + * This is useful for overriding [View.getClipBounds] to communicate the view's clipping region to + * the Android framework (e.g. for [View.getGlobalVisibleRect] calculations). + * + * @param view The view whose padding box to compute + * @param outRect The rect to populate with the padding box bounds + */ + internal fun getPaddingBoxRect(view: View, outRect: Rect) { + val composite = getCompositeBackgroundDrawable(view) + val computedBorderInsets = + composite?.borderInsets?.resolve(composite.layoutDirection, view.context) + if (computedBorderInsets == null) { + outRect.set(0, 0, view.width, view.height) + return + } + + val left = (computedBorderInsets.left.dpToPx()).toInt() + val top = (computedBorderInsets.top.dpToPx()).toInt() + val right = (view.width.toFloat() - computedBorderInsets.right.dpToPx()).toInt() + val bottom = (view.height.toFloat() - computedBorderInsets.bottom.dpToPx()).toInt() + + outRect.set(left, top, right, bottom) + } + /** * Clips the canvas to the padding box of the view. * 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 5c63fed3be4..0d2f5faba3b 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 @@ -37,6 +37,7 @@ import com.facebook.react.touch.OnInterceptTouchEventListener import com.facebook.react.touch.ReactHitSlopView import com.facebook.react.touch.ReactInterceptingViewGroup import com.facebook.react.uimanager.BackgroundStyleApplicator.clipToPaddingBox +import com.facebook.react.uimanager.BackgroundStyleApplicator.getPaddingBoxRect import com.facebook.react.uimanager.BackgroundStyleApplicator.setBackgroundColor import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius @@ -820,6 +821,39 @@ public open class ReactViewGroup public constructor(context: Context?) : invalidate() } + /** + * Returns the clip bounds for this view based on the overflow property. + * + * When overflow is hidden or scroll, returns the padding box rect (the area inside the borders) + * so that systems querying [View.getClipBounds] can determine the view's clipping region. Returns + * null when overflow is visible (no clipping). + */ + override fun getClipBounds(): Rect? { + if ( + ReactNativeFeatureFlags.syncAndroidClipBoundsWithOverflow() && + _overflow != null && + _overflow != Overflow.VISIBLE + ) { + val rect = Rect() + getPaddingBoxRect(this, rect) + return rect + } + return super.getClipBounds() + } + + /** See [getClipBounds]. */ + override fun getClipBounds(outRect: Rect): Boolean { + if ( + ReactNativeFeatureFlags.syncAndroidClipBoundsWithOverflow() && + _overflow != null && + _overflow != Overflow.VISIBLE + ) { + getPaddingBoxRect(this, outRect) + return true + } + return super.getClipBounds(outRect) + } + override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) { if ( needsIsolatedLayer(this) &&