Skip to content

Improve message actions menu TalkBack accessibility#6484

Merged
andremion merged 5 commits into
developfrom
fix/compose-message-actions-a11y
Jun 8, 2026
Merged

Improve message actions menu TalkBack accessibility#6484
andremion merged 5 commits into
developfrom
fix/compose-message-actions-a11y

Conversation

@andremion

@andremion andremion commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

AND-1180

Goal

The long-press message actions menu and the emoji reactions surfaces had four screen-reader gaps:

  1. Reactions announced as checkboxes. ReactionToggle used Modifier.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.
  2. No status confirmation on toggle. Tapping a reaction emitted no screen-reader signal that the toggle state changed.
  3. Menu opening leaked host activity label. Opening the long-press actions menu announced the host activity's android:label ("Chat Sample Compose") before the focused element, with no signal of the new surface.
  4. Emoji picker sheet focused the drag handle. Opening the picker via "+" in the reactions bar landed TalkBack on the drag handle, with no signal that the picker surface had appeared.

Implementation

Four feature-scoped commits.

1. Expose reactions as toggle buttons to TalkBack.

In ReactionToggle, switch the toggleable role from Role.Checkbox to Role.Button and attach an explicit stateDescription of "Pressed" / "Not pressed". TalkBack now reads "[emoji], button, pressed / not pressed, double tap to toggle" — the canonical Android toggle-button pattern, since Compose has no Role.ToggleButton. Internal composable; no public API change. This overrides #6440's deliberate Role.Checkbox decision in favour of the design source-of-truth.

2. Announce reaction add and remove.

Wrap ReactionToggle.onValueChange to fire view.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's ReactionMenuOptionItem factory delegates to the same ReactionToggle. 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 inner Column inside the Dialog. TalkBack treats paneTitle changes 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.

