feat(search): shared keyboard navigation + ARIA for search bars (LC-157 phase 1) #209

Merged
nrupard merged 2 commits from feat/lc-157-search-keyboard-nav into main 2026-05-26 15:36:35 +02:00
Owner

What

Phase 1 of LC-157 (unify search). The four generic search bars (sidebar people / messages, enclave invite, group add-members) already shared an identical HTMX debounce/fetch/swap; the real gap the audit flagged was no keyboard navigation and no listbox/option ARIA. Their result item semantics genuinely differ (two navigate via links, two submit inline forms), so this layers one shared interaction over the existing searches rather than rewriting each endpoint's result rendering into a single fragment shape.

How

  • New assets/search.js: an input opts in with data-lc-search + data-lc-search-results="#container". The module adds ArrowUp/ArrowDown over the [role=option] items (manages aria-activedescendant + a highlight), Enter to activate the highlighted option (clicks it if it is a link/button, else the first link/button inside - so a row's navigation anchor or its Invite/Add form submit both work), and Escape to clear. HTMX still owns the fetch; htmx:afterSettle reflects aria-expanded and clears the stale highlight. Loaded once with delegated listeners, so it survives result swaps.
  • The four result fragments gain role="listbox" on the wrapper and role="option" + a unique id per item; the four inputs gain the data-lc-search attrs + combobox ARIA.
  • No handler/endpoint changes. Existing behavior preserved, including plain-Enter re-fetch when nothing is highlighted.

Deferred (sub-issue)

The composer mention combobox already has full ARIA; folding in the slash-command popover (no keyboard nav today) + deeper consolidation.

Test

just check (templates compile), targeted route/db tests for the affected searches (routes_enclave, routes_user_groups, db_search) all green; just build-css regenerates the highlight utility. JS keyboard behavior is not unit-tested in this repo (consistent with the existing untested composer combobox JS); the change is additive and preserves the existing HTMX fetch.

Part of the LC-159 post-audit story.

🤖 Generated with Claude Code

## What Phase 1 of LC-157 (unify search). The four generic search bars (sidebar people / messages, enclave invite, group add-members) already shared an identical HTMX debounce/fetch/swap; the real gap the audit flagged was no keyboard navigation and no listbox/option ARIA. Their result item semantics genuinely differ (two navigate via links, two submit inline forms), so this layers one shared interaction over the existing searches rather than rewriting each endpoint's result rendering into a single fragment shape. ## How - New `assets/search.js`: an input opts in with `data-lc-search` + `data-lc-search-results="#container"`. The module adds ArrowUp/ArrowDown over the `[role=option]` items (manages `aria-activedescendant` + a highlight), Enter to activate the highlighted option (clicks it if it is a link/button, else the first link/button inside - so a row's navigation anchor or its Invite/Add form submit both work), and Escape to clear. HTMX still owns the fetch; `htmx:afterSettle` reflects `aria-expanded` and clears the stale highlight. Loaded once with delegated listeners, so it survives result swaps. - The four result fragments gain `role="listbox"` on the wrapper and `role="option"` + a unique id per item; the four inputs gain the `data-lc-search` attrs + combobox ARIA. - No handler/endpoint changes. Existing behavior preserved, including plain-Enter re-fetch when nothing is highlighted. ## Deferred (sub-issue) The composer mention combobox already has full ARIA; folding in the slash-command popover (no keyboard nav today) + deeper consolidation. ## Test `just check` (templates compile), targeted route/db tests for the affected searches (routes_enclave, routes_user_groups, db_search) all green; `just build-css` regenerates the highlight utility. JS keyboard behavior is not unit-tested in this repo (consistent with the existing untested composer combobox JS); the change is additive and preserves the existing HTMX fetch. Part of the LC-159 post-audit story. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(search): shared keyboard navigation + ARIA for search bars (LC-157 phase 1)
All checks were successful
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 9s
check-secrets / Nosey parker (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 9s
Check / clippy + fmt + tests (pull_request) Successful in 2m54s
ddf8906a4e
The four generic search bars (sidebar people, sidebar messages, enclave invite, group add-members) already shared an identical HTMX debounce/fetch/swap, but none supported keyboard navigation and none used listbox/option ARIA - the actual gap the LC-148 audit flagged. Rather than rewrite each endpoint's result rendering into one fragment shape (their item semantics genuinely differ: two navigate via links, two submit inline forms), this layers one shared interaction over the existing searches.

New assets/search.js: an input opts in with `data-lc-search` + `data-lc-search-results="#container"`; the module adds ArrowUp/ArrowDown to move the active `[role=option]` (managing aria-activedescendant + a highlight), Enter to activate the highlighted option (clicks it if it is a link/button, else the first link/button inside it - so a result row's navigation anchor or its Invite/Add form submit both work), and Escape to clear the highlight. HTMX still owns the fetch; afterSettle reflects aria-expanded and clears the stale highlight. Loaded once with delegated listeners, so it survives result swaps.

The four result fragments gain `role="listbox"` on the wrapper and `role="option"` + a unique id on each item; the four inputs gain the data-lc-search attributes + combobox ARIA. No handler/endpoint changes; existing search behavior (including plain-Enter re-fetch when nothing is highlighted) is preserved.

The composer mention combobox already has full ARIA; folding the slash-command popover (which lacks keyboard nav) and deeper consolidation are deferred to a follow-up sub-issue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(search): namespace option ids so they are document-unique (LC-157)
All checks were successful
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Kingfisher (push) Successful in 6s
check-secrets / TruffleHog (push) Successful in 7s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 1m23s
d75cdc1f41
Review follow-up: option ids were lc-opt-{user_id}, which collides when several search lists render on the same page - the sidebar people/message searches coexist with the main page's invite/group searches, and the enclave settings page renders one add-member search per group (the same user can appear in several). Duplicate ids make aria-activedescendant resolve to the first matching element in the document, so a screen reader announces the wrong option (navigation itself was already container-scoped and unaffected). Namespace per search: lc-opt-people-/-msg-/-invite-/-group-{group_id}- so every option id is unique across the document.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-157-search-keyboard-nav 2026-05-26 15:36:36 +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!209
No description provided.