Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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()) {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
name="state_off_description"
description="a switch in its disabled state"
>off</string>
<string
name="state_unchecked_description"
description="a checkbox, radio button, or other checkable widget which is not checked"
>not checked</string>
<string
name="state_mixed_description"
description="a checkbox, radio button, or other widget which is both checked and unchecked"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.testutils.shadows.ShadowArguments
import org.assertj.core.api.Assertions.assertThat
Expand Down Expand Up @@ -382,4 +383,97 @@ class ReactAccessibilityDelegateTest {
assertThat(result).isTrue()
assertThat(view.getTag(R.id.accessibility_state_expanded)).isEqualTo(false)
}

@Test
fun testOnInitializeAccessibilityNodeInfo_uncheckedCheckbox_setsStateDescription() {
val info = initializeNodeInfoWithCheckedState(AccessibilityRole.CHECKBOX, checked = false)

assertThat(info.isCheckable).isTrue()
assertThat(info.isChecked).isFalse()
assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_unchecked_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_checkedCheckbox_leavesStateDescriptionUnset() {
val info = initializeNodeInfoWithCheckedState(AccessibilityRole.CHECKBOX, checked = true)

assertThat(info.isCheckable).isTrue()
assertThat(info.isChecked).isTrue()
assertThat(info.stateDescription).isNull()
}

@Test
fun testOnInitializeAccessibilityNodeInfo_uncheckedRadio_setsStateDescription() {
val info = initializeNodeInfoWithCheckedState(AccessibilityRole.RADIO, checked = false)

assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_unchecked_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_uncheckedWithoutRole_setsStateDescription() {
val info = initializeNodeInfoWithCheckedState(role = null, checked = false)

assertThat(info.isCheckable).isTrue()
assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_unchecked_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_switchOff_setsStateDescription() {
val info = initializeNodeInfoWithCheckedState(AccessibilityRole.SWITCH, checked = false)

assertThat(info.isChecked).isFalse()
assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_off_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_switchOn_setsStateDescription() {
val info = initializeNodeInfoWithCheckedState(AccessibilityRole.SWITCH, checked = true)

assertThat(info.isChecked).isTrue()
assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_on_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_roleProp_switchOff_setsStateDescription() {
view.setTag(R.id.role, ReactAccessibilityDelegate.Role.SWITCH)
view.setTag(R.id.accessibility_state, JavaOnlyMap().apply { putBoolean("checked", false) })
val info = AccessibilityNodeInfoCompat.obtain()

accessibilityDelegate.onInitializeAccessibilityNodeInfo(view, info)

assertThat(info.stateDescription)
.isEqualTo(view.context.getString(R.string.state_off_description))
}

@Test
fun testOnInitializeAccessibilityNodeInfo_noCheckedState_leavesStateDescriptionUnset() {
view.setTag(R.id.accessibility_role, AccessibilityRole.CHECKBOX)
view.setTag(R.id.accessibility_state, JavaOnlyMap().apply { putBoolean("selected", true) })
val info = AccessibilityNodeInfoCompat.obtain()

accessibilityDelegate.onInitializeAccessibilityNodeInfo(view, info)

assertThat(info.isSelected).isTrue()
assertThat(info.stateDescription).isNull()
}

private fun initializeNodeInfoWithCheckedState(
role: AccessibilityRole?,
checked: Boolean,
): AccessibilityNodeInfoCompat {
if (role != null) {
view.setTag(R.id.accessibility_role, role)
}
view.setTag(R.id.accessibility_state, JavaOnlyMap().apply { putBoolean("checked", checked) })
val info = AccessibilityNodeInfoCompat.obtain()

accessibilityDelegate.onInitializeAccessibilityNodeInfo(view, info)

return info
}
}
Loading