From b22182ae5c8f161f50ffc3fa943ee637aa869ef9 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Wed, 13 May 2026 13:29:26 -0500 Subject: [PATCH] a11y: listbox/option semantics + keyboard nav (#1099 phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. -