fix(keys): a key a focused widget already handled fires no shortcut

The player's settings tabs switched tab on an arrow and prevented its default, yet the player's
window listener still sought the video 10 s. dispatch() now skips a defaultPrevented key for
every scope; listNav's private copy of the same guard goes.
This commit is contained in:
2026-09-27 17:28:10 +02:00
parent 8e09dd85f8
commit 2525a12264
3 changed files with 22 additions and 5 deletions
+5 -1
View File
@@ -322,7 +322,7 @@ const stepsOf = (chord: string): string[] => chord.split(" ");
* whether to `preventDefault`, because only it knows if it actually acted) or a sequence's first
* step was taken. `handlers` is a mapped type over the scope's ids, so the table and the
* implementation cannot drift without tsc saying so. Skipped while typing unless the row opts in
* with `allowInInput` (Escape, and only Escape), and — for a page-level scope — while a layer that
* with `allowInInput` (Escape, and only Escape), on a key a focused widget already handled, and — for a page-level scope — while a layer that
* owns the keyboard is open. The rule lives here rather than in each listener so a new page-level
* surface cannot forget it. */
export function dispatch<S extends KeyScope>(
@@ -331,6 +331,10 @@ export function dispatch<S extends KeyScope>(
handlers: { readonly [K in ShortcutId<S>]: (e: KeyboardEvent) => void },
): boolean {
if (sequenceEvents.has(e)) return false;
// A key a focused widget already handled is not a shortcut: every listener here is on `window`,
// so a tablist or a keyboard drag (dnd-kit, on the document) has acted on it and prevented its
// default first. The player's settings tabs switched tab AND sought the video 10 s (E28 UAT).
if (e.defaultPrevented) return false;
// An Escape a layer already took (a menu that just closed) is spent: a page or a player reacting to
// it too is the double close the layer stack exists to prevent.
if (e.key === "Escape" && escapeHandled(e)) return false;