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
+10 -5
View File
@@ -42,11 +42,13 @@ export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; thr
setShow(!!el && el.scrollTop > threshold);
}, [dep, threshold]);
const toTop = () =>
(scrollerRef.current ?? document.querySelector("main"))?.scrollTo({
top: 0,
behavior: "smooth",
});
// A keyboard press hides the button it was on: the focus goes to the page it scrolled (its <main>
// is a focus rest, not a Tab stop), not left on an invisible control (E28 review F7).
const toTop = (e: React.MouseEvent) => {
const main = scrollerRef.current ?? document.querySelector("main");
main?.scrollTo({ top: 0, behavior: "smooth" });
if (e.detail === 0) main?.focus({ preventScroll: true });
};
// No aria-label/title attributes: ad-block "annoyance" cosmetic filters (e.g. Brave Shields)
// commonly hide floating corner buttons via attribute selectors like [aria-label="Back to top"].
@@ -57,6 +59,9 @@ export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; thr
<Pressable
align="center"
onClick={toTop}
// Hidden, it only goes transparent (so it can fade): out of the Tab order as well.
tabIndex={show ? undefined : -1}
aria-hidden={show ? undefined : true}
className={`fixed bottom-5 right-5 z-chrome inline-flex items-center justify-center size-12 ${SURFACE.floating} glass-hover text-fg hover:text-accent transition-all duration-200 ${
show ? "opacity-100 translate-y-0" : "opacity-0 translate-y-3 pointer-events-none"
}`}