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 = `
`;
+ 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");