diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index ff33c2d5bdd6..1ab89cc87bbd 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3201,6 +3201,7 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo public fun onDropViewInstance (Landroid/view/View;)V public fun onLayoutChange (Landroid/view/View;IIIIIIII)V protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; + protected fun resetDisabledFromAccessibilityState (Landroid/view/View;)V public fun setAccessibilityActions (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V public fun setAccessibilityCollection (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V public fun setAccessibilityCollectionItem (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V @@ -6511,6 +6512,8 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V public fun receiveCommand (Lcom/facebook/react/views/view/ReactViewGroup;ILcom/facebook/react/bridge/ReadableArray;)V public fun receiveCommand (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public synthetic fun resetDisabledFromAccessibilityState (Landroid/view/View;)V + protected fun resetDisabledFromAccessibilityState (Lcom/facebook/react/views/view/ReactViewGroup;)V public fun setAccessibilityOrder (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V public fun setAccessible (Lcom/facebook/react/views/view/ReactViewGroup;Z)V public fun setBackfaceVisibility (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index db7cc32456fb..7458c513a3b2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -420,10 +420,8 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta } } - private static void resetDisabledFromAccessibilityState(@NonNull View view) { - if (Boolean.TRUE.equals(view.getTag(R.id.accessibility_state_disabled))) { - view.setEnabled(true); - } + protected void resetDisabledFromAccessibilityState(@NonNull T view) { + // Other managers may receive partial state updates or use enabled for properties like editable. view.setTag(R.id.accessibility_state_disabled, null); } 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 363f1336d000..843dbe5c4b86 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 @@ -182,6 +182,7 @@ public open class ReactViewGroup public constructor(context: Context?) : */ private fun initView() { clipChildren = false + isEnabled = true _removeClippedSubviews = false inSubviewClippingLoop = false 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 21ef11182da3..318777272647 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 @@ -10,6 +10,7 @@ package com.facebook.react.views.view import android.graphics.Rect import android.view.View import com.facebook.common.logging.FLog +import com.facebook.react.R import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.DynamicFromObject import com.facebook.react.bridge.JSApplicationIllegalArgumentException @@ -97,6 +98,16 @@ public open class ReactViewManager : ReactClippingViewManager() view.isFocusable = accessible } + override fun resetDisabledFromAccessibilityState(view: ReactViewGroup) { + if ( + javaClass == ReactViewManager::class.java && + view.getTag(R.id.accessibility_state_disabled) == true + ) { + view.isEnabled = true + } + super.resetDisabledFromAccessibilityState(view) + } + @ReactProp(name = ViewProps.ACCESSIBILITY_ORDER) public open fun setAccessibilityOrder(view: ReactViewGroup, nativeIds: ReadableArray?) { if (!ReactNativeFeatureFlags.enableAccessibilityOrder()) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt index 95e47b23e9a2..8c0df2d58b66 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt @@ -10,11 +10,13 @@ package com.facebook.react.uimanager import android.view.View.OnFocusChangeListener +import android.widget.FrameLayout import com.facebook.react.R import com.facebook.react.bridge.BridgeReactContext import com.facebook.react.bridge.DynamicFromObject import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.views.view.ReactViewGroup import com.facebook.react.views.view.ReactViewManager @@ -71,6 +73,51 @@ class BaseViewManagerTest { Assertions.assertThat(view.isSelected).isEqualTo(true) } + @Test + fun testClearingAccessibilityDisabledRestoresTapTarget() { + val root = FrameLayout(themedReactContext) + root.id = 1 + root.layout(0, 0, 300, 300) + view.id = 3 + root.addView(view) + view.layout(0, 0, 200, 100) + + for (state in listOf(JavaOnlyMap(), JavaOnlyMap.of("disabled", null), null)) { + viewManager.setViewState(view, JavaOnlyMap.of("disabled", true)) + Assertions.assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 50f, root)).isEqualTo(1) + + viewManager.setViewState(view, state) + + Assertions.assertThat(view.isEnabled).isTrue() + Assertions.assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 50f, root)).isEqualTo(3) + Assertions.assertThat(view.getTag(R.id.accessibility_state_disabled)).isNull() + } + } + + @Test + fun testClearingAccessibilityDisabledPreservesOtherState() { + val state = JavaOnlyMap.of("busy", true, "checked", "mixed") + viewManager.setViewState(view, JavaOnlyMap.of("disabled", true)) + + viewManager.setViewState(view, state) + + val nativeState = view.getTag(R.id.accessibility_state) as ReadableMap + Assertions.assertThat(view.isEnabled).isTrue() + Assertions.assertThat(nativeState.getBoolean("busy")).isTrue() + Assertions.assertThat(nativeState.getString("checked")).isEqualTo("mixed") + Assertions.assertThat(state.hasKey("disabled")).isFalse() + } + + @Test + fun testViewManagerSubclassPreservesDisabledDuringPartialUpdates() { + val subclassManager = object : ReactViewManager() {} + subclassManager.setViewState(view, JavaOnlyMap.of("disabled", true)) + + subclassManager.setViewState(view, JavaOnlyMap.of("busy", true)) + + Assertions.assertThat(view.isEnabled).isFalse() + } + @Test fun testAccessibilityStateDisabledRemovedReenablesView() { viewManager.setViewState(view, JavaOnlyMap.of("disabled", true)) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt index 216dd5522be9..e7194a7f9bcf 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt @@ -242,6 +242,18 @@ class ReactTextInputPropertyTest { assertThat(view.isEnabled).isTrue } + @Test + fun testClearingAccessibilityDisabledPreservesNonEditableInput() { + manager.updateProperties( + view, + buildStyles("editable", false, "accessibilityState", JavaOnlyMap.of("disabled", true)), + ) + + manager.updateProperties(view, buildStyles("accessibilityState", JavaOnlyMap.of("busy", true))) + + assertThat(view.isEnabled).isFalse() + } + @Test fun testAutoCompleteExtendedHints() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt index 56383ff7efeb..0a3851f35fdb 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt @@ -34,6 +34,16 @@ class ReactViewGroupTest { context = Robolectric.buildActivity(Activity::class.java).create().get() } + @Test + fun `recycling restores enabled state`() { + val view = ReactViewGroup(context) + view.isEnabled = false + + view.recycleView() + + assertThat(view.isEnabled).isTrue() + } + @Test fun `View clipping - ensure allChildren properly resizes when adding views in sequence`() { val rvg = ReactViewGroup(context) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index 8dc2c403abc8..cbc996e5120f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -8,6 +8,7 @@ #include "HostPlatformViewProps.h" #include +#include #include #include @@ -842,6 +843,10 @@ folly::dynamic HostPlatformViewProps::getDiffProps( if (accessibilityState != oldProps->accessibilityState) { updateAccessibilityStateProp( result, accessibilityState, oldProps->accessibilityState); + if (std::strcmp(getDiffPropsImplementationTarget(), "View") == 0) { + result["accessibilityState"]["disabled"] = + accessibilityState.has_value() && accessibilityState->disabled; + } } if (accessibilityLabel != oldProps->accessibilityLabel) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/AccessibilityPropsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/AccessibilityPropsTest.cpp index 38c1d82b9694..35fea9f432a4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/AccessibilityPropsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/AccessibilityPropsTest.cpp @@ -185,4 +185,65 @@ TEST(AccessibilityPropsTest, unset_selected_omits_selected_trait) { hasTrait(props.accessibilityTraits, AccessibilityTraits::Selected)); } +#if defined(ANDROID) && defined(RN_SERIALIZABLE_STATE) + +TEST(AccessibilityPropsTest, keeps_view_disabled_during_busy_updates) { + ViewProps oldProps; + oldProps.accessibilityState = AccessibilityState{.disabled = true}; + ViewProps newProps = oldProps; + newProps.accessibilityState->busy = true; + + auto diff = newProps.getDiffProps(&oldProps); + + EXPECT_EQ(diff["accessibilityState"]["disabled"], true); + EXPECT_EQ(diff["accessibilityState"]["busy"], true); +} + +TEST(AccessibilityPropsTest, keeps_view_enabled_during_busy_updates) { + ViewProps oldProps; + oldProps.accessibilityState = AccessibilityState{}; + ViewProps newProps = oldProps; + newProps.accessibilityState->busy = true; + + auto diff = newProps.getDiffProps(&oldProps); + + EXPECT_EQ(diff["accessibilityState"]["disabled"], false); +} + +TEST( + AccessibilityPropsTest, + removing_view_accessibility_state_clears_disabled) { + ViewProps oldProps; + oldProps.accessibilityState = AccessibilityState{.disabled = true}; + ViewProps newProps = oldProps; + newProps.accessibilityState.reset(); + + auto diff = newProps.getDiffProps(&oldProps); + + EXPECT_EQ(diff["accessibilityState"]["disabled"], false); +} + +TEST( + AccessibilityPropsTest, + leaves_other_components_disabled_updates_unchanged) { + class CustomViewProps : public ViewProps { + public: + ComponentName getDiffPropsImplementationTarget() const override { + return "CustomView"; + } + }; + + CustomViewProps oldProps; + oldProps.accessibilityState = AccessibilityState{.disabled = true}; + CustomViewProps newProps = oldProps; + newProps.accessibilityState->busy = true; + + auto diff = newProps.getDiffProps(&oldProps); + + EXPECT_EQ(diff["accessibilityState"].count("disabled"), 0); + EXPECT_EQ(diff["accessibilityState"]["busy"], true); +} + +#endif + } // namespace facebook::react