mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(studio): make canvas selection hit intended elements (#1907)
* test(studio): add design-panel QA fixture and triage matrix Fixture project covering all panel-editable element archetypes, plus the QA findings matrix from the design-panel bug campaign. * fix(studio): make canvas selection hit intended elements - honor author pointer-events:none in hit-testing (was selecting invisible overlays) - pause playback before mousedown sampling; fall back to hover selection on null resolve - invalidate committed selection when the active composition changes - double-click keeps selection and defers to multi-candidate click cycling * fix(studio): close remaining selection-layer review findings - hoverSelection fallback now wired at all 3 mousedown call sites (box-click, blocked-drag, plain overlay click) instead of just the overlay path - pointer-events override detection reads computed style, not inline style, so a CSS-class opt-in (not just inline style=) on a descendant is honored - defensively remove the pointer-events override before the group-fallback check too, closing a theoretical gap in the no-elementsFromPoint branch - a click that resolves to nothing (dead-zone / deselect) no longer leaves playback paused if it was already playing
This commit is contained in:
@@ -345,6 +345,101 @@ describe("DomEditOverlay", () => {
|
||||
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("passes the tracked hover selection when clicking the existing selection box", async () => {
|
||||
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
||||
Element.prototype.getBoundingClientRect = function (): DOMRect {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 800,
|
||||
bottom: 450,
|
||||
width: 800,
|
||||
height: 450,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
};
|
||||
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
const selection: DomEditSelection = {
|
||||
element: document.createElement("div"),
|
||||
id: "hero-title",
|
||||
selector: ".hero-title",
|
||||
selectorIndex: 0,
|
||||
sourceFile: "index.html",
|
||||
tagName: "div",
|
||||
label: "Hero Title",
|
||||
textContent: "Hello",
|
||||
textFields: [],
|
||||
capabilities: {
|
||||
canEditText: true,
|
||||
canEditLayout: true,
|
||||
canMove: false,
|
||||
canApplyManualOffset: false,
|
||||
canApplyManualSize: false,
|
||||
canApplyManualRotation: false,
|
||||
canAdjustOpacity: true,
|
||||
canAdjustFill: true,
|
||||
canAdjustBorderRadius: true,
|
||||
canAdjustStroke: true,
|
||||
canAdjustShadow: true,
|
||||
canAdjustZIndex: true,
|
||||
},
|
||||
computedStyle: {
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
},
|
||||
};
|
||||
const hoverSelection: DomEditSelection = { ...selection, id: "hovered-sibling" };
|
||||
const onCanvasMouseDown = vi.fn();
|
||||
const iframeRef = { current: document.createElement("iframe") as HTMLIFrameElement | null };
|
||||
|
||||
function Harness() {
|
||||
return React.createElement(DomEditOverlay, {
|
||||
...createOverlayProps({
|
||||
iframeRef,
|
||||
selection,
|
||||
hoverSelection,
|
||||
onSelectionChange: () => {},
|
||||
}),
|
||||
onCanvasMouseDown,
|
||||
});
|
||||
}
|
||||
|
||||
act(() => {
|
||||
root.render(React.createElement(Harness));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
});
|
||||
|
||||
const selectionBox = host.querySelector(
|
||||
'[data-dom-edit-selection-box="true"]',
|
||||
) as HTMLDivElement;
|
||||
expect(selectionBox).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
selectionBox.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onCanvasMouseDown).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ hoverSelection }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditCoordinateScale", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||
import { type DomEditSelection } from "./domEditing";
|
||||
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
||||
import { useMarqueeGestures } from "./marqueeCommit";
|
||||
import { MarqueeOverlay } from "./MarqueeOverlay";
|
||||
import { groupAwareOverlayRect, resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
||||
@@ -44,7 +45,7 @@ interface DomEditOverlayProps {
|
||||
allowCanvasMovement?: boolean;
|
||||
onCanvasMouseDown: (
|
||||
event: React.MouseEvent<HTMLDivElement>,
|
||||
options?: { preferClipAncestor?: boolean },
|
||||
options?: PreviewMouseDownOptions,
|
||||
) => void;
|
||||
onCanvasPointerMove: (
|
||||
event: React.PointerEvent<HTMLDivElement>,
|
||||
@@ -277,6 +278,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
iframeRef,
|
||||
boxRef,
|
||||
selectionRef,
|
||||
hoverSelectionRef,
|
||||
overlayRectRef,
|
||||
groupOverlayItemsRef,
|
||||
gestureRef,
|
||||
@@ -336,7 +338,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
// Allow clicks anywhere on the overlay — GSAP-translated elements can
|
||||
// extend beyond the composition rect into the gray zone, and users need
|
||||
// to select/deselect them by clicking there.
|
||||
onCanvasMouseDown(event, { preferClipAncestor: false });
|
||||
onCanvasMouseDown(event, { hoverSelection: hoverSelectionRef.current });
|
||||
if (event.shiftKey) {
|
||||
suppressNextBoxMouseDownRef.current = true;
|
||||
suppressNextBoxClickRef.current = true;
|
||||
@@ -401,7 +403,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onCanvasMouseDown(event, { preferClipAncestor: false });
|
||||
onCanvasMouseDown(event, { hoverSelection: hoverSelectionRef.current });
|
||||
};
|
||||
|
||||
const suppressBoxMouseDown = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
||||
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
||||
import type { SnapContext } from "./snapTargetCollection";
|
||||
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
||||
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
||||
|
||||
export type GestureKind = "drag" | "resize" | "rotate";
|
||||
|
||||
@@ -161,6 +162,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
boxRef: RefObject<HTMLDivElement | null>;
|
||||
selectionRef: RefObject<DomEditSelection | null>;
|
||||
hoverSelectionRef: RefObject<DomEditSelection | null>;
|
||||
overlayRectRef: RefObject<OverlayRect | null>;
|
||||
groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
|
||||
gestureRef: RefObject<GestureState | null>;
|
||||
@@ -194,9 +196,6 @@ export type UseDomEditOverlayGesturesOptions = {
|
||||
o?: { preferClipAncestor?: boolean },
|
||||
) => Promise<DomEditSelection | null>
|
||||
>;
|
||||
onCanvasMouseDown: (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
o?: { preferClipAncestor?: boolean },
|
||||
) => void;
|
||||
onCanvasMouseDown: (e: React.MouseEvent<HTMLDivElement>, o?: PreviewMouseDownOptions) => void;
|
||||
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
||||
};
|
||||
|
||||
@@ -369,6 +369,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
||||
opts.suppressNextBoxClickRef.current = true;
|
||||
opts.onCanvasMouseDown(e as unknown as React.MouseEvent<HTMLDivElement>, {
|
||||
preferClipAncestor: false,
|
||||
hoverSelection: opts.hoverSelectionRef.current,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user