diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.kt
index 6a227761f256..8f44bd3a7ad6 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.kt
@@ -99,7 +99,7 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
// state is changeable.
val accessibilityState = host.getTag(R.id.accessibility_state) as ReadableMap?
if (accessibilityState != null) {
- setState(info, accessibilityState)
+ setState(info, accessibilityState, accessibilityRole, host.context)
}
val accessibilityActions = host.getTag(R.id.accessibility_actions) as ReadableArray?
@@ -626,6 +626,8 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
private fun setState(
info: AccessibilityNodeInfoCompat,
accessibilityState: ReadableMap,
+ role: AccessibilityRole?,
+ context: Context,
) {
val i = accessibilityState.keySetIterator()
while (i.hasNextKey()) {
@@ -639,10 +641,34 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
val boolValue = value.asBoolean()
info.isCheckable = true
info.isChecked = boolValue
+ // TalkBack only speaks the unchecked state of a checkable node, and only avoids
+ // announcing "checked" and "on" together for a switch, when the node carries a
+ // stateDescription (API 30+), as native CompoundButton sets.
+ val stateDescription = getCheckedStateDescription(boolValue, role, context)
+ if (stateDescription != null) {
+ info.stateDescription = stateDescription
+ }
}
}
}
+ private fun getCheckedStateDescription(
+ checked: Boolean,
+ role: AccessibilityRole?,
+ context: Context,
+ ): String? =
+ if (role == AccessibilityRole.SWITCH) {
+ context.getString(
+ if (checked) R.string.state_on_description else R.string.state_off_description
+ )
+ } else if (!checked) {
+ context.getString(R.string.state_unchecked_description)
+ } else {
+ // Leave the checked state to TalkBack, which already announces it in the device
+ // language.
+ null
+ }
+
// TODO: Eventually support for other languages on talkback
@JvmStatic
public fun setRole(
diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/strings.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/strings.xml
index a2f6ad526315..1a032eb255ae 100644
--- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/strings.xml
+++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/strings.xml
@@ -96,6 +96,10 @@
name="state_off_description"
description="a switch in its disabled state"
>off
+ not checked