fix(core): a section whose root sets pointer-events:none stays pickable

This commit is contained in:
Miguel Ángel
2026-09-23 23:29:18 -04:00
parent 8027c758d1
commit 47d7e990d5
2 changed files with 69 additions and 4 deletions
+42
View File
@@ -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 =
'<style>[data-composition-id="intro"] > [data-hf-inner-root] { pointer-events: none }' +
" .vignette { pointer-events: none }</style>";
document.body.innerHTML = `<div data-composition-id="intro" data-composition-src="intro.html">
<div data-hf-inner-root="true"><div class="card"><code id="code">tl.to()</code></div>
<div class="vignette" id="vignette"></div></div></div>`;
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();
+27 -4
View File
@@ -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<T>(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");