fix(menus): one tick for every choice; a listbox holds its options only

- The saved views, the view switcher, the year filter and the table's single-choice filter mark the
  chosen row with MenuCheck, as DESIGN asks, not a trailing tick, a dot or a filled row; a contract
  test holds every checkable menu row to it. keepSelectedPaint had no caller left and is gone.
- A combobox's panel carries its listbox role on an inner PopoverList (listInside), so the failed
  load, Loading, No matches and the 'n more' line sit beside the options, not inside the listbox. The
  Plex search box's Loading line likewise.
- The Plex sidebar's playlist picker offers Create only once the lists have loaded: over a failed load
  it pushed the error and its Retry out at the first keystroke.
This commit is contained in:
2026-09-27 19:43:04 +02:00
parent cda626be10
commit e31a588f19
12 changed files with 266 additions and 116 deletions
@@ -1,19 +1,35 @@
import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const root = new URL("../", import.meta.url);
const sources = () =>
readdirSync(root, { recursive: true, encoding: "utf8" })
.filter((f) => f.endsWith(".tsx") && !f.includes(".test."))
.map((f) => ({ f, src: readFileSync(new URL(f.replace(/\\/g, "/"), root), "utf8") }));
// E28 review L2: `ui/MenuCheck` is the ONE drawn check box of a checkable row (S3e3b replaced seven
// hand-drawn copies), yet FilterSelect and Combobox still painted their own — a 14px box beside the
// menus' 16px one. The filled-box paint is the tell: it may live in MenuCheck only.
describe("the drawn check box", () => {
it("is painted by MenuCheck alone", () => {
const root = new URL("../", import.meta.url);
const offenders = readdirSync(root, { recursive: true, encoding: "utf8" })
.filter((f) => f.endsWith(".tsx") && !f.includes(".test.") && !f.endsWith("MenuCheck.tsx"))
.filter((f) =>
/border-accent bg-accent text-accent-fg/.test(
readFileSync(new URL(f.replace(/\\/g, "/"), root), "utf8"),
),
);
const offenders = sources()
.filter(({ f }) => !f.endsWith("MenuCheck.tsx"))
.filter(({ src }) => /border-accent bg-accent text-accent-fg/.test(src))
.map(({ f }) => f);
expect(offenders).toEqual([]);
});
// Round 2 (P6): the player's track rows marked the choice only by an accent label — which the
// highlight's white text wiped — and four more menus ticked it their own way (a trailing tick, a
// dot, a filled row). A file with a checkable menu row draws MenuCheck, and no tick of its own.
it("marks every checkable menu row, with no tick of its own beside it", () => {
const offenders = sources()
.filter(({ src }) => {
const checkable = (src.match(/role="menuitem(?:radio|checkbox)"/g) ?? []).length;
const drawn = (src.match(/<MenuCheck\b/g) ?? []).length;
return checkable > drawn || (checkable > 0 && /<Check\b/.test(src));
})
.map(({ f }) => f);
expect(offenders).toEqual([]);
});
});