Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: unit test the popover focus helpers

The helpers take the active element and body as arguments, so the cases
(focus into the box, keep a control that took focus, restore only when
focus was lost, find the trigger) run under node's test runner.
This commit is contained in:
iris 2026-10-21 22:44:44 +02:00 • committed by mara
commit 9338496e24
4 changed files with 107 additions and 10 deletions

View file

@ -69,14 +69,18 @@ export function Popover({
// to the trigger after a close that left focus on nothing.
const placed = pos !== null;
useEffect(() => {
if (open && placed) focusInto(ref.current);
if (open && placed) focusInto(ref.current, document.activeElement);
}, [open, placed]);
useEffect(() => {
if (open) {
wasOpen.current = true;
} else if (wasOpen.current) {
wasOpen.current = false;
restoreFocus(triggerOf(anchorRef.current));
restoreFocus(
triggerOf(anchorRef.current),
document.activeElement,
document.body,
);
}
}, [open, anchorRef]);

View file

@ -10,18 +10,24 @@ export const FOCUSABLE =
/** Move focus into `box`, unless it is already there: onto its first
* focusable control, or onto the box itself when it has none. A control that
* took focus on its own (a text field) keeps it. */
export function focusInto(box: HTMLElement | null): void {
if (!box || box.contains(document.activeElement)) return;
export function focusInto(
box: HTMLElement | null,
active: Element | null,
): void {
if (!box || box.contains(active)) return;
(box.querySelector<HTMLElement>(FOCUSABLE) ?? box).focus();
}
/** After a close, put focus back on `trigger`, but only when the close left
* it on nothing: removing the focused control from the page leaves focus on
* `<body>`. A close from a click on another control leaves that control
* `body`. A close from a click on another control leaves that control
* focused. */
export function restoreFocus(trigger: HTMLElement | null): void {
const active = document.activeElement;
if (!active || active === document.body) trigger?.focus();
export function restoreFocus(
trigger: HTMLElement | null,
active: Element | null,
body: Element,
): void {
if (!active || active === body) trigger?.focus();
}
/** The control that opens a popover: the anchor itself when Tab reaches it,

View file

@ -415,7 +415,7 @@ export function Table<T>({
useEffect(() => {
if (openFilterKey === null) return;
popoverControlRef.current?.focus();
focusInto(popoverRef.current);
focusInto(popoverRef.current, document.activeElement);
}, [openFilterKey]);
// After a close that left focus on nothing (Escape, or the control that
@ -426,7 +426,11 @@ export function Table<T>({
if (openFilterKey !== null) {
lastOpenKey.current = openFilterKey;
} else if (lastOpenKey.current !== null) {
restoreFocus(filterBtnRefs.current.get(lastOpenKey.current) ?? null);
restoreFocus(
filterBtnRefs.current.get(lastOpenKey.current) ?? null,
document.activeElement,
document.body,
);
lastOpenKey.current = null;
}
}, [openFilterKey]);