docs: record the a11y tail, and four names written where nothing reads
Phase 3 shipped in six landings and a11y.md is closed, which closes all four audits from 2026-08-11. The pass's one lesson is that an accessible name is computed on the element carrying the role, and every tier we check with looks somewhere else: the audit read the source and credited a name that was never computed, an AX sweep read the tree and reported a placeholder-only box as clean, and a component test asserted the attribute and pinned the bug it existed to prevent. Six of the audit's claims turned out to be wrong or smaller than written, and one of the plan's own findings was false — the page header's sort control is named on all nine views. All of them are written down, which is where a third of the value of the last two plans came from.
This commit is contained in:
@@ -18,7 +18,7 @@ here has disappeared.
|
||||
|
||||
## Read this part before you fail
|
||||
|
||||
Fourteen things cost a cycle each the first time. They are here, not in a
|
||||
Fifteen things cost a cycle each the first time. They are here, not in a
|
||||
reference, because you need them *before* the failure, not after.
|
||||
|
||||
- **Time out every binding call.** A bound Go method called with wrong
|
||||
@@ -54,6 +54,16 @@ reference, because you need them *before* the failure, not after.
|
||||
`label` to `aria-labelledby` — so a new dialog that forgets to call
|
||||
the helper from `updated()` is invisible to
|
||||
`getByRole('dialog', {name})`.
|
||||
- **A name is computed on the element carrying the *role*, and Web
|
||||
Awesome puts the role in its own shadow root.** `aria-label` on a
|
||||
`<wa-slider>` or a `<wa-dialog>` host never reaches the tree. Use the
|
||||
component's own `label` (plus `styles/wa-slider-label.css.ts`, since
|
||||
a slider's is visible) or `utils/name-dialog.ts`. And in the light
|
||||
DOM, a `<label>` that is a *sibling* of its control with no `for`
|
||||
names nothing — that was 24 of the 93 controls on Settings.
|
||||
**`getFullAXTree` is how you check, and "0 unnamed" is not the whole
|
||||
answer**: a `placeholder` is an accname fallback, so a box labelled
|
||||
only by one reports clean.
|
||||
- **The a11y snapshot cannot check an accessible name on a dialog.**
|
||||
`playwright-cli snapshot` prints `- dialog [ref=…]` with no name
|
||||
whether the dialog is named by `aria-labelledby`, by `aria-label`,
|
||||
@@ -66,7 +76,7 @@ reference, because you need them *before* the failure, not after.
|
||||
`--browser=webkit` is CI-only; local work is Chromium. CI runs it
|
||||
with `if: !cancelled()` so a chromium failure does not silently
|
||||
skip it, which it did for two sessions.
|
||||
- **CI's `e2e` job is green on both engines** (54 specs each) since the
|
||||
- **CI's `e2e` job is green on both engines** (88 specs each) since the
|
||||
container got an audio device that keeps time. If playback specs
|
||||
start failing there again, check the **`The sink plays at real time`**
|
||||
step first: ALSA's `null` plugin consumes 3000 ms of audio in 2.96 ms,
|
||||
@@ -74,9 +84,9 @@ reference, because you need them *before* the failure, not after.
|
||||
reads as an app bug and cost two sessions of that suspicion.
|
||||
- **`make e2e` needs `SEED=default`.** Its specs assert on fixture
|
||||
content — unicode tracks, the fixture artists, a known playable file.
|
||||
Run against the `bulk` seed a measurement session left behind and 13
|
||||
of 36 fail, in a list that reads exactly like a regression in
|
||||
whatever you are holding. `make dev-headless SEED=default` first.
|
||||
Run against the `bulk` seed a measurement session left behind and a
|
||||
third of them fail (13 of 36, when it was measured), in a list that
|
||||
reads exactly like a regression in whatever you are holding. `make dev-headless SEED=default` first.
|
||||
- **…and the suite spends state it cannot always give back.**
|
||||
`view-lifecycle.spec.ts` **skips an autotag album** on every run, out
|
||||
of the eleven the seed has, and does not put it back — so around the
|
||||
@@ -180,7 +190,7 @@ and a CI step over every commit in a push) rejects a subject that is not
|
||||
`--no-verify` commit skips it locally and meets it in CI.
|
||||
|
||||
Two things about the e2e tier that are not obvious until they bite.
|
||||
**The 36 specs share one backend process in file order**, so a spec
|
||||
**The 88 specs share one backend process in file order**, so a spec
|
||||
that leaves the app somewhere passes alone and fails the suite — leave
|
||||
the UI as you found it, and *wait* for it rather than trusting the
|
||||
click to have finished. The queue panel's width is animated and the
|
||||
|
||||
@@ -2028,3 +2028,104 @@ Six more things worth keeping:
|
||||
palette rewrite, twice, because the component tier has no `:root` and
|
||||
renders the fallbacks. The tier that *did* catch things was a unit
|
||||
test over the palette table and a probe against the running app.
|
||||
|
||||
## A name lives where the role is, and neither the audit nor the sweep looks there
|
||||
|
||||
Plan 008 phase 3: the tail of `a11y.md`, which closes it — and with it
|
||||
all four audits from 2026-08-11.
|
||||
|
||||
The generalisation, and it is the whole of this pass: **an accessible
|
||||
name is computed on the element carrying the role, and every way we
|
||||
have of checking one looks somewhere else.** The audit read the
|
||||
*source* and credited a name that was never computed. My AX sweep read
|
||||
the *tree* and reported a weak name as no problem. A component test
|
||||
asserted the *attribute* and pinned the bug it was written to prevent.
|
||||
Three tiers, three different wrong answers, all about the same
|
||||
property.
|
||||
|
||||
Concretely, and each of these is a finding:
|
||||
|
||||
| where it was written | where the role is | computed name |
|
||||
|---|---|---|
|
||||
| `aria-label` on `<wa-slider>` | a div in its shadow root, `aria-labelledby="label"` | `""` |
|
||||
| `<label>` beside a `<select>` in `config-field` | the select | `""` |
|
||||
| `placeholder` on Explore's search input | the input | the placeholder |
|
||||
| `label` on `<wa-progress-bar>` | inner div's `aria-label` | correct |
|
||||
|
||||
The first is `wa-dialog`'s trap one component over and cost two
|
||||
sessions in 007. The fix is different, though, and the difference is
|
||||
worth keeping: `wa-progress-bar`'s `label` *is* an `aria-label` and is
|
||||
invisible, so it is just the right API; `wa-slider`'s `label` is
|
||||
**visible**, so the name comes from the library's own property and
|
||||
`styles/wa-slider-label.css.ts` hides it by part. That is preferred
|
||||
over `name-dialog.ts`'s reach into the shadow root for one reason —
|
||||
if Web Awesome renames the part, the label becomes *visible* and
|
||||
correctly named, rather than silently nameless again. Choose the
|
||||
failure you would rather have.
|
||||
|
||||
Nine more things worth keeping:
|
||||
|
||||
- **A sweep for empty names cannot see a weak one.** A `placeholder`
|
||||
is an accname fallback, so `getFullAXTree` reported the whole Explore
|
||||
view *clean* — which is why `a11y.26` survived four phases of people
|
||||
looking for exactly this class of bug. "0 unnamed" answers a
|
||||
narrower question than it reads as, which is the third time in this
|
||||
plan a *count* has done that.
|
||||
- **The count that sent Phase 1 hunting was wrong in both halves.**
|
||||
"Two unnamed native `<select>`s, one of them the page header's sort
|
||||
control on nine views": the sort control is named `Sort: ` by its
|
||||
wrapping `<label>` (`from: relatedElement`) on all nine, and the two
|
||||
unnamed roles were one `config-field` select and the *seek bar*.
|
||||
Recorded as a finding in the plan, believed for a phase, false.
|
||||
- **…and the thing it was pointing at was nine times bigger.** With
|
||||
Settings' sections expanded: **24 of 93 controls unnamed**, every
|
||||
`config-field` select and toggle and all eighteen column checkboxes.
|
||||
No finding names it, and `a11y.6` is not wrong — it says in its own
|
||||
line that it scanned every `<button>`. Same shape as phase 2's
|
||||
contrast number.
|
||||
- **A fix's own test can be pinning the bug.** `transport.test.ts`
|
||||
asserted `aria-label` on the `wa-slider` host under the title
|
||||
"carries an accessible name". It passed for six phases. *Run the
|
||||
existing tests* found it — third plan running that this is the rule
|
||||
that pays.
|
||||
- **`a11y.21`'s mechanism does not exist, and the real one is on the
|
||||
other axis.** "The 4em bars grow while the viewport does not and
|
||||
anything that no longer fits is clipped" — the middle grid row is
|
||||
`1fr` and absorbs them exactly: at 200% text on 800×600 the bars go
|
||||
64 → 128px, the panel 472 → 344px, and the footer still lands on 600.
|
||||
Nothing is clipped vertically. Horizontally the shell is 784px inside
|
||||
a 320px viewport (400% page zoom, the width 1.4.10 names) with 464px
|
||||
of it behind `overflow: hidden`. Measure the axis the finding does
|
||||
not mention.
|
||||
- **`overflow: hidden` still permits programmatic scrolling**, so
|
||||
`scrollLeft = 9999` returns a healthy 464 on the build that has the
|
||||
bug. My first spec passed against the broken build for that reason.
|
||||
A wheel gesture is the probe. Fifth entry in this plan's "the probe
|
||||
was wrong, not the code" column, and the tell was the oldest one
|
||||
there is: **it could not fail.**
|
||||
- **A synthetic `MouseEvent` does not reach a delegated handler.**
|
||||
Three probes in a row reported a queue row as never becoming active;
|
||||
`getByTestId('queue-row').dblclick()` made it active immediately.
|
||||
Delegation reads things a hand-built event does not carry.
|
||||
- **A finding can be half-closed by a phase that was not about it,
|
||||
and the half that remains is smaller than the sentence.** `a11y.34`
|
||||
reads "the sort direction is a 10px glyph *or nothing*" — Phase 1's
|
||||
`aria-sort` closed the *or nothing*, leaving one declaration. Second
|
||||
time in this plan (`a11y.11` was the first), and both times reading
|
||||
the sentence rather than the residue would have built something that
|
||||
already existed.
|
||||
- **The state a fix lands in, again, and it was three pixels.**
|
||||
`a11y.29` takes the subtitle's bottom margin away with the `<h3>`,
|
||||
which *shortens* the flex-centred title block and moves it **down**
|
||||
into the bar's clip — the hgroup had measured 67px inside a 64px bar
|
||||
since before any of this, and the descenders of "meant to bee." were
|
||||
cut. Found by reading a screenshot of the fix, which is the fifth
|
||||
regression in three plans that only a PNG has caught.
|
||||
|
||||
And one thing that went right and is worth copying: **the marker for
|
||||
`a11y.22` is a shape drawn in padding the row already had.** The track
|
||||
list's grid columns are computed from the host width, so anything in
|
||||
the flow moves every cell on the playing row and nothing else. A
|
||||
`::before` triangle in the 8px left padding costs no layout, and both
|
||||
tiers assert it is *absent* on the other rows — a marker that renders
|
||||
everywhere satisfies "the playing row has one" for free.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 008 — The last audit, and the one binding that outlived six phases
|
||||
|
||||
**Status:** active — Phases 1 and 2 shipped.
|
||||
**Status:** active — Phases 1, 2 and 3 shipped. Phase 4 is all that
|
||||
remains, and `a11y.md` is closed.
|
||||
**Branch:** main
|
||||
**Created:** 2026-08-12
|
||||
**Follows:** 007-ui-reconciliation
|
||||
@@ -64,17 +65,20 @@ fixed until it has been reproduced in the running app.
|
||||
| `15` | Major | `now-playing` is not among the four files carrying `prefers-reduced-motion`. WCAG 2.2.2: moving content over 5 s with no pause mechanism. |
|
||||
| `14` | Major | `combobox.ts` has no `aria-controls`, no `aria-activedescendant`, no option ids. |
|
||||
| `11` | Major | No `altKey` handler in `queue-panel`. The *other* half of this finding — "no keyboard path to add a track to the queue or a playlist" — was closed by Phase 5's `MenuKeyboard`. |
|
||||
| `21` | Minor | `body { height: 100vh; overflow: hidden }` unchanged. WCAG 1.4.10. |
|
||||
| `22` | Minor | `queue-panel` gained `aria-current`; `track-list` did not, and neither has a non-colour marker. |
|
||||
| `24` | Minor | No `title` on the truncating element in `track-info`, `playlist-view`, `queue-panel` or `track-list`. |
|
||||
| `25` | Minor | `<wa-progress-bar value=…>` with no label, verbatim as filed. |
|
||||
| — | new | **Two unnamed native `<select>`s**, one of them `page-header`'s sort control on nine views. Not in the audit: `a11y.6` scanned `<button>`. Found in the AX tree while reproducing `14`. Belongs with `26`. |
|
||||
| `21` | Minor | `body { height: 100vh; overflow: hidden }` unchanged. WCAG 1.4.10. **Shipped — and the stated mechanism was wrong; the failure is horizontal.** |
|
||||
| `22` | Minor | `queue-panel` gained `aria-current`; `track-list` did not, and neither has a non-colour marker. **Shipped.** |
|
||||
| `24` | Minor | No `title` on the truncating element in `track-info`, `playlist-view`, `queue-panel` or `track-list`. **Shipped.** |
|
||||
| `25` | Minor | `<wa-progress-bar value=…>` with no label, verbatim as filed. **Shipped — it was named "Progress", not unnamed.** |
|
||||
| — | ~~new~~ | ~~**Two unnamed native `<select>`s**, one of them `page-header`'s sort control on nine views.~~ **False.** The sort control is named "Sort: " by its wrapping `<label>` on all nine. The two unnamed roles were **one** `config-field` select and the **seek bar**. See Phase 3's list. |
|
||||
| — | new | **24 of 93 controls on Settings unnamed** — every `config-field` select and toggle, all eighteen column checkboxes. **Shipped, 0 of 93.** |
|
||||
| — | new | **Both `wa-slider`s have no accessible name**, which `a11y.md` files under *what is already correct*. **Shipped.** |
|
||||
| `26` | Minor | Explore's search box is named by its placeholder only — which *is* an accname fallback, so an AX sweep reports it clean. `search-bar` was already fixed. **Shipped.** |
|
||||
| `28` | ~~dropped~~ | **Measured, stays dropped.** One header *label* clips at 800×600; zero data cells do. |
|
||||
| `29` | Polish | `<h3 class="subtitle">` for type size. |
|
||||
| `30` | Polish | No skip link anywhere. |
|
||||
| `32` | Polish | `title="Remove from queue"`, not identifying the track. |
|
||||
| `34` | Polish | The 10 px sort arrow, unchanged. |
|
||||
| — | — | Colour contrast, never measured. |
|
||||
| `29` | Polish | `<h3 class="subtitle">` for type size. **Shipped.** |
|
||||
| `30` | Polish | No skip link anywhere. **Shipped.** |
|
||||
| `32` | Polish | `title="Remove from queue"`, not identifying the track. **Shipped.** |
|
||||
| `34` | Polish | The 10 px sort arrow, unchanged. **Shipped — half of it was closed by Phase 1's `aria-sort`.** |
|
||||
| — | — | Colour contrast, never measured. **Measured and fixed in Phase 2.** |
|
||||
|
||||
## Ordering principle
|
||||
|
||||
@@ -400,6 +404,106 @@ Two of them are not one-liners and should be treated as such:
|
||||
- **`22`** asks for a non-colour marker on the playing row, which is a
|
||||
visual change to the densest list in the app and moves a baseline.
|
||||
|
||||
### Phase 3 — what actually shipped
|
||||
|
||||
Six landings rather than one, ordered by risk, each reproduced in the
|
||||
running app before anything was written and each watched failing on the
|
||||
pre-fix build.
|
||||
|
||||
- **Web Awesome's two hidden roles.** `label` on both `wa-slider`s and
|
||||
on `wa-progress-bar` (`25`), plus `styles/wa-slider-label.css.ts`,
|
||||
which hides the slider's visible label by part and puts back the 8px
|
||||
margin `#slider` takes as soon as one exists.
|
||||
- **Settings' form controls.** `for`/`id` in `config-field`,
|
||||
`aria-label` on the eighteen column toggles and thirty-six column
|
||||
arrows, and the action's name on every `shortcut-capture`.
|
||||
**24 unnamed of 93 → 0.**
|
||||
- **`24` and `32`.** `title` on the four clipping surfaces, on the
|
||||
track-list *cell* rather than on what is inside it; and a queue row's
|
||||
remove button named after its own track.
|
||||
- **`29`, `30`, `34`.** A skip link, `<h3>` → `<p>`, and the sort arrow
|
||||
at the type scale's floor. Plus the state that landed in: the hgroup
|
||||
measured 67px in a 64px bar and the subtitle's descenders were
|
||||
clipped once the h3's bottom margin went with it.
|
||||
- **`22`.** A triangle in each row's own left padding, in both lists,
|
||||
and `aria-current` on the track-list row.
|
||||
- **`21`.** `overflow-x: auto` — measured, and the finding's stated
|
||||
mechanism is not the one that exists.
|
||||
|
||||
And `26`'s remaining half, found last: Explore's search box.
|
||||
|
||||
Pinned by `wa-control-names.test.ts` (4), `settings-names.test.ts`
|
||||
(11), `aria-tail.test.ts` (+5), `queue-reorder.test.ts` (+3),
|
||||
`e2e/specs/control-names.spec.ts` (3), `e2e/specs/skip-link.spec.ts`
|
||||
(4), `e2e/specs/layout-overflow.spec.ts` (+6) and
|
||||
`e2e/specs/playback.spec.ts` (+1). `make ui-test` 649 → **672**;
|
||||
`make e2e` 74 → **88**.
|
||||
|
||||
#### Where the plan was wrong — Phase 3
|
||||
|
||||
Ten things. The first four are the audit or the plan being wrong about
|
||||
where a control's name lives.
|
||||
|
||||
- **The two unnamed `<select>`s from Phase 1 were one `<select>` and a
|
||||
slider, and neither was the page header's.** `page-header`'s sort
|
||||
control computes "Sort: " from its wrapping `<label>`, on every one
|
||||
of the nine views — checked with `getFullAXTree`, `from:
|
||||
relatedElement`. The other unnamed role was the **seek bar**, which
|
||||
`a11y.md` lists under *what is already correct*. Fourth probe error
|
||||
in two passes, and the same shape as the rest: read at the wrong
|
||||
level.
|
||||
- **`aria-label` on a Web Awesome host does not name the control.**
|
||||
`wa-slider` puts `role="slider"` on a div in its own shadow root
|
||||
pointing `aria-labelledby` at an empty internal `<label>`, and that
|
||||
IDREF outranks the host's `aria-label`. Both sliders computed `""`.
|
||||
Exactly `wa-dialog`'s trap one component over, and the audit made
|
||||
exactly the same mistake in the opposite direction — it read the
|
||||
source and credited a name that was never computed.
|
||||
`volume-control` did not even have the `aria-label` it is credited
|
||||
with.
|
||||
- **`a11y.25` is not "unnamed".** `wa-progress-bar` falls back to the
|
||||
localised word *progress*, so it announced "Progress, 45%" — named
|
||||
after the widget rather than after the work. Same fix, smaller claim.
|
||||
- **Settings was full of unnamed controls and no finding says so.** 24
|
||||
of 93. `a11y.6` is not wrong: it says in its own line that it scanned
|
||||
every `<button>`. Third time this pass that a count in the audit was
|
||||
answering a narrower question than it reads as.
|
||||
- **A placeholder is an accessible name.** Explore's search box
|
||||
therefore reported *clean* in an AX sweep of all eleven views, which
|
||||
is why `a11y.26` outlived four phases of people looking for exactly
|
||||
this. A sweep for empty names cannot see a weak one.
|
||||
- **`a11y.21`'s mechanism does not exist.** "The 4em bars grow while
|
||||
the viewport does not, and anything that no longer fits is clipped
|
||||
with no scrollbar" — the middle row is `1fr` and absorbs them
|
||||
exactly. At 200% text on 800×600 the bars go 64 → 128 and the panel
|
||||
472 → 344, footer still on 600. The real failure is horizontal, which
|
||||
the finding does not mention: 784px of app in a 320px viewport, 464px
|
||||
of it unreachable.
|
||||
- **…and the obvious probe for it passes on the broken build.**
|
||||
`overflow: hidden` still permits *programmatic* scrolling, so
|
||||
`scrollLeft = 9999` returns a healthy number on the build with the
|
||||
bug. It did. The spec is a wheel gesture now.
|
||||
- **A fix's own test was pinning the bug.** `transport.test.ts`
|
||||
asserted `aria-label` on the `wa-slider` host and called it "carries
|
||||
an accessible name". Running the existing suite is what found it,
|
||||
for the third plan running.
|
||||
- **`a11y.34` was half closed by Phase 1 and nobody had noticed.** "The
|
||||
sort direction is a 10px glyph *or nothing*" — it is announced now,
|
||||
via the `aria-sort` Phase 1 added. What was left is one declaration.
|
||||
- **The queue's `aria-current` is dead in the common path.** A track
|
||||
started from the *track list* leaves the queue's `currentIndex` at
|
||||
−1, so the panel has no current row at all — which is why `22`'s
|
||||
marker looked broken the first time it was checked in the running
|
||||
app. Pre-existing, not fixed here, and the reason the e2e case plays
|
||||
from the queue.
|
||||
|
||||
And one that is about the harness rather than the audit: **a synthetic
|
||||
`MouseEvent` does not reach a delegated handler the way a real gesture
|
||||
does.** Three probes in a row reported the queue row as never becoming
|
||||
active; `page.getByTestId('queue-row').dblclick()` made it active
|
||||
immediately. Same family as everything above — the probe was wrong, not
|
||||
the code.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — `tracklist.delete`, and the operation behind it
|
||||
|
||||
@@ -130,7 +130,7 @@ meaningless against the seed's one empty playlist, so it builds ten
|
||||
It wraps every bound Go method, so "did that refetch the library" is a
|
||||
fact rather than an inference. It is not a spec and does not run in CI.
|
||||
|
||||
**The cheapest tier needs none of that.** `make ui-test` runs 480
|
||||
**The cheapest tier needs none of that.** `make ui-test` runs 672
|
||||
Vitest tests in a real Chromium in ~2 s with no Wails, no backend, no
|
||||
seeded library and no virtual display, because `frontend/wailsjs/` is a
|
||||
pure passthrough to `window.go` / `window.runtime` and
|
||||
@@ -586,6 +586,76 @@ only ever moved by an arrow key, so a row reached by a click or by Tab
|
||||
left it at 0 and `Enter` played the first track in the queue from any
|
||||
focused row.
|
||||
|
||||
**A name is computed where the role is, and that is rarely where you
|
||||
wrote it.** Four surfaces wrote a name somewhere the accessibility tree
|
||||
never looked. `wa-slider` puts `role="slider"` on a div in its own
|
||||
shadow root pointing `aria-labelledby` at an empty internal `<label>`,
|
||||
which outranks the host's `aria-label` — so both sliders computed a
|
||||
name of `""`, and `a11y.md` lists both under *what is already correct*.
|
||||
The name comes from `label` now, the library's own API, and
|
||||
`styles/wa-slider-label.css.ts` hides it: preferred over reaching into
|
||||
the shadow root the way `name-dialog.ts` must, because if Web Awesome
|
||||
renames the part the label becomes *visible and correctly named*
|
||||
rather than silently nameless. Its second rule is load-bearing —
|
||||
`#slider` takes an 8px margin the moment a label exists, which grows
|
||||
the bar 6px → 14px and moves the transport with it.
|
||||
`wa-progress-bar`'s `label` *is* an `aria-label` and is invisible, so
|
||||
there it is just the right attribute.
|
||||
|
||||
The same thing in the light DOM: `config-field` rendered a `<label>`
|
||||
as a **sibling** with no `for`, so **24 of 93 controls on Settings**
|
||||
computed an empty name. They use `for`/`id` (a fixed id, safe only
|
||||
because each field is its own shadow root) rather than `aria-label`,
|
||||
for what it buys beyond the name — the label text becomes a click
|
||||
target. And three surfaces are named but identify nothing, which is
|
||||
the same fault one step milder: three shortcut buttons announced
|
||||
themselves as "S", thirty-six column arrows as "Move up", and every
|
||||
queue row's remove button as "Remove from queue".
|
||||
|
||||
**Checking any of this needs the browser's own answer, and "0 unnamed"
|
||||
is not it.** A `placeholder` is an accname fallback, so an
|
||||
`Accessibility.getFullAXTree` sweep of all eleven views reported
|
||||
Explore's search box — the audit's own `a11y.26` — as clean. A sweep
|
||||
for *empty* names cannot see a *weak* one.
|
||||
|
||||
**The shell scrolls sideways and not down.** `body` is
|
||||
`overflow-x: auto; overflow-y: hidden`, and both halves are measured.
|
||||
Vertically there is nothing to fix: the middle grid row is `1fr` and
|
||||
absorbs the 4em bars exactly — at 200% text on an 800×600 window the
|
||||
bars go 64 → 128px, the main panel 472 → 344px, and the footer still
|
||||
lands on 600. Horizontally the shell is 784px inside a 320px viewport
|
||||
(400% page zoom, the width WCAG 1.4.10 names) and 464px of it,
|
||||
including the job indicator and the queue button, used to sit behind
|
||||
`overflow: hidden`. Keeping the vertical axis fixed is what keeps the
|
||||
transport where a desktop player's transport belongs. At every size
|
||||
this app promises, no scrollbar appears. Note that `overflow: hidden`
|
||||
still permits *programmatic* scrolling, so a probe that sets
|
||||
`scrollLeft` passes on the broken build; the spec uses a wheel gesture.
|
||||
|
||||
**The playing row is a shape, not a hue.** `track-list` and
|
||||
`queue-panel` draw a `::before` triangle in each row's own left
|
||||
padding, plus `aria-current` — before, both rows were a background tint
|
||||
and a text colour and nothing else (WCAG 1.4.1). It is in the padding
|
||||
because the track list's grid columns are computed from the host width,
|
||||
so a marker in the flow moves every cell on the playing row and nothing
|
||||
else. Both tiers assert it is **absent** on the other rows: a marker
|
||||
that renders everywhere satisfies "the playing row has one" for free.
|
||||
One thing to know before checking it — a track started from the *track
|
||||
list* leaves the queue's `currentIndex` at −1, so the panel has no
|
||||
current row at all in that flow, which reads exactly like the marker
|
||||
not working.
|
||||
|
||||
**The first thing Tab reaches is a skip link.** Two details are
|
||||
load-bearing and neither is the link's text. It is `position: absolute`
|
||||
in **both** states, because `body` is a grid with named areas and an
|
||||
in-flow extra child is auto-placed into one of them. And `<main>`
|
||||
carries `tabindex="-1"`, or the fragment moves the scroll, leaves the
|
||||
tab sequence exactly where it was, and looks like it worked. The
|
||||
subtitle beside it is a `<p>`, which is also what an `hgroup` is
|
||||
supposed to contain — and dropping the `<h3>`'s bottom margin shortened
|
||||
the flex-centred title block enough to move it down into the 4em bar's
|
||||
clip, so `.title` zeroes both margins.
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user