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
@@ -19,7 +19,13 @@ const h = vi.hoisted(() => ({
vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
return {
...actual,
useTranslation: () => ({
t: (key: string, o?: { name?: string }) =>
key === "common.order.dragNamed" ? `${key}:${o?.name}` : key,
}),
};
});
// A full feed filter set that differs from the sidebar default ONLY in `tags`, so useActiveFilters
@@ -225,7 +231,7 @@ describe("PersonalViewsModal — reordering without a drag (WCAG 2.5.7)", () =>
it("picks a view up from the keyboard, as its grip's instructions say", async () => {
CTX.views = [view(5, "Docs", 0), view(6, "Music", 1)];
mount();
const grip = (await screen.findAllByRole("button", { name: "views.reorder" }))[0]!;
const grip = await screen.findByRole("button", { name: "common.order.dragNamed:Docs" });
grip.focus();
fireEvent.keyDown(grip, { code: "Space", key: " " });
await waitFor(() => expect(document.body.textContent).toContain("common.dnd.pickedUp"));