diff --git a/web/src/hooks/use-keyboard-listener.tsx b/web/src/hooks/use-keyboard-listener.tsx index d887a2bd96..c12c360743 100644 --- a/web/src/hooks/use-keyboard-listener.tsx +++ b/web/src/hooks/use-keyboard-listener.tsx @@ -7,6 +7,16 @@ export type KeyModifiers = { shift: boolean; }; +const handledByShortcut = new WeakSet(); + +// Radix dismisses a dialog or menu on Escape from a capture-phase listener and +// calls preventDefault() without stopping propagation, so a page shortcut would +// otherwise act on the same press. Keys another shortcut hook handled still get +// through, since their listener order changes with every render. +function handledElsewhere(event: KeyboardEvent): boolean { + return event.defaultPrevented && !handledByShortcut.has(event); +} + export default function useKeyboardListener( keys: string[], listener?: (key: string | null, modifiers: KeyModifiers) => boolean, @@ -27,6 +37,10 @@ export default function useKeyboardListener( return; } + if (handledElsewhere(e)) { + return; + } + const modifiers = { down: true, repeat: e.repeat, @@ -63,7 +77,10 @@ export default function useKeyboardListener( } } else if (keys.includes(e.key) && listener) { const preventDefault = listener(e.key, modifiers); - if (preventDefault) e.preventDefault(); + if (preventDefault) { + e.preventDefault(); + handledByShortcut.add(e); + } } else if ( listener && (e.key === "Shift" || e.key === "Control" || e.key === "Meta")