diff --git a/packages/core/src/runtime/picker.test.ts b/packages/core/src/runtime/picker.test.ts index e454a101c6..2d69808ac7 100644 --- a/packages/core/src/runtime/picker.test.ts +++ b/packages/core/src/runtime/picker.test.ts @@ -25,6 +25,12 @@ beforeAll(() => { } }); +function ancestorsOf(el: Element): Element[] { + const chain: Element[] = []; + for (let at = el.parentElement; at; at = at.parentElement) chain.push(at); + return chain; +} + function createMockPostMessage() { return vi.fn(); } @@ -150,6 +156,42 @@ describe("createPickerModule", () => { } }); + it("picks inside a mounted section whose root sets pointer-events:none", () => { + const picker = createPickerModule({ postMessage: createMockPostMessage() }); + picker.installPickerApi(); + // The mount rescopes the section's own root rule onto its inner root. + document.head.innerHTML = + '"; + document.body.innerHTML = `
+
tl.to()
+
`; + const code = document.getElementById("code")!; + // Paint order under the point, top first; like a browser, drop what has pointer-events:none. + const painted = [document.getElementById("vignette")!, code, code.parentElement!]; + const originalElementsFromPoint = document.elementsFromPoint; + Object.defineProperty(document, "elementsFromPoint", { + configurable: true, + value: vi.fn(() => + [...painted, ...ancestorsOf(code.parentElement!)].filter( + (el) => getComputedStyle(el).pointerEvents !== "none", + ), + ), + }); + + const api = (window as any).__HF_PICKER_API; + try { + expect(api.getCandidatesAtPoint(10, 10)[0]?.selector).toBe("#code"); + expect(api.pickAtPoint(10, 10)?.selector).toBe("#code"); + expect(document.head.querySelectorAll("style")).toHaveLength(1); + } finally { + Object.defineProperty(document, "elementsFromPoint", { + configurable: true, + value: originalElementsFromPoint, + }); + } + }); + it("pickAtPoint returns null for invalid coords", () => { const picker = createPickerModule({ postMessage: createMockPostMessage() }); picker.installPickerApi(); diff --git a/packages/core/src/runtime/picker.ts b/packages/core/src/runtime/picker.ts index c995ca263e..53fb81565a 100644 --- a/packages/core/src/runtime/picker.ts +++ b/packages/core/src/runtime/picker.ts @@ -20,6 +20,23 @@ const PICKER_BLOCK_SELECTOR = [ "[data-hyper-shader-loading]", ].join(","); +// A composition root's pointer-events:none is about playback, not editing: it inherits into the whole section +// and hides it from the hit test. Roots take pointer events again while the picker looks; an element that +// sets pointer-events:none itself (a vignette, a cursor) still passes through. +const PICKABLE_ROOTS_CSS = + "[data-composition-id],[data-hf-inner-root]{pointer-events:auto!important}"; + +function withPickableCompositionRoots(run: () => T): T { + const style = document.createElement("style"); + style.textContent = PICKABLE_ROOTS_CSS; + (document.head ?? document.documentElement).appendChild(style); + try { + return run(); + } finally { + style.remove(); + } +} + export type PickerModule = { enablePickMode: () => void; disablePickMode: () => void; @@ -183,14 +200,20 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule { clientY: number, limit?: number, ): RuntimePickerElementInfo[] { - return getPickCandidatesFromPoint(clientX, clientY, limit).map(extractElementInfo); + return withPickableCompositionRoots(() => + getPickCandidatesFromPoint(clientX, clientY, limit), + ).map(extractElementInfo); } function onPickMouseMove(event: MouseEvent): void { if (!pickModeActive) return; - const candidates = getPickCandidatesFromPoint(event.clientX, event.clientY, 1); - const target = candidates[0] ?? (isElementNode(event.target) ? event.target : null); - if (!isPickableElement(target)) return; + const target = withPickableCompositionRoots(() => { + const hit = + getPickCandidatesFromPoint(event.clientX, event.clientY, 1)[0] ?? + (isElementNode(event.target) ? event.target : null); + return isPickableElement(hit) ? hit : null; + }); + if (!target) return; if (pickModeHighlightEl === target) return; if (pickModeHighlightEl) { pickModeHighlightEl.classList.remove("__hf-pick-highlight");