fix(a11y): restore TalkBack tab states, native toggle/checkbox/radio roles, in-locale Connect strings#45
Merged
Conversation
Captures the four TalkBack regressions reported by blind users and the plan to fix them using native Role.Tab/Role.Switch/Role.Checkbox semantics plus in-app-locale Connect-button strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restores 'selected' state announcement and eliminates the triple read of each tab label by merging descendants and dropping the Icon's redundant contentDescription.
Drops custom cd_profile_tab_selected/unselected strings in favor of the platform's native Tab role announcement.
Drops custom cd_toggle_row_on/off strings. The native Switch role makes TalkBack announce on/off naturally, fulfilling the user's 'use native controls' request.
Single focus stop per config, native checked/not-checked announcement, honors the row's enabled parameter for the screen reader.
Migrates cd_connect_button_* into WhiteDnsStrings / WhiteDnsL10n so the button's accessibility description follows the in-app language picker, not the device system locale. Adds merged semantics so the button is a single focus stop.
These keys are no longer referenced from Kotlin code now that bottom nav, profile tabs, ToggleRow, ParallelTestConfigRow, and the Connect button use native Role-based semantics or the in-app WhiteDnsL10n pipeline.
Language, Theme, and Connection mode segmented controls each had
.clickable(enabled = !selected) on the option boxes, which:
- gave the selected option no Role, so TalkBack never announced
"selected" when the language (or theme/mode) changed
- made the selected option non-focusable for the screen reader
Switching to .selectable(selected, role = Role.RadioButton, onClick)
+ .semantics(mergeDescendants = true) {} fixes both: TalkBack now
announces "<label>, radio button, selected/not selected" with a
single focus stop per option. The ConnectionMode control keeps its
outer enabled flag wired through to selectable.enabled.
Merged
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Restores the screen-reader behaviors that regressed after the recent bottom-nav and Parallel Test changes, and removes the custom contentDescription strings in favor of native Compose roles so TalkBack announces state ("selected", "checked", "on/off") automatically — and in the user's chosen in-app language.
Addresses user-reported issues:
What changed
Modifier.selectable(role = Role.Tab)withmergeDescendants, so TalkBack announces"<Tab name>, tab, selected"once.Modifier.toggleable(role = Role.Switch); innerSwitchhasonCheckedChange = null+clearAndSetSemantics {}so the row owns the gesture and TalkBack reads"<label>, switch, on/off".Modifier.toggleable(role = Role.Checkbox); inner indicator is non-semantic.Modifier.selectable(role = Role.RadioButton)withmergeDescendantsso each option announces its own selected/unselected state.WhiteDnsStringsCompositionLocal (English + Persian) so they follow the in-app language picker, not the system locale.cd_*content-description resources superseded by native semantics are removed.Test plan
./gradlew :app:compileDebugKotlinclean"<name>, tab, selected"exactly once per stopon/off,checked/unchecked,selected/not selected)