Skip to content

Commit 2e2da47

Browse files
Stop routing TalkBack's click to removed accessibility actions on Android
A view whose accessibilityActions declared activate kept dispatching it on TalkBack's click after the actions were removed or replaced with an empty list, so the view's own click never ran. setAccessibilityActions returned before writing the tag when the prop was null, and ReactAccessibilityDelegate routed an action through a map of every action it had built node info with, never dropping one. setAccessibilityActions now writes null to the tag, and the delegate resolves an action against the view's current accessibilityActions.
1 parent e04aaf6 commit 2e2da47

3 files changed

Lines changed: 74 additions & 9 deletions

File tree

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -473,11 +473,7 @@ private void updateViewContentDescription(@NonNull T view) {
473473
}
474474

475475
@ReactProp(name = ViewProps.ACCESSIBILITY_ACTIONS)
476-
public void setAccessibilityActions(T view, ReadableArray accessibilityActions) {
477-
if (accessibilityActions == null) {
478-
return;
479-
}
480-
476+
public void setAccessibilityActions(T view, @Nullable ReadableArray accessibilityActions) {
481477
view.setTag(R.id.accessibility_actions, accessibilityActions);
482478
}
483479

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
5757
host?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
5858
}
5959
}
60-
private val accessibilityActionsMap = HashMap<Int, String?>()
6160
private var accessibilityLabelledBy: View? = null
6261

6362
init {
@@ -130,7 +129,6 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
130129
val actionId: Int =
131130
actionIdMap.get(actionName)
132131
?: customActionIdMap.getOrPut(actionName) { customActionCounter++ }
133-
accessibilityActionsMap[actionId] = actionName
134132
val accessibilityAction = AccessibilityActionCompat(actionId, actionLabel)
135133
info.addAction(accessibilityAction)
136134
}
@@ -225,9 +223,10 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
225223
if (action == AccessibilityNodeInfoCompat.ACTION_EXPAND) {
226224
host.setTag(R.id.accessibility_state_expanded, true)
227225
}
228-
if (accessibilityActionsMap.containsKey(action)) {
226+
val actionName = findDeclaredActionName(host, action)
227+
if (actionName != null) {
229228
val eventData = createMap()
230-
eventData.putString("actionName", accessibilityActionsMap[action])
229+
eventData.putString("actionName", actionName)
231230
if (
232231
action == AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id &&
233232
args?.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE) == true
@@ -273,6 +272,22 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
273272
return super.performAccessibilityAction(host, action, args)
274273
}
275274

275+
private fun findDeclaredActionName(host: View, actionId: Int): String? {
276+
val accessibilityActions =
277+
host.getTag(R.id.accessibility_actions) as ReadableArray? ?: return null
278+
for (i in 0..<accessibilityActions.size()) {
279+
val action = accessibilityActions.getMap(i)
280+
if (action == null || !action.hasKey("name")) {
281+
continue
282+
}
283+
val actionName = action.getString("name")
284+
if ((actionIdMap[actionName] ?: customActionIdMap[actionName]) == actionId) {
285+
return actionName
286+
}
287+
}
288+
return null
289+
}
290+
276291
/**
277292
* Schedule a command for sending an accessibility event. Note: A command is used to ensure that
278293
* accessibility events are sent at most one in a given time frame to save system resources while

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package com.facebook.react.uimanager
1111

1212
import android.view.View.OnFocusChangeListener
13+
import android.view.accessibility.AccessibilityNodeInfo
1314
import com.facebook.react.R
1415
import com.facebook.react.bridge.BridgeReactContext
1516
import com.facebook.react.bridge.DynamicFromObject
@@ -162,4 +163,57 @@ class BaseViewManagerTest {
162163
viewManager.setAccessibilityLabelledBy(view, DynamicFromObject(JavaOnlyArray()))
163164
Assertions.assertThat(view.getTag(R.id.labelled_by)).isNull()
164165
}
166+
167+
@Test
168+
fun testDeclaredActivateActionTakesTheAccessibilityClick() {
169+
applyAccessibilityActions(JavaOnlyArray.of(JavaOnlyMap.of("name", "activate")))
170+
view.createAccessibilityNodeInfo()
171+
172+
Assertions.assertThat(performAccessibilityClick()).isEqualTo(0)
173+
}
174+
175+
@Test
176+
fun testRemovedAccessibilityActionsNoLongerTakeTheAccessibilityClick() {
177+
applyAccessibilityActions(JavaOnlyArray.of(JavaOnlyMap.of("name", "activate")))
178+
view.createAccessibilityNodeInfo()
179+
applyAccessibilityActions(null)
180+
181+
Assertions.assertThat(performAccessibilityClick()).isEqualTo(1)
182+
}
183+
184+
@Test
185+
fun testEmptiedAccessibilityActionsNoLongerTakeTheAccessibilityClick() {
186+
applyAccessibilityActions(JavaOnlyArray.of(JavaOnlyMap.of("name", "activate")))
187+
view.createAccessibilityNodeInfo()
188+
applyAccessibilityActions(JavaOnlyArray())
189+
190+
Assertions.assertThat(performAccessibilityClick()).isEqualTo(1)
191+
}
192+
193+
@Test
194+
fun testRemovedAccessibilityActionsLeaveTheNodeInfo() {
195+
applyAccessibilityActions(
196+
JavaOnlyArray.of(JavaOnlyMap.of("name", "openMenu", "label", "Open menu"))
197+
)
198+
applyAccessibilityActions(null)
199+
200+
Assertions.assertThat(
201+
view.createAccessibilityNodeInfo().actionList.map { it.label?.toString() }
202+
)
203+
.doesNotContain("Open menu")
204+
}
205+
206+
private fun applyAccessibilityActions(accessibilityActions: JavaOnlyArray?) {
207+
viewManager.updateProperties(
208+
view,
209+
ReactStylesDiffMap(JavaOnlyMap.of(ViewProps.ACCESSIBILITY_ACTIONS, accessibilityActions)),
210+
)
211+
}
212+
213+
private fun performAccessibilityClick(): Int {
214+
var clicks = 0
215+
view.setOnClickListener { clicks++ }
216+
view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, null)
217+
return clicks
218+
}
165219
}

0 commit comments

Comments
 (0)