docs: record the second Phase 5 pass and what it corrected
Build & publish Arch package / arch-package (push) Successful in 1m57s
CI / check (push) Successful in 2m19s
Search index maintenance / maintain-index (push) Successful in 6s
CI / e2e (push) Failing after 3m2s

Three of a11y.md's findings describe a build that no longer exists —
one fixed by a phase that was not about it, one whose stated mechanism
stopped being true when Phase 4 bundled the icons, and one that
reproduces as a different shape. The generalisation is that a finding
has a date as well as a magnitude and a mechanism.

Also records the two bad versions of the duplicate-shelf rule that the
*existing* tests caught, the eleven e2e specs that landing on Home
broke and the one of them that was a real bug, and the second CI e2e
failure on a commit that changed no application code.
This commit is contained in:
2026-08-12 11:44:58 -04:00
parent 862e8a0468
commit 63d11c3f9c
3 changed files with 314 additions and 1 deletions
+87 -1
View File
@@ -237,7 +237,24 @@ See `.planning/plans/active/005-agent-development-harness.md`.
says so. Its queries (`sql/queries/home.sql`) return album ids only
and are joined back to `GetAllAlbumsWithDetails` in Go, so the album
projection has one definition. A shelf with nothing behind it is
omitted, never rendered empty.
omitted, never rendered empty — **and so is a shelf that repeats the
one above it**, which is the same rule one step further: "On repeat"
was "Pick up where you left off" reordered, because a small library
has one signal and answers several questions with the same albums.
Two guards make that safe, and both were arrived at by breaking the
existing tests: only shelves of three or more albums are judged (two
rows of one overlap by 100% whenever they agree at all), and only
when the shelf is **not showing the whole library** — a repeat is a
fault only if a different row was possible. Measured against a fixed
shelf size instead, an 11-album library kept three identical shelves
while a 13-album one lost them.
**The app lands here**, from `index.ts` after the stores are wired.
`index.html` still renders the track list eagerly and it is still what
paints first — it is the cached `tracks` view, so the navigation is a
class toggle plus one chunk rather than a second render of the shell.
`app-sidebar`'s default `activeView` is `home` to match, because the
sidebar does not hear a `navigate` it did not send.
- `profiling` — pprof server on `:6060`, compiled out in non-dev builds via build tags (`internal/dev/`).
**Explore catalog** (`backend/explore/`): the searchable MusicBrainz/
@@ -347,6 +364,75 @@ Destructive actions ask once, through `confirmAction()`
(`components/confirm-dialog/`), which is a `wa-dialog` and so brings the
focus trap and Escape the hand-rolled overlays do not have.
**Every dialog in the app is a `wa-dialog`, and there is no sixth
pattern.** The four hand-rolled autotag overlays and the remove-library
confirmation had no `role`, no `aria-modal`, no focus trap and no focus
restore — including the two gating an irreversible on-disk metadata
rewrite. The split is by *shape*, not by owner: a dialog that only asks
a question is a `confirmAction()` call (title, message, impact,
confirm/cancel), and a dialog carrying **input** is a `<wa-dialog>` in
the host's own template. Both remaining autotag dialogs render
unconditionally with `?open` deciding which is up — mounting one on
demand puts the element and its `showModal()` in the same update.
`autotag-view`'s last document keydown listener died with them; it
existed only because its dialogs could not close themselves.
**A menu has a keyboard model, and it is one model.**
`utils/context-menu-controller.ts` exports **`MenuKeyboard`** — focus
the first item on open, Arrow/Home/End to move (wrapping, as a menu
does and a listbox does not), Enter/Space to activate, Escape or Tab to
close, and focus back to the element it opened from. It is standalone
rather than part of `ContextMenuController` because `playlist-view`
renders a menu without that controller, and two menus with two keyboard
models is exactly what this is for. `isContextMenuKey()` is the
Shift+F10 / ContextMenu-key test, and `openFrom(el)` is the keyboard
open: anchored to the element, restoring focus to it.
Four things in it are load-bearing, and two of them are only visible
against the real components:
- **The items are not items yet when the host finishes updating.**
`wa-dropdown-item` sets its `role` in its *own* first update, so a
`[role^="menuitem"]` query at `updateComplete` finds nothing — which
reads exactly like a menu that opened and refused to take focus.
- **`focus()` on a popup that has not positioned itself is a silent
no-op**, so the first focus is retried across a few frames.
- **Focus is only taken back if the menu had it.** A click elsewhere
closes the menu too, and pulling focus to the row the user
right-clicked a moment ago is worse than leaving it.
- **Web Awesome keys an item's tabindex and highlight off `active`**, so
moving focus without setting it leaves the highlight on whichever
item the mouse last touched.
Three lists had no focused row to open a menu *from* — the queue panel
and both playlist detail views — and gained a roving tab stop through
`utils/roving-rows.ts`. **`track-list` deliberately does not use it**:
its equivalent predates this, carries selection semantics (shift-extend,
ctrl-toggle) the other three do not have, and is pinned by its own
tests.
**Async surfaces say what they are doing.** `styles/sr-only.css.ts`
carries the visually-hidden class and the rule that comes with it: a
live region must be **in the DOM before the text it announces is**,
because most screen readers announce a change to a region they are
already watching and ignore one that appears with its content already
in it. So these regions render unconditionally and empty, and only
their text changes. Four surfaces have one — the track list (loading,
failed, and how many rows a search matched), Explore's search,
`now-playing` (in **both** render branches, so it exists before the
first track arrives) and `job-indicator`, whose label swings between
"Scanning Music", "3 background jobs" and "Finished". The notification
surface already had one from Phase 3.
**A selectable grid is a listbox.** The four grids that ctrl/shift-select
(`artists-view`, `genres-view`, `cover-grid`, and the queue) are
`role="listbox" aria-multiselectable` over `role="option"` cards, not
rows of `role="button"`: `aria-selected` on a button is *invalid* and is
dropped outright, so the state the whole ctrl/shift interaction exists
to produce was invisible to anything but a sighted user. `track-list`'s
column headers carry `aria-sort` (Phase 1 added `role="columnheader"`
without it) and are activated by Enter/Space as well as by a click.
**One keyboard authority.** No component owns a document keydown
listener for its own shortcuts; it registers *panel-scoped* bindings
(`autotag.*`, `tracklist.*` in `backend/shortcuts/config.go`) and