Improve message actions menu TalkBack accessibility#6484
Conversation
`ReactionToggle` is used both in the long-press reactions bar and in the full emoji picker. Its `Modifier.toggleable` was wired with `role = Role.Checkbox`, so TalkBack announced reactions as "[emoji], checkbox, not checked, double tap to toggle". Reactions are tap-to-add / tap-to-remove toggle buttons, not checkboxes — the Figma a11y principle for role calls this out explicitly with the example "Thumbs up, toggle button, not pressed" vs the don't case "Thumbs up sign, checkbox". Switch the role to `Role.Button` and attach an explicit `stateDescription` of "Pressed" / "Not pressed" so TalkBack reads "[emoji], button, pressed / not pressed, double tap to toggle" — the canonical Android toggle-button pattern, since Compose has no `Role.ToggleButton`. Two new strings (`stream_compose_reactions_pressed` and `stream_compose_reactions_not_pressed`) translated across the 7 supported locales. Internal-only composable; no public API change. This overrides the deliberate `Role.Checkbox` decision from #6440 in favour of the design source-of-truth.
Tapping a reaction toggle in either the long-press reactions bar or the full emoji picker grid produced no screen-reader confirmation. TalkBack re-focused the underlying message but emitted no signal that the toggle state changed, leaving SR users unsure whether the action took effect. Wrap `ReactionToggle.onValueChange` to fire a polite live region announce before forwarding the new state to the caller. Both surfaces inherit the behaviour automatically because the full emoji picker's `ReactionMenuOptionItem` factory delegates to the same `ReactionToggle`. The announce string carries the emoji character as `%1$s`. TalkBack reads the character with its locale-native name (e.g. `👍` → "Thumbs up sign"), so the announce reads naturally as "Thumbs up sign reaction added" without maintaining a per-reaction name table in the SDK. Two new strings (`stream_compose_reactions_added` and `stream_compose_reactions_removed`) translated across the 7 supported locales.
When the long-press menu opened, TalkBack leaked the host activity's
`android:label` ("Chat Sample Compose") before the focused element,
giving screen-reader users no signal that a new surface had appeared.
Declare `paneTitle = "Message actions"` on the inner `Column` inside
the `Dialog`. TalkBack treats `paneTitle` changes as a window-state
event and announces the title on dialog entry, so the new opening
announcement reads "Message actions, [first focused item]" instead of
the host activity label.
One new string (`stream_compose_message_actions_menu_title`)
translated across the 7 supported locales.
When the emoji picker `ModalBottomSheet` opened from the "+" in the reactions bar, TalkBack focused the drag handle and announced "Collapsed, drag handle, actions available, swipe up or swipe down". Screen-reader users had no signal that the emoji picker had appeared and the drag handle drowned out the actual content. `ModalBottomSheet` swallows the standard `paneTitle` window-state event (verified in #6466 / `PollDialogHeader`), so announce the title programmatically via `view.announceForAccessibility` in a `LaunchedEffect`. TalkBack now reads "Emoji picker" on sheet open. Hide the drag handle from the accessibility tree via `Modifier.semantics { hideFromAccessibility() }`. The handle stays visually present and remains draggable for sighted users; TalkBack focus skips it and lands on the first emoji in the picker grid instead. One new string (`stream_compose_emoji_picker_title`) translated across the 7 supported locales. No public API changes.
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
WalkthroughThis PR adds accessibility support to three chat UI components: ReactionToggle now announces state changes and provides semantics labels; ReactionsPicker bottom sheet announces its title via LaunchedEffect to work around ModalBottomSheet's paneTitle suppression; MessageActions menu gains a paneTitle semantic. All three components' accessibility strings are localized across eight language variants. ChangesAccessibility Enhancements for Chat UI Components
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|


AND-1180
Goal
The long-press message actions menu and the emoji reactions surfaces had four screen-reader gaps:
ReactionToggleusedModifier.toggleable(role = Role.Checkbox), so TalkBack read "[emoji] sign, checkbox, not checked, double tap to toggle". The Figma a11y principle calls this out explicitly as the Don't case; reactions are toggle buttons (tap to add / tap to remove), not checkboxes.android:label("Chat Sample Compose") before the focused element, with no signal of the new surface.Implementation
Four feature-scoped commits.
1. Expose reactions as toggle buttons to TalkBack.
In
ReactionToggle, switch the toggleable role fromRole.CheckboxtoRole.Buttonand attach an explicitstateDescriptionof "Pressed" / "Not pressed". TalkBack now reads "[emoji], button, pressed / not pressed, double tap to toggle" — the canonical Android toggle-button pattern, since Compose has noRole.ToggleButton. Internal composable; no public API change. This overrides #6440's deliberateRole.Checkboxdecision in favour of the design source-of-truth.2. Announce reaction add and remove.
Wrap
ReactionToggle.onValueChangeto fireview.announceForAccessibility("[emoji] reaction added" / "removed")before forwarding the new state. Both the long-press reactions bar and the full emoji picker grid benefit automatically because the picker'sReactionMenuOptionItemfactory delegates to the sameReactionToggle. The string carries the emoji character as%1$s; TalkBack reads the character with its locale-native name, so the announce reads naturally as "Thumbs up sign reaction added" without maintaining a per-reaction name table.3. Announce the message actions menu pane title.
Declare
Modifier.semantics { paneTitle = "Message actions" }on the innerColumninside theDialog. TalkBack treatspaneTitlechanges as a window-state event and announces the title on dialog entry, replacing the previously-leaked host activity label.4. Announce the emoji picker sheet.
ModalBottomSheetswallows the standardpaneTitlewindow-state event (verified in #6466 /PollDialogHeader), so fall back to programmatic announce viaview.announceForAccessibilityin aLaunchedEffect. Hide the sheet's drag handle from the accessibility tree viaModifier.semantics { hideFromAccessibility() }— the handle stays visually present and remains draggable for sighted users; TalkBack focus skips it and lands on the first emoji in the picker grid.New strings translated across the 7 supported locales:
stream_compose_reactions_pressed,stream_compose_reactions_not_pressedstream_compose_reactions_added,stream_compose_reactions_removedstream_compose_message_actions_menu_titlestream_compose_emoji_picker_titleNo public API changes.
Testing
Enable TalkBack on a physical device. Run the Compose sample.
Summary by CodeRabbit
New Features
Localization