Repository navigation
fix(frontend): stop Enter on a column-filter button from sorting - #1388
Conversation
…table Opportunities headers were focusable role=button <th> elements wrapping the column-filter button. The th keydown handler ran onActivate() for any Enter/Space that bubbled up, and the filter button only stopped click propagation, so pressing Enter on a focused filter button re-sorted the table, never opened the filter, and dropped focus to <body>. Sort state was also invisible to assistive tech, and the nested controls tripped axe's nested-interactive rule. Each sortable <th> is now a plain column header carrying aria-sort, with a real sort <button> (label + indicator) and the filter button as siblings. The th keeps its click listener so the whole header is still a mouse target; the sort button's native Enter/Space activation reaches it as a click, and the custom keydown handler is gone. After a sort the re-rendered sort button gets focus back. The filter button gets a 24px minimum target size (WCAG 2.5.8) and both buttons a focus-visible ring. Refs LeanerCloud#755 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00. Or wait 59 minutes for your next included review. View limit details
Comment |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
…eyboard e2e The sidebar entries are links now (LeanerCloud#730), so the spec main added in LeanerCloud#1388 can no longer find them by role=tab. Refs LeanerCloud#730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No textual conflicts. Checked main's changes next to this branch's: LeanerCloud#1344 moved the YTD sparkline into loadYtdSparkline(), which looks up .kpi-tile-spark[data-spark-key="ytd"]; lib/kpi-tile.ts still renders that slot for the YTD tile. CSS added by LeanerCloud#1336/LeanerCloud#1332/LeanerCloud#1388 (sticky section nav z-index 5, table scroll shadows z-index 9, sort buttons) sits below the dialog z-index ladder and does not touch card or dialog surfaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1371) * fix(frontend): make the sidebar plain links, move focus on route change, add sub-tab keys The sidebar declared the ARIA tab pattern (role=tablist/tab, aria-selected) without its keyboard model, so screen readers announced "tab, 2 of 6" and arrow keys did nothing. It is page navigation, so it is now plain links with aria-current="page", and the top-level page containers drop their tabpanel role. Activating a sidebar link, or going back/forward, left focus on the link and announced nothing. switchTab now takes a focusContent option, set by the sidebar click handler and the popstate router, which focuses a visually hidden <h2 tabindex="-1"> page heading at the top of each page. The no-access placeholder keeps that heading. The mobile drawer no longer pulls focus back to the hamburger when a link click has already moved it. The Inventory and Admin sub-tab lists keep role=tablist, so they now get its keyboard model (lib/tablist.ts): a roving tabindex kept in sync by the sub-tab switchers, Left/Right with wrap-around, Home/End, hidden admin-only tabs skipped. Activation is manual (Enter/Space) because each selection fetches data and pushes a history entry. Refs #730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(frontend): cover the sub-tab keyboard model and sidebar link semantics Jest tests for lib/tablist.ts (roving tabindex, arrows with wrap, Home/End, vertical orientation, hidden admin-only tabs, manual activation) and for index.html's tab semantics, plus a Playwright spec that drives the real keys in Chromium: Enter on a sidebar link focuses the new page heading, Back restores focus, and the Admin and Inventory sub-tabs move focus with the arrow keys and select with Enter/Space. Refs #730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): count main's Trends sub-tab in the tab-semantics test Main added a fourth Inventory sub-tab (Trends, #1317), so index.html now has 8 role=tab buttons (4 Inventory + 4 Admin). The merge gave Trends the same roving tabindex as the others; update the expected count. Refs #730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): open Opportunities by its sidebar link in the header-keyboard e2e The sidebar entries are links now (#730), so the spec main added in #1388 can no longer find them by role=tab. Refs #730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In the Opportunities table, pressing Enter on a focused column-filter button re-sorted the table, never opened the filter, and dropped focus to the page body, so keyboard users could not filter. The header was a focusable
role=button<th>wrapping the filter button, with noaria-sort. This PR makes each header a plain column header witharia-sortand separate sort and filter buttons.Closes #755
What changed
frontend/src/recommendations.ts:buildListMarkuprenders each sortable header as<th class="sortable" data-sort=... aria-sort=ascending|descending>(aria-sorton the active column only), containing a<button class="sort-btn">with the label and sort indicator, and the existing filter button as a sibling.role=button,tabindexandaria-labelare removed from the<th>.<th>click listener stays (the whole header is still a mouse target, and the sort button's native Enter/Space arrives as a click); the custom keydown handler that sorted on any bubbled Enter/Space is removed. After a sort, focus moves to the re-rendered sort button for the same column.frontend/src/styles/components.css: a reset style for.sort-btnso it looks like the old header text,:focus-visiblerings for the sort and filter buttons, and a 24px minimum width/height forth .column-filter-btn(WCAG 2.5.8).recommendations.test.tsand a newtests-e2e/recommendations-header-keyboard.spec.ts.Commit:
270fb90e.Why this approach
This follows the issue's suggested fix (a real
<th>witharia-sort, sort and filter buttons as siblings) rather than the smallest patch, ane.target !== thguard in the keydown handler, which would leave the nested-interactive markup and the missingaria-sort. The<th>click listener and theth.sortable/data-sorthooks are kept, so existing tests and mouse behavior are unchanged. The sort button's accessible name is the column label (e.g. "Service") andaria-sortcarries the direction, so screen readers announce "Service, column header, sorted ascending, button".Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.Verification
Run on this branch's head
270fb90eon top of maina14dc8f2, fromfrontend/:npm run lint: 0 errors (142 warnings already on main). Typecheck and build pass.--randomize. The wholerecommendations.test.tsfile fails 2 tests under--randomizeon main as well (see test(frontend): recommendations.test.ts fails 6 tests under random ordering (order-dependent state) #803), not caused by this change.e2e-local.sh): 160/160 passed, including the newrecommendations-header-keyboard.spec.ts: Enter on Filter Service opens the dialog without re-sorting; keyboard sort setsaria-sortand keeps focus on the sort button; nothhasrole=buttonortabindex; every filter button is at least 24x24.scripts/test-git-secrets-allowlist.sh: 137 PASS.Notes for review
th .column-filter-btn.:focus-visibleoutline.aria-sorton theth. Clicking anywhere in the header still sorts.Overlaps
colHeaderreturn line inrecommendations.ts(it adds anumclass toth.sortable): textual conflict, resolve by keeping both (class="sortable${numClass}"plus this markup).th .column-filter-btn.activea few lines below the CSS changed here: small context conflict.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code