// Keyboard focus helpers shared by `Popover` and the table's filter popovers, // which both render into a portal at the end of ``. Tab from the trigger // would otherwise skip past the box, and closing it would leave focus on // nothing. /** What Tab can reach. */ export const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; /** 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, active: Element | null, ): void { if (!box || box.contains(active)) return; (box.querySelector(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 * focused. */ 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, * else the first focusable control inside it. */ export function triggerOf(anchor: HTMLElement | null): HTMLElement | null { if (!anchor) return null; return anchor.matches(FOCUSABLE) ? anchor : anchor.querySelector(FOCUSABLE); }