Skip to content

a11y: listbox semantics + keyboard nav (phase 2 canary intake)#1153

Merged
joelteply merged 1 commit into
canaryfrom
intake/a11y-phase2-canary
May 14, 2026
Merged

a11y: listbox semantics + keyboard nav (phase 2 canary intake)#1153
joelteply merged 1 commit into
canaryfrom
intake/a11y-phase2-canary

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

Refs #1127
Refs #1099
Supersedes the canary-intake portion of #1111.

Proof

  • Precommit: TypeScript compilation passed; staged ESLint triggered repo-wide baseline and dropped from 5464 to 5462; browser smoke skipped because local stack did not answer ./jtag ping.
  • Pre-push: TypeScript clean; ESLint ratchet held at 5462; Rust compile/tests skipped because no Rust-relevant changes; Docker skipped because no Rust/docker changes.
  • Conflict resolution: kept first-run empty states and added role=listbox/role=option semantics plus keyboard activation for custom room/user list overrides.

Builds on phase 1 (PR #1103). Behavior-preserving — adds ARIA
listbox/option semantics + keyboard navigation to the room and user
lists, plus accessible labels on chat message rows so screen readers
can navigate transcript message-by-message.

ReactiveListWidget — base class additions:
  - role="listbox" + aria-label (from listTitle) on the default
    container in `render()`. Subclasses that override render() add
    role=listbox locally (see RoomList/UserList below).
  - getRenderFunction sets role="option" + tabindex=0 + aria-label
    (via new virtual `getItemLabel()`) + aria-selected on every
    .list-item wrapper.
  - Enter / Space on a focused item activates it (mirrors click).
  - New `onListKeydown` handler attached in firstUpdated():
    ArrowDown / ArrowUp move focus between siblings, Home / End jump
    to first/last. Scoped to the container so it doesn't interfere
    with chat composer or other keyboard handling.

RoomListWidget:
  - role=listbox + aria-label="Rooms and direct messages" on the
    container in its render() override.
  - Overrides getItemLabel(): for rooms → "Room {name} — {topic}";
    for DMs → "Direct message with {name}" or "Group DM: {name},
    {count} members".

UserListWidget:
  - role=listbox + aria-label="Users and personas" on the container.
  - Overrides getItemLabel(): "{name}, {persona|agent|user}, {status}"
    so a screen reader hears the kind and presence state.

ChatWidget — getRenderFunction:
  - role="article" + aria-label on each .message-row (sender name +
    timestamp + " sending" if optimistic). Combined with phase 1's
    role=log + aria-live=polite on the messages-container, the chat
    transcript is now navigable per-message via screen reader rotor.

Out of scope (phase 3 follow-ups):
  - Dynamic aria-selected updates when the active room/user changes
    after initial render (current value is set at item-creation time
    only — limitation noted in PR description).
  - Roving tabindex (currently every item is tabindex=0).
  - Color-contrast audit across themes.
  - <div onclick> → <button> migration.
  - axe-core lint gate.

`npm run build:ts` is green. Not visually validated locally —
keyboard + screen-reader walkthrough is in the PR test plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply merged commit 0e8e623 into canary May 14, 2026
4 checks passed
@joelteply joelteply deleted the intake/a11y-phase2-canary branch May 14, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant