fix(shortcuts): bare-key shortcuts never fire while a dialog is open
Single-key shortcuts (1/2/3 device switch) are skipped while any editor dialog or modal is open (role=dialog/alertdialog, aria-modal, dialog[open], .modal-backdrop) or focus is inside one. Dialog-like page content inside the canvas frame does not count. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aae94b1da2
commit
25de75c34f
2 files changed
+62
No files matched your search
@@ -171,6 +171,22 @@ function focusOutsideCanvas(el: Element | null): boolean {
|
||||
return !!el.closest('button, a[href], summary, [role="button"], [role="menuitem"], [role="tab"]');
|
||||
}
|
||||
|
||||
/** Anything that is a dialog or modal in the editor chrome. Matches inside the
|
||||
* canvas frame are page content (an HtmlBlock, a Gallery lightbox), not UI. */
|
||||
const DIALOG_SELECTOR = '[role="dialog"], [role="alertdialog"], [aria-modal="true"], dialog[open], .modal-backdrop';
|
||||
const isChromeDialog = (d: Element) => !d.closest('.canvas-device-frame');
|
||||
|
||||
/** A dialog/modal is open (modal or not), or focus is inside one. */
|
||||
export function editorDialogOpen(focused: Element | null = document.activeElement): boolean {
|
||||
const inside = focused && focused !== document.body ? focused.closest(DIALOG_SELECTOR) : null;
|
||||
if (inside && isChromeDialog(inside)) return true;
|
||||
return Array.from(document.querySelectorAll(DIALOG_SELECTOR)).some(isChromeDialog);
|
||||
}
|
||||
|
||||
/** Single printable key with no modifier ('1', '2', '3'): these belong to the
|
||||
* canvas only, never to an open dialog (#39 follow-up). */
|
||||
const isBareCharShortcut = (spec: string) => spec.length === 1;
|
||||
|
||||
function shortcutsOf(cmd: Command): string[] {
|
||||
return cmd.shortcut ? [cmd.shortcut, ...(cmd.shortcuts ?? [])] : [];
|
||||
}
|
||||
@@ -193,6 +209,7 @@ export function useCommandShortcuts(): void {
|
||||
const hit = shortcutsOf(cmd).find((spec) => matchShortcut(e, spec));
|
||||
if (!hit) continue;
|
||||
if ((hit === 'tab' || hit === 'shift+tab' || hit === 'escape') && focusOutsideCanvas(document.activeElement)) continue;
|
||||
if (isBareCharShortcut(hit) && editorDialogOpen(document.activeElement)) continue;
|
||||
if (typing && !cmd.allowWhileTyping) continue;
|
||||
if (!isEnabled(cmd, c)) continue;
|
||||
e.preventDefault();
|
||||
|
||||
@@ -134,6 +134,51 @@ describe('device keys 1/2/3', () => {
|
||||
key('3', mod);
|
||||
expect(device).toBe('desktop');
|
||||
});
|
||||
|
||||
// #39 follow-up: bare keys never fire while any dialog/modal is open.
|
||||
test.each([
|
||||
['an aria-modal dialog', { role: 'dialog', 'aria-modal': 'true' }],
|
||||
['a non-modal role=dialog (e.g. a popover)', { role: 'dialog' }],
|
||||
['an alertdialog', { role: 'alertdialog' }],
|
||||
['a Modal backdrop without a role', { class: 'modal-backdrop' }],
|
||||
])('are ignored while %s is open, even with focus on the body', (_label, attrs) => {
|
||||
const d = document.createElement('div');
|
||||
for (const [k, v] of Object.entries(attrs)) d.setAttribute(k, v);
|
||||
document.body.appendChild(d);
|
||||
try {
|
||||
key('3');
|
||||
expect(device).toBe('desktop');
|
||||
} finally { d.remove(); }
|
||||
key('3');
|
||||
expect(device).toBe('mobile');
|
||||
});
|
||||
|
||||
test('are ignored while focus is inside a dialog', () => {
|
||||
const d = document.createElement('div');
|
||||
d.setAttribute('role', 'dialog');
|
||||
const btn = document.createElement('button');
|
||||
d.appendChild(btn);
|
||||
document.body.appendChild(d);
|
||||
btn.focus();
|
||||
try {
|
||||
key('2');
|
||||
expect(device).toBe('desktop');
|
||||
} finally { d.remove(); }
|
||||
});
|
||||
|
||||
test('a role=dialog inside the canvas frame (page content) does not block them', () => {
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'canvas-device-frame';
|
||||
const d = document.createElement('div');
|
||||
d.setAttribute('role', 'dialog');
|
||||
d.setAttribute('aria-modal', 'true');
|
||||
frame.appendChild(d);
|
||||
document.body.appendChild(frame);
|
||||
try {
|
||||
key('2');
|
||||
expect(device).toBe('tablet');
|
||||
} finally { frame.remove(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe('site commands (mod+s)', () => {
|
||||
|
||||
Reference in new issue
Block a user