diff --git a/apps/studio-ui/src/content-pane/snapshot-frame.component.test.tsx b/apps/studio-ui/src/content-pane/snapshot-frame.component.test.tsx index 5c05121e..8b372f89 100644 --- a/apps/studio-ui/src/content-pane/snapshot-frame.component.test.tsx +++ b/apps/studio-ui/src/content-pane/snapshot-frame.component.test.tsx @@ -35,4 +35,13 @@ describe('SnapshotFrame', () => { expect(onPickNode).not.toHaveBeenCalled() }) + + it.each([true, false])('never lets a clicked link navigate the frame to the live site (pick mode %s)', (pickMode) => { + const { container } = render( undefined} onPickNode={() => undefined} />) + const link = loadedLink(container) + + const notCancelled = fireEvent.click(link) + + expect(notCancelled).toBe(false) + }) }) diff --git a/apps/studio-ui/src/content-pane/snapshot-frame.component.tsx b/apps/studio-ui/src/content-pane/snapshot-frame.component.tsx index ecaa6e22..bf59bd4b 100644 --- a/apps/studio-ui/src/content-pane/snapshot-frame.component.tsx +++ b/apps/studio-ui/src/content-pane/snapshot-frame.component.tsx @@ -46,6 +46,9 @@ export function SnapshotFrame ({ html, pickMode, showHidden, hoveredSelector, on const handleMouseOver = (event: MouseEvent): void => { propsRef.current.onHoverNode(nodeIdOf(event.target)) } const handleMouseOut = (): void => { propsRef.current.onHoverNode(undefined) } const handleClick = (event: MouseEvent): void => { + // The snapshot is a picture of the page, not the page: a link must never navigate this frame to the live + // site (which usually refuses to be framed), whether or not a pick is under way. + if (linkOf(event.target) !== undefined) event.preventDefault() if (!propsRef.current.pickMode) return const nodeId = nodeIdOf(event.target) if (nodeId === undefined) return @@ -97,6 +100,13 @@ export function SnapshotFrame ({ html, pickMode, showHidden, hoveredSelector, on ) } +/** The link at or above `target`, if any (same cross-realm caveat as `nodeIdOf`). */ +function linkOf (target: EventTarget | null): Element | undefined { + const element = (target as Node | null)?.nodeType === Node.ELEMENT_NODE ? (target as Element).closest('a[href], area[href]') : null + + return element ?? undefined +} + /** The `data-oc-node` id of the stamped element at or above `target`. The target belongs to the iframe's own realm, so `target instanceof Element` is false for it: test the node type instead (issue #153). */ function nodeIdOf (target: EventTarget | null): string | undefined { const element = (target as Node | null)?.nodeType === Node.ELEMENT_NODE ? (target as Element).closest(`[${CSS.escape(NODE_ID_ATTRIBUTE)}]`) : null diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json index 4a78ab17..07ead00e 100644 --- a/apps/vscode-extension/package.json +++ b/apps/vscode-extension/package.json @@ -1,6 +1,6 @@ { "name": "@opencraw/vscode", - "version": "0.1.1", + "version": "0.1.2", "private": true, "description": "OpenCraw in the editor: starts the Studio server and opens a folder, recipe or document in the browser.", "type": "module",