diff --git a/docs/bridge-protocol.md b/docs/bridge-protocol.md index 904b5af4..bf7e30c5 100644 --- a/docs/bridge-protocol.md +++ b/docs/bridge-protocol.md @@ -145,6 +145,8 @@ A keydown inside an iframe never reaches the parent document, and every admin wi | `os-window-switch` | iframe → parent | `{ direction: 'prev' \| 'next' }` | `` ` `` / `` Shift+` ``. Cycles window focus on the active desktop. | | `os-window-close-all` | iframe → parent | *(none)* | `⌥⌘W` / `Ctrl+Alt+W`. Runs the shell's confirm-then-`closeAll()` path (see [`javascript-reference.md`](./javascript-reference.md#batch-close--closeall)). | +The Cmd/Ctrl+K forwarder yields to a keydown that arrives already `defaultPrevented`: that is the block editor's canvas re-dispatching a chord its rich-text Add Link shortcut has already handled, and Core's own palette bails on the same flag. + The backtick forwarder — and only that one — applies a **text-entry gate** before forwarding: a bare key with no modifier has to yield to whatever the user is typing into, including the `IFRAME` case that catches Gutenberg's block canvas re-dispatching cloned keydowns. The other two carry modifiers that type nothing, so they forward from anywhere. ### Background heartbeat throttle — `os-window-active` diff --git a/src/chromeless-bridge.js b/src/chromeless-bridge.js index ff3c1484..eb67977b 100644 --- a/src/chromeless-bridge.js +++ b/src/chromeless-bridge.js @@ -2202,11 +2202,20 @@ * against Gutenberg / TinyMCE / plugin handlers bound to the same * shortcut. Shift/Alt modifiers pass through so user shortcuts using * those combos keep working. + * + * A keydown that arrives already `defaultPrevented` was claimed by + * the editor before it reached this document: Gutenberg's block + * canvas is a nested iframe, its rich-text Add Link shortcut handles + * Cmd+K in there, and the canvas re-dispatches the cloned keydown up + * to this document with the flag set. Core's own palette bails on + * that flag, so we do too — otherwise Cmd+K in a block opens the + * palette instead of the link popover. */ document.addEventListener( 'keydown', function ( e ) { if ( ! ( e.metaKey || e.ctrlKey ) ) return; if ( e.key !== 'k' && e.key !== 'K' ) return; if ( e.shiftKey || e.altKey ) return; + if ( e.defaultPrevented ) return; e.preventDefault(); e.stopImmediatePropagation(); diff --git a/tests/vitest/chromeless-bridge-links.test.ts b/tests/vitest/chromeless-bridge-links.test.ts index 4bcd25f3..60f009c2 100644 --- a/tests/vitest/chromeless-bridge-links.test.ts +++ b/tests/vitest/chromeless-bridge-links.test.ts @@ -23,7 +23,8 @@ * the parent can no longer add on our behalf. * * The page is a list screen (Media), so the same harness also checks - * that a content-change broadcast refreshes it. + * that a content-change broadcast refreshes it, and that Cmd+K + * yields to a block editor that has already claimed it. * * @vitest-environment-options { "url": "http://localhost/wp-admin/upload.php?openstation_chromeless=1" } */ @@ -574,3 +575,36 @@ describe( 'chromeless bridge: a content-change broadcast', () => { } } ); } ); + +describe( 'chromeless bridge: the Cmd+K forwarder', () => { + /** Presses Cmd+K on the iframe Gutenberg re-dispatches keydowns from. */ + function pressCmdK( handledInCanvas: boolean ): void { + document.body.innerHTML = ''; + const event = new KeyboardEvent( 'keydown', { + key: 'k', + metaKey: true, + bubbles: true, + cancelable: true, + } ); + // The canvas clones a keydown its own handlers already claimed + // with the flag set — the rich-text Add Link shortcut does. + if ( handledInCanvas ) { + event.preventDefault(); + } + document.querySelector( 'iframe' )?.dispatchEvent( event ); + } + + function paletteMessages(): Array< Record< string, unknown > > { + return posted.filter( ( m ) => m.type === 'os-palette-cycle' ); + } + + test( 'opens the shell palette when nothing in the page claimed it', () => { + pressCmdK( false ); + expect( paletteMessages() ).toHaveLength( 1 ); + } ); + + test( 'yields to the editor that already claimed it for Add Link', () => { + pressCmdK( true ); + expect( paletteMessages() ).toHaveLength( 0 ); + } ); +} );