Repository navigation
Add global keyboard shortcuts and one roving tab index to the interop module - #2500
Merged
Merged
Conversation
This was referenced Oct 19, 2026
pournasserian
added a commit
that referenced
this pull request
Oct 19, 2026
…hecks (#2585) Closes #2582 ## What The fourth slice of F-DSK (#2422): keyboard-first batch review (DSK-04, UX-02) on the merged interop module (#2500). It also adds the Playwright browser tests and the automated WCAG 2.1 AA checks (UX-04) that #2434 left for this epic. - **`admin.js` roving lists**: a `[data-ui="rove"]` list of `[data-ui="rove-item"]` elements uses the existing `rove` helper. - J and K move to the next and previous item from anywhere outside a field, a menu or another modal. From outside the list, J enters at the item in the tab order and K at the last one. - The arrow keys, Home and End move while focus is in the list. - The list is read on every key, so items DiffView adds in a later batch are reached too. - **Desk**: - The rows rove, with only the first in the tab order, and Enter opens a set. - Keys 1 to 6 switch queues through `aria-keyshortcuts`. - A set link carries its queue (`sets/{id}?queue=awaiting-me`). - When the set view comes back to an empty queue, the desk says what was done. - **Set view**: - The diff items rove: `DiffView` gets an opt-in `Roving` parameter, used by the island. `/ui` and other uses keep every summary in the tab order. - Shortcuts: A approves, R opens the reject dialog, P publishes, N opens the next set in the queue and B goes back. The keys show on the buttons and are hidden from screen readers, which read `aria-keyshortcuts`. - After an approve or reject from a queue, the page redirects to the next set in that queue with "The previous set was approved", or back to the queue once it is empty. - **`ShortcutHelp`** (`Components/Review`): a dialog opened with `?` that lists the page's keys with `Kbd`, on the desk and the set view. - **`TableBody`** passes extra attributes through, for `data-ui="rove"`. - **Accessibility fixes from the axe run on `/ui`**: - `CommandSeparator` is `aria-hidden` instead of `role="separator"`, which a listbox may not hold. - `Kbd` text uses `text-foreground`, because `text-muted-foreground` on `bg-muted` was 4.3:1 in the light theme. - **CI**: caches `~/.cache/ms-playwright`, installs Chromium with `playwright.ps1 install --with-deps chromium` after the build, and sets `FLUENTCMS_TEST_BROWSER`. CONTRIBUTING says how to run the browser tests locally. ## New test packages - `Microsoft.Playwright` 1.63.0 (MIT), in `FluentCMS.IntegrationTests` only. - `Deque.AxeCore.Playwright` 4.14.0 (MIT), which runs axe-core in the page, in `FluentCMS.IntegrationTests` only. It brings `Deque.AxeCore.Commons` (MPL-2.0), which carries axe-core itself. Neither ships with the product. MPL-2.0 is file-level copyleft and applies only if the package's own files are changed and distributed, which this repository does not do. ## Why - **Browser tests in the integration project, behind a variable.** They need the real host, `AdminHost` and the same data setup as the screen tests. Skipping without `FLUENTCMS_TEST_BROWSER` matches the PostgreSQL tests, so `dotnet test` still needs only the .NET SDK. - **J and K as well as arrows.** Arrows only move inside a list, so they never take over page scrolling. J and K reach the list from anywhere with one hand, which is what makes a ten-item review fast. - **Move on after a decision.** A batch is a queue of sets. Going straight to the next set saves a round trip per set, and the decision is already saved before the redirect. - **The island says when it is interactive.** When the circuit attaches, Blazor replaces the prerendered items, which drops focus from a summary that J reached before then. `ChangeSetItems` marks itself `data-interactive="true"`, and the keyboard test waits for it. A full run under heavy local load lost focus that way before the marker was added. On a reviewer's screen this window is the circuit's start, usually well under a second. - **Not in this PR**: per-item approval of a routing sample (RTE-04) needs routing's sampling, so this slice approves whole sets. A revert shortcut is left out because a single key should not open a revert by accident. ## Tests Browser (Playwright, headless Chromium, against the host): - **UX-02**: with the keyboard alone, the reviewer signs in, presses J, opens the ten-item set and steps through all ten items. Only the focused summary is in the tab order, and K moves back. A approves the set and lands on the next set in the queue with the done note, in well under two minutes (asserted), and the set is approved. - **Reject by keyboard**: 2 switches to awaiting anyone. J and Enter open the set. R puts focus in the reason, and typing, Tab, Tab and Enter reject it. The reviewer lands on the now empty queue with the note, and the set is rejected. - **Typing and the help**: letters typed in the reason field are not shortcuts. `?` opens the help, and A does nothing while it is open. B goes back to the queue. - **WCAG 2.1 AA**: axe-core (`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`) reports no violation on `/ui`, the desk, the stale facts queue and the set view, in LTR and RTL, light and dark. - A smoke test that `admin.js` is served by the test host. bUnit: - `DiffView` with `Roving` marks the list and summaries, and only the first summary is tabbable. Without it, nothing changes. - Desk rows rove, queues have keys 1 to 6, the help lists five keys, set links carry the queue, and the done notes. - Set view: A and R are declared, back and next links follow the queue with B and N, the done note shows, and the help lists eight keys. Without a queue, it goes back to the desk and has no next link. Integration: approving from a queue redirects to the next set with `done=approved`, and rejecting the last one redirects to the queue with `done=rejected`, which the desk shows. `dotnet build -warnaserror`, the full suite with the browser tests, and markdownlint pass locally against a `postgres:17` container as well as SQLite. One loaded full run had `EntityRelationsTests` (PostgreSQL) fail once; it passed three reruns, and this PR does not touch it. ## Checklist - [x] Linked to an issue a maintainer has confirmed - [x] Targets `v2` - [x] Tests or docs updated where behaviour changed - [x] No tool-generated co-author trailers or footers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #2434
What
admin.jsbecomes the one interop module #2434 asks for. It handles focus trapping, the roving tab index and global keyboard shortcuts, alongside the overlays, the theme and the reconnect display it already ran. It stays a set of delegated document listeners that read the page when an event arrives, so it works over an interactive-server circuit, while the circuit is down and after a reconnect, with no per-component interop.aria-keyshortcuts, ARIA's own attribute, so assistive technology announces them too. For example,aria-keyshortcuts="Control+K Meta+K"means either chord.roveandroveIndexreplace the separate code in menus and tabs: one item is in the tab order, the arrow keys, Home and End move focus, and horizontal groups follow the direction. Command keeps focus in its input and movesaria-activedescendantwith the same wrap-around./ui. Ctrl+K or ⌘K focuses the command palette. Alt+T focuses the toasts, as Sonner does; theToasterregion now hastabindex="-1"andaria-keyshortcuts="Alt+T".CONTRIBUTING.md: how to declare a shortcut, and thatadmin.jsis the only script and no component calls into it.Boxes on #2434
To tick when this merges: "One small JavaScript interop module for what the platform does not cover: focus trapping, roving tab index and global keyboard shortcuts. It is the only JavaScript in the admin."
Why
aria-keyshortcutsrather than a registry. A registry, in C# or in JavaScript, would have to be rebuilt after every render and after a reconnect, and someone would have to keep it in step with the markup. An attribute on the element is in the page whenever the element is, carries its own accessibility, and lets the module stay stateless.No new dependency.
Tests
bUnit cannot run the module, so its keyboard behaviour was checked by hand; the tests pin what the module relies on:
InteropModuleTests:admin.jsis the only script inwwwroot.IJSRuntime.App.razorloads scripts.aria-keyshortcuts.ToasterTests: the region is focusable from script only and declares Alt+T.Checked by hand on
/uiover a live circuit:Blazor.pauseCircuit(), reconnect card showing), Alt+T moves focus to the toast region and the dialog still opens.Blazor.resumeCircuit(), Ctrl+K works again.dotnet build -warnaserror, the full suite and markdownlint pass locally.Checklist
v2