fix(a11y,layout): deep-review fixes — menu roles, sort aria-pressed, rem rail threshold
Epic-end deep-review of the branch surfaced six verified findings; fix them. a11y (medium): FilterSelect and SortControl opened their AnchoredPopover in mode:"listbox" but never passed a role, so the surface fell through to role="menu" while the rows rendered role="option"+aria-selected — options orphaned outside a listbox, so AT could not announce the selected value. Switch both to the house menu pattern (as ViewSwitcher already uses): a menu surface with menuitemradio (single) / menuitemcheckbox (multi) rows carrying aria-checked, scoped by a role=group. Trigger stays a button+haspopup=menu. a11y (low): the SortControl asc/desc direction segment signalled the active direction by colour alone. Add aria-pressed so the current direction is programmatically exposed, matching SegmentedToggle. correctness (low): PageShell converted a rem maxWidth to px with a hardcoded 16 to decide the pinned-rail overlay threshold, but index.css scales the root font by --font-scale (default 1.06 ~ 17px, slider 0.9-1.3), so the threshold was wrong at every text size on the rem pages (playlists is the only one with a pinned rail). Use the live root font-size, scale the 36rem slack the same way, and recompute when --font-scale changes (a guarded MutationObserver — the slider fires no resize). tests: cover the two gaps the review flagged — ItemCount (plural, 1M+ overflow, undefined icon-only, tooltip) and the PageShell rail-pinned-w/content-mw threshold math incl. the font-scale scaling. Updated the FilterSelect/ SortControl role queries to the new menuitemradio/checkbox roles. Verified in the running app: menu surfaces carry role=group + menuitemradio/ checkbox with 0 orphaned options, and the sort direction segment exposes aria-pressed. Dropped two review findings (max-width duplication, pill-class reuse) as refuted on adversarial verification. Gate green: tsc, 372 vitest, prettier, eslint (0 err).
This commit is contained in:
@@ -75,20 +75,34 @@ export default function PageShell({
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--content-mw", maxWidth ?? "none");
|
||||
// Room the pinned rail needs: its own width (~16rem) on each side of the centred content, plus the
|
||||
// nav rail (~4rem). Below that, or on a full-width page (no maxWidth), pin in-flow (w-67).
|
||||
// nav rail (~4rem) = ~36rem. Below that, or on a full-width page (no maxWidth), pin in-flow (w-67).
|
||||
let lastPxPerRem = 0;
|
||||
const apply = () => {
|
||||
// Convert the rem maxWidth (and the 36rem slack) against the LIVE root font-size, not a hardcoded
|
||||
// 16: index.css scales the root by --font-scale (default 1.06 ≈ 17px, slider 0.9–1.3), and the page's
|
||||
// own content wrapper is a rem `max-w-*`, so a fixed 16 mis-sizes the threshold at every text size.
|
||||
const pxPerRem = parseFloat(getComputedStyle(root).fontSize) || 16;
|
||||
lastPxPerRem = pxPerRem;
|
||||
const mwPx = maxWidth
|
||||
? maxWidth.endsWith("rem")
|
||||
? parseFloat(maxWidth) * 16
|
||||
? parseFloat(maxWidth) * pxPerRem
|
||||
: parseFloat(maxWidth)
|
||||
: null;
|
||||
const roomy = mwPx != null && window.innerWidth >= mwPx + 576;
|
||||
const roomy = mwPx != null && window.innerWidth >= mwPx + 36 * pxPerRem;
|
||||
root.style.setProperty("--rail-pinned-w", roomy ? "2.25rem" : "16.75rem");
|
||||
};
|
||||
apply();
|
||||
window.addEventListener("resize", apply);
|
||||
// The text-size slider changes --font-scale (hence the root font-size) WITHOUT firing resize, so the
|
||||
// rem→px threshold would go stale. Re-run when the root font actually changes; the pxPerRem guard keeps
|
||||
// our own --rail-pinned-w write (a rem string, no font change) from re-triggering the observer.
|
||||
const fontObserver = new MutationObserver(() => {
|
||||
if ((parseFloat(getComputedStyle(root).fontSize) || 16) !== lastPxPerRem) apply();
|
||||
});
|
||||
fontObserver.observe(root, { attributes: true, attributeFilter: ["style"] });
|
||||
return () => {
|
||||
window.removeEventListener("resize", apply);
|
||||
fontObserver.disconnect();
|
||||
root.style.removeProperty("--content-mw");
|
||||
root.style.removeProperty("--rail-pinned-w");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user