swarm-ui: make popovers and table filters usable from the keyboard
Opening a popover or a table filter now moves focus into it, closing one that left focus on nothing puts it back on the trigger, and the triggers carry aria-haspopup and aria-controls. The shared Popover covers every caller; the table filter popover, which has its own portal, does the same.
This commit is contained in:
parent
bf9d07aedc
commit
d4f58b3667
1 changed files with 109 additions and 6 deletions
34
frontend/packages/swarm-ui/src/ui/popover/focus.ts
Normal file
34
frontend/packages/swarm-ui/src/ui/popover/focus.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Keyboard focus helpers shared by `Popover` and the table's filter popovers,
|
||||
// which both render into a portal at the end of `<body>`. 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): void {
|
||||
if (!box || box.contains(document.activeElement)) 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
|
||||
* focused. */
|
||||
export function restoreFocus(trigger: HTMLElement | null): void {
|
||||
const active = document.activeElement;
|
||||
if (!active || active === document.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<HTMLElement>(FOCUSABLE);
|
||||
}
|
||||
Loading…
Reference in a new issue