From 5b245767d615d0fc58a44de7d46b33be2103e304 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Thu, 3 Jul 2025 12:56:35 -0700 Subject: [PATCH] Remove Virtual View accessibilityOrder implementation (#52297) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52297 Doing virtual views is the only way of making it possible to add the host view into the order. This however is too complex for very little gain, we are opting to go for a cleaner solution with the trade off of not being able to add the host view. Changelog: [Internal] Reviewed By: joevilches Differential Revision: D77278752 fbshipit-source-id: 709b995f51a9a03f6d07f2e24f8aea21d62d95c4 --- .../ReactAndroid/api/ReactAndroid.api | 1 - .../react/uimanager/BaseViewManager.java | 7 - .../uimanager/ReactAccessibilityDelegate.java | 130 +------------ .../react/uimanager/ReactAxOrderHelper.kt | 181 ------------------ 4 files changed, 8 insertions(+), 311 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9aab9261e2c..158530563dc 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3659,7 +3659,6 @@ 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 6fccc44a85e..07ac1cad632 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,7 +19,6 @@ import android.view.accessibility.AccessibilityEvent; 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; @@ -188,12 +187,6 @@ public abstract class BaseViewManager mAxOrderViews; private Handler mHandler; private final HashMap mAccessibilityActionsMap; - @Nullable - private AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener = - null; - @Nullable View mAccessibilityLabelledBy; static { @@ -116,7 +107,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { // problems, so leave it alone. if (!ViewCompat.hasAccessibilityDelegate(view) && (view.getTag(R.id.accessibility_role) != null - || view.getTag(R.id.accessibility_order) != null || view.getTag(R.id.accessibility_state) != null || view.getTag(R.id.accessibility_actions) != null || view.getTag(R.id.react_test_id) != null @@ -142,7 +132,10 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { return mView; } - private void populateAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (host.getTag(R.id.accessibility_state_expanded) != null) { final boolean accessibilityStateExpanded = (boolean) host.getTag(R.id.accessibility_state_expanded); @@ -259,52 +252,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { } } - @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) { - List axOrderIdsList = new ArrayList<>(); - Set axOrderSet = new HashSet<>(); - for (int i = 0; i < axOrderIds.size(); i++) { - String id = axOrderIds.getString(i); - if (id != null) { - axOrderIdsList.add(id); - axOrderSet.add(id); - } - } - - mAxOrderViews = ReactAxOrderHelper.processAxOrderTree(mView, axOrderIdsList, axOrderSet); - } - return; - } - - populateAccessibilityNodeInfo(host, info); - } - @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); @@ -475,61 +422,17 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { @Override protected int getVirtualViewAt(float x, float y) { - if (mAxOrderViews == null) { - return HOST_ID; - } - - int closestViewId = HOST_ID; - int smallestArea = Integer.MAX_VALUE; - - for (int i = 0; i < mAxOrderViews.size(); i++) { - Rect bounds = ReactAxOrderHelper.getVirtualViewBounds(mView, mAxOrderViews.get(i)); - if (bounds.contains((int) x, (int) y)) { - int area = bounds.width() * bounds.height(); - if (area < smallestArea) { - smallestArea = area; - closestViewId = i; - } - } - } - - return closestViewId; + return INVALID_ID; } @Override - protected void getVisibleVirtualViews(List virtualViewIds) { - if (mAxOrderViews != null && !mAxOrderViews.isEmpty()) { - for (int i = 0; i < mAxOrderViews.size(); i++) { - virtualViewIds.add(i); - } - } - } + protected void getVisibleVirtualViews(List virtualViewIds) {} @Override protected void onPopulateNodeForVirtualView( int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) { - if (mView.getTag(R.id.accessibility_order) != null) { - if (mAxOrderViews.size() <= virtualViewId) { - node.setContentDescription(""); - node.setBoundsInParent(new Rect(0, 0, 1, 1)); - return; - } - - View virtualView = mAxOrderViews.get(virtualViewId); - - node.setContentDescription(""); - if (virtualView == mView) { - if (mView.getContentDescription() != null) { - node.setContentDescription(mView.getContentDescription()); - } - - populateAccessibilityNodeInfo(mView, node); - node.setBoundsInParent(new Rect(0, 0, mView.getWidth(), mView.getHeight())); - } else { - node.setBoundsInParent(ReactAxOrderHelper.getVirtualViewBounds(mView, virtualView)); - } - node.addChild(virtualView); - } + node.setContentDescription(""); + node.setBoundsInParent(new Rect(0, 0, 1, 1)); } @Override @@ -540,10 +443,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { @Override public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { - if (mView.getTag(R.id.accessibility_order) != null) { - return super.getAccessibilityNodeProvider(host); - } - return null; } @@ -1121,17 +1020,4 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { } } } - - // 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 deleted file mode 100644 index 12c139c8a50..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.uimanager - -import android.graphics.Rect -import android.view.View -import android.view.ViewGroup -import com.facebook.react.R -import com.facebook.react.bridge.ReadableArray - -private object ReactAxOrderHelper { - /** - * Processes the View tree that begins at the View with AccessibilityOrder set - * - * Disables accessibility for views not included in the specified accessibility order. - * - * This method emulates iOS's focusing order behavior to facilitate cross-platform code sharing. - * It disables accessibility for views that are either not part of the accessibility order or - * don't have a container that belongs to the accessibility order. - * - * The container/element concept is borrowed from iOS, where a "container" is a non-accessible - * view with children, and an "element" is any accessible view. - * - * @return an array of views following the accessibility order - */ - @JvmStatic - fun processAxOrderTree( - root: View, - axOrderIds: MutableList, - axOrderSet: MutableSet - ): List { - val axOrderViews = Array(axOrderIds.size) { mutableListOf() }.toMutableList() - - fun traverseAndBuildAxOrder(parent: View, view: View, containerId: String?) { - val nativeId = view.getTag(R.id.view_tag_native_id) as String? - - val isContained = (containerId != null && axOrderSet.contains(containerId)) - val isIncluded = (nativeId != null && axOrderSet.contains(nativeId)) - - val isNestedAxOrder = view.getTag(R.id.accessibility_order) != null && view != parent - - if (isIncluded && view.isFocusable) { - axOrderViews[axOrderIds.indexOf(nativeId)].add(view) - if (parent != view) { - view.setTag(R.id.accessibility_order_parent, parent) - } - } else if (isContained && view.isFocusable) { - axOrderViews[axOrderIds.indexOf(containerId)].add(view) - if (parent != view) { - view.setTag(R.id.accessibility_order_parent, parent) - } - } - - if (isNestedAxOrder) { - val nestedOrder = view.getTag(R.id.accessibility_order) as ReadableArray - for (i in 0 until nestedOrder.size()) { - val id = nestedOrder.getString(i) - if (id != null) { - val insertIdx = axOrderIds.indexOf(nativeId) + 1 - if (insertIdx < axOrderIds.size) { - axOrderIds.add(axOrderIds.indexOf(nativeId) + 1 + i, id) - axOrderViews.add(axOrderIds.indexOf(nativeId) + 1 + i, mutableListOf()) - } else { - axOrderIds.add(id) - axOrderViews.add(mutableListOf()) - } - - axOrderSet.add(id) - } - } - } - - // Don't traverse the children of a nested accessibility order - if (view is ViewGroup) { - val axChildren: ArrayList = getAxChildren(view) - - // If the View is a "container" (Not focusable but is included in the order) We add all its - // children to the order. - if (containerId != null) { - for (i in 0 until axChildren.size) { - traverseAndBuildAxOrder(parent, axChildren[i], containerId) - } - } else if (!view.isFocusable && isIncluded) { - for (i in 0 until axChildren.size) { - traverseAndBuildAxOrder(parent, axChildren[i], nativeId) - } - } else { - for (i in 0 until axChildren.size) { - traverseAndBuildAxOrder(parent, axChildren[i], null) - } - } - } - - 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( - root, - root, - null, - ) - - val result = mutableListOf() - for (viewList in axOrderViews) { - for (view in viewList) { - if (view != null) { - result.add(view) - } - } - } - - root.setTag(R.id.accessibility_order_dirty, false) - - return result - } - - @JvmStatic - fun getVirtualViewBounds(host: View, virtualView: View): Rect { - var currentView: View = virtualView - val viewBoundsInParent = - Rect(virtualView.left, virtualView.top, virtualView.right, virtualView.bottom) - while (currentView.parent != host && currentView != host) { - val parent = currentView.parent as View - viewBoundsInParent.top += parent.top - viewBoundsInParent.bottom += parent.top - viewBoundsInParent.left += parent.left - viewBoundsInParent.right += parent.left - currentView = parent - } - - return viewBoundsInParent - } - - private fun getAxChildren(host: ViewGroup): ArrayList { - val axChildren: ArrayList = ArrayList() - - // When host has an accessibilityNodeProvider it means the order is not default so ViewGroup's - // method bails, in this case we should just add the children to the node in the edge case where - // we need to coopt at accessibilityOrderParent level - if (host.accessibilityNodeProvider != null) { - for (i in 0 until host.childCount) { - val child = host.getChildAt(i) - if (child != null) { - axChildren.add(child) - } - } - } else { - // This extracts the children of host sorted in accessibility order, this is by layout - // top to bottom, left to right - host.addChildrenForAccessibility(axChildren) - } - 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) - } - } - } - } -}