diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 4e84da68b656..88e9c1ce3c98 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3659,6 +3659,7 @@ public class com/facebook/react/uimanager/ReactAccessibilityDelegate : androidx/ public static final field TOP_ACCESSIBILITY_ACTION_EVENT Ljava/lang/String; public static final field sActionIdMap Ljava/util/HashMap; public fun (Landroid/view/View;ZI)V + public fun cleanUp ()V public static fun createNodeInfoFromView (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat; public fun getAccessibilityNodeProvider (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat; protected fun getHostView ()Landroid/view/View; 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 ff335564809a..6fccc44a85e3 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 @@ -19,6 +19,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import com.facebook.common.logging.FLog; import com.facebook.react.R; @@ -186,6 +187,16 @@ public void onDropViewInstance(@NonNull T view) { if (focusChangeListener instanceof BaseVMFocusChangeListener) { ((BaseVMFocusChangeListener) focusChangeListener).detach(view); } + + AccessibilityDelegateCompat axDelegate = ViewCompat.getAccessibilityDelegate(view); + + if (axDelegate instanceof ReactAccessibilityDelegate) { + ((ReactAccessibilityDelegate) axDelegate).cleanUp(); + } + + if (view instanceof ViewGroup) { + ((ViewGroup) view).setOnHierarchyChangeListener(null); + } } // Currently, layout listener is only attached when transform or transformOrigin is set. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 3e85bd7c98df..598b46f2e7b1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -16,6 +16,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -72,6 +73,10 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { private Handler mHandler; private final HashMap mAccessibilityActionsMap; + @Nullable + private AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener = + null; + @Nullable View mAccessibilityLabelledBy; static { @@ -256,10 +261,29 @@ private void populateAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompa @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); // If we set an accessibility order then all the focusing logic should go through our custom // virtual view tree hierarchy and ignore the default path ReadableArray axOrderIds = (ReadableArray) mView.getTag(R.id.accessibility_order); if (axOrderIds != null && axOrderIds.size() != 0) { + info.setContentDescription(""); + info.setFocusable(false); + + AccessibilityManager am = + (AccessibilityManager) host.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (accessibilityStateChangeListener == null && am != null) { + AccessibilityManager.AccessibilityStateChangeListener newAccessibilityStateChangeListener = + enabled -> { + if (!enabled) { + ReactAxOrderHelper.restoreSubtreeFocusability(host); + host.setTag(R.id.accessibility_order_dirty, true); + } + }; + + am.addAccessibilityStateChangeListener(newAccessibilityStateChangeListener); + accessibilityStateChangeListener = newAccessibilityStateChangeListener; + } Boolean isAxOrderDirty = (Boolean) mView.getTag(R.id.accessibility_order_dirty); if (isAxOrderDirty != null && isAxOrderDirty) { @@ -278,7 +302,6 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo return; } - super.onInitializeAccessibilityNodeInfo(host, info); populateAccessibilityNodeInfo(host, info); } @@ -1098,4 +1121,17 @@ public static AccessibilityRole fromValue(@Nullable String value) { } } } + + // In case a view with accessibilityOrder is unmounted we need a way to clean up the listener on + // this delegate + public void cleanUp() { + if (accessibilityStateChangeListener != null) { + AccessibilityManager am = + (AccessibilityManager) mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (am != null) { + am.removeAccessibilityStateChangeListener(accessibilityStateChangeListener); + } + accessibilityStateChangeListener = null; + } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt index f8570f046fb5..12c139c8a50f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt @@ -10,7 +10,6 @@ package com.facebook.react.uimanager import android.graphics.Rect import android.view.View import android.view.ViewGroup -import android.widget.TextView import com.facebook.react.R import com.facebook.react.bridge.ReadableArray @@ -76,10 +75,6 @@ private object ReactAxOrderHelper { } } - if (!isIncluded && !isContained && parent != view && view !is TextView) { - view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO - } - // Don't traverse the children of a nested accessibility order if (view is ViewGroup) { val axChildren: ArrayList = getAxChildren(view) @@ -100,6 +95,13 @@ private object ReactAxOrderHelper { } } } + + if (!isIncluded && !isContained && parent != view) { + if (view.getTag(R.id.original_focusability) == null) { + view.setTag(R.id.original_focusability, view.isFocusable) + } + view.isFocusable = false + } } traverseAndBuildAxOrder( @@ -159,4 +161,21 @@ private object ReactAxOrderHelper { } return axChildren } + + @JvmStatic + public fun restoreSubtreeFocusability(view: View) { + val originalFocusability = view.getTag(R.id.original_focusability) + if (originalFocusability is Boolean) { + view.isFocusable = originalFocusability + } + + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + val child = view.getChildAt(i) + if (child != null) { + restoreSubtreeFocusability(child) + } + } + } + } } diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 5c1b9e434c8d..eb8ccbc8c808 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -15,6 +15,9 @@ + + +