fix(a11y): chips, grips, the panel tab and Back to top keep the focus somewhere real

- F5: a chip's × pressed from the keyboard hands the focus to the next chip (else the previous one,
  else what follows) before it unmounts — in DismissibleChip, so all five chip rows get it
  (`focusNeighbour` in lib/useFocusKeeper).
- F4: a drag grip names what it lifts (`common.order.dragNamed`: panel groups, personal views);
  the two generic "Drag to reorder" keys are gone.
- F6: the side panel's edge tab is out of the Tab order while the card covers it, names its panel,
  and a keyboard pin moves the focus into the card.
- F7: Back to top is no Tab stop while hidden, and a keyboard press leaves the focus on the page.
This commit is contained in:
2026-09-27 15:01:22 +02:00
parent 0b1d905866
commit 5ebb4cb506
17 changed files with 173 additions and 23 deletions
@@ -78,12 +78,31 @@ describe("SidePanel · a mouse on a wide window", () => {
it("pins from the tab, as before", () => {
stubMedia({ coarse: false, narrow: false, wide: true });
const { onToggleCollapse } = mount();
fireEvent.click(screen.getByRole("button", { name: "sidebar.pin" }));
fireEvent.click(screen.getByRole("button", { name: "sidebar.pinNamed:Filters" }));
expect(onToggleCollapse).toHaveBeenCalledOnce();
expect(screen.queryByRole("dialog")).toBeNull();
});
});
// E28 review F6: the pinned (or peeking) card covers the tab, which only goes transparent — yet it
// stayed a Tab stop, an invisible control the focus landed on, named just "Unpin panel".
describe("SidePanel · the tab under an open card", () => {
it("is out of the Tab order while the card is open, and names its panel", () => {
stubMedia({ coarse: false, narrow: false, wide: true });
globalThis.ResizeObserver ??= class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
mount(false); // pinned
const tab = screen.getByRole("button", { name: "sidebar.unpinNamed:Filters", hidden: true });
expect(tab.tabIndex).toBe(-1);
cleanup();
mount(true);
expect(screen.getByRole("button", { name: "sidebar.pinNamed:Filters" }).tabIndex).toBe(0);
});
});
describe("SidePanel · on touch", () => {
it("opens a drawer from the tab instead of pinning, and Esc closes it", () => {
stubMedia({ coarse: true, narrow: false, wide: false });