ModalBottomSheet swallows the standard paneTitle window-state event (verified in #6466 / PollDialogHeader), so fall back to programmatic announce via view.announceForAccessibility in a LaunchedEffect. Hide the sheet's 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.

Migrated ReactionsPicker to the StreamCardBottomSheet wrapper introduced in #6480 so the emoji picker matches the rest of the SDK's card sheets (32dp top corners, backgroundCoreElevation1 container, Stream scrim). Added an optional dragHandle parameter to StreamCardBottomSheet (internal API; defaults to BottomSheetDefaults.DragHandle()) so the emoji picker can pass its accessibility-hidden handle. All other callers of StreamCardBottomSheet keep the default and are unaffected.

New strings translated across the 7 supported locales:

  • stream_compose_reactions_pressed, stream_compose_reactions_not_pressed
  • stream_compose_reactions_added, stream_compose_reactions_removed
  • stream_compose_message_actions_menu_title
  • stream_compose_emoji_picker_title

No public API changes.

Testing

Enable TalkBack on a physical device. Run the Compose sample.

  1. Long-press a message in the chat. Expected: TalkBack announces "Message actions" on dialog open, then reads the focused reaction as "[emoji], button, not pressed, double tap to toggle".
  2. Double-tap a reaction in the reactions bar. Expected: TalkBack announces "[emoji] reaction added" (live region). Double-tap again to remove: "[emoji] reaction removed".
  3. From the reactions bar, double-tap the "+" button to open the full emoji picker sheet. Expected: TalkBack announces "Emoji picker" then bottom sheet context. Focus does not land on the drag handle. Swipe to navigate the grid; tap an emoji to fire the same add/remove announce as step 2.
  4. Disable TalkBack and re-test all three surfaces to confirm visible behaviour is unchanged (the drag handle is still visible and draggable).

Summary by CodeRabbit

  • New Features

    • Enhanced accessibility for the message actions menu with proper semantic labeling.
    • Improved reactions picker with better screen reader support and accessibility announcements.
    • Added accessible state descriptions for reaction buttons to better communicate interaction states.
  • Localization

    • Added new UI text strings across 8+ languages (Spanish, French, Hindi, Indonesian, Italian, Japanese, Korean, and English) for emoji picker, message actions menu, and reaction states.

@andremion andremion added the pr:improvement Improvement label Jun 2, 2026
@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.86 MB 5.86 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.09 MB 11.09 MB 0.00 MB 🟢
stream-chat-android-compose 12.50 MB 12.51 MB 0.01 MB 🟢

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This 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.

Changes

Accessibility Enhancements for Chat UI Components

Layer / File(s) Summary
ReactionToggle accessibility state and announcements
src/main/java/io/getstream/chat/android/compose/ui/components/reactions/ReactionToggle.kt, src/main/res/values*/strings.xml
ReactionToggle uses Role.Button instead of checkbox, computes "pressed/not pressed" and "added/removed" strings, announces state changes via LocalView.announceForAccessibility() when toggled, and sets semantics { stateDescription } for screen readers across eight locales (default, es, fr, hi, in, it, ja, ko).
ReactionsPicker bottom-sheet accessibility announcement
src/main/java/io/getstream/chat/android/compose/ui/components/reactionpicker/ReactionsPicker.kt, src/main/res/values*/strings.xml
ReactionsPicker fetches emoji picker title from resources, uses LaunchedEffect(view.announceForAccessibility(...)) to announce the sheet since ModalBottomSheet suppresses standard paneTitle events, and hides the dragHandle from accessibility tree via semantics modifier (eight locales).
MessageActions menu paneTitle accessibility
src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/MessageActions.kt, src/main/res/values*/strings.xml
MessageActions Column sets paneTitle semantics using R.string.stream_compose_message_actions_menu_title, making the overlay's purpose clear to screen readers (eight locales).
Generated Compose singleton API export
stream-chat-android-compose/api/stream-chat-android-compose.api
Automatic API dump entry for the new ComposableSingletons$ReactionsPickerKt Compose lambda caching singleton.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • GetStream/stream-chat-android#6440: Modifies reaction UI accessibility semantics and adds reaction-related string resources, directly overlapping with ReactionToggle.kt and accessibility labeling.

Suggested reviewers

  • gpunto
  • VelikovPetar

Poem

A rabbit hops through screens and speaks,
Announcing toggles, reactions peak!
With localized whispers in eight tongues,
Accessibility's song is sweetly sung. 🐰🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary goal of the PR: improving accessibility (TalkBack) for the message actions menu, which is the central focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively addresses all required template sections with clear goal, implementation details, testing instructions, and string translations across locales.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/compose-message-actions-a11y

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@andremion andremion force-pushed the fix/compose-message-actions-a11y branch from b115247 to ed66b41 Compare June 3, 2026 08:28
andremion added 4 commits June 8, 2026 11:17
`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.
@andremion andremion force-pushed the fix/compose-message-actions-a11y branch from ed66b41 to e70d4a2 Compare June 8, 2026 10:24
@andremion andremion marked this pull request as ready for review June 8, 2026 10:33
@andremion andremion requested a review from a team as a code owner June 8, 2026 10:33

@gpunto gpunto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a couple of suggestions

1. StreamCardBottomSheet bakes `hideFromAccessibility()` into its
   default drag handle instead of exposing a `dragHandle` parameter for
   the emoji picker to override. The handle has no value for TalkBack
   users, so hiding it from the accessibility tree is the right default
   for every Stream card sheet rather than opt-in per call site.

2. `ReactionToggle` announces are renamed from added/removed to
   selected/unselected (variables and `stream_compose_reactions_*`
   keys). The component does not know whether a caller uses it to add
   or remove a reaction, so selected/unselected matches its actual
   semantic. Translations updated across the 7 supported locales.

KDoc cleanup: dropped the rationale paragraph from
`StreamCardBottomSheet` so the contract describes behaviour only.
@sonarqubecloud

sonarqubecloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
69.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@andremion andremion merged commit 2c2a578 into develop Jun 8, 2026
18 of 19 checks passed
@andremion andremion deleted the fix/compose-message-actions-a11y branch June 8, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants