Skip to content

Improve message actions menu TalkBack accessibility#6484

Draft
andremion wants to merge 4 commits into
developfrom
fix/compose-message-actions-a11y
Draft

Improve message actions menu TalkBack accessibility#6484
andremion wants to merge 4 commits into
developfrom
fix/compose-message-actions-a11y

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented Jun 2, 2026

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.

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 added 4 commits June 2, 2026 15:02
`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 added the pr:improvement Improvement label Jun 2, 2026
@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

✅ 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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.84 MB 5.84 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.07 MB 11.07 MB 0.00 MB 🟢
stream-chat-android-compose 12.46 MB 12.47 MB 0.00 MB 🟢

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31546d4f-c6a7-4a3c-a322-3204ec04a5b2

📥 Commits

Reviewing files that changed from the base of the PR and between fa7a6d3 and b115247.

📒 Files selected for processing (12)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageactions/MessageActions.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/reactionpicker/ReactionsPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/reactions/ReactionToggle.kt
  • stream-chat-android-compose/src/main/res/values-es/strings.xml
  • stream-chat-android-compose/src/main/res/values-fr/strings.xml
  • stream-chat-android-compose/src/main/res/values-hi/strings.xml
  • stream-chat-android-compose/src/main/res/values-in/strings.xml
  • stream-chat-android-compose/src/main/res/values-it/strings.xml
  • stream-chat-android-compose/src/main/res/values-ja/strings.xml
  • stream-chat-android-compose/src/main/res/values-ko/strings.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml

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.
Description check ✅ Passed The description comprehensively covers Goal, Implementation, and Testing sections with detailed explanations of the four accessibility fixes and their rationale. However, the UI Changes section with before/after screenshots is missing, and the Contributor Checklist section is incomplete.
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.

✏️ 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.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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.

1 participant