mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): allow Comment/Inspect picker to select iframe-backed components (#2254)
This commit is contained in:
parent
7fc4362ba8
commit
716f06cb73
3 changed files with 84 additions and 4 deletions
|
|
@ -1261,6 +1261,11 @@ function meaningfulDomFallbackTarget(el) {
|
||||||
html[data-od-comment-mode] body * { cursor: crosshair !important; }
|
html[data-od-comment-mode] body * { cursor: crosshair !important; }
|
||||||
html[data-od-inspect-mode] body * { cursor: crosshair !important; }
|
html[data-od-inspect-mode] body * { cursor: crosshair !important; }
|
||||||
html[data-od-comment-mode][data-od-comment-mode-kind="pod"] body * { cursor: cell !important; }
|
html[data-od-comment-mode][data-od-comment-mode-kind="pod"] body * { cursor: cell !important; }
|
||||||
|
/* Nested iframes (e.g. shared device frames) consume clicks in their own browsing context.
|
||||||
|
While picker modes are on, disable pointer events on outer-document iframes so the
|
||||||
|
hit target resolves to an annotated ancestor (card, shell) in this document. */
|
||||||
|
html[data-od-comment-mode] body iframe,
|
||||||
|
html[data-od-inspect-mode] body iframe { pointer-events: none !important; }
|
||||||
</style>`;
|
</style>`;
|
||||||
return injectBeforeBodyEnd(injectBeforeHeadEnd(doc, style), script);
|
return injectBeforeBodyEnd(injectBeforeHeadEnd(doc, style), script);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ function extractBridgeScript(srcdoc: string): string {
|
||||||
return match[1];
|
return match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractSelectionBridgeStyle(srcdoc: string): string {
|
||||||
|
const match = srcdoc.match(
|
||||||
|
/<style data-od-selection-bridge-style>[\s\S]*?<\/style>/,
|
||||||
|
);
|
||||||
|
return match?.[0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function markVisible(win: { document: Document }, selector: string): void {
|
function markVisible(win: { document: Document }, selector: string): void {
|
||||||
const el = win.document.querySelector(selector);
|
const el = win.document.querySelector(selector);
|
||||||
if (!el) throw new Error(`selector not found: ${selector}`);
|
if (!el) throw new Error(`selector not found: ${selector}`);
|
||||||
|
|
@ -67,14 +74,18 @@ function setupBridgeDom(
|
||||||
commentBridge: mode === 'comment',
|
commentBridge: mode === 'comment',
|
||||||
});
|
});
|
||||||
const script = extractBridgeScript(srcdoc);
|
const script = extractBridgeScript(srcdoc);
|
||||||
|
const bridgeStyle = extractSelectionBridgeStyle(srcdoc);
|
||||||
|
|
||||||
// Build a fresh JSDOM that mirrors the iframe runtime: `window.parent`
|
// Build a fresh JSDOM that mirrors the iframe runtime: `window.parent`
|
||||||
// is what the bridge calls into, and we spy on its postMessage to
|
// is what the bridge calls into, and we spy on its postMessage to
|
||||||
// capture the messages it would emit to the host.
|
// capture the messages it would emit to the host.
|
||||||
const dom = new JSDOM(`<!doctype html><html><body>${bodyHtml}</body></html>`, {
|
const dom = new JSDOM(
|
||||||
runScripts: 'outside-only',
|
`<!doctype html><html><head>${bridgeStyle}</head><body>${bodyHtml}</body></html>`,
|
||||||
pretendToBeVisual: true,
|
{
|
||||||
});
|
runScripts: 'outside-only',
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
const win = dom.window;
|
const win = dom.window;
|
||||||
const parentPostMessage = vi.fn();
|
const parentPostMessage = vi.fn();
|
||||||
// jsdom defaults `window.parent` to `window` itself for top-level
|
// jsdom defaults `window.parent` to `window` itself for top-level
|
||||||
|
|
@ -285,4 +296,64 @@ describe('selection bridge — empty annotation surface (#890)', () => {
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies pointer-events none to body iframes while Comment picker is active', async () => {
|
||||||
|
const { win } = setupBridgeDom(
|
||||||
|
'<article data-od-id="card"><div>Tablet edition</div><iframe id="f" src="about:blank"></iframe></article>',
|
||||||
|
'comment',
|
||||||
|
['#f'],
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => win.setTimeout(resolve, 10));
|
||||||
|
const iframe = win.document.getElementById('f');
|
||||||
|
expect(iframe).not.toBeNull();
|
||||||
|
expect(win.getComputedStyle(iframe!).pointerEvents).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies pointer-events none to body iframes while Inspect picker is active', async () => {
|
||||||
|
const { win } = setupBridgeDom(
|
||||||
|
'<article data-od-id="card"><iframe id="f" src="about:blank"></iframe></article>',
|
||||||
|
'inspect',
|
||||||
|
['#f'],
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => win.setTimeout(resolve, 10));
|
||||||
|
expect(win.getComputedStyle(win.document.getElementById('f')!).pointerEvents).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores iframe pointer events after Comment mode is disabled', async () => {
|
||||||
|
const { win } = setupBridgeDom(
|
||||||
|
'<article data-od-id="card"><iframe id="f" src="about:blank"></iframe></article>',
|
||||||
|
'comment',
|
||||||
|
['#f'],
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => win.setTimeout(resolve, 10));
|
||||||
|
const iframe = win.document.getElementById('f')!;
|
||||||
|
expect(win.getComputedStyle(iframe).pointerEvents).toBe('none');
|
||||||
|
|
||||||
|
win.dispatchEvent(
|
||||||
|
new win.MessageEvent('message', {
|
||||||
|
data: { type: 'od:comment-mode', enabled: false, mode: 'picker' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(win.getComputedStyle(iframe).pointerEvents).toBe('auto');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('posts od:comment-target for the annotated card when the device-frame iframe is clicked', async () => {
|
||||||
|
const { win, parentPostMessage } = setupBridgeDom(
|
||||||
|
'<article data-od-id="tablet-card" class="frame-card"><div class="meta">Tablet edition</div><iframe id="f" class="tablet-frame" title="Tablet edition" src="about:blank"></iframe></article>',
|
||||||
|
'comment',
|
||||||
|
['#f', '.frame-card'],
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => win.setTimeout(resolve, 10));
|
||||||
|
parentPostMessage.mockClear();
|
||||||
|
|
||||||
|
win.document.getElementById('f')!.dispatchEvent(
|
||||||
|
new win.MouseEvent('click', { bubbles: true, cancelable: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickMessages = parentPostMessage.mock.calls
|
||||||
|
.map((call) => call[0])
|
||||||
|
.filter((message) => message?.type === 'od:comment-target');
|
||||||
|
expect(clickMessages).toHaveLength(1);
|
||||||
|
expect(clickMessages[0].elementId).toBe('tablet-card');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ describe('buildSrcdoc', () => {
|
||||||
expect(srcdoc).toContain("type: 'od:preview-scroll'");
|
expect(srcdoc).toContain("type: 'od:preview-scroll'");
|
||||||
expect(srcdoc).toContain("type: 'od:preview-scroll-request'");
|
expect(srcdoc).toContain("type: 'od:preview-scroll-request'");
|
||||||
expect(srcdoc).toContain('data-od-selection-bridge-style');
|
expect(srcdoc).toContain('data-od-selection-bridge-style');
|
||||||
|
expect(srcdoc).toContain('html[data-od-comment-mode] body iframe');
|
||||||
|
expect(srcdoc).toContain('html[data-od-inspect-mode] body iframe');
|
||||||
|
expect(srcdoc).toContain('pointer-events: none !important');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits free-pin fallback coordinates in viewport space', () => {
|
it('emits free-pin fallback coordinates in viewport space', () => {
|
||||||
|
|
@ -230,6 +233,7 @@ describe('buildSrcdoc', () => {
|
||||||
expect(srcdoc).not.toContain('data-od-selection-bridge');
|
expect(srcdoc).not.toContain('data-od-selection-bridge');
|
||||||
expect(srcdoc).not.toContain("type: 'od:comment-target'");
|
expect(srcdoc).not.toContain("type: 'od:comment-target'");
|
||||||
expect(srcdoc).not.toContain("type: 'od:inspect-overrides'");
|
expect(srcdoc).not.toContain("type: 'od:inspect-overrides'");
|
||||||
|
expect(srcdoc).not.toContain('html[data-od-comment-mode] body iframe');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression for nexu-io/open-design#892: imported designs (e.g. Claude
|
// Regression for nexu-io/open-design#892: imported designs (e.g. Claude
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue