Skip to content

feat(a11y): improve accessibility across dialogs, forms, menus, media, and focus flows#3146

Merged
MartinCupela merged 37 commits intomasterfrom
feat/accessibility
Apr 28, 2026
Merged

feat(a11y): improve accessibility across dialogs, forms, menus, media, and focus flows#3146
MartinCupela merged 37 commits intomasterfrom
feat/accessibility

Conversation

@MartinCupela
Copy link
Copy Markdown
Contributor

@MartinCupela MartinCupela commented Apr 27, 2026

🎯 Goal

Improve accessibility across stream-chat-react to move key UX paths closer to WCAG 2.1 AA by fixing keyboard navigation gaps, dialog/menu semantics, live announcements, and focus management.

πŸ›  Implementation details

This PR consolidates and expands accessibility work across dialogs, forms, menus, message interactions, media controls, and notifications.

1) Screen reader foundations

  • Added a reusable VisuallyHidden utility for SR-only content.
  • Introduced shared AriaLiveRegion + useAriaLiveAnnouncer infrastructure.
  • Standardized live-region usage and added re-announcement handling for repeated messages.
  • Added a NotificationAnnouncer component and new-message announcement support.

2) Keyboard interaction + semantic correctness

  • Converted clickable non-semantic wrappers to proper keyboard-accessible behavior (Enter/Space).
  • Improved keyboard support in message surfaces (bounced messages, quoted message jump, mentions, context menus, remaining clickable controls).
  • Generalized keyboard navigation patterns in ContextMenu. Dropdown (keys Up, Down, Escape)

3) Dialog, modal, and prompt semantics

  • Applied dialog semantics (role="dialog", aria-modal, label/description wiring with fallback behavior) across portal/modal primitives.
  • Added reusable header ID plumbing for Prompt/Alert/Viewer primitives and wired high-impact callsites.
  • Ensured focus return behavior after closing the Attachment Selector dialog.

4) Forms, controls, and media

  • Refactored SwitchField to native switch semantics with preserved styling.
  • Improved SearchBar accessibility semantics.
  • Added accessibility coverage for audio player controls and remaining button variants.

5) Visual/accessibility polish

  • Added/standardized focus-visible styles.
  • Added prefers-reduced-motion support.
  • Hid decorative icons from assistive tech.
  • Enforced alt text expectations on BaseImage / Avatar.

6) AI Skill to promote future maintenance

βœ… Validation

  • Added/updated targeted tests for new a11y primitives and behavior (including dialog regression + jest-axe coverage).
  • Verified no intended visual regressions while preserving existing interaction flows.

πŸ’₯ Breaking changes

None expected.
Changes are behaviorally additive and focused on semantics, keyboard support, and assistive technology compatibility.

🎨 UI Changes

Mostly non-visual.
Any visual changes are limited to focus indicators and reduced-motion behavior.

…n-reader-only text

add VisuallyHidden component using inline sr-only styles (clip, 1px box, absolute positioning)
preserve content in the accessibility tree while hiding it visually
export utility from src/components/VisuallyHidden/index.ts for reuse in upcoming WCAG tasks
…LiveRegion

Add a shared AriaLiveRegion provider and useAriaLiveAnnouncer hook so components
can announce dynamic updates from one consistent place.
This improves WCAG 2.1 AA compliance with polite/assertive live regions and
reliable re-announcement of repeated messages via clear-then-set microtask logic.
Include tests for both regions and repeated announcement behavior.
Add semantic button behavior to MessageUI’s inner wrapper for bounced messages:
- switch keyboard handling to onKeyDown (Enter/Space)
- add role="button", tabIndex=0, and i18n aria-label
- keep non-interactive/failed states without button semantics to avoid nested-interactive violations
Add button semantics to interactive QuotedMessagePreviewUI only:
- set role="button", tabIndex={0}, and aria-label via t('aria/Jump to quoted message')
- handle Enter/Space on keydown to trigger jump behavior
- keep non-interactive preview usages unchanged
…Anchor

- set role="dialog" and aria-modal="true" when trapFocus is enabled
- forward aria-labelledby/aria-describedby and use aria-label as fallback
- add DialogPortal regression + jest-axe coverage
- mark WCAG task 11 complete in specs/wcag-compliance
@MartinCupela MartinCupela requested a review from oliverlaz as a code owner April 27, 2026 15:03
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 89.98700% with 77 lines in your changes missing coverage. Please review.
βœ… Project coverage is 83.53%. Comparing base (cd3a9c0) to head (48f162d).
⚠️ Report is 13 commits behind head on master.

Files with missing lines Patch % Lines
src/components/Dialog/components/Viewer.tsx 0.00% 15 Missing ⚠️
src/components/Dialog/components/ContextMenu.tsx 81.57% 14 Missing ⚠️
...omponents/AudioPlayback/components/keyboardSeek.ts 77.41% 7 Missing ⚠️
...components/Accessibility/NotificationAnnouncer.tsx 93.90% 5 Missing ⚠️
...ts/MessageList/hooks/useReducedMotionPreference.ts 70.58% 5 Missing ⚠️
...rc/components/Reactions/MessageReactionsDetail.tsx 50.00% 5 Missing ⚠️
...ssibility/hooks/useIncomingMessageAnnouncements.ts 96.29% 3 Missing ⚠️
.../components/MessageList/VirtualizedMessageList.tsx 62.50% 3 Missing ⚠️
...TextareaComposer/SuggestionList/SuggestionList.tsx 62.50% 3 Missing ⚠️
...c/components/Accessibility/useAriaLiveAnnouncer.ts 75.00% 2 Missing ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3146      +/-   ##
==========================================
+ Coverage   82.79%   83.53%   +0.73%     
==========================================
  Files         419      434      +15     
  Lines       12270    12918     +648     
  Branches     3951     4157     +206     
==========================================
+ Hits        10159    10791     +632     
- Misses       2111     2127      +16     

β˜” View full report in Codecov by Sentry.
πŸ“’ Have feedback on the report? Share it here.

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Size Change: +54.1 kB (+8.75%) πŸ”

Total Size: 672 kB

πŸ“¦ View Changed
Filename Size Change
dist/cjs/emojis.js 3.01 kB +1 B (+0.03%)
dist/cjs/index.js 267 kB +26.2 kB (+10.88%) ⚠️
dist/cjs/useNotificationApi.js 45.4 kB +45.4 kB (new file) πŸ†•
dist/cjs/WithAudioPlayback.js 0 B -44.7 kB (removed) πŸ†
dist/css/index.css 39.3 kB +284 B (+0.73%)
dist/es/emojis.mjs 2.52 kB -2 B (-0.08%)
dist/es/index.mjs 265 kB +26.4 kB (+11.05%) ⚠️
dist/es/useNotificationApi.mjs 45.2 kB +45.2 kB (new file) πŸ†•
dist/es/WithAudioPlayback.mjs 0 B -44.5 kB (removed) πŸ†
ℹ️ View Unchanged
Filename Size
dist/cjs/audioProcessing.js 1.32 kB
dist/cjs/mp3-encoder.js 1.27 kB
dist/css/emoji-picker.css 178 B
dist/css/emoji-replacement.css 456 B
dist/es/audioProcessing.mjs 1.31 kB
dist/es/mp3-encoder.mjs 756 B

compressed-size-action

@MartinCupela MartinCupela merged commit 917b7f5 into master Apr 28, 2026
13 of 14 checks passed
@MartinCupela MartinCupela deleted the feat/accessibility branch April 28, 2026 11:55
github-actions Bot pushed a commit that referenced this pull request May 4, 2026
## [14.1.0](v14.0.1...v14.1.0) (2026-05-04)

### Bug Fixes

* add ScrollToLatestMessageButton to ComponentContext ([#3159](#3159)) ([952c125](952c125))
* allow user blocking only in DM-type channels ([#3139](#3139)) ([deda536](deda536))
* decouple msg bubble width from reaction list width ([#3142](#3142)) ([980c233](980c233))
* export AttachmentSelectorContext from the SDK ([#3158](#3158)) ([68efeb5](68efeb5))
* font & box shadow fixes ([#3135](#3135)) ([6d04cdf](6d04cdf)), closes [#3134](#3134)
* limit reactions host width (segmented/bottom) ([#3154](#3154)) ([be50105](be50105))
* make search results scrollable ([#3152](#3152)) ([ead6cb5](ead6cb5))
* **MessageList:** prevent message pagination too early on mount ([#3143](#3143)) ([12e282f](12e282f))
* prevent cutting off button outlines in ContextMenu components ([#3151](#3151)) ([b3469f0](b3469f0))
* remove scrollbar gutters from VML ([#3148](#3148)) ([4a6a8ae](4a6a8ae))

### Features

* **a11y:** improve accessibility across dialogs, forms, menus, media, and focus flows ([#3146](#3146)) ([917b7f5](917b7f5))
* change textarea default placeholder text ([#3150](#3150)) ([45b1836](45b1836))
* introduce `MessageUI` to `ComponentContext` ([#3140](#3140)) ([16af18d](16af18d))

### Refactors

* message styling ([#3136](#3136)) ([cd3a9c0](cd3a9c0))
@stream-ci-bot
Copy link
Copy Markdown

πŸŽ‰ This PR is included in version 14.1.0 πŸŽ‰

The release is available on:

Your semantic-release bot πŸ“¦πŸš€

isekovanic added a commit to GetStream/stream-chat-react-native that referenced this pull request May 6, 2026
## Summary

- Lands an **opt-in** accessibility layer for VoiceOver (iOS) and
TalkBack (Android), aligned with primitive shapes from
[`stream-chat-react#3146`](GetStream/stream-chat-react#3146).
- A11y is **OFF by default** β€” pass `<Chat accessibility={{ enabled:
true }}>` to enable. Existing integrators see no change. When disabled,
no announcer mounts, no listeners attach, and `useA11yLabel`
short-circuits the `t()` call so hot list paths stay free.
- Foundation is in place; gesture-alternative rewrites for
`AudioRecorder` and `ImageGallery`, plus the lint plugin and Reassure
benchmark, are explicitly deferred (called out in commits and docs).

## What's in this PR

7 conventional commits, each independently reviewable:

1. **`feat(a11y): add opt-in accessibility announcer, context, and
hooks`** β€” `AccessibilityContext` (flat config with `'auto' | 'always' |
'never'` enums), `AccessibilityAnnouncer` + `useAccessibilityAnnouncer`
(mirrors `useAriaLiveAnnouncer` from React), `NotificationAnnouncer`,
`useIncomingMessageAnnouncements` (verbatim port β€” same
throttle/batch/bounded-id semantics), utility hooks under
`package/src/a11y/` (`useScreenReaderEnabled`,
`useReducedMotionPreference`, `useResolvedModalAccessibilityProps`,
`useAnnounceOnStateChange`, `useA11yLabel`). `<Chat>` wires the provider
stack. `aria/*` keys added to all 12 locales (English values;
translations are a follow-up β€” `validate-translations` passes). 16 new
unit tests.
2. **`feat(a11y): wire accessibility into base UI primitives`** β€” Avatar
(accepts `name` + auto-composes `aria/Avatar of {{name}}`), Button (role
+ disabled/selected state), Input (label/hint/state + assertive live
region for validation errors), Indicators (live-region progressbar,
hidden decorative dots, alert role on error), ProgressControl
(`progressbar` / `adjustable` + `accessibilityValue`), BottomSheetModal
(uses `useResolvedModalAccessibilityProps` β€” `accessibilityViewIsModal`
on iOS, `importantForAccessibility='yes'` on Android).
3. **`feat(a11y): add accessible message actions, reactions, and reply
preview`** β€” MessageActionList (`role='menu'`), MessageActionListItem
(`role='menuitem'`, action title as label), MessageReactionPicker
(`role='menu'`), ReactionButton (composed label via `useA11yLabel`),
Reply (role+title).
4. **`feat(a11y): announce incoming messages and label scroll-to-latest
button`** β€” MessageList wires `useIncomingMessageAnnouncements({
channel, ownUserId, activeThreadId, threadList })`. ScrollToBottomButton
announces unread count.
5. **`feat(a11y): announce AI typing, label channel preview status,
mount NotificationAnnouncer`** β€” AITypingIndicatorView wraps in polite
live region + debounced announcer; ChannelMessagePreviewDeliveryStatus
composes a Sending/Sent/Delivered/Read label; Channel mounts
`<NotificationAnnouncer />` so connection-state transitions reach SR
users.
6. **`feat(a11y): add radio/checkbox semantics to poll vote button`** β€”
PollOption VoteButton: `role='radio'` for single-vote polls,
`'checkbox'` for multi-vote polls, `accessibilityState={{ checked,
selected }}`, option text as label.
7. **`chore(a11y): add maintenance skill and integrator docs`** β€”
`.claude/skills/accessibility/SKILL.md` (RN-adapted port of React's
`.cursor/skills/accessibility/SKILL.md`) and `ai-docs/accessibility.md`
covering the opt-in default, config schema, localization, public
hooks/components, cross-SDK parity, and platform notes.

## Cross-SDK alignment

| React (web) | This PR (RN) |
|---|---|
| `useAriaLiveAnnouncer` | `useAccessibilityAnnouncer` (identical call
shape) |
| `useIncomingMessageAnnouncements` | identical params + throttle/batch
logic |
| `aria/*` i18n namespace | shared verbatim |
| `AriaLiveRegion` provider | `AccessibilityAnnouncer` (uses
`AccessibilityInfo.announceForAccessibility`) |
| `NotificationAnnouncer` | same component name; RN announces connection
state (no shared notifications queue exists yet) |
| `useResolvedModalAriaProps` | `useResolvedModalAccessibilityProps`
(`accessibilityViewIsModal` iOS / `importantForAccessibility` Android) |

Mobile-only deviation: `<Chat accessibility={...}>` config object β€” RN
needs gesture-alternative toggles (`audioRecorderTapMode`,
`imageGalleryScreenReaderMode`, `messageActionsTrigger`) that don't
exist on web.

## Out of scope (explicitly deferred)

- AudioRecordingButton tap-mode swap (gated on `audioRecorderTapMode`).
- ImageGallery screen-reader-mode UI swap (gated on
`imageGalleryScreenReaderMode`).
- Composed Message label + rotor `accessibilityActions` menu (touches
the 1180-line `Message.tsx`).
- AttachmentPicker modal trap, AudioAttachment scrub
`accessibilityValue`, AutoCompleteInput suggestion-list semantics.
- ESLint `eslint-plugin-react-native-a11y` rules β€” adds a runtime dep,
flagged for separate review.
- Reassure perf benchmark for `MessageList` with a11y on/off β€” adds a
runtime dep, flagged for separate review.
- Translations of `aria/*` keys to non-English locales β€” English values
ship in all 12 so `validate-translations` passes; translators can fill
in later.

## Native handler note

The plan included an `AccessibilityInfo` handler in `native.ts`
(mirroring how `Audio`/`Sound`/etc. are abstracted). Skipped:
`AccessibilityInfo` from `react-native` is identical on bare RN and
Expo, so a wrapper handler would be unnecessary indirection.

## Test plan

- [x] `cd package && yarn tsc --noEmit` β€” no new errors (only
pre-existing ones unrelated to this branch: `AttachmentPickerContent`,
`useIsCommandDisabled`, `usePendingAttachmentUpload`).
- [x] `cd package && TZ=UTC npx jest src/a11y
src/components/Accessibility src/contexts/accessibilityContext` β€” 16/16
passing.
- [x] `cd package && yarn lint` on touched dirs β€” clean (snapshot files
have pre-existing `no-undef` errors, untouched).
- [ ] Manual SampleApp smoke on iOS w/ VoiceOver β€” verify
incoming-message announcement, modal focus trap, AITypingIndicatorView
announcement.
- [ ] Manual SampleApp smoke on Android w/ TalkBack β€” verify same flows,
plus `accessibilityLiveRegion` triggers.
- [ ] Render with `accessibility={{ enabled: false }}` and confirm zero
behavioral change vs current main.
- [ ] Render with `accessibility={{ enabled: true,
forceScreenReaderMode: true }}` and confirm accessible variants render
where applicable.

Plan checked in at `.claude/plans/2026-05-04-accessibility.md`.

---------

Co-authored-by: Ivan Sekovanikj <ivan.sekovanikj@getstream.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants