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 { Check, Pencil, Pin, PinOff, RotateCcw } from "lucide-react";
|
||||
import { useScrollFade } from "../../lib/useScrollFade";
|
||||
@@ -65,6 +65,7 @@ export default function SidePanel({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const cardRef = useRef<HTMLElement>(null);
|
||||
const tabRef = useRef<HTMLButtonElement>(null);
|
||||
const fade = useScrollFade();
|
||||
const peek = useHoverFocusWithin();
|
||||
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 —
|
||||
// adjusted during render, so it cannot come back open on the next turn.
|
||||
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 ? (
|
||||
<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 —
|
||||
// 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(),
|
||||
);
|
||||
}}
|
||||
ref={tabRef}
|
||||
// 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.
|
||||
tabIndex={expanded ? -1 : undefined}
|
||||
|
||||
Reference in New Issue
Block a user