From e0a9ab5b9bdf2924ead9ed5738cda8f30fd794c7 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 3 Nov 2026 00:30:06 +0300 Subject: [PATCH] Keep Android disabled-state resets scoped to core views Clearing accessibilityState.disabled re-enabled every view whose disabled state came from accessibility, so a TextInput with editable={false} became editable again. BaseViewManager now tracks and clears the accessibility disabled tag in setViewState and calls the protected resetDisabledFromAccessibilityState hook only when accessibility had disabled the view. The base hook does nothing; ReactViewManager overrides it to re-enable the view, so core views and their subclasses keep the Pressable fix while other managers keep the enabled state they control. --- .../ReactAndroid/api/ReactAndroid.api | 3 ++ .../react/uimanager/BaseViewManager.java | 27 +++++------ .../react/views/view/ReactViewManager.kt | 4 ++ .../react/uimanager/BaseViewManagerTest.kt | 48 +++++++++++++++++++ .../textinput/ReactTextInputPropertyTest.kt | 14 ++++++ 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a5fa388742a7..526f6f266bbc 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 @@ -6520,6 +6521,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 bef69e2102f3..6c416464aad6 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 @@ -397,8 +397,19 @@ public void setAccessibilityCollectionItem( @ReactProp(name = ViewProps.ACCESSIBILITY_STATE) public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilityState) { + if (accessibilityState != null + && accessibilityState.hasKey("disabled") + && !accessibilityState.isNull("disabled")) { + boolean disabled = accessibilityState.getBoolean("disabled"); + view.setEnabled(!disabled); + view.setTag(R.id.accessibility_state_disabled, disabled); + } else { + if (Boolean.TRUE.equals(view.getTag(R.id.accessibility_state_disabled))) { + resetDisabledFromAccessibilityState(view); + } + view.setTag(R.id.accessibility_state_disabled, null); + } if (accessibilityState == null) { - resetDisabledFromAccessibilityState(view); return; } if (accessibilityState.hasKey("expanded")) { @@ -420,13 +431,6 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta view.setSelected(false); } view.setTag(R.id.accessibility_state, accessibilityState); - if (accessibilityState.hasKey("disabled") && !accessibilityState.isNull("disabled")) { - boolean disabled = accessibilityState.getBoolean("disabled"); - view.setEnabled(!disabled); - view.setTag(R.id.accessibility_state_disabled, disabled); - } else { - resetDisabledFromAccessibilityState(view); - } // For states which don't have corresponding methods in // AccessibilityNodeInfo, update the view's content description @@ -452,12 +456,7 @@ 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); - } - view.setTag(R.id.accessibility_state_disabled, null); - } + protected void resetDisabledFromAccessibilityState(@NonNull T view) {} private void updateViewContentDescription(@NonNull T view) { final String accessibilityLabel = (String) view.getTag(R.id.accessibility_label); 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..94d1e25c8b1a 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 @@ -97,6 +97,10 @@ public open class ReactViewManager : ReactClippingViewManager() view.isFocusable = accessible } + override fun resetDisabledFromAccessibilityState(view: ReactViewGroup) { + view.isEnabled = true + } + @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 666aa2bd71e2..654707d717d3 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 @@ -13,12 +13,14 @@ import android.util.DisplayMetrics import android.view.View import android.view.View.OnFocusChangeListener import android.view.accessibility.AccessibilityNodeInfo +import android.widget.FrameLayout import androidx.core.view.ViewCompat 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.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests @@ -95,6 +97,52 @@ 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 testViewManagerSubclassReenablesViewWhenAccessibilityDisabledIsCleared() { + val subclassManager = object : ReactViewManager() {} + subclassManager.setViewState(view, JavaOnlyMap.of("disabled", true)) + + subclassManager.setViewState(view, JavaOnlyMap.of("busy", true)) + + Assertions.assertThat(view.isEnabled).isTrue() + Assertions.assertThat(view.getTag(R.id.accessibility_state_disabled)).isNull() + } + @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 dac9993d5ecc..79d06d709e2a 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 @@ -238,6 +238,20 @@ class ReactTextInputPropertyTest { assertThat(view.isEnabled).isTrue } + @Test + fun testClearingAccessibilityDisabledPreservesNonEditableInput() { + for (state in listOf(JavaOnlyMap(), JavaOnlyMap.of("disabled", null), null)) { + manager.updateProperties( + view, + buildStyles("editable", false, "accessibilityState", JavaOnlyMap.of("disabled", true)), + ) + + manager.updateProperties(view, buildStyles("accessibilityState", state)) + + assertThat(view.isEnabled).isFalse() + } + } + @Test fun testAutoCompleteExtendedHints() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {