fix(ui): dialogs, menus and busy buttons keep the keyboard focus where it belongs
Measured in Chrome before each change: - A dialog opened from a menu lost its focus: the menu row ran its action and closed in one batch, and floating-ui's focus return (a microtask) moved the focus to the row BEHIND the confirm dialog's scrim 7 ms after it opened, where Enter activated the row. MenuItemList now closes first and runs the action a task later; the dialog also captures the row, not a dying menu item, as its restore target. - A side panel opened as a flyout collapsed the moment a dialog opened from inside it, unmounting the row the dialog hands the focus back to. useHoverFocusWithin holds while the focus is inside an aria-modal dialog. - ui/Button's loading state set native disabled; Chrome moved the focus of the pressed button to <body> and never gave it back. Loading is now aria-disabled with a click guard that also cancels implicit submission. - Escape/Enter in the playlist rail's rename field dropped the focus on <body>. The row refocuses from its ref callback when it re-attaches (a post-commit focus sticks; the earlier attempt ran before the button existed). - PlayerModal was the one aria-modal shell without the dialog contract: it is now named by its title, traps Tab, restores the focus to its opener and shares one counted body scroll lock with every dialog.
This commit is contained in:
@@ -83,8 +83,15 @@ export function MenuItemList({
|
||||
label={it.label}
|
||||
title={it.title}
|
||||
onSelect={() => {
|
||||
it.onSelect();
|
||||
// Close FIRST, act once the menu has handed the focus back. floating-ui returns the
|
||||
// focus to the trigger from a microtask queued as the menu unmounts, so an action run
|
||||
// in the same batch had its focus taken back by it: measured in Chrome (E11 S2), a
|
||||
// confirm dialog opened from the rail's menu landed its focus on Cancel and lost it
|
||||
// 7 ms later to the row BEHIND its scrim, where Enter activated the row. A fresh task
|
||||
// runs after that microtask — and a dialog opened there captures the trigger, not a
|
||||
// dying menu item, as the element to restore the focus to.
|
||||
onClose();
|
||||
setTimeout(it.onSelect, 0);
|
||||
}}
|
||||
className={`${ROW}${it.danger ? " text-danger" : ""}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user