fix: a11y continuous improvements#3632
Merged
Merged
Conversation
Contributor
SDK Size
|
oliverlaz
approved these changes
Jun 8, 2026
isekovanic
added a commit
that referenced
this pull request
Jun 9, 2026
## 🎯 Goal Follow up `a11y` pass on top of #3632. Covers the message row, the attachment thumbnail gallery, the full screen image/video gallery overlay, the reactions UI, the message cell annotations and date separators, the channel preview date, and the poll option row. VoiceOver and TalkBack now read each surface with proper labels, units and focus behavior and the gallery traps focus while open. ## 🛠 Implementation details - Message row sender label - the bubble's `Pressable` now announces "Message from {sender}" / "Message from you". Messages with interactive children (poll, quoted message, attachments, shared location) opt out of being a single focus stop so VO/TalkBack can drill into the children; an iOS-only absolute-fill `View` carries the sender label so the speaker is still announced when focus lands inside the inner content. - `hasInteractiveAccessibilityContent` override - new field on `AccessibilityConfig` that receives the SDK's baseline boolean and lets integrators extend the "is this message interactive?" decision without losing the defaults. Resolved upstream in `useCreateMessageContext` and exposed as a stable boolean on `MessageContext`. - Android compose gate on text bodies - `MessageTextContainer` and the `Poll` header now set `accessible accessibilityRole='text'` so TalkBack auto-composes the descendant `<Text>` content. Conditional on `hasInteractiveAccessibilityContent` for the message text to avoid double-announce on plain messages. - Gallery thumbnails - the thumbnail `Pressable` now carries an `a11y/Gallery Image` or `a11y/Gallery Video` label (added across all 13 locales), explicit `role='button'`, and the existing `a11y/Double tap to open` hint. Announces as "Gallery video, button. Double tap to open." - Full-screen gallery modal trap - `accessibilityViewIsModal` on the `ImageGallery` root and the `MessageOverlayHostLayer` host on iOS. Android gets a new `OverlayA11yShield` sibling wrapper that flips `importantForAccessibility='no-hide-descendants'` when either the gallery or the message context menu is active. - Full-screen gallery adjustable cycling - the gallery backdrop is now the focus target with `role='adjustable'`, `value={ text: 'N of M' }`, increment/decrement actions, and an `accessibilityLabel` of "Image Gallery". Swipe up/down cycles through assets. - Gallery focus return on dismiss - the image gallery state store gained a `requesterNode` field. `Gallery.tsx` captures the thumbnail's node handle via `findNodeHandle` and passes it through `openImageGallery`; on unmount the gallery restores focus via `AccessibilityInfo.setAccessibilityFocus` inside an rAF. - Reaction announcements - `ReactionListItem` gains `role='button'` (trips the Android compose gate so TalkBack reads the emoji's CLDR name instead of the reaction `type` string) and `state.selected`. `ReactionButton` drops a broken `accessibilityLabelKey` that was literally untranslated across every locale. - Thread reply indicator - the `MessageReplies` `Pressable` now has `role='button'`, an `accessibilityLabel` reading the reply count ("1 Reply" / "N Replies"), and an `accessibilityHint` of "Double tap to view thread" (new i18n key across all 13 locales). Participant avatars in the indicator are hidden from the a11y tree so they no longer announce one-by-one. - Decorative dot separators - the middle dots in `SentToChannelHeader` ("Replied to a thread · View") and `MessageReminderHeader` ("Reminder set · 5 minutes left") are now marked decorative so focus moves directly from the label to the action. - Date announcements (iOS) - VoiceOver was reading numeric dates literally ("zero four slash zero eight..."). Added a `getDateStringForA11y` helper that substitutes `LL` for the calendar `sameElse` slot, preserving locale relative words like "Today" / "Yesterday". Wired into `InlineDateSeparator` (announces "April 8, 2026") and `ChannelPreviewStatus` (announces "10:30 AM" for same-day messages via a `sameDay: 'LT'` override). TalkBack honors the override too, no platform branching needed. - Poll option vote count - the bare count `Text` on each poll option row now carries an `accessibilityLabel` using the existing `{{count}} votes` plural key. Announces "0 votes" / "1 vote" / "N votes" instead of just the bare number. ### New shared components and exports - `OverlayA11yShield` (`package/src/components/Accessibility/`) - Android-only focus-trap shield. Subscribes to the overlay context and the message-overlay store to compute `isAnyOverlayActive`, then wraps `{children}` in a `View` that flips `importantForAccessibility='no-hide-descendants'` and `accessibilityElementsHidden` while an overlay is active. On iOS it's a pure passthrough selected at module load. - `getDateStringForA11y` (`package/src/utils/i18n/getDateString.ts`) - sibling helper to `getDateString`. Produces a TTS-friendly calendar string by substituting `LL` for the `sameElse` slot in the locale's calendar formats, with an optional `calendarFormatOverrides` parameter for callers whose visible format diverges from the locale defaults. - `AccessibilityConfig.hasInteractiveAccessibilityContent` - new override hook for integrators (see above). - `MessageContext.hasInteractiveAccessibilityContent` - new stable boolean field consumed by `MessageContent` and `MessageTextContainer`. - `ImageGalleryState.requesterNode` and the matching `openImageGallery({ requesterNode? })` argument. ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Goal
Cross platform a11y fixes across the message surface (message row, footer, reactions, replies) and the channel preview list. VoiceOver / TalkBack now read each surface with proper labels, no misleading affordances, and correct focus behavior.
🛠 Implementation details
VoiceOverfocus on the navigation back button. Reply now focuses the composer's text input when a screen reader is active, and announces "Replying to {user}" + "Editing message" viauseAnnounceOnShowso the user knows the composer entered reply/edit mode.announced.
accessible+accessibilityRole='text'(cross-platform compose), withMessageStatusrendering a HiddenA11yText for the localized status string. Footer announces as one stop: "Read, 11:05 AM".
Pressable's auto compose (emoji names + "you reacted" tag for own + "and N more reactions" suffix), with hint "Double tap to view reactions". Segmented reactions stay as per-itemPressables with the same "you reacted" tagging.accessible={false}to cover any interactive children (attachments,quoted_message,poll,shared_location). Plain text messages keep their single focus stop behavior; rich messages drill in.New shared components
CompositeAccessibilityProbe(package/src/components/Accessibility/) - wraps nonTextvisual content (avatars, icons, badges) so it announces as one focus stop with a curated label. Internally renders a hiddenTextsibling + a decorative markedViewaround children. Used byChannelAvatar,ChannelPreviewMutedStatus,ChannelMessagePreviewDeliveryStatus. Solves the iOS vs Android composition asymmetry where Android'simportantForAccessibility='no-hide-descendants'leaks through nestedaccessible={true}descendants.HiddenA11yText(package/src/components/Accessibility/) - visually invisibleTextwhose only job is to splice extra info into a parent's compose loop (e.g. "you reacted", "and N more reactions", localized delivery status). Different concern from the probe - this is for adding text to a composition chain, not for collapsing a subtree into one stop.Both are exported from the SDK so consumers building their own custom message / preview UIs can reuse the patterns.
🎨 UI Changes
iOS
Android
🧪 Testing
☑️ Checklist
developbranch