Skip to content

Commit 39a496c

Browse files
AndrewBlackMetameta-codesync[bot]
authored andcommitted
Add set-progress accessibility actions on Android
Summary: # Robot Summary ## Overview Add the standard Android `ACTION_SET_PROGRESS` accessibility action to React Native. Include its numeric progress argument in the JavaScript event. Update Flow, deprecated TypeScript, and generated public API types for the new action and event value. ## Motivation This base diff lets Android accessibility services set adjustable controls to an absolute value. The later diffs use it for relative gaze slider drags. ## Practical Impact React Native sliders can receive an absolute progress value from Android accessibility services. Changelog: [Android][Added] - Add support for direct progress accessibility actions Differential Revision: D124925585
1 parent ca8504b commit 39a496c

5 files changed

Lines changed: 159 additions & 63 deletions

File tree

‎packages/react-native/Libraries/Components/View/ViewAccessibility.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ export type AccessibilityActionName =
137137
* Generated when a screen reader user decrements an adjustable component.
138138
*/
139139
| 'decrement'
140+
/**
141+
* Generated when an accessibility service sets an adjustable component to a specific value.
142+
* @platform android
143+
*/
144+
| 'setProgress'
140145
/**
141146
* Generated when a TalkBack user places accessibility focus on the component and double taps and holds one finger on the screen.
142147
* @platform android
@@ -162,7 +167,7 @@ export type AccessibilityActionInfo = Readonly<{
162167

163168
// The info included in the event sent to onAccessibilityAction
164169
export type AccessibilityActionEvent = NativeSyntheticEvent<
165-
Readonly<{actionName: string, ...}>,
170+
Readonly<{actionName: string, value?: number, ...}>,
166171
>;
167172

168173
export type AccessibilityState = {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.os.Message
1515
import android.view.View
1616
import android.view.ViewGroup
1717
import android.view.accessibility.AccessibilityEvent
18+
import android.view.accessibility.AccessibilityNodeInfo
1819
import android.widget.EditText
1920
import androidx.core.view.ViewCompat
2021
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -227,6 +228,15 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
227228
if (accessibilityActionsMap.containsKey(action)) {
228229
val eventData = createMap()
229230
eventData.putString("actionName", accessibilityActionsMap[action])
231+
if (
232+
action == AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id &&
233+
args?.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE) == true
234+
) {
235+
eventData.putDouble(
236+
"value",
237+
args.getFloat(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE).toDouble(),
238+
)
239+
}
230240
val reactContext = host.context as ReactContext
231241
if (reactContext.hasActiveReactInstance()) {
232242
val reactTag = host.id
@@ -576,6 +586,7 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
576586
"longpress" to AccessibilityActionCompat.ACTION_LONG_CLICK.id,
577587
"increment" to AccessibilityActionCompat.ACTION_SCROLL_FORWARD.id,
578588
"decrement" to AccessibilityActionCompat.ACTION_SCROLL_BACKWARD.id,
589+
"setProgress" to AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id,
579590
"expand" to AccessibilityActionCompat.ACTION_EXPAND.id,
580591
"collapse" to AccessibilityActionCompat.ACTION_COLLAPSE.id,
581592
)

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

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

1212
import android.os.Bundle
13+
import android.view.accessibility.AccessibilityNodeInfo
1314
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
1415
import com.facebook.react.R
1516
import com.facebook.react.bridge.BridgeReactContext
1617
import com.facebook.react.bridge.JavaOnlyArray
1718
import com.facebook.react.bridge.JavaOnlyMap
19+
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
20+
import com.facebook.react.bridge.UIManager
21+
import com.facebook.react.bridge.WritableMap
1822
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
23+
import com.facebook.react.uimanager.common.UIManagerType
24+
import com.facebook.react.uimanager.events.Event
25+
import com.facebook.react.uimanager.events.EventDispatcher
26+
import com.facebook.react.uimanager.events.RCTEventEmitter
1927
import com.facebook.react.views.view.ReactViewGroup
2028
import com.facebook.testutils.shadows.ShadowArguments
2129
import org.assertj.core.api.Assertions.assertThat
2230
import org.junit.Before
2331
import org.junit.Test
2432
import org.junit.runner.RunWith
33+
import org.mockito.Mockito.mockStatic
34+
import org.mockito.kotlin.argumentCaptor
35+
import org.mockito.kotlin.eq
36+
import org.mockito.kotlin.mock
37+
import org.mockito.kotlin.verify
38+
import org.mockito.kotlin.whenever
2539
import org.robolectric.RobolectricTestRunner
2640
import org.robolectric.RuntimeEnvironment
2741
import org.robolectric.annotation.Config
@@ -38,6 +52,7 @@ class ReactAccessibilityDelegateTest {
3852
fun setUp() {
3953
ReactNativeFeatureFlagsForTests.setUp()
4054
reactContext = BridgeReactContext(RuntimeEnvironment.getApplication())
55+
reactContext.initializeWithInstance(createMockCatalystInstance())
4156
themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1)
4257
view = ReactViewGroup(themedReactContext).apply { id = 100 }
4358
accessibilityDelegate = ReactAccessibilityDelegate(view, false, 0)
@@ -231,6 +246,58 @@ class ReactAccessibilityDelegateTest {
231246
)
232247
}
233248

249+
@Test
250+
fun testPerformAccessibilityAction_setProgressAction_isExposedAndHandled() {
251+
val accessibilityActions = JavaOnlyArray()
252+
val action = JavaOnlyMap().apply { putString("name", "setProgress") }
253+
accessibilityActions.pushMap(action)
254+
view.setTag(R.id.accessibility_actions, accessibilityActions)
255+
256+
val nodeInfo = AccessibilityNodeInfoCompat.obtain()
257+
accessibilityDelegate.onInitializeAccessibilityNodeInfo(view, nodeInfo)
258+
259+
assertThat(nodeInfo.actionList.map { it.id })
260+
.contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id)
261+
262+
val args =
263+
Bundle().apply { putFloat(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE, 42f) }
264+
val eventDispatcher = mock<EventDispatcher>()
265+
val uiManager = mock<UIManager>()
266+
whenever(uiManager.eventDispatcher).thenReturn(eventDispatcher)
267+
val uiManagerHelper = mockStatic(UIManagerHelper::class.java)
268+
try {
269+
uiManagerHelper
270+
.`when`<UIManager?> {
271+
UIManagerHelper.getUIManager(themedReactContext, UIManagerType.FABRIC)
272+
}
273+
.thenReturn(uiManager)
274+
275+
val result =
276+
accessibilityDelegate.performAccessibilityAction(
277+
view,
278+
AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id,
279+
args,
280+
)
281+
282+
assertThat(result).isTrue()
283+
val eventCaptor = argumentCaptor<Event<*>>()
284+
verify(eventDispatcher).dispatchEvent(eventCaptor.capture())
285+
val eventEmitter = mock<RCTEventEmitter>()
286+
eventCaptor.firstValue.dispatch(eventEmitter)
287+
val eventDataCaptor = argumentCaptor<WritableMap>()
288+
verify(eventEmitter)
289+
.receiveEvent(
290+
eq(view.id),
291+
eq(ReactAccessibilityDelegate.TOP_ACCESSIBILITY_ACTION_EVENT),
292+
eventDataCaptor.capture(),
293+
)
294+
assertThat(eventDataCaptor.firstValue.getString("actionName")).isEqualTo("setProgress")
295+
assertThat(eventDataCaptor.firstValue.getDouble("value")).isEqualTo(42.0)
296+
} finally {
297+
uiManagerHelper.close()
298+
}
299+
}
300+
234301
@Test
235302
fun testPerformAccessibilityAction_adjustableRoleWithTextValue_doesNotScheduleEvent() {
236303
val accessibilityActions = JavaOnlyArray()

0 commit comments

Comments
 (0)