Skip to content

Commit 1adaea3

Browse files
jorge-cabfacebook-github-bot
authored andcommitted
Fix View Coopting View edge case on Android (facebook#52066)
Summary: Pull Request resolved: facebook#52066 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] Differential Revision: D76745057
1 parent 2b9ef20 commit 1adaea3

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

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

Lines changed: 43 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,31 @@ 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+
ReactAxOrderHelper.restoreSubtreeFocusability(host);
276+
host.setTag(R.id.accessibility_order_dirty, true);
277+
};
278+
279+
am.addAccessibilityStateChangeListener(newAccessibilityStateChangeListener);
280+
accessibilityStateChangeListener = newAccessibilityStateChangeListener;
281+
282+
host.addOnAttachStateChangeListener(
283+
new View.OnAttachStateChangeListener() {
284+
@Override
285+
public void onViewAttachedToWindow(@NonNull View v) {}
286+
287+
@Override
288+
public void onViewDetachedFromWindow(@NonNull View v) {
289+
cleanUpAccessibilityStateChangeListener();
290+
}
291+
});
292+
}
293+
264294
Boolean isAxOrderDirty = (Boolean) mView.getTag(R.id.accessibility_order_dirty);
265295
if (isAxOrderDirty != null && isAxOrderDirty) {
266296
List<String> axOrderIdsList = new ArrayList<>();
@@ -1098,4 +1128,17 @@ public static AccessibilityRole fromValue(@Nullable String value) {
10981128
}
10991129
}
11001130
}
1131+
1132+
// In case a view with accessibilityOrder is unmounted we need a way to clean up the listener on
1133+
// this delegate
1134+
private void cleanUpAccessibilityStateChangeListener() {
1135+
AccessibilityManager am =
1136+
(AccessibilityManager) mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
1137+
AccessibilityManager.AccessibilityStateChangeListener localAccessibilityStateChangeListener =
1138+
accessibilityStateChangeListener;
1139+
if (localAccessibilityStateChangeListener != null && am != null) {
1140+
am.removeAccessibilityStateChangeListener(localAccessibilityStateChangeListener);
1141+
}
1142+
accessibilityStateChangeListener = null;
1143+
}
11011144
}

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

Lines changed: 22 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,11 @@ private object ReactAxOrderHelper {
10095
}
10196
}
10297
}
98+
99+
if (!isIncluded && !isContained && parent != view) {
100+
view.setTag(R.id.original_focusability, view.isFocusable)
101+
view.isFocusable = false
102+
}
103103
}
104104

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

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)