Skip to content

Add keyboard-first review with roving focus, browser tests and WCAG checks - #2585

Merged
pournasserian merged 2 commits into
v2from
2582-keyboard-review
Oct 19, 2026
Merged

pournasserian merged 2 commits into
v2from
2582-keyboard-review

Conversation

@pournasserian

Copy link
Copy Markdown
Member

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

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 19, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:desk labels Oct 19, 2026
@pournasserian
pournasserian merged commit 6f9e44f into v2 Oct 19, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2582-keyboard-review branch October 19, 2026 05:47
pournasserian added a commit that referenced this pull request Oct 19, 2026
Closes #2621

## What

The third slice of F-FRM (#2426): pickers for reference and media fields
(FRM-01), backed by search (`ISearch`, #2566, #2606) and the media
library (#2543).

- **`PickerField`** (`Components/Forms`) is an interactive island inside
the static generated form. `FieldWidget` renders it for reference and
media fields.
- Over the circuit it shows the current choice, a Clear button and a
search box. The choice is a hidden input named `Form.Values[field]`, so
the form posts and saves exactly as before.
- **Entries**: as the person types, it searches the reference's target
type (at most 10 direct hits). Each hit shows its first text value, or
an entity's first text fact, with its state and a short id.
- **Media**: it filters the library by file name or folder, newest
first, with a preview for images.
- The matches are a `listbox` of buttons that rove with the module from
#2585. ArrowDown moves from the search box into the list, the arrows
move through it, Enter chooses and Escape clears the matches.
- Before the circuit attaches, or when it is lost, the prerendered
island is the plain select from #2613, so a form posted then still
saves.
- **`admin.js`**: a `data-ui="rove-input"` box moves focus to the first
item of the list its `aria-controls` names on ArrowDown.

## Why

- **An island, not a page in interactive mode.** Only the search needs
the circuit. The form, its other fields and the save stay static, so the
save path from #2613 does not change, and a lost circuit costs only the
search.
- **A hidden input in the island.** The static form collects it with the
other fields, and no server state has to survive between the choice and
the save.
- **`FieldOption[]` rather than an interface.** An island's parameters
cross into the circuit as JSON. With `IReadOnlyList<FieldOption>`, the
browser refused the root components with "The list of component
operations is not valid". The parameter says why in a comment.
- **Not in this PR**: field filters in the picker (`SearchQuery.Fields`
exists for it once a field's settings name a filter), list-of-reference
widgets, and rule hints (FRM-03, needs F-RUL #2419).

## Tests

- bUnit (`PickerFieldTests`):
  - Before the circuit, a picker is the select with the stored choice.
- An entry picker searches its target type and names hits by their first
text fact. It wires the listbox to the search box and the rove module,
and choosing posts the id and shows the name. Clear empties the choice.
  - A search with no match says so, and Escape clears the matches.
- A media picker finds an asset by file name, case-insensitively, with
its preview, and posts its id.
- `GeneratedFormTests` renders the reference and media widgets before
the circuit, as selects.
- Browser (`FormPickerTests`), keyboard alone: type the title, Tab to
the picker, type part of a product's name, press ArrowDown and Enter,
then write the rationale and save. The stored entry references the
product.

`dotnet build -warnaserror`, the unit and bUnit suite, the browser tests
and the SQLite integration suite pass locally. The local PostgreSQL run
on a loaded machine had nine failures in untouched classes, the same
pattern as the last three PRs. CI runs the full PostgreSQL suite. No new
package and no new project reference.

## 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
pournasserian added a commit that referenced this pull request Oct 20, 2026
…properly (#2661)

Closes #2626

## What

Follow-ups from the independent review of #2585 (F-DSK, #2422).

- **A "Single-key shortcuts" switch in the shortcuts dialog, on by
default** (WCAG 2.1 SC 2.1.4, which UX-04 targets).
- Off turns off every shortcut without Control, Alt or Meta: the action
letters, the queue numbers, J and K, and ?. The help stays one click
away.
- The choice is kept in this browser's local storage. If storage refuses
it, it lasts for the page.
- `admin.js` shows the stored choice when the switch is rendered, after
each enhanced navigation (`enhancedload`), and when the switch takes
focus.
- **J and K also match their physical keys,** as the other letters do,
so they work on Persian, Hebrew and other layouts.
- **After an approve or reject from a queue, the page goes to the set
that followed this one before the decision.** Skipping a set and
approving the next no longer lands back on the skipped one.
- **The shortcut dialogs list every key:** the arrow keys, Home and End
on both views, and Space beside Enter for folding an item.

The island's readiness (finding 7) stays as documented, as the issue
says.

A letter that types another Latin letter on a non-QWERTY layout can
still fire that letter's shortcut. That is existing behaviour, filed as
#2662.

## Tests

Browser:

- **Switch off:**
- With single-key shortcuts off, A leaves the set awaiting review and J
moves nothing.
- After a reload, J and ? still do nothing. A neither approves nor
leaves the page, which is checked after a pause, not at once. The switch
is still off.
- After an enhanced navigation to the desk, the switch still shows off.
  - With `admin.js` made to ignore the stored choice, this test fails.
- **Physical keys:** a keydown with the J key's code and a Persian
character moves to the next item.
- **Arrow keys:** ArrowDown, ArrowUp, Home and End move through the
desk's rows and the set's items.
- **Field check:** "Big bold jab" typed into a generated form's title
stays in the field, and B doesn't go back. This is the field check
outside any dialog, which the old test couldn't reach.
- **Queue order:** skipping the first set with N and approving the
second lands on the third.

bUnit: the dialogs list the new keys, and the switch is rendered on.

`dotnet build -warnaserror`, the unit and bUnit suite and the keyboard
browser tests pass locally. No new package and no new project reference.

## 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyboard-first batch review with roving focus, Playwright tests and WCAG checks

1 participant