Skip to content

Commit 1f80c34

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Fix "detected text" annoucement
Summary: There was a strange bug reported to us with `accessibilityOrder` where an OCR model would announce text on the screen sometimes. This would happen if a focused `View` without a label would wrap `Text` that was was not included in the `accessibilityOrder` array. What happens under the hood is we have a focused `View` trying to coopt a label. It finds no accessibility nodes under it (because the `Text` has `importantForAccessibility` set to `NO`) so it falls back to this weird TalkBack behavior. It both reads the text from the TextView and announces an OCR announcement - leading to repeating the text. To fix this we just check if we are going to coopt text and if we do then we do not set `importantForAccessibility`. Behavior here changes a bit. Links are not accessible without having to reference the Text, setting `accessible={true}` on non-referenced `Text` will lead to be focusable. These are both less bad than what we had before though, so I think this tradeoff is fine. Changelog: [Internal] Reviewed By: jorge-cab Differential Revision: D74101530
1 parent f493689 commit 1f80c34

File tree

1 file changed

+25
-8
lines changed

1 file changed

+25
-8
lines changed

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.facebook.react.uimanager
99

1010
import android.view.View
1111
import android.view.ViewGroup
12+
import android.widget.TextView
1213
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
1314
import com.facebook.react.R
1415
import com.facebook.react.bridge.ReadableArray
@@ -90,7 +91,11 @@ private object ReactAxOrderHelper {
9091
): Array<View?> {
9192
val axOrderViews = arrayOfNulls<View?>(axOrderIds.size)
9293

93-
fun traverseAndDisableAxFromExcludedViews(view: View, parent: View) {
94+
fun traverseAndDisableAxFromExcludedViews(
95+
view: View,
96+
parent: View,
97+
hasCooptingAncestor: Boolean
98+
) {
9499
val nativeId = view.getTag(R.id.view_tag_native_id) as String?
95100

96101
val isIncluded = nativeId != null && axOrderSet.contains(nativeId)
@@ -101,28 +106,40 @@ private object ReactAxOrderHelper {
101106
view, view.isFocusable, view.importantForAccessibility)
102107
}
103108

109+
// There is a strange bug with TalkBack where if a focused view has nothing to announce, and
110+
// there are no accessibility node's below it, then it will run OCR model on its bounds and
111+
// announce any text it sees. Also, if there is a TextView below the View backing the node,
112+
// it will announce that text too. This can happen frequently with our implementation here
113+
// we change importantForAccessibility for TextView's that are not included in
114+
// accessibilityOrder thus kicking off the logic described. To avoid this double announcement
115+
// we just do not set importantForAccessibility on TextView's in the case where someone is
116+
// coopting them. We will not focus the text since they got coopted.
104117
if (isIncluded) {
105118
axOrderViews[axOrderIds.indexOf(nativeId)] = view
106-
} else {
119+
} else if (!(view is TextView && hasCooptingAncestor)) {
107120
// Save original state before disabling
108121
view.setTag(R.id.original_important_for_ax, view.importantForAccessibility)
109122
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
110123
}
111124

125+
val wantsToCoopt = isIncluded && view.contentDescription.isNullOrEmpty()
126+
112127
if (view is ViewGroup) {
113-
// Continue to try to disable children if this view is not included and is focusable.
114-
// This view being focusable means it's an element, and not a container which means its
115-
// presence doesn't imply all its children should be focusable. And if its not included we
116-
// still want to attempt to disable the children of the container
128+
// Continue to try to disable children if this view is not included and is
129+
// focusable. This view being focusable means it's an element, and not a container which
130+
// means its presence doesn't imply all its children should be focusable. And if its not
131+
// included we still want to attempt to disable the children of the container
117132
if (!isIncluded || view.isFocusable()) {
118133
for (i in 0 until view.childCount) {
119-
traverseAndDisableAxFromExcludedViews(view.getChildAt(i), parent)
134+
traverseAndDisableAxFromExcludedViews(view.getChildAt(i), parent, wantsToCoopt)
120135
}
121136
}
122137
}
123138
}
124139

125-
traverseAndDisableAxFromExcludedViews(root, root)
140+
// Technically we don't know if there is a coopting ancestor here, as it could be above the root
141+
// but those cases should be fairly rare
142+
traverseAndDisableAxFromExcludedViews(root, root, false)
126143

127144
return axOrderViews
128145
}

0 commit comments

Comments
 (0)