From e1ae619fce906954a4ddd4cc7c4e459ff68060bd Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Wed, 23 Jul 2025 18:08:29 -0700 Subject: [PATCH] Implement accessibilityOrder by building the accessibilityTree through addChildrenForAccessibility (#52743) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52743 We can build an accessibility tree for Talkback by overriding addChildrenForAccessibility of ViewGroup. With this we just manually build a tree that contains the elements we care about in the order we want. We also try to keep most of the tree intact so that coopting works properly Changelog: [Internal] EDITS: After backout, fixed the issue where we were incorrectly setting the `accessibility_order_parent` tag the ReactAxOrderHelper class instead of the actual view. Also, made the cast safe to prevent any unexpected issues. Also refactored the ReactAxOrderHelper functions to not have the block scoped `traverse` functions in favor of just looping through the children of a view when calling them Reviewed By: joevilches Differential Revision: D78669715 fbshipit-source-id: e714367c28e722ce42895531cf18e6f2dc926556 --- .../Components/ScrollView/ScrollView.js | 2 +- .../Components/TextInput/TextInput.flow.js | 2 +- .../ReactAndroid/api/ReactAndroid.api | 16 +++- .../react/uimanager/BaseViewManager.java | 53 ------------ .../uimanager/BaseViewManagerDelegate.kt | 3 - .../react/uimanager/ReactAxOrderHelper.kt | 85 +++++++++++++++++++ .../react/views/view/ReactViewGroup.kt | 80 +++++++++++++++++ .../react/views/view/ReactViewManager.kt | 33 +++++++ .../main/res/views/uimanager/values/ids.xml | 5 +- 9 files changed, 216 insertions(+), 63 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index 2f7b9faf86f..8f4f297ed15 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -673,7 +673,7 @@ type ScrollViewBaseProps = $ReadOnly<{ }>; export type ScrollViewProps = $ReadOnly<{ - ...ViewProps, + ...Omit, ...ScrollViewPropsIOS, ...ScrollViewPropsAndroid, ...ScrollViewBaseProps, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index acefa1ffc95..5fa8811144b 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -1031,7 +1031,7 @@ type TextInputBaseProps = $ReadOnly<{ }>; export type TextInputProps = $ReadOnly<{ - ...Omit, + ...Omit, ...TextInputIOSProps, ...TextInputAndroidProps, ...TextInputBaseProps, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9332a05ac23..84baa964fe8 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3287,7 +3287,6 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo public fun setAccessibilityLabel (Landroid/view/View;Ljava/lang/String;)V public fun setAccessibilityLabelledBy (Landroid/view/View;Lcom/facebook/react/bridge/Dynamic;)V public fun setAccessibilityLiveRegion (Landroid/view/View;Ljava/lang/String;)V - public fun setAccessibilityOrder (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V public fun setAccessibilityRole (Landroid/view/View;Ljava/lang/String;)V public fun setAccessibilityValue (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V public fun setBackgroundColor (Landroid/view/View;I)V @@ -3778,6 +3777,14 @@ public final class com/facebook/react/uimanager/ReactAccessibilityDelegate$Role public static fun values ()[Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; } +public final class com/facebook/react/uimanager/ReactAxOrderHelper { + public static final field INSTANCE Lcom/facebook/react/uimanager/ReactAxOrderHelper; + public final fun buildAxOrderList (Landroid/view/View;Landroid/view/View;Ljava/util/List;[Landroid/view/View;)V + public static final fun cleanUpAxOrder (Landroid/view/View;)V + public final fun disableFocusForSubtree (Landroid/view/View;Ljava/util/List;)V + public static final fun restoreFocusability (Landroid/view/View;)V +} + public abstract interface class com/facebook/react/uimanager/ReactClippingProhibitedView { } @@ -6561,6 +6568,8 @@ public final class com/facebook/react/views/view/ReactDrawableHelper { public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView, com/facebook/react/uimanager/ReactZIndexedViewGroup { public fun (Landroid/content/Context;)V + public fun addChildrenForAccessibility (Ljava/util/ArrayList;)V + public final fun cleanUpAxOrderListener ()V protected fun dispatchDraw (Landroid/graphics/Canvas;)V public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z public fun dispatchProvideStructure (Landroid/view/ViewStructure;)V @@ -6568,6 +6577,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun draw (Landroid/graphics/Canvas;)V 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; protected fun getChildDrawingOrder (II)I public fun getClippingRect (Landroid/graphics/Rect;)V public fun getHitSlopRect ()Landroid/graphics/Rect; @@ -6587,6 +6597,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun onViewAdded (Landroid/view/View;)V public fun onViewRemoved (Landroid/view/View;)V public fun requestLayout ()V + public final fun setAxOrderList (Ljava/util/List;)V public final fun setBackfaceVisibility (Ljava/lang/String;)V public final fun setBackfaceVisibilityDependantOpacity ()V public fun setBackgroundColor (I)V @@ -6623,12 +6634,15 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun nextFocusLeft (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusRight (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusUp (Lcom/facebook/react/views/view/ReactViewGroup;I)V + public synthetic fun onDropViewInstance (Landroid/view/View;)V + public fun onDropViewInstance (Lcom/facebook/react/views/view/ReactViewGroup;)V public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)Lcom/facebook/react/views/view/ReactViewGroup; public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V 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 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 public fun setBackgroundImage (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)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 07ac1cad632..6fbd22d7a51 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 @@ -30,7 +30,6 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.ReactConstants; -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.annotations.ReactProp; @@ -312,61 +311,9 @@ public abstract class BaseViewManager - mViewManager.setAccessibilityOrder(view, value as ReadableArray?) ViewProps.OPACITY -> mViewManager.setOpacity(view, (value as Double?)?.toFloat() ?: 1.0f) - ViewProps.OUTLINE_COLOR -> mViewManager.setOutlineColor(view, value as Int?) ViewProps.OUTLINE_OFFSET -> 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 new file mode 100644 index 00000000000..dc216837f1b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAxOrderHelper.kt @@ -0,0 +1,85 @@ +/* + * 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.view.View +import android.view.ViewGroup +import com.facebook.react.R + +public object ReactAxOrderHelper { + @JvmStatic + public fun cleanUpAxOrder(view: View) { + val originalFocusability = view.getTag(R.id.original_focusability) as Boolean? + if (originalFocusability != null) { + view.isFocusable = originalFocusability + } + + val axOrderParent = view.getTag(R.id.accessibility_order_parent) as View? + if (axOrderParent != null) { + view.setTag(R.id.accessibility_order_parent, null) + } + + if (view is ViewGroup) { + for (i in 0..) { + if (!axOrderList.contains(view.getTag(R.id.view_tag_native_id))) { + if (view.getTag(R.id.original_focusability) == null) { + view.setTag(R.id.original_focusability, view.isFocusable) + } + view.isFocusable = false + } + + if (view is ViewGroup) { + for (i in 0.., + result: Array, + ) { + val nativeId = view.getTag(R.id.view_tag_native_id) + view.setTag(R.id.accessibility_order_parent, parent) + + if (axOrderList.contains(nativeId)) { + val idx = axOrderList.indexOf(nativeId) + if (idx != -1) { + result[idx] = view + } + } + + if (view is ViewGroup) { + for (i in 0..? = null + private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null private var needsOffscreenAlphaCompositing = false private var backfaceOpacity = 0f private var backfaceVisible = false private var childrenRemovedWhileTransitioning: MutableSet? = null + private var accessibilityStateChangeListener: + AccessibilityManager.AccessibilityStateChangeListener? = + null init { initView() @@ -931,6 +939,78 @@ public open class ReactViewGroup public constructor(context: Context?) : alpha = 0f } + override fun addChildrenForAccessibility(outChildren: ArrayList) { + val axOrderParent = getTag(R.id.accessibility_order_parent) + var axOrderParentOrderList: MutableList? = null + if (axOrderParent is ReactViewGroup) { + axOrderParentOrderList = (axOrderParent as ReactViewGroup?)?.axOrderList + } + + val axOrder: MutableList<*>? = axOrderList + if (axOrder != null) { + + val am: AccessibilityManager? = + this.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager? + if (accessibilityStateChangeListener == null && am != null) { + val newAccessibilityStateChangeListener = + AccessibilityManager.AccessibilityStateChangeListener { enabled -> + if (!enabled) { + for (i in 0..(axOrder.size) + + for (i in 0..() return preparedView } + override fun onDropViewInstance(view: ReactViewGroup) { + super.onDropViewInstance(view) + view.cleanUpAxOrderListener() + } + @ReactProp(name = "accessible") public open fun setAccessible(view: ReactViewGroup, accessible: Boolean) { view.isFocusable = accessible } + @ReactProp(name = ViewProps.ACCESSIBILITY_ORDER) + public open fun setAccessibilityOrder(view: ReactViewGroup, nativeIds: ReadableArray?) { + if (!ReactNativeFeatureFlags.enableAccessibilityOrder()) { + return + } + + for (i in 0 until view.childCount) { + ReactAxOrderHelper.cleanUpAxOrder(view.getChildAt(i)) + } + + if (nativeIds == null) { + view.axOrderList = null + return + } + + val axOrderList = mutableListOf() + + for (i in 0 until nativeIds.size()) { + val id = nativeIds.getString(i) + if (id != null) { + axOrderList.add(id) + } + } + + view.axOrderList = axOrderList + } + @ReactProp(name = "hasTVPreferredFocus") public open fun setTVPreferredFocus(view: ReactViewGroup, hasTVPreferredFocus: Boolean) { if (hasTVPreferredFocus) { 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 eb8ccbc8c80..a7faf3fd60c 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 @@ -12,10 +12,7 @@ - - - - +