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:
@@ -2,8 +2,9 @@
|
||||
//
|
||||
// The rendering integration of SortControl (its pickSort/heal decision logic is covered in
|
||||
// columnSort.test.ts). Labels are plain props here — no i18n needed. It's a floating-ui popover now
|
||||
// (a labeled pill + a listbox with a footer direction segment), so we stub the two autoUpdate
|
||||
// observers and wrap in a FloatingTree; options only exist while open → role queries pass { hidden: true }.
|
||||
// (a labeled pill + a menu of `menuitemradio` sort keys with a footer direction segment), so we stub
|
||||
// the two autoUpdate observers and wrap in a FloatingTree; rows only exist while open → role queries
|
||||
// pass { hidden: true }.
|
||||
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { FloatingTree } from "@floating-ui/react";
|
||||
@@ -55,22 +56,17 @@ describe("SortControl", () => {
|
||||
it("renders one option per SortOption, no no-sort row without defaultLabel", () => {
|
||||
renderControl({ key: "title", dir: "asc" });
|
||||
openPanel();
|
||||
expect(screen.getAllByRole("option", { hidden: true }).map((o) => o.textContent)).toEqual([
|
||||
"Date",
|
||||
"Title",
|
||||
"Shuffle",
|
||||
]);
|
||||
expect(
|
||||
screen.getAllByRole("menuitemradio", { hidden: true }).map((o) => o.textContent),
|
||||
).toEqual(["Date", "Title", "Shuffle"]);
|
||||
});
|
||||
|
||||
it("adds a leading no-sort option when defaultLabel is set (tables)", () => {
|
||||
renderControl(null, { defaultLabel: "None" });
|
||||
openPanel();
|
||||
expect(screen.getAllByRole("option", { hidden: true }).map((o) => o.textContent)).toEqual([
|
||||
"None",
|
||||
"Date",
|
||||
"Title",
|
||||
"Shuffle",
|
||||
]);
|
||||
expect(
|
||||
screen.getAllByRole("menuitemradio", { hidden: true }).map((o) => o.textContent),
|
||||
).toEqual(["None", "Date", "Title", "Shuffle"]);
|
||||
});
|
||||
|
||||
it("summarises the trigger with the first option (not blank) for an unavailable key without defaultLabel", () => {
|
||||
@@ -81,7 +77,7 @@ describe("SortControl", () => {
|
||||
it("picking a new option fires onChange at that option's defaultDir", () => {
|
||||
const { onChange } = renderControl({ key: "title", dir: "asc" });
|
||||
openPanel();
|
||||
fireEvent.click(screen.getAllByRole("option", { hidden: true })[0]!); // Date
|
||||
fireEvent.click(screen.getAllByRole("menuitemradio", { hidden: true })[0]!); // Date
|
||||
expect(onChange).toHaveBeenCalledWith({ key: "date", dir: "desc" });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user