Skip to content

Commit 6dff8bc

Browse files
jorge-cabfacebook-github-bot
authored andcommitted
Fix View Coopting View edge case on Android (#52066)
Summary: Before, to disable views that were excluded from the order we were setting them to be not important for accessibility. This however breaks coopting behavior of parent views, because parent views will not announce content descriptions of children that are not important for accessibility. Instead of disabling by setting `important for accessibility = no` now we just set `isFocusable = false` which disables focusing but still allows parent views to coopt We also add functionality to restore view focusability when enabling disabling screen readers since `isFocusable` changes keyboard focusability and when screen readers are disabled we don't want to change it. Changelog: [Internal] Reviewed By: joevilches Differential Revision: D76745057
1 parent 65ae3da commit 6dff8bc

File tree

5 files changed

+73
-5
lines changed

5 files changed

+73
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3659,6 +3659,7 @@ public class com/facebook/react/uimanager/ReactAccessibilityDelegate : androidx/
36593659
public static final field TOP_ACCESSIBILITY_ACTION_EVENT Ljava/lang/String;
36603660
public static final field sActionIdMap Ljava/util/HashMap;
36613661
public fun <init> (Landroid/view/View;ZI)V
3662+
public fun cleanUp ()V
36623663
public static fun createNodeInfoFromView (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat;
36633664
public fun getAccessibilityNodeProvider (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat;
36643665
protected fun getHostView ()Landroid/view/View;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import androidx.annotation.ColorInt;
2020
import androidx.annotation.NonNull;
2121
import androidx.annotation.Nullable;
22+
import androidx.core.view.AccessibilityDelegateCompat;
2223
import androidx.core.view.ViewCompat;
2324
import com.facebook.common.logging.FLog;
2425
import com.facebook.react.R;
@@ -186,6 +187,16 @@ public void onDropViewInstance(@NonNull T view) {
186187
if (focusChangeListener instanceof BaseVMFocusChangeListener) {
187188
((BaseVMFocusChangeListener) focusChangeListener).detach(view);
188189
}
190+
191+
AccessibilityDelegateCompat axDelegate = ViewCompat.getAccessibilityDelegate(view);
192+
193+
if (axDelegate instanceof ReactAccessibilityDelegate) {
194+
((ReactAccessibilityDelegate) axDelegate).cleanUp();
195+
}
196+
197+
if (view instanceof ViewGroup) {
198+
((ViewGroup) view).setOnHierarchyChangeListener(null);
199+
}
189200
}
190201

191202
// Currently, layout listener is only attached when transform or transformOrigin is set.

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import android.view.View;
1717
import android.view.ViewGroup;
1818
import android.view.accessibility.AccessibilityEvent;
19+
import android.view.accessibility.AccessibilityManager;
1920
import android.widget.EditText;
2021
import androidx.annotation.NonNull;
2122
import androidx.annotation.Nullable;
@@ -72,6 +73,10 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
7273
private Handler mHandler;
7374
private final HashMap<Integer, String> mAccessibilityActionsMap;
7475

76+
@Nullable
77+
private AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener =
78+
null;
79+
7580
@Nullable View mAccessibilityLabelledBy;
7681

7782
static {
@@ -261,6 +266,22 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
261266
ReadableArray axOrderIds = (ReadableArray) mView.getTag(R.id.accessibility_order);
262267
if (axOrderIds != null && axOrderIds.size() != 0) {
263268

269+
AccessibilityManager am =
270+
(AccessibilityManager) host.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
271+
272+
if (accessibilityStateChangeListener == null && am != null) {
273+
AccessibilityManager.AccessibilityStateChangeListener newAccessibilityStateChangeListener =
274+
enabled -> {
275+
if (!enabled) {
276+
ReactAxOrderHelper.restoreSubtreeFocusability(host);
277+
host.setTag(R.id.accessibility_order_dirty, true);
278+
}
279+
};
280+
281+
am.addAccessibilityStateChangeListener(newAccessibilityStateChangeListener);
282+
accessibilityStateChangeListener = newAccessibilityStateChangeListener;
283+
}
284+
264285
Boolean isAxOrderDirty = (Boolean) mView.getTag(R.id.accessibility_order_dirty);
265286
if (isAxOrderDirty != null && isAxOrderDirty) {
266287
List<String> axOrderIdsList = new ArrayList<>();
@@ -1098,4 +1119,17 @@ public static AccessibilityRole fromValue(@Nullable String value) {
10981119
}
10991120
}
11001121
}
1122+
1123+
// In case a view with accessibilityOrder is unmounted we need a way to clean up the listener on
1124+
// this delegate
1125+
public void cleanUp() {
1126+
if (accessibilityStateChangeListener != null) {
1127+
AccessibilityManager am =
1128+
(AccessibilityManager) mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
1129+
if (am != null) {
1130+
am.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
1131+
}
1132+
accessibilityStateChangeListener = null;
1133+
}
1134+
}
11011135
}

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package com.facebook.react.uimanager
1010
import android.graphics.Rect
1111
import android.view.View
1212
import android.view.ViewGroup
13-
import android.widget.TextView
1413
import com.facebook.react.R
1514
import com.facebook.react.bridge.ReadableArray
1615

@@ -76,10 +75,6 @@ private object ReactAxOrderHelper {
7675
}
7776
}
7877

79-
if (!isIncluded && !isContained && parent != view && view !is TextView) {
80-
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
81-
}
82-
8378
// Don't traverse the children of a nested accessibility order
8479
if (view is ViewGroup) {
8580
val axChildren: ArrayList<View> = getAxChildren(view)
@@ -100,6 +95,13 @@ private object ReactAxOrderHelper {
10095
}
10196
}
10297
}
98+
99+
if (!isIncluded && !isContained && parent != view) {
100+
if (view.getTag(R.id.original_focusability) == null) {
101+
view.setTag(R.id.original_focusability, view.isFocusable)
102+
}
103+
view.isFocusable = false
104+
}
103105
}
104106

105107
traverseAndBuildAxOrder(
@@ -159,4 +161,21 @@ private object ReactAxOrderHelper {
159161
}
160162
return axChildren
161163
}
164+
165+
@JvmStatic
166+
public fun restoreSubtreeFocusability(view: View) {
167+
val originalFocusability = view.getTag(R.id.original_focusability)
168+
if (originalFocusability is Boolean) {
169+
view.isFocusable = originalFocusability
170+
}
171+
172+
if (view is ViewGroup) {
173+
for (i in 0 until view.childCount) {
174+
val child = view.getChildAt(i)
175+
if (child != null) {
176+
restoreSubtreeFocusability(child)
177+
}
178+
}
179+
}
180+
}
162181
}

packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
<!-- tag is used to store the current state of the accessibility order tree-->
1616
<item type="id" name="accessibility_order_dirty"/>
1717

18+
<!-- tag is used to store the original focusability value of a view within the accessibility order tree if it was changed-->
19+
<item type="id" name="original_focusability"/>
20+
1821
<!-- tag is used to store the nativeID tag -->
1922
<item type="id" name="view_tag_instance_handle"/>
2023

0 commit comments

Comments
 (0)