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