Composer attachment picker accessibility improvements#6404
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
SDK Size Comparison 📏
|
WalkthroughThe PR enhances accessibility across the Stream Chat Compose UI by adding configurable content descriptions to UI elements, introducing semantic annotations for screen readers, creating an attachment announcer composable, and adding comprehensive localized strings for attachment states and picker controls across multiple languages. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 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. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/images/ImagesPicker.kt (1)
185-192:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't clear semantics on the video badge wrapper.
clearAndSetSemantics {}removes the duration text exposed byVideoBadge, so TalkBack loses the only useful metadata for video thumbnails. Keep the badge semantics intact and hide only the decorative icon.♻️ Suggested fix
if (isVideo) { VideoBadge( modifier = Modifier .align(Alignment.BottomStart) .padding(StreamTokens.spacingXs) - .clearAndSetSemantics {}, durationInSeconds = attachmentMetaData.videoLength, ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/images/ImagesPicker.kt` around lines 185 - 192, The VideoBadge wrapper in ImagesPicker currently calls Modifier.clearAndSetSemantics {}, which strips the badge's accessible duration text; remove that call from the modifier on VideoBadge so the badge's semantics (e.g., durationInSeconds) remain exposed. Instead, mark only the decorative play icon inside VideoBadge as non-essential for accessibility (i.e., hide that icon's semantics) so TalkBack still reads the video duration while the icon is ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt`:
- Around line 145-153: The semantics modifier is applied before the clickable
modifier which allows clickable to add/override its own semantic click action;
in FilesPicker (FilesPicker.kt) move the .semantics { role = Role.Button;
selected = fileItem.isSelected; onClick(label = onClickLabel) {
onItemSelected(fileItem); true } } so it is applied after the .clickable {
onItemSelected(fileItem) } call to preserve the custom onClick label; make the
same change for DefaultImagesPickerItem in ImagesPicker.kt where the semantics
block is currently before .clickable.
---
Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/images/ImagesPicker.kt`:
- Around line 185-192: The VideoBadge wrapper in ImagesPicker currently calls
Modifier.clearAndSetSemantics {}, which strips the badge's accessible duration
text; remove that call from the modifier on VideoBadge so the badge's semantics
(e.g., durationInSeconds) remain exposed. Instead, mark only the decorative play
icon inside VideoBadge as non-essential for accessibility (i.e., hide that
icon's semantics) so TalkBack still reads the video duration while the icon is
ignored.
🪄 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: 823b8b4d-c141-465c-9707-fa64e62edffa
📒 Files selected for processing (22)
stream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ComposerCancelIcon.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/images/ImagesPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageComposerAttachmentAnnouncer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentTypePicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/CreatePollScreen.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollCreationHeader.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/attachments/MessageComposerAttachmentAudioRecordItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/attachments/MessageComposerAttachmentFileItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/attachments/MessageComposerAttachmentMediaItem.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
…sibility The attachments toggle button in the message composer announced as "Attachments, double tap to activate" in both states, which is misleading when the picker is already open and tapping closes it. Switch to a state-aware content description: "Open attachments" when the picker is hidden, "Close attachments" when it is shown. Removes the now-unused stream_compose_attachments string from all locales; the new strings live under the stream_compose_message_composer_* namespace alongside other composer button labels.
The picker tabs (Media, Camera, Files, Polls, Commands) used a FilledIconToggleButton, which emits Role.Checkbox + a toggleable state. TalkBack announced an active tab as "Checked, Media, checkbox, double tap to toggle" — wrong role and a misleading action hint. Override the per-tab semantics with semantics(mergeDescendants = true) to declare Role.Tab + selected, while keeping the underlying focusable / focused / clickable semantics intact so the tab stays discoverable to accessibility services. Role.Tab takes precedence over the lingering ToggleableState in TalkBack's announcement, so the active tab now announces as "Media, tab, selected".
The image/video grid items in the attachment picker had no contentDescription, no role, and no selected state in the accessibility tree. TalkBack focused them silently, leaving users unable to identify or select photos or videos. Add merged semantics on each item (contentDescription "Photo" or "Video", Role.Button, selected = isSelected, plus the existing testTag) and mark the decorative RadioCheck and VideoBadge overlays as non-semantic with clearAndSetSemantics so they no longer announce phantom checkbox state. Translations added for the 7 supported locales.
TalkBack announced "Double tap to activate" for file items in the picker, which is misleading because tapping selects the file for attachment, not activates it. Add merged semantics on each file row with Role.Button, the current selected state, and an onClick action labelled "select" so TalkBack now announces "Double tap to select". Mark the decorative RadioCheck and RadioButton indicators with clearAndSetSemantics so they no longer contribute phantom toggle/radio state to the merged announcement. Translations added for the 7 supported locales.
The "x" button on each composer attachment chip announced as "Cancel" — misleading because the button removes a specific attachment from the message, not abandons an unrelated action. Add an optional contentDescription parameter to ComposerCancelIcon (defaulted to the existing stream_compose_cancel for backwards compatibility) and pass "Remove attachment" from the three composer attachment chip call sites: media, file, and audio recording. Other ComposerCancelIcon usages (edit indicator, quoted message, link preview) keep the original "Cancel" label since they aren't attachment chips. Translations added for the 7 supported locales.
Tapping a thumbnail to add an attachment, or capturing a photo via the
camera tab, played only a brief system sound and made no TalkBack
announcement, leaving users without confirmation that the attachment
was added. Removing an attachment was equally silent.
Add an internal MessageComposerLiveRegion composable that mounts inside
the composer's root column (always present) and watches attachment list
size. On size increase it announces the type of the latest attachment
("Photo attached", "Video attached", "Audio recording attached", or
"File attached"); on size decrease it announces "Attachment removed".
The text is hosted in a hidden Text marked with LiveRegionMode.Polite +
invisibleToUser, so it is not navigable but is announced when its value
changes. Translations added for the 7 supported locales.
Camera capture flows through the same composer state, so the same live
region also confirms photo capture without further wiring.
When a picker item is already selected, double-tapping toggles it off rather than selecting it. The action hint should reflect that — TalkBack should announce "double tap to remove" for already-selected items, and "double tap to select" for not-yet-selected items. Branch the onClick semantic action label on the item's selected state in both the image grid (DefaultImagesPickerItem) and the file list row (DefaultFilesPickerItem). Translations added for the 7 supported locales.
When the Create Poll sheet opened, TalkBack jumped straight to announcing the IME and the auto-focused Question field, never mentioning the screen the user had just entered. Mark the "Create Poll" title in PollCreationHeader as a heading and add a paneTitle semantic property on the screen root so accessibility services announce the new pane on entry.
Real-device TalkBack testing surfaced three adjustments to the original
audit fixes:
- Composer +/× button: hearing "Close attachments" right after opening
the picker reads backwards. Switch to the standard expandable-button
pattern — contentDescription stays as "Attachments" + stateDescription
"collapsed" / "expanded". Add paneTitle on the picker root so the new
pane is announced on entry.
- Picker tabs leaked the underlying ToggleableState ("checked") through
semantics(mergeDescendants = true). Switch to clearAndSetSemantics so
only Role.Tab + selected reach TalkBack.
- The Compose liveRegion semantic on a hidden Text never fired —
TalkBack skips zero-size / transparent nodes. Switch to
View.announceForAccessibility() via LocalView. Rename the composable
to MessageComposerAttachmentAnnouncer.
.com>
Move the picker item's accessibility click label from a duplicated
`semantics { onClick(label = …) }` block onto `Modifier.clickable`'s
`onClickLabel` parameter, removing the order dependency between the
two modifiers.
32e481a to
e7d3033
Compare
Extract `announceAddedAttachment` to `internal` (annotated `@VisibleForTesting`) and add a JVM unit test exercising the image / video / audio recording / file / null branches.
gpunto
left a comment
There was a problem hiding this comment.
LG! Left a nitpick you can safely ignore
Replaces empty-block clearAndSetSemantics {} on the picker checkmark/radio
icons, the video duration badge, and the file-type label with the more
intent-revealing semantics { hideFromAccessibility() }. Non-empty
clearAndSetSemantics {…} sites that replace semantics with a clean set are
left as-is — hideFromAccessibility() would hide the node entirely.
|
|
🚀 Available in v7.1.0 |
* Disambiguate composer attachment remove buttons for TalkBack The (x) remove button on each queued attachment in the Compose composer announced as "Remove attachment" for every item, so TalkBack users could not distinguish between multiple remove buttons. Replace the single contentDescription with per-type, name-aware labels: - Photos / videos: "Remove photo IMG_0023" / "Remove video clip.mp4" (fallback "Remove photo" / "Remove video" when the attachment has no title or name) - Other files: "Remove file report.pdf" (fallback "Remove file") - Voice recordings: "Remove voice message" Adds matching translations for the 7 supported locales. The previously generic stream_compose_remove_attachment resource has no remaining callers and is dropped from all locales, mirroring the cleanup done in PR #6404 for stream_compose_attachments. * Cover unnamed-fallback branches of attachment remove descriptions Add three Paparazzi snapshot tests rendering each composer attachment item (image, video, file) with a copy of the existing preview data that clears the title and name, exercising the previously uncovered fallback arms of fileAttachmentRemoveDescription and mediaAttachmentRemoveDescription. Extract a shared MessageComposerAttachmentItem helper to dedupe the preview-handler wiring across the four media-item previews.


Goal
Address the accessibility audit (Notion DB). All findings concentrate on the attachment picker.
Implementation
ImagesPicker.ktRole.TabviaclearAndSetSemantics(suppressesToggleableState)AttachmentTypePicker.ktcontentDescription = "Attachments"+stateDescription = "collapsed" / "expanded".paneTitleon the picker root announces the new pane on entryMessageComposerLeadingContent.kt,AttachmentPicker.ktView.announceForAccessibility()MessageComposerAttachmentAnnouncer.kt,MessageInput.ktisSelectedImagesPicker.kt,FilesPicker.ktcontentDescriptionparameter onComposerCancelIconComposerCancelIcon.kt+ 3 chip call sitespaneTitle+heading()CreatePollScreen.kt,PollCreationHeader.ktDesigner-aligned scope
Audit A7 (focus → text input on chip remove) is resolved by the new "Attachment removed" announcement: the literal fix pops the keyboard mid-picker, and TalkBack's natural left-to-right / top-to-bottom progression after removal lands on the mic — the announcement provides the audible confirmation. Designer accepted; A7 is closed in Notion. The focus halves of A4 and A8 share the same root cause (Compose programmatic focus moves don't reliably propagate to TalkBack across our merged-semantics tab nodes) and ship in the same state pending designer alignment.
API surface
ComposerCancelIcongains a defaultedcontentDescription: Stringparameter — additive, non-breaking per project convention.apiDumpreflects the addition.stream_compose_attachments). Theoretical soft binary break for any consumer that referenced it; in practice unused.Strings & translations
New string IDs added in the
_message_composer_*,_attachment_picker_*, and_attachment_added_*/_attachment_removednamespaces. Translations added for the seven supported locales (it / in / ja / fr / hi / ko / es).Testing
All scenarios assume the Compose sample app, a channel with media/files/audio attachments, and TalkBack enabled on a real device with a working TTS engine. Single-tap focuses; double-tap activates.
+. "Collapsed, attachments, button, double tap to activate." Activate it. "Expanded, attachments, button" + "Attachment picker" (paneTitle).×: "Remove attachment, button". Activate: "Attachment removed".develop. Paparazzi snapshots unchanged.API impact: one additive defaulted parameter on
ComposerCancelIcon. Everything else internal.No visible UI changes.
Summary by CodeRabbit
Release Notes
New Features
API Changes
ComposerCancelIconnow accepts a customizable content description parameter.Localization