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
+9 -1
View File
@@ -3,6 +3,7 @@ import type { ButtonHTMLAttributes, ReactNode } from "react";
import clsx from "clsx";
import { X } from "lucide-react";
import { FocusSafeButton } from "./FocusSafeButton";
import { focusNeighbour } from "../../lib/useFocusKeeper";
import { chipClass, type ChipSize, type ChipVariant } from "./chipStyles";
// The chip family (docs/DESIGN.md "One primitive per button family"): a small rounded-full token that
@@ -113,7 +114,14 @@ export function DismissibleChip({
)}
<FocusSafeButton
type="button"
onClick={onDismiss}
onClick={(e) => {
// From the keyboard the × holds the focus, and the chip unmounts: hand the focus to the
// next chip (else the previous one, else what follows the row) first, not to <body> (E28
// review F5). A mouse press on it leaves the focus where it was.
const chip = e.currentTarget.parentElement;
if (chip && e.currentTarget === document.activeElement) focusNeighbour(chip);
onDismiss();
}}
aria-label={dismissLabel}
title={dismissLabel}
className="-my-px grid size-6 shrink-0 place-items-center rounded-full transition state-layer touch-target"