Improve message composer TalkBack accessibility: action verbs, banner merge, audio recording lifecycle#6441
Improve message composer TalkBack accessibility: action verbs, banner merge, audio recording lifecycle#6441andremion wants to merge 5 commits into
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
|
@CodeRabbit review |
SDK Size Comparison 📏
|
✅ Actions performedReview triggered.
|
WalkthroughThis PR comprehensively enhances accessibility across the Stream Chat Android Compose UI by introducing dedicated audio recording state announcements and focus management, adding semantic metadata to message composer components, and providing localized strings in seven language variants. ChangesAccessibility Enhancements for Audio Recording and Message Composer
🎯 3 (Moderate) | ⏱️ ~25 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingAccessibility.kt`:
- Around line 80-96: The LaunchedEffect is calling onTransitionConsumed()
unconditionally which can clear cancelRequested before the Idle ->
Idle-with-cancel transition is handled; change this so onTransitionConsumed() is
only invoked when a relevant transition was actually processed (i.e., when you
call view.announceForAccessibility for RecordingState.Hold or for
RecordingState.Idle with cancelRequested). Locate the LaunchedEffect block using
recordingState, previousWasIdle, cancelRequested and move the
onTransitionConsumed() call into the branches that perform
view.announceForAccessibility (or set a local handled flag and call
onTransitionConsumed() after the when only if handled is true).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 59b53ea3-4496-4593-9f9b-d2cb091ed485
📒 Files selected for processing (16)
stream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/QuotedMessage.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingAccessibility.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingButton.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerEditIndicator.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputTrailingContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.ktstream-chat-android-compose/src/main/res/values-es/strings.xmlstream-chat-android-compose/src/main/res/values-fr/strings.xmlstream-chat-android-compose/src/main/res/values-hi/strings.xmlstream-chat-android-compose/src/main/res/values-in/strings.xmlstream-chat-android-compose/src/main/res/values-it/strings.xmlstream-chat-android-compose/src/main/res/values-ja/strings.xmlstream-chat-android-compose/src/main/res/values-ko/strings.xmlstream-chat-android-compose/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
- stream-chat-android-compose/api/stream-chat-android-compose.api
`MessageComposerSendButton`, `MessageComposerSaveButton`, and the
attachments toggle in `MessageComposerLeadingContent` are wrapped in
Material3 `FilledIconButton`, which does not expose an `onClickLabel`.
TalkBack therefore announced each as `"<icon label>, button, double-tap
to activate"` — the generic activate hint had no verb.
Layer a `Modifier.semantics { onClick(label = …) { …; true } }` on each
button so TalkBack reads a concrete action:
- Send → `"Send message"` (`stream_compose_message_composer_send_action`).
- Save → `"Save changes"` (`stream_compose_message_composer_save_action`).
- Attachments → state-aware label that pairs with the existing
`stateDescription`: `"Open attachments"` when collapsed,
`"Close attachments"` when expanded
(`stream_compose_message_composer_attachments_open` /
`…_close`).
The semantics action call-through (`onClick(); true`) keeps the actual
click handler intact while overriding only the announced verb. The
attachments toggle button is also extracted into a private
`AttachmentPickerButton` composable to keep
`MessageComposerLeadingContent` under detekt's method-length cap.
Translates the four new strings across all 7 supported locales. No
public API surface change.
Wraps the edit-indicator row and the reply quoted-message in
`Modifier.semantics(mergeDescendants = true) {}` so TalkBack reads the
banner as a single chunk instead of fragment by fragment. Cancel icons
now announce the action being cancelled ("Cancel editing" / "Cancel
reply") rather than the generic "Cancel".
When replying to your own message, the username Text leaf overrides
`contentDescription` to "Reply to you" so TalkBack mirrors the
"Reply to {name}" symmetry while the visible "You" label stays compact.
Mic button content description includes the press-and-hold gesture hint and exposes Role.Button, so TalkBack reads "Record audio message. Press and hold to record. Button." without a misleading "double-tap to activate" hint. One-shot announcements via View.announceForAccessibility on recording state transitions: - Idle → Hold: "Recording started. Slide left to cancel, up to lock." - non-Idle → Idle: "Recording cancelled", gated on a cancelRequested flag set by wrapped onCancelRecording / onDeleteRecording. Confirm and send paths leave the flag unset so they stay silent and let the attachment item announce itself. Focus moves on state transitions: - Hold → Locked: focus on the merged recording-bar row. - Locked → Overview: focus on the merged playback row. Overview's outer Row is split into a separately-focusable play button + an inner focusable summary so the play action stays reachable. - A 100ms delay before requestFocus gives Compose layout and the accessibility tree time to settle, beating TalkBack's fallback recovery from the previously-focused node. Cancel and Delete focus is left to Compose's natural recovery — focus returns to the mic button when it becomes visible again.
Move `onTransitionConsumed()` inside the announcement branches so the caller's `cancelRequested` flag only clears when an announcement has actually fired. The bug described in the review doesn't manifest with the current state writers (the wrapped `onCancelRecording` / `onDeleteRecording` set the flag and `setState(Idle)` in the same Compose snapshot) but separating "transition observed" from "transition consumed" tightens the helper's contract and makes it robust to future changes to the flag writers or the `LaunchedEffect` keys.
fd286d5 to
da0224f
Compare
|


Goal
The Compose message composer was underserved on TalkBack: action buttons announced as generic "double-tap to activate" with no verb, the Edit / Reply banner read fragment-by-fragment, the audio recording flow had no lifecycle announcements, and TalkBack focus drifted out of the composer on state transitions. This PR brings the composer up to a usable TalkBack baseline.
Implementation
Three feature-scoped commits.
Action verbs on Send / Save / attach buttons.
FilledIconButtondoesn't expose anonClickLabel, so TalkBack defaulted to "double-tap to activate" with no verb. LayeredModifier.semantics { onClick(label = …) { …; true } }on each button so the verb reads asSend message,Save changes,Open attachments/Close attachments(the attachments toggle pairs the label with the existingstateDescription). ExtractedAttachmentPickerButtonfromMessageComposerLeadingContentto keep the parent under detekt's method-length cap.Edit / Reply banner merge for a11y.
Wrapped the edit-indicator row and reply quoted-message in
Modifier.semantics(mergeDescendants = true) {}so TalkBack reads each banner as a single chunk. Cancel icons announce the action they undo (Cancel editing/Cancel reply) instead of a generic "Cancel". When replying to your own message, the usernameTextleaf overridescontentDescriptionto "Reply to you" so the post-preposition pronoun form is grammatically correct in es / it / fr (the visible "You" label stays compact).Audio recording lifecycle a11y.
contentDescriptionincludes the press-and-hold gesture hint, exposesRole.Button— TalkBack reads"Record audio message. Press and hold to record. Button.".View.announceForAccessibilityon recording state transitions:Idle → Holdannounces "Recording started. Slide left to cancel, up to lock.";non-Idle → Idleannounces "Recording cancelled" when gated by acancelRequestedflag set by wrappedonCancelRecording/onDeleteRecording(confirm / send paths stay silent and let the attachment item announce itself).Hold → Lockedlands on the merged recording-bar row,Locked → Overviewlands on the merged playback row. Overview's outer Row is split into a separately-focusable play button + an inner focusable summary so the play action stays reachable.Confirmbutton label is state-aware: "Send recording" whenaudioRecordingSendOnCompleteis enabled, "Confirm recording" otherwise.requestFocuslets Compose finish layout and the accessibility tree settle before we move focus, so it wins over TalkBack's fallback recovery.All new strings translated across the 7 supported locales (es / fr / hi / in / it / ja / ko). No public API changes.
Testing
Enable TalkBack on a physical device or emulator. For each scenario, expected announcement is in bold.
1. Composer trailing-button verbs
2. Composer leading-button label
3. Edit / Reply banner
4. Audio recording lifecycle
audioRecordingSendOnCompleteflag (ChatTheme.config.composer). In Overview, focus the Confirm button → reads "Send recording" when the flag is true, "Confirm recording" when false.Summary by CodeRabbit
Release Notes
New Features
Localization