Frontend: TokenSelect/MenuSelect listbox arrow-key and typeahead (M5) #244

Closed
opened 2026-05-31 04:41:22 +00:00 by PlasticDigits · 8 comments
PlasticDigits commented 2026-05-31 04:41:22 +00:00 (Migrated from gitlab.com)

Reference

Gap analysis: gaps/GAP_1780200149.md — finding M5. Related open issue: #214 (broader a11y audit).

Current codebase

Custom listbox components TokenSelect and MenuSelect use shared usePortalListbox (frontend-dapp/src/components/ui/PortalListbox.tsx):

  • Mouse: click to open/select; outside click closes.
  • Keyboard: Escape closes only (PortalListbox.tsx:76-78).
  • Missing: Arrow Up/Down navigation, Home/End, typeahead character search, aria-activedescendant / roving tabindex per WAI-ARIA listbox pattern.

TokenSelect.tsx:74-139 — trigger is <button> with aria-haspopup="listbox" but options lack active option semantics for keyboard.

Used on Swap, Trade, Pool, Charts, Limits — high-traffic token/pair pickers.

Why this is needed

Keyboard-only and screen-reader users cannot efficiently select tokens/pairs. Fails WCAG 2.1 operable guidelines and project axe E2E aspirations (#214). Mobile users with external keyboards (tablet) also affected.

Constraints / guardrails

  • Extend usePortalListbox or shared hook — fix both TokenSelect and MenuSelect in one pass.
  • Preserve portal positioning, flip-above-anchor, mobile bottom nav inset behavior.
  • Do not break existing click selection or Escape dismiss.
  • Match visual focus styles to existing theme.
  • Typeahead: match by symbol prefix (case-insensitive); wrap or stop at list ends per APG.
  • Add role="option" + aria-selected on items; manage focus per APG listbox pattern.

Relevant files

Path Role
frontend-dapp/src/components/ui/PortalListbox.tsx Shared keyboard hook
frontend-dapp/src/components/ui/TokenSelect.tsx Token picker
frontend-dapp/src/components/ui/MenuSelect.tsx Generic menu picker
frontend-dapp/src/components/ui/__tests__/ Unit tests
frontend-dapp/e2e/a11y-critical-routes.spec.ts axe smoke (extend if needed)
  1. Add roving activeIndex state in usePortalListbox or consumer.
  2. On trigger keydown: ArrowDown/Up open and move; printable chars start typeahead buffer with timeout.
  3. Enter/Space select active option; Escape close and return focus to trigger.
  4. Set aria-activedescendant on listbox pointing to active option id.
  5. Vitest: keyboard events; optional Playwright keyboard spec on Swap token select.

Acceptance criteria

  • Arrow keys move active option while open.
  • Enter selects active option and closes.
  • Typeahead jumps to first matching symbol.
  • Escape closes and restores focus to trigger.
  • aria-activedescendant / aria-selected correct.
  • TokenSelect and MenuSelect both fixed.

Test plan — all paths

Path Test
Click select Unchanged
ArrowDown x3 + Enter Third option selected
Type "ul" Jumps to ULUNA (or first match)
Escape Closes; focus on trigger
Empty options Disabled trigger
Long list scroll Active item scrolled into view
MenuSelect variant Same keyboard behavior

Run: make test-frontend

Test plan — attack / abuse vectors

Vector Expected
Rapid key repeat No duplicate onChange spam
Typeahead injection chars Treated as literal search only

Verification criteria

  • Vitest keyboard interaction tests pass.
  • Manual: Tab to token select, select via keyboard only on Swap page.
  • axe devTools: no listbox-related violations on Swap (spot check).
## Reference Gap analysis: [`gaps/GAP_1780200149.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/gaps/GAP_1780200149.md) — finding **M5**. Related open issue: #214 (broader a11y audit). ## Current codebase Custom listbox components `TokenSelect` and `MenuSelect` use shared `usePortalListbox` (`frontend-dapp/src/components/ui/PortalListbox.tsx`): - **Mouse:** click to open/select; outside click closes. - **Keyboard:** Escape closes only (`PortalListbox.tsx:76-78`). - **Missing:** Arrow Up/Down navigation, Home/End, typeahead character search, `aria-activedescendant` / roving tabindex per [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/). `TokenSelect.tsx:74-139` — trigger is `<button>` with `aria-haspopup="listbox"` but options lack active option semantics for keyboard. Used on Swap, Trade, Pool, Charts, Limits — high-traffic token/pair pickers. ## Why this is needed Keyboard-only and screen-reader users cannot efficiently select tokens/pairs. Fails WCAG 2.1 operable guidelines and project axe E2E aspirations (#214). Mobile users with external keyboards (tablet) also affected. ## Constraints / guardrails - Extend `usePortalListbox` or shared hook — fix both `TokenSelect` and `MenuSelect` in one pass. - Preserve portal positioning, flip-above-anchor, mobile bottom nav inset behavior. - Do not break existing click selection or Escape dismiss. - Match visual focus styles to existing theme. - Typeahead: match by symbol prefix (case-insensitive); wrap or stop at list ends per APG. - Add `role="option"` + `aria-selected` on items; manage focus per APG listbox pattern. ## Relevant files | Path | Role | |------|------| | `frontend-dapp/src/components/ui/PortalListbox.tsx` | Shared keyboard hook | | `frontend-dapp/src/components/ui/TokenSelect.tsx` | Token picker | | `frontend-dapp/src/components/ui/MenuSelect.tsx` | Generic menu picker | | `frontend-dapp/src/components/ui/__tests__/` | Unit tests | | `frontend-dapp/e2e/a11y-critical-routes.spec.ts` | axe smoke (extend if needed) | ## Recommended direction 1. Add roving `activeIndex` state in `usePortalListbox` or consumer. 2. On trigger keydown: ArrowDown/Up open and move; printable chars start typeahead buffer with timeout. 3. Enter/Space select active option; Escape close and return focus to trigger. 4. Set `aria-activedescendant` on listbox pointing to active option id. 5. Vitest: keyboard events; optional Playwright keyboard spec on Swap token select. ## Acceptance criteria - [ ] Arrow keys move active option while open. - [ ] Enter selects active option and closes. - [ ] Typeahead jumps to first matching symbol. - [ ] Escape closes and restores focus to trigger. - [ ] `aria-activedescendant` / `aria-selected` correct. - [ ] TokenSelect and MenuSelect both fixed. ## Test plan — all paths | Path | Test | |------|------| | Click select | Unchanged | | ArrowDown x3 + Enter | Third option selected | | Type "ul" | Jumps to ULUNA (or first match) | | Escape | Closes; focus on trigger | | Empty options | Disabled trigger | | Long list scroll | Active item scrolled into view | | MenuSelect variant | Same keyboard behavior | Run: `make test-frontend` ## Test plan — attack / abuse vectors | Vector | Expected | |--------|----------| | Rapid key repeat | No duplicate onChange spam | | Typeahead injection chars | Treated as literal search only | ## Verification criteria - [ ] Vitest keyboard interaction tests pass. - [ ] Manual: Tab to token select, select via keyboard only on Swap page. - [ ] axe devTools: no listbox-related violations on Swap (spot check).
PlasticDigits commented 2026-05-31 05:40:18 +00:00 (Migrated from gitlab.com)

mentioned in commit 703866769a

mentioned in commit 703866769a157f2e79bb2b50f6b69ecc93242ec8
PlasticDigits commented 2026-05-31 05:40:18 +00:00 (Migrated from gitlab.com)

mentioned in commit 56131f0609

mentioned in commit 56131f0609887d180f9a2c1f44016174eab71a07
PlasticDigits commented 2026-05-31 05:40:41 +00:00 (Migrated from gitlab.com)

Implementation summary (merged to main @ 56131f0)

Added WAI-ARIA listbox keyboard support for TokenSelect and MenuSelect via a shared usePortalListboxKeyboard hook (Arrow Up/Down with wrap, Home/End, symbol/label typeahead, Enter/Space select, Escape/Tab close with focus restore). Positioning/outside-click remain in usePortalListbox.

Files touched

  • frontend-dapp/src/components/ui/usePortalListboxKeyboard.ts + portalListboxKeyboard.ts
  • TokenSelect.tsx, MenuSelect.tsx, PortalListbox.tsx, index.css
  • Vitest: MenuSelect.keyboard.test.tsx, TokenSelect.keyboard.test.tsx, portalListboxKeyboard.test.ts
  • Docs: docs/frontend.md (keyboard invariants), gaps/GAP_1780200149.md (M5)
  • Agent skill: skills/AGENTS_FRONTEND_PORTAL_LISTBOX_KEYBOARD.md (cross-linked from CLS skill + frontend.md)

Verification checklist

  • Tab to Swap You Pay / You Receive token select → open with ArrowDown → navigate with arrows → Enter selects without mouse
  • Type symbol prefix (e.g. us → USTC) while listbox open
  • Escape closes menu and returns focus to trigger
  • Trade / Charts / Limits MenuSelect pair pickers behave the same
  • Click selection unchanged; long lists scroll active row into view
  • make test-frontend passes (752 tests incl. new keyboard suite)

Follow-ups

  • Optional Playwright keyboard spec on Swap token select (not blocking; Vitest covers hook + both components)
  • Broader axe listbox audit remains under #214

@qa-agent-team — please run the checklist above on /swap, /trade, and /limits (desktop + tablet external keyboard if available) and spot-check axe devTools on Swap for listbox-related violations.

Leaving issue open pending QA sign-off.

## Implementation summary (merged to `main` @ 56131f0) Added WAI-ARIA listbox keyboard support for **`TokenSelect`** and **`MenuSelect`** via a shared **`usePortalListboxKeyboard`** hook (Arrow Up/Down with wrap, Home/End, symbol/label typeahead, Enter/Space select, Escape/Tab close with focus restore). Positioning/outside-click remain in **`usePortalListbox`**. ### Files touched - `frontend-dapp/src/components/ui/usePortalListboxKeyboard.ts` + `portalListboxKeyboard.ts` - `TokenSelect.tsx`, `MenuSelect.tsx`, `PortalListbox.tsx`, `index.css` - Vitest: `MenuSelect.keyboard.test.tsx`, `TokenSelect.keyboard.test.tsx`, `portalListboxKeyboard.test.ts` - Docs: `docs/frontend.md` (keyboard invariants), `gaps/GAP_1780200149.md` (M5) - Agent skill: `skills/AGENTS_FRONTEND_PORTAL_LISTBOX_KEYBOARD.md` (cross-linked from CLS skill + frontend.md) ### Verification checklist - [ ] Tab to Swap **You Pay** / **You Receive** token select → open with **ArrowDown** → navigate with arrows → **Enter** selects without mouse - [ ] Type symbol prefix (e.g. `us` → USTC) while listbox open - [ ] **Escape** closes menu and returns focus to trigger - [ ] Trade / Charts / Limits **MenuSelect** pair pickers behave the same - [ ] Click selection unchanged; long lists scroll active row into view - [ ] `make test-frontend` passes (752 tests incl. new keyboard suite) ### Follow-ups - Optional Playwright keyboard spec on Swap token select (not blocking; Vitest covers hook + both components) - Broader axe listbox audit remains under **#214** **@qa-agent-team** — please run the checklist above on `/swap`, `/trade`, and `/limits` (desktop + tablet external keyboard if available) and spot-check axe devTools on Swap for listbox-related violations. Leaving issue **open** pending QA sign-off.
PlasticDigits commented 2026-05-31 07:37:55 +00:00 (Migrated from gitlab.com)

mentioned in commit a9bf99e4cd

mentioned in commit a9bf99e4cd41002d72be5c3aa789a985f051116d
PlasticDigits commented 2026-05-31 07:37:59 +00:00 (Migrated from gitlab.com)

Verification complete (@ a9bf99e on main)

Verified GitLab #244 (M5 — TokenSelect / MenuSelect WAI-ARIA listbox keyboard) in worktree verify/issue-244.

What was verified

  • Vitest: make test-frontend — 758 tests pass, including MenuSelect.keyboard.test.tsx, TokenSelect.keyboard.test.tsx, portalListboxKeyboard.test.ts.
  • Manual keyboard (Swap /): Tab → You Pay trigger → ArrowDown opens listbox → ArrowDown×3 + Enter selects third option (RUBY) → Escape closes and restores trigger focus → rapid typeahead us jumps to USTC.
  • Manual keyboard (Trade /trade): Trading pair MenuSelect opens with ArrowDown (shared hook).
  • ARIA spot-check (Swap): listbox exposes role="listbox", aria-activedescendant, options use role="option" + aria-selected; no listbox-related issues in the accessibility tree during keyboard navigation.

Bug found & fixed

Rapid multi-char typeahead from a closed trigger could reset the buffer when the second key arrived before React open committed (e.g. us → SLATE instead of USTC). Fixed with synchronous openRef in usePortalListboxKeyboard.ts + regression test (fireEvent.keyDown rapid de on closed MenuSelect).

  • docs/frontend.md — keyboard invariant documents openRef
  • skills/AGENTS_FRONTEND_PORTAL_LISTBOX_KEYBOARD.md — typeahead/openRef rule
  • Gap M5 already marked fixed in gaps/GAP_1780200149.md

Checklist (all pass)

  • Arrow keys move active option while open
  • Enter selects active option and closes
  • Typeahead jumps to first matching symbol/label prefix
  • Escape closes and restores focus to trigger
  • aria-activedescendant / aria-selected correct
  • TokenSelect and MenuSelect both fixed
  • Vitest keyboard interaction tests pass
  • Manual keyboard-only selection on Swap
  • ARIA/listbox spot-check on Swap (formal axe E2E for /swap remains under #214)

Closing — all acceptance and verification criteria satisfied.

## Verification complete (@ `a9bf99e` on `main`) Verified GitLab **#244** (M5 — `TokenSelect` / `MenuSelect` WAI-ARIA listbox keyboard) in worktree `verify/issue-244`. ### What was verified - **Vitest:** `make test-frontend` — **758** tests pass, including `MenuSelect.keyboard.test.tsx`, `TokenSelect.keyboard.test.tsx`, `portalListboxKeyboard.test.ts`. - **Manual keyboard (Swap `/`):** Tab → **You Pay** trigger → ArrowDown opens listbox → ArrowDown×3 + Enter selects third option (RUBY) → Escape closes and restores trigger focus → rapid typeahead `us` jumps to **USTC**. - **Manual keyboard (Trade `/trade`):** **Trading pair** `MenuSelect` opens with ArrowDown (shared hook). - **ARIA spot-check (Swap):** listbox exposes `role="listbox"`, `aria-activedescendant`, options use `role="option"` + `aria-selected`; no listbox-related issues in the accessibility tree during keyboard navigation. ### Bug found & fixed **Rapid multi-char typeahead from a closed trigger** could reset the buffer when the second key arrived before React `open` committed (e.g. `us` → **SLATE** instead of **USTC**). Fixed with synchronous **`openRef`** in `usePortalListboxKeyboard.ts` + regression test (`fireEvent.keyDown` rapid `de` on closed `MenuSelect`). ### Docs / agent cross-links - `docs/frontend.md` — keyboard invariant documents `openRef` - `skills/AGENTS_FRONTEND_PORTAL_LISTBOX_KEYBOARD.md` — typeahead/`openRef` rule - Gap **M5** already marked fixed in `gaps/GAP_1780200149.md` ### Checklist (all pass) - [x] Arrow keys move active option while open - [x] Enter selects active option and closes - [x] Typeahead jumps to first matching symbol/label prefix - [x] Escape closes and restores focus to trigger - [x] `aria-activedescendant` / `aria-selected` correct - [x] `TokenSelect` and `MenuSelect` both fixed - [x] Vitest keyboard interaction tests pass - [x] Manual keyboard-only selection on Swap - [x] ARIA/listbox spot-check on Swap (formal axe E2E for `/swap` remains under **#214**) Closing — all acceptance and verification criteria satisfied.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-31 07:38:00 +00:00
PlasticDigits commented 2026-06-12 04:46:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #361

mentioned in issue #361
PlasticDigits commented 2026-07-12 11:13:48 +00:00 (Migrated from gitlab.com)

mentioned in issue #481

mentioned in issue #481
PlasticDigits commented 2026-08-25 01:57:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #632

mentioned in issue #632
Sign in to join this conversation.
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
code/cl8y-dex-terraclassic#244
No description provided.