Skip to content

Commit e1ae619

Browse files
jorge-cabfacebook-github-bot
authored andcommitted
Implement accessibilityOrder by building the accessibilityTree through addChildrenForAccessibility (#52743)
Summary: Pull Request resolved: #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
1 parent 2de4984 commit e1ae619

9 files changed

Lines changed: 216 additions & 63 deletions

File tree

packages/react-native/Libraries/Components/ScrollView/ScrollView.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ type ScrollViewBaseProps = $ReadOnly<{
673673
}>;
674674

675675
export type ScrollViewProps = $ReadOnly<{
676-
...ViewProps,
676+
...Omit<ViewProps, 'experimental_accessibilityOrder'>,
677677
...ScrollViewPropsIOS,
678678
...ScrollViewPropsAndroid,
679679
...ScrollViewBaseProps,

packages/react-native/Libraries/Components/TextInput/TextInput.flow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ type TextInputBaseProps = $ReadOnly<{
10311031
}>;
10321032

10331033
export type TextInputProps = $ReadOnly<{
1034-
...Omit<ViewProps, 'style'>,
1034+
...Omit<ViewProps, 'style' | 'experimental_accessibilityOrder'>,
10351035
...TextInputIOSProps,
10361036
...TextInputAndroidProps,
10371037
...TextInputBaseProps,

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3287,7 +3287,6 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo
32873287
public fun setAccessibilityLabel (Landroid/view/View;Ljava/lang/String;)V
32883288
public fun setAccessibilityLabelledBy (Landroid/view/View;Lcom/facebook/react/bridge/Dynamic;)V
32893289
public fun setAccessibilityLiveRegion (Landroid/view/View;Ljava/lang/String;)V
3290-
public fun setAccessibilityOrder (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
32913290
public fun setAccessibilityRole (Landroid/view/View;Ljava/lang/String;)V
32923291
public fun setAccessibilityValue (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V
32933292
public fun setBackgroundColor (Landroid/view/View;I)V
@@ -3778,6 +3777,14 @@ public final class com/facebook/react/uimanager/ReactAccessibilityDelegate$Role
37783777
public static fun values ()[Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
37793778
}
37803779

3780+
public final class com/facebook/react/uimanager/ReactAxOrderHelper {
3781+
public static final field INSTANCE Lcom/facebook/react/uimanager/ReactAxOrderHelper;
3782+
public final fun buildAxOrderList (Landroid/view/View;Landroid/view/View;Ljava/util/List;[Landroid/view/View;)V
3783+
public static final fun cleanUpAxOrder (Landroid/view/View;)V
3784+
public final fun disableFocusForSubtree (Landroid/view/View;Ljava/util/List;)V
3785+
public static final fun restoreFocusability (Landroid/view/View;)V
3786+
}
3787+
37813788
public abstract interface class com/facebook/react/uimanager/ReactClippingProhibitedView {
37823789
}
37833790

@@ -6561,13 +6568,16 @@ public final class com/facebook/react/views/view/ReactDrawableHelper {
65616568

65626569
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 {
65636570
public fun <init> (Landroid/content/Context;)V
6571+
public fun addChildrenForAccessibility (Ljava/util/ArrayList;)V
6572+
public final fun cleanUpAxOrderListener ()V
65646573
protected fun dispatchDraw (Landroid/graphics/Canvas;)V
65656574
public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z
65666575
public fun dispatchProvideStructure (Landroid/view/ViewStructure;)V
65676576
protected fun dispatchSetPressed (Z)V
65686577
public fun draw (Landroid/graphics/Canvas;)V
65696578
protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z
65706579
public fun endViewTransition (Landroid/view/View;)V
6580+
public final fun getAxOrderList ()Ljava/util/List;
65716581
protected fun getChildDrawingOrder (II)I
65726582
public fun getClippingRect (Landroid/graphics/Rect;)V
65736583
public fun getHitSlopRect ()Landroid/graphics/Rect;
@@ -6587,6 +6597,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro
65876597
public fun onViewAdded (Landroid/view/View;)V
65886598
public fun onViewRemoved (Landroid/view/View;)V
65896599
public fun requestLayout ()V
6600+
public final fun setAxOrderList (Ljava/util/List;)V
65906601
public final fun setBackfaceVisibility (Ljava/lang/String;)V
65916602
public final fun setBackfaceVisibilityDependantOpacity ()V
65926603
public fun setBackgroundColor (I)V
@@ -6623,12 +6634,15 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
66236634
public fun nextFocusLeft (Lcom/facebook/react/views/view/ReactViewGroup;I)V
66246635
public fun nextFocusRight (Lcom/facebook/react/views/view/ReactViewGroup;I)V
66256636
public fun nextFocusUp (Lcom/facebook/react/views/view/ReactViewGroup;I)V
6637+
public synthetic fun onDropViewInstance (Landroid/view/View;)V
6638+
public fun onDropViewInstance (Lcom/facebook/react/views/view/ReactViewGroup;)V
66266639
public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View;
66276640
protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)Lcom/facebook/react/views/view/ReactViewGroup;
66286641
public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V
66296642
public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
66306643
public fun receiveCommand (Lcom/facebook/react/views/view/ReactViewGroup;ILcom/facebook/react/bridge/ReadableArray;)V
66316644
public fun receiveCommand (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
6645+
public fun setAccessibilityOrder (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
66326646
public fun setAccessible (Lcom/facebook/react/views/view/ReactViewGroup;Z)V
66336647
public fun setBackfaceVisibility (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V
66346648
public fun setBackgroundImage (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import com.facebook.react.bridge.ReadableType;
3131
import com.facebook.react.common.MapBuilder;
3232
import com.facebook.react.common.ReactConstants;
33-
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
3433
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
3534
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
3635
import com.facebook.react.uimanager.annotations.ReactProp;
@@ -312,61 +311,9 @@ public void setTestId(@NonNull T view, @Nullable String testId) {
312311
public void setNativeId(@NonNull T view, @Nullable String nativeId) {
313312
view.setTag(R.id.view_tag_native_id, nativeId);
314313

315-
/*
316-
* If we change the nativeId we need to notify the relevant accessibility parent to update the
317-
* focusing order.
318-
*/
319-
if (view.getTag(R.id.accessibility_order_parent) != null) {
320-
ViewGroup accessibilityParent = (ViewGroup) view.getTag(R.id.accessibility_order_parent);
321-
322-
accessibilityParent.setTag(R.id.accessibility_order_dirty, true);
323-
324-
accessibilityParent.notifySubtreeAccessibilityStateChanged(
325-
accessibilityParent, accessibilityParent, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
326-
}
327-
328314
ReactFindViewUtil.notifyViewRendered(view);
329315
}
330316

331-
@ReactProp(name = ViewProps.ACCESSIBILITY_ORDER)
332-
public void setAccessibilityOrder(@NonNull T view, @Nullable ReadableArray nativeIds) {
333-
if (!ReactNativeFeatureFlags.enableAccessibilityOrder()) {
334-
return;
335-
}
336-
337-
view.setTag(R.id.accessibility_order, nativeIds);
338-
view.setTag(R.id.accessibility_order_dirty, true);
339-
340-
if (view instanceof ViewGroup) {
341-
((ViewGroup) view)
342-
.setOnHierarchyChangeListener(
343-
new ViewGroup.OnHierarchyChangeListener() {
344-
@Override
345-
public void onChildViewAdded(View parent, View child) {
346-
view.setTag(R.id.accessibility_order_dirty, true);
347-
348-
// We also want to listen to changes on the hierarchy of nested ViewGroups
349-
if (child instanceof ViewGroup) {
350-
ViewGroup childGroup = (ViewGroup) child;
351-
childGroup.setOnHierarchyChangeListener(this);
352-
for (int i = 0; i < childGroup.getChildCount(); i++) {
353-
onChildViewAdded(childGroup, childGroup.getChildAt(i));
354-
}
355-
}
356-
}
357-
358-
@Override
359-
public void onChildViewRemoved(View parent, View child) {
360-
view.setTag(R.id.accessibility_order_dirty, true);
361-
}
362-
});
363-
364-
((ViewGroup) view)
365-
.notifySubtreeAccessibilityStateChanged(
366-
view, view, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
367-
}
368-
}
369-
370317
@ReactProp(name = ViewProps.ACCESSIBILITY_LABELLED_BY)
371318
public void setAccessibilityLabelledBy(@NonNull T view, @Nullable Dynamic nativeId) {
372319
if (nativeId.isNull()) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,7 @@ public abstract class BaseViewManagerDelegate<
9191
val dynamicFromObject: Dynamic = DynamicFromObject(value)
9292
mViewManager.setAccessibilityLabelledBy(view, dynamicFromObject)
9393
}
94-
ViewProps.ACCESSIBILITY_ORDER ->
95-
mViewManager.setAccessibilityOrder(view, value as ReadableArray?)
9694
ViewProps.OPACITY -> mViewManager.setOpacity(view, (value as Double?)?.toFloat() ?: 1.0f)
97-
9895
ViewProps.OUTLINE_COLOR -> mViewManager.setOutlineColor(view, value as Int?)
9996

10097
ViewProps.OUTLINE_OFFSET ->
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager
9+
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import com.facebook.react.R
13+
14+
public object ReactAxOrderHelper {
15+
@JvmStatic
16+
public fun cleanUpAxOrder(view: View) {
17+
val originalFocusability = view.getTag(R.id.original_focusability) as Boolean?
18+
if (originalFocusability != null) {
19+
view.isFocusable = originalFocusability
20+
}
21+
22+
val axOrderParent = view.getTag(R.id.accessibility_order_parent) as View?
23+
if (axOrderParent != null) {
24+
view.setTag(R.id.accessibility_order_parent, null)
25+
}
26+
27+
if (view is ViewGroup) {
28+
for (i in 0..<view.childCount) {
29+
cleanUpAxOrder(view.getChildAt(i))
30+
}
31+
}
32+
}
33+
34+
@JvmStatic
35+
public fun restoreFocusability(view: View) {
36+
val originalFocusability = view.getTag(R.id.original_focusability) as Boolean?
37+
if (originalFocusability != null) {
38+
view.isFocusable = originalFocusability
39+
}
40+
41+
if (view is ViewGroup) {
42+
for (i in 0..<view.childCount) {
43+
restoreFocusability(view.getChildAt(i))
44+
}
45+
}
46+
}
47+
48+
public fun disableFocusForSubtree(view: View, axOrderList: MutableList<*>) {
49+
if (!axOrderList.contains(view.getTag(R.id.view_tag_native_id))) {
50+
if (view.getTag(R.id.original_focusability) == null) {
51+
view.setTag(R.id.original_focusability, view.isFocusable)
52+
}
53+
view.isFocusable = false
54+
}
55+
56+
if (view is ViewGroup) {
57+
for (i in 0..<view.childCount) {
58+
disableFocusForSubtree(view.getChildAt(i), axOrderList)
59+
}
60+
}
61+
}
62+
63+
public fun buildAxOrderList(
64+
view: View,
65+
parent: View,
66+
axOrderList: MutableList<*>,
67+
result: Array<View?>,
68+
) {
69+
val nativeId = view.getTag(R.id.view_tag_native_id)
70+
view.setTag(R.id.accessibility_order_parent, parent)
71+
72+
if (axOrderList.contains(nativeId)) {
73+
val idx = axOrderList.indexOf(nativeId)
74+
if (idx != -1) {
75+
result[idx] = view
76+
}
77+
}
78+
79+
if (view is ViewGroup) {
80+
for (i in 0..<view.childCount) {
81+
buildAxOrderList(view.getChildAt(i), parent, axOrderList, result)
82+
}
83+
}
84+
}
85+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.view.MotionEvent
2222
import android.view.View
2323
import android.view.ViewGroup
2424
import android.view.ViewStructure
25+
import android.view.accessibility.AccessibilityManager
2526
import com.facebook.common.logging.FLog
2627
import com.facebook.react.R
2728
import com.facebook.react.bridge.ReactNoCrashSoftException
@@ -49,6 +50,7 @@ import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel
4950
import com.facebook.react.uimanager.PointerEvents
5051
import com.facebook.react.uimanager.PointerEvents.Companion.canBeTouchTarget
5152
import com.facebook.react.uimanager.PointerEvents.Companion.canChildrenBeTouchTarget
53+
import com.facebook.react.uimanager.ReactAxOrderHelper
5254
import com.facebook.react.uimanager.ReactClippingProhibitedView
5355
import com.facebook.react.uimanager.ReactClippingViewGroup
5456
import com.facebook.react.uimanager.ReactClippingViewGroupHelper.calculateClippingRect
@@ -63,6 +65,7 @@ import com.facebook.react.uimanager.style.BorderStyle
6365
import com.facebook.react.uimanager.style.LogicalEdge
6466
import com.facebook.react.uimanager.style.Overflow
6567
import com.facebook.react.views.view.CanvasUtil.enableZ
68+
import java.util.ArrayList
6669
import kotlin.concurrent.Volatile
6770
import kotlin.math.max
6871

@@ -139,12 +142,17 @@ public open class ReactViewGroup public constructor(context: Context?) :
139142
public override var hitSlopRect: Rect? = null
140143
public override var pointerEvents: PointerEvents = PointerEvents.AUTO
141144

145+
public var axOrderList: MutableList<String>? = null
146+
142147
private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null
143148
private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
144149
private var needsOffscreenAlphaCompositing = false
145150
private var backfaceOpacity = 0f
146151
private var backfaceVisible = false
147152
private var childrenRemovedWhileTransitioning: MutableSet<Int>? = null
153+
private var accessibilityStateChangeListener:
154+
AccessibilityManager.AccessibilityStateChangeListener? =
155+
null
148156

149157
init {
150158
initView()
@@ -931,6 +939,78 @@ public open class ReactViewGroup public constructor(context: Context?) :
931939
alpha = 0f
932940
}
933941

942+
override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
943+
val axOrderParent = getTag(R.id.accessibility_order_parent)
944+
var axOrderParentOrderList: MutableList<String>? = null
945+
if (axOrderParent is ReactViewGroup) {
946+
axOrderParentOrderList = (axOrderParent as ReactViewGroup?)?.axOrderList
947+
}
948+
949+
val axOrder: MutableList<*>? = axOrderList
950+
if (axOrder != null) {
951+
952+
val am: AccessibilityManager? =
953+
this.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager?
954+
if (accessibilityStateChangeListener == null && am != null) {
955+
val newAccessibilityStateChangeListener =
956+
AccessibilityManager.AccessibilityStateChangeListener { enabled ->
957+
if (!enabled) {
958+
for (i in 0..<childCount) {
959+
ReactAxOrderHelper.restoreFocusability(getChildAt(i))
960+
}
961+
}
962+
}
963+
964+
am.addAccessibilityStateChangeListener(newAccessibilityStateChangeListener)
965+
accessibilityStateChangeListener = newAccessibilityStateChangeListener
966+
}
967+
968+
val result = arrayOfNulls<View?>(axOrder.size)
969+
970+
for (i in 0..<childCount) {
971+
ReactAxOrderHelper.buildAxOrderList(getChildAt(i), this, axOrder, result)
972+
}
973+
974+
for (i in result.indices) {
975+
val view = result[i]
976+
if (view != null) {
977+
if (view.isFocusable) {
978+
outChildren.add(view)
979+
} else {
980+
view.addChildrenForAccessibility(outChildren)
981+
}
982+
}
983+
}
984+
} else if (axOrderParentOrderList != null) {
985+
// view is a container so add its children normally
986+
if (!isFocusable) {
987+
super.addChildrenForAccessibility(outChildren)
988+
return
989+
990+
// If this view can coopt, turn the focusability off its children but add them to the tree
991+
} else if (isFocusable && (contentDescription == null || contentDescription == "")) {
992+
super.addChildrenForAccessibility(outChildren)
993+
for (i in 0..<childCount) {
994+
ReactAxOrderHelper.disableFocusForSubtree(getChildAt(i), axOrderParentOrderList)
995+
}
996+
// if this view is focusable and has a contentDescription then we don't care about its
997+
// descendants for accessibility
998+
} else if (isFocusable && !(contentDescription == null || contentDescription == "")) {
999+
return
1000+
}
1001+
} else {
1002+
super.addChildrenForAccessibility(outChildren)
1003+
}
1004+
}
1005+
1006+
public fun cleanUpAxOrderListener() {
1007+
val am = this.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
1008+
if (am != null) {
1009+
accessibilityStateChangeListener?.let { am.removeAccessibilityStateChangeListener(it) }
1010+
}
1011+
accessibilityStateChangeListener = null
1012+
}
1013+
9341014
private companion object {
9351015
private const val ARRAY_CAPACITY_INCREMENT = 12
9361016
private val defaultLayoutParam = LayoutParams(0, 0)

0 commit comments

Comments
 (0)