feat(a11y): skip link, message-list live region, composer + popup labels (LC-197) #248

Merged
nrupard merged 2 commits from feat/lc-197-a11y into main 2026-05-28 16:02:18 +02:00
Owner

LC-197 a11y polish. High-impact slice from the audit; the full axe/Lighthouse sweep + arrow-key nav map stay open in the ticket as follow-up.

What's in this PR

Skip link. base.html gets a <a href="#main"> as the first focusable element in <body>. sr-only focus:not-sr-only keeps it invisible until keyboard focus lands on it, then it becomes a positioned button anchored to the top-left. Satisfies WCAG 2.4.1 (Bypass Blocks) on every page that defines #main (the main app pages via layout.html). Pages without #main (login, register, error) still render the link; it just scroll-jumps to the top of the page, which is harmless.

Message-list live region. room/messages.html's #messages container now carries role="log", aria-live="polite", aria-relevant="additions", and a translated aria-label. Polite over assertive so reading isn't interrupted; additions only so deletions / edits don't re-announce (an edited message announcing itself again would be noisy).

Composer accessible name. The textarea had full combobox semantics (role=combobox, aria-controls, aria-haspopup) but no accessible name - the dynamic placeholder doesn't count as a label. Added aria-label="a11y-composer-message-input"; visible placeholder stays.

Status picker dialog role. The popup root went from a generic <div tabindex="-1"> to role="dialog" + translated aria-label so SR users hear "Set status dialog" on open. Existing keyboard escape handler and focus trap are unchanged.

Reaction picker aria-haspopup. The "+" button now matches the other popover triggers (notify dropdown, scheduled / reminder modals) so an SR user knows pressing it opens an overlay.

i18n

4 new keys in en/es common.ftl under a new ## A11y section: a11y-skip-to-content, a11y-messages-region-label, a11y-composer-message-input, a11y-status-picker-dialog-label. i18n_catalog parity test still green.

Not in this PR (still open under LC-197)

  • Arrow-key nav across menus (some already have it; not all).
  • Comprehensive contrast audit per theme (manual; needs real browser).
  • Full axe / Lighthouse zero-violation sweep across every route.

Tests

just test + just test-saas green. LC-77 webhook + email-inbox render fixtures regenerated (only delta: aria-haspopup="true" on the reaction "+" button propagates into the message render). i18n catalog parity green.

🤖 Generated with Claude Code

LC-197 a11y polish. High-impact slice from the audit; the full axe/Lighthouse sweep + arrow-key nav map stay open in the ticket as follow-up. ### What's in this PR **Skip link.** `base.html` gets a `<a href="#main">` as the first focusable element in `<body>`. `sr-only focus:not-sr-only` keeps it invisible until keyboard focus lands on it, then it becomes a positioned button anchored to the top-left. Satisfies WCAG 2.4.1 (Bypass Blocks) on every page that defines `#main` (the main app pages via `layout.html`). Pages without `#main` (login, register, error) still render the link; it just scroll-jumps to the top of the page, which is harmless. **Message-list live region.** `room/messages.html`'s `#messages` container now carries `role="log"`, `aria-live="polite"`, `aria-relevant="additions"`, and a translated `aria-label`. Polite over assertive so reading isn't interrupted; `additions` only so deletions / edits don't re-announce (an edited message announcing itself again would be noisy). **Composer accessible name.** The textarea had full combobox semantics (`role=combobox`, `aria-controls`, `aria-haspopup`) but no accessible name - the dynamic placeholder doesn't count as a label. Added `aria-label="a11y-composer-message-input"`; visible placeholder stays. **Status picker dialog role.** The popup root went from a generic `<div tabindex="-1">` to `role="dialog"` + translated `aria-label` so SR users hear "Set status dialog" on open. Existing keyboard escape handler and focus trap are unchanged. **Reaction picker `aria-haspopup`.** The "+" button now matches the other popover triggers (notify dropdown, scheduled / reminder modals) so an SR user knows pressing it opens an overlay. ### i18n 4 new keys in `en/es` `common.ftl` under a new `## A11y` section: `a11y-skip-to-content`, `a11y-messages-region-label`, `a11y-composer-message-input`, `a11y-status-picker-dialog-label`. `i18n_catalog` parity test still green. ### Not in this PR (still open under LC-197) - Arrow-key nav across menus (some already have it; not all). - Comprehensive contrast audit per theme (manual; needs real browser). - Full axe / Lighthouse zero-violation sweep across every route. ### Tests `just test` + `just test-saas` green. LC-77 webhook + email-inbox render fixtures regenerated (only delta: `aria-haspopup="true"` on the reaction "+" button propagates into the message render). i18n catalog parity green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(a11y): skip link, message-list live region, composer + popup labels (LC-197)
Some checks failed
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 6s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Failing after 15s
76107264a6
LC-189 polish epic, a11y slice. Targeted at the highest-impact gaps the audit surfaced; the broad axe/Lighthouse sweep + arrow-key map are out of scope for this commit and stay in LC-197 as follow-up.

base.html: a `<a href="#main">` skip link as the first focusable element in the body, with `sr-only focus:not-sr-only` so it's invisible until keyboard focus lands on it (the WCAG 2.4.1 bypass-blocks pattern). Pages without #main (login etc.) still satisfy the requirement on every page that does.

room/messages.html: `#messages` now carries `role="log"`, `aria-live="polite"`, `aria-relevant="additions"`, and `aria-label="a11y-messages-region-label"`. New OOB-appended messages are announced once via the live region; deletions/edits are suppressed (which is intentional - re-announcing an edited message would be noisy for SR users).

room/composer.html: textarea gains an `aria-label` (translated). The placeholder is dynamic ("Message #room-name") and didn't survive as a label for SR users; the visible placeholder stays.

status/picker.html: popup root gains `role="dialog"` + translated `aria-label` so SR users hear "Set status dialog" when the picker opens (it was a generic <div>).

partials/reaction_bar.html: reaction "+" button gains `aria-haspopup="true"` to advertise the popover semantics consistent with the other picker triggers (notify dropdown, scheduled modal).

i18n: 4 new keys in en/es common.ftl under an a11y section (skip link, messages region label, composer label, status picker dialog label).

LC-77 webhook + email-inbox render fixtures regenerated to pick up the reaction-bar `aria-haspopup` attr (no other change). just test + just test-saas green; i18n_catalog parity green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(a11y): honest semantics for status picker + reaction trigger (LC-197)
Some checks failed
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 7s
Check / clippy + fmt + tests (pull_request) Failing after 17s
Create release / Create release from merged PR (pull_request) Has been skipped
8711c62b52
Self-review on PR #248 caught two ARIA inaccuracies.

status/picker.html: was `role="dialog"`. Dialogs are expected to trap focus per the ARIA Authoring Practices; ours only handles Escape and outside-click, so Tab past the last form element escapes back to the page. `role="region"` honors the actual behaviour - SR users still hear the labelled region announcement, without the focus-trap promise. The i18n key name still reads "...dialog-label" since renaming churns es/en for no SR-visible benefit.

partials/reaction_bar.html: `aria-haspopup="true"` defaults to "menu" semantics, but the reaction picker is an overlay UI like the poll / scheduled / reminder triggers, which use `aria-haspopup="dialog"`. Matched them for consistency.

LC-77 fixtures regenerated (only delta: `aria-haspopup="true"` -> `aria-haspopup="dialog"`).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-197-a11y 2026-05-28 16:02:18 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!248
No description provided.