Peter Heck
Can confirm; I have access now; as expected, the h2 is inside an iframe, but here it gets a bit weird.
I'm writing this overly detailed deliberately, in case you want to share with the Etch team also, as I think CommandUI needs some additional hardening against such edge cases, but the bug I think lies in Etch.
CommandUI runs inside the root HTML document. Etch also runs in the root and then loads its iframe;
So the hierarchy is roughly
| - CommandUI
| - Etch
| -- Etch Iframe
For each keyboard event that CUI receives, it runs extensive checks to see if it should ignore the event;
The core code is roughly the following, with some omissions for clarity:
export function eventIsFromInputLike(event: KeyboardEvent): boolean {
let source = event.target as HTMLElement;
if (source instanceof HTMLIFrameElement) {
const iframeDocument = source?.contentWindow?.document;
if (!iframeDocument) {
// Probably a cross-origin iframe - assume it's an input
// Cross-origin iframes do not allow direct access to their document due to security restrictions.
// As a result, it's safest to assume that any event coming from a cross-origin iframe is input-like,
// since we cannot determine its exact source. This assumption helps prevent ignoring important user interactions.
return true;
}
const activeElement = iframeDocument.activeElement;
const defaultView = iframeDocument.defaultView;
if (
activeElement &&
defaultView &&
activeElement instanceof defaultView.HTMLElement
) {
source = activeElement as HTMLElement;
}
}
// Finally, check if the target is an input-like element
return source instanceof HTMLInputElement ||
source instanceof HTMLTextAreaElement ||
source.isContentEditable;
}
I added a debug key handler in the Chrome console, attached to the window and the Etch iframe. The top-level handler always sees an event whose target is not an element; it has no tagName (the "—" in the log below), which is consistent with the event being dispatched on window rather than on a DOM element.
[IFRAME] keydown "e" | trusted:true | target:H2 | targetCE:true | deepFocus:H2(CE:true)
[TOP-win] keydown "e" | trusted:false | target:— | targetCE:— | deepFocus:H2(CE:true)
[IFRAME] keyup "e" | trusted:true | target:H2 | targetCE:true | deepFocus:H2(CE:true)
[TOP-win] keyup "e" | trusted:false | target:— | targetCE:— | deepFocus:H2(CE:true)
My best guess is that the canvas re-dispatches synthetic keydown/keyup to the parent window with isTrusted:false and target:null, so no parent-side event handler can attribute them.
I think the correct fix lies in Etch: ideally, don't forward these events to the parent at all. Native key events are already scoped to the iframe, and the canvas's own handling can live there. If forwarding is needed for some reason, then dispatch the event on the focused element (not window) with bubbles: true and composed: true, so that target and composedPath() resolve correctly and any parent-side handler can attribute the event.
Used code:
/* EtchKeyProbe — paste in browser console. Edit a heading block, press keys.
* Each physical keypress should show ONE iframe row (trusted, target=H2) and,
* if Etch forwards, a TOP row that's trusted:false / target:null.
* deepFocus = where focus actually is (works even when the event's target is junk).
* Run __stopProbe() to remove. */
(() => {
window.__stopProbe?.();
// Walk focus through same-origin iframes + open shadow roots → the real focused el.
const deepActive = (d = document) => {
let el = d.activeElement;
try {
while (el instanceof HTMLIFrameElement && el.contentDocument)
el = el.contentDocument.activeElement;
while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
} catch {}
return el;
};
const log = (where) => (e) => {
const f = deepActive();
console.log(
`[${where}] ${e.type} "${e.key}"`,
`| trusted:${e.isTrusted}`,
`| target:${e.target?.tagName ?? '—'}`,
`| targetCE:${e.target?.isContentEditable ?? '—'}`,
`| deepFocus:${f?.tagName ?? '—'}(CE:${f?.isContentEditable ?? '—'})`
);
};
const fns = [];
const on = (tgt, where) => {
if (!tgt) return;
['keydown', 'keyup'].forEach((t) => {
const fn = log(where);
tgt.addEventListener(t, fn, true); // capture: see it before app handlers
fns.push(() => tgt.removeEventListener(t, fn, true));
});
console.log('attached:', where);
};
on(document, 'TOP-doc');
on(window, 'TOP-win');
let idoc; try { idoc = document.querySelector('#etch-iframe')?.contentWindow?.document; } catch {}
idoc ? on(idoc, 'IFRAME') : console.warn('iframe not reachable');
window.__stopProbe = () => { fns.forEach((f) => f()); delete window.__stopProbe; console.log('stopped'); };
console.log('probe running — press keys in the canvas. __stopProbe() to stop.');
})();
I'll think about some ways to patch around this and, as a good fallback, allow disabling the global shortcuts more granularly, so that you could disable them only inside Etch, probably by URL;