|
| 1 | +--- |
| 2 | +name: accessibility |
| 3 | +description: Maintain WCAG-focused accessibility in stream-chat-react. Use when changing interactive components, dialogs, menus, forms, media controls, notifications, focus behavior, keyboard flows, aria attributes, or screen-reader announcements. |
| 4 | +--- |
| 5 | + |
| 6 | +# Accessibility Maintenance (stream-chat-react) |
| 7 | + |
| 8 | +Use this skill whenever code changes can affect keyboard users, screen readers, focus behavior, motion preferences, or semantic HTML/ARIA. |
| 9 | + |
| 10 | +## Non-negotiable rules |
| 11 | + |
| 12 | +1. Prefer native semantics first (`button`, `input`, `label`, `img`, etc.). Use ARIA roles only when native semantics cannot represent the widget. |
| 13 | +2. Do not add hardcoded English accessibility labels. Use i18n keys (`t('aria/...')`) for user-facing `aria-label`/`aria-description`/announcement strings. |
| 14 | +3. Keep one focusable interactive target per action. Avoid duplicate focus stops and nested-interactive patterns. |
| 15 | +4. If a control is keyboard-activatable, support Enter/Space behavior and visible focus. |
| 16 | +5. Decorative visuals stay hidden from AT (`aria-hidden`, `focusable="false"` for SVG icons). |
| 17 | +6. Keep changes additive and backward-compatible for SDK consumers. |
| 18 | + |
| 19 | +## Where to put what |
| 20 | + |
| 21 | +- **Cross-cutting accessibility decisions and scope changes** |
| 22 | + - `specs/wcag-compliance/decisions.md` (append-only decision log) |
| 23 | + - `specs/wcag-compliance/plan.md` and `specs/wcag-compliance/state.json` (task tracking) |
| 24 | +- **Shared a11y primitives** |
| 25 | + - `src/components/Accessibility/` (`AriaLiveRegion`, announcer hooks, notification announcer) |
| 26 | + - `src/components/VisuallyHidden/` |
| 27 | +- **Global reduced-motion/focus behavior styles** |
| 28 | + - `src/styling/accessibility.scss` |
| 29 | + - `src/styling/base.scss` (shared focus tokens) |
| 30 | +- **Component-level semantics and keyboard handling** |
| 31 | + - in the component itself under `src/components/**` |
| 32 | +- **Translations for new `aria/...` keys** |
| 33 | + - all locale files in `src/i18n/*.json` (never leave empty values) |
| 34 | +- **Tests** |
| 35 | + - nearest component test folder: `src/components/**/__tests__/` |
| 36 | + - include `jest-axe` checks for semantic/ARIA-sensitive changes |
| 37 | + |
| 38 | +## Patterns to follow |
| 39 | + |
| 40 | +### 1) Accessible names and descriptions |
| 41 | + |
| 42 | +- Prefer `aria-labelledby` when visible label text exists. |
| 43 | +- Use `aria-label` only as fallback when label-by-id is not available. |
| 44 | +- For dialog surfaces: |
| 45 | + - provide `role` and `aria-modal` for modal behavior |
| 46 | + - wire `aria-labelledby` and `aria-describedby` to visible title/description nodes |
| 47 | +- For images: |
| 48 | + - always render `alt` (`''` when decorative) |
| 49 | + |
| 50 | +### 2) Keyboard behavior |
| 51 | + |
| 52 | +- Native controls: rely on native keyboard behavior whenever possible. |
| 53 | +- Non-native interactive wrappers (only when unavoidable): |
| 54 | + - `role="button"` |
| 55 | + - `tabIndex={0}` |
| 56 | + - `onKeyDown` for Enter/Space activation |
| 57 | + - prevent default on Space when needed to avoid scroll side-effects |
| 58 | +- Menus/listboxes/tabs: |
| 59 | + - use role-appropriate child roles (`menu`/`menuitem`, `listbox`/`option`, `tablist`/`tab`/`tabpanel`) |
| 60 | + - keep role/attribute combinations valid (example: `menuitemradio` with `aria-checked`) |
| 61 | + |
| 62 | +### 3) Live regions and announcements |
| 63 | + |
| 64 | +- Prefer centralized announcers over ad-hoc `aria-live` scattered across components: |
| 65 | + - `AriaLiveRegion` + `useAriaLiveAnnouncer` |
| 66 | + - `NotificationAnnouncer` |
| 67 | +- Use `polite` for non-urgent updates; `assertive` for urgent/error updates. |
| 68 | +- For repeated announcements, clear then set message (small delay) to force re-announcement. |
| 69 | +- For modals, do not use live regions for static body content; rely on correct dialog semantics + focus management. |
| 70 | + |
| 71 | +### 4) Focus management |
| 72 | + |
| 73 | +- Maintain visible focus indicators (do not remove outlines without replacement). |
| 74 | +- When trapping focus in dialogs, ensure focus enters the dialog and is restored on close. |
| 75 | +- After closing transient dialogs/popovers, restore focus to the invoking trigger when expected. |
| 76 | + |
| 77 | +### 5) Motion preferences |
| 78 | + |
| 79 | +- Respect `prefers-reduced-motion` in both CSS and JS behavior: |
| 80 | + - CSS transitions/animations minimized in `accessibility.scss` |
| 81 | + - JS-driven scrolling/animation behavior downgraded to non-smooth where needed |
| 82 | + |
| 83 | +## ARIA attribute guardrails |
| 84 | + |
| 85 | +- Use ARIA as contract, not decoration: |
| 86 | + - if role implies state, provide matching state attribute (`aria-selected`, `aria-checked`, etc.) only when valid for that role |
| 87 | + - never attach unsupported attributes to roles just to satisfy a visual state |
| 88 | +- `aria-hidden` is for decorative/non-essential content only, never for focusable controls. |
| 89 | +- Icon-only controls must carry an accessible name on the control element itself. |
| 90 | + |
| 91 | +## i18n rules for accessibility text |
| 92 | + |
| 93 | +1. New accessibility labels/announcements must use `t('aria/...')` or established translation topics. |
| 94 | +2. Add keys to all locales in `src/i18n/*.json`. |
| 95 | +3. For dynamic values, always use i18n interpolation syntax (for example `t('aria/{{count}} new messages', { count })` or equivalent existing key shape), never string concatenation. |
| 96 | +4. Run translation validation/lint flow; no empty translation values. |
| 97 | + |
| 98 | +## Testing requirements per accessibility change |
| 99 | + |
| 100 | +Minimum: |
| 101 | + |
| 102 | +- unit tests for new keyboard/focus/semantics behavior in nearest `__tests__` folder |
| 103 | +- one `jest-axe` assertion for components where semantics changed |
| 104 | + |
| 105 | +Recommended: |
| 106 | + |
| 107 | +- regression tests for: |
| 108 | + - Enter/Space activation |
| 109 | + - role/state attributes |
| 110 | + - focus restore on close |
| 111 | + - reduced-motion behavior where logic branches in JS |
| 112 | + |
| 113 | +## Execution workflow (copy this checklist) |
| 114 | + |
| 115 | +- [ ] Identify the interaction type (button/menu/dialog/listbox/form/slider/live region) |
| 116 | +- [ ] Choose native element first; fall back to ARIA only if necessary |
| 117 | +- [ ] Add or correct label wiring (`aria-labelledby` preferred, `aria-label` fallback) |
| 118 | +- [ ] Verify keyboard path (Tab + Enter/Space + arrow keys where pattern requires) |
| 119 | +- [ ] Verify focus visibility and focus restore behavior |
| 120 | +- [ ] Ensure decorative visuals are hidden from AT and icon-only controls are named |
| 121 | +- [ ] Add/update i18n keys for new accessibility text across all locales |
| 122 | +- [ ] Add/update tests (`jest-axe` where semantics changed) |
| 123 | +- [ ] Append rationale to `specs/wcag-compliance/decisions.md` for cross-cutting decisions |
| 124 | + |
| 125 | +## Common mistakes to avoid |
| 126 | + |
| 127 | +- Hardcoded English `aria-label` values in component code. |
| 128 | +- Adding `tabIndex`/roles to containers that only capture backdrop clicks. |
| 129 | +- Creating two focusable wrappers for one action path. |
| 130 | +- Introducing invalid role/attribute pairs (for example `aria-selected` on plain buttons). |
| 131 | +- Using live regions to force modal text announcement instead of fixing dialog semantics. |
0 commit comments