fix(side-panel): the key's focus steps into the peeked card in the commit that opens it
A frame later never came in a page the browser was not painting (measured: a hidden page runs no requestAnimationFrame), and the focus stayed on the inert tab under the card.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, type ReactNode } from "react";
|
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Check, Pencil, Pin, PinOff, RotateCcw } from "lucide-react";
|
import { Check, Pencil, Pin, PinOff, RotateCcw } from "lucide-react";
|
||||||
import { useScrollFade } from "../../lib/useScrollFade";
|
import { useScrollFade } from "../../lib/useScrollFade";
|
||||||
@@ -65,6 +65,7 @@ export default function SidePanel({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const cardRef = useRef<HTMLElement>(null);
|
const cardRef = useRef<HTMLElement>(null);
|
||||||
|
const tabRef = useRef<HTMLButtonElement>(null);
|
||||||
const fade = useScrollFade();
|
const fade = useScrollFade();
|
||||||
const peek = useHoverFocusWithin();
|
const peek = useHoverFocusWithin();
|
||||||
const drawerMode = useMediaQuery(DRAWER_QUERY, false);
|
const drawerMode = useMediaQuery(DRAWER_QUERY, false);
|
||||||
@@ -77,6 +78,16 @@ export default function SidePanel({
|
|||||||
// Turning the tablet to a width where the panel is a card again, or pinning it, ends the drawer —
|
// Turning the tablet to a width where the panel is a card again, or pinning it, ends the drawer —
|
||||||
// adjusted during render, so it cannot come back open on the next turn.
|
// adjusted during render, so it cannot come back open on the next turn.
|
||||||
if (drawerOpen && (!drawerMode || pinned)) setDrawerOpen(false);
|
if (drawerOpen && (!drawerMode || pinned)) setDrawerOpen(false);
|
||||||
|
// A key landing on the tab peeks the card open over it, and the tab then goes inert below — with
|
||||||
|
// the focus still on it: a hidden control, out of the tree, that Enter still pinned (round 2, F1).
|
||||||
|
// The focus steps into the card as it opens, onto its pin: the same control. In the commit that
|
||||||
|
// mounts the card, not a frame later — a frame never comes in a page the browser is not painting.
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const tab = tabRef.current;
|
||||||
|
if (!expanded || pinned || drawerMode || !tab || document.activeElement !== tab) return;
|
||||||
|
if (tab.matches(":focus-visible"))
|
||||||
|
cardRef.current?.querySelector<HTMLElement>("button")?.focus();
|
||||||
|
}, [expanded, pinned, drawerMode]);
|
||||||
|
|
||||||
const titleNode = onTitleClick ? (
|
const titleNode = onTitleClick ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -160,15 +171,7 @@ export default function SidePanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A key landing on the tab peeks the card open over it, and the tab then goes inert below —
|
ref={tabRef}
|
||||||
// with the focus still on it: a hidden control, out of the tree, that Enter still pinned
|
|
||||||
// (round 2, F1). The focus steps into the card instead, onto its pin: the same control.
|
|
||||||
onFocus={(e) => {
|
|
||||||
if (drawerMode || pinned || !e.currentTarget.matches(":focus-visible")) return;
|
|
||||||
requestAnimationFrame(() =>
|
|
||||||
cardRef.current?.querySelector<HTMLElement>("button")?.focus(),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
// Under an open card the tab is transparent and inert: out of the Tab order AND of the
|
// Under an open card the tab is transparent and inert: out of the Tab order AND of the
|
||||||
// accessibility tree, where it would repeat the card's own pin button by name.
|
// accessibility tree, where it would repeat the card's own pin button by name.
|
||||||
tabIndex={expanded ? -1 : undefined}
|
tabIndex={expanded ? -1 : undefined}
|
||||||
|
|||||||
Reference in New Issue
Block a user