fix(ui): a picker whose list failed says so, and takes no pick while one is being written

- Combobox gains `error` (said in the list, not "No matches") and a soft `disabled` (keeps the
  focus, announced, opens and changes nothing).
- UserPicker passes both: the share dialog's recipient picker is off while a share is pending, and
  the share and quota pickers say when the user list failed to load.
- ChannelPicker and the Plex playlist/collection pickers say a failed load too.
- The channel menu's Tags row says loading / failed instead of "no tags yet".
This commit is contained in:
2026-09-27 14:47:46 +02:00
parent 70f98b352f
commit 17bb14fb18
10 changed files with 102 additions and 6 deletions
@@ -276,6 +276,33 @@ describe("Combobox", () => {
// PageUp/PageDown (E11, owner request): the sidebar's channel filter and the Plex pickers run to
// hundreds of rows, and floating-ui's list navigation has only the arrows. jsdom has no layout, so a
// page is the fixed fallback of 10 rows; the measured page is pinned in lib/listPaging.
// E28 review J1 + J2: a failed list said "No matches" (an error dressed as an empty result), and a
// picker whose pick was still being written took another pick.
describe("Combobox · honest states", () => {
it("says the list failed to load, not that nothing matched", () => {
wrap(
<Combobox label="User" options={[]} selected={[]} onChange={vi.fn()} error="Load failed" />,
);
fireEvent.focus(screen.getByRole("combobox"));
expect(screen.getByText("Load failed")).toBeTruthy();
expect(screen.queryByText("common.noResults")).toBeNull();
});
it("disabled: keeps the focus, opens nothing and picks nothing", () => {
const onChange = vi.fn();
wrap(<Combobox label="Genre" options={OPTIONS} selected={[]} onChange={onChange} disabled />);
const field = screen.getByRole("combobox");
field.focus();
fireEvent.focus(field);
expect(screen.queryByRole("option", { hidden: true })).toBeNull();
fireEvent.keyDown(field, { key: "ArrowDown" });
fireEvent.keyDown(field, { key: "Enter" });
expect(onChange).not.toHaveBeenCalled();
expect(field.getAttribute("aria-disabled")).toBe("true");
expect(document.activeElement).toBe(field); // not natively disabled: the focus stays put
});
});
describe("Combobox · paging", () => {
const rows = (disabled: number[] = []) =>
Array.from({ length: 25 }, (_, i) => ({ value: `Row ${i}`, disabled: disabled.includes(i) }));