mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +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:
@@ -3,6 +3,7 @@ import { liveTime, usePlayerStore } from "../player";
|
||||
import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
|
||||
import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
|
||||
import { type DomEditSelection } from "../components/editor/domEditing";
|
||||
import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection";
|
||||
import { trackStudioEvent } from "../utils/studioTelemetry";
|
||||
|
||||
// ── Types ──
|
||||
@@ -16,16 +17,12 @@ export interface UsePreviewInteractionParams {
|
||||
// From useDomSelection
|
||||
applyDomSelection: (
|
||||
selection: DomEditSelection | null,
|
||||
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
||||
options?: ApplyDomSelectionOptions,
|
||||
) => void;
|
||||
resolveDomSelectionFromPreviewPoint: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
options?: {
|
||||
preferClipAncestor?: boolean;
|
||||
skipSourceProbe?: boolean;
|
||||
activeGroupElement?: HTMLElement | null;
|
||||
},
|
||||
options?: ResolveDomSelectionOptions,
|
||||
) => Promise<DomEditSelection | null>;
|
||||
resolveAllDomSelectionsFromPreviewPoint: (
|
||||
clientX: number,
|
||||
@@ -46,6 +43,11 @@ interface ClickCycleState {
|
||||
at: number;
|
||||
}
|
||||
|
||||
export interface PreviewMouseDownOptions {
|
||||
preferClipAncestor?: boolean;
|
||||
hoverSelection?: DomEditSelection | null;
|
||||
}
|
||||
|
||||
const CYCLE_RADIUS_PX = 6;
|
||||
const CYCLE_WINDOW_MS = 600;
|
||||
// Manual double-click window. `e.detail` can't be trusted here: the first click
|
||||
@@ -72,9 +74,19 @@ export function usePreviewInteraction({
|
||||
const cycleRef = useRef<ClickCycleState | null>(null);
|
||||
const lastDownRef = useRef<{ t: number; x: number; y: number } | null>(null);
|
||||
|
||||
const pausePreviewPlayback = useCallback(() => {
|
||||
const pausedTime = pauseStudioPreviewPlayback(previewIframeRef.current);
|
||||
const playerStore = usePlayerStore.getState();
|
||||
playerStore.setIsPlaying(false);
|
||||
if (pausedTime != null) {
|
||||
playerStore.setCurrentTime(pausedTime);
|
||||
liveTime.notify(pausedTime);
|
||||
}
|
||||
}, [previewIframeRef]);
|
||||
|
||||
const handlePreviewCanvasMouseDown = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
||||
async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => {
|
||||
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
||||
|
||||
// Manual double-click detection (see DOUBLE_CLICK_MS): the first click
|
||||
@@ -87,12 +99,26 @@ export function usePreviewInteraction({
|
||||
downTs - lastDown.t < DOUBLE_CLICK_MS &&
|
||||
Math.hypot(e.clientX - lastDown.x, e.clientY - lastDown.y) < DOUBLE_CLICK_RADIUS_PX);
|
||||
lastDownRef.current = { t: downTs, x: e.clientX, y: e.clientY };
|
||||
const wasPlaying = usePlayerStore.getState().isPlaying;
|
||||
pausePreviewPlayback();
|
||||
// A click that resolves to nothing (dead-zone / deselect) shouldn't leave
|
||||
// playback paused — pausing before sampling only exists to keep the hit
|
||||
// target stable while resolving; resume if nothing was selected.
|
||||
const resumeIfNothingSelected = () => {
|
||||
if (wasPlaying) usePlayerStore.getState().setIsPlaying(true);
|
||||
};
|
||||
|
||||
// Double-click a group → drill into it and select the child under the
|
||||
// pointer (resolve with the group as the explicit drill-in scope, since the
|
||||
// activeGroupElement state hasn't re-rendered yet within this handler).
|
||||
if (isDoubleClick && !e.shiftKey) {
|
||||
const hit = await resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY);
|
||||
const cycle = cycleRef.current;
|
||||
const hasStackCycleAtSpot =
|
||||
cycle !== null &&
|
||||
cycle.candidates.length > 1 &&
|
||||
Math.hypot(e.clientX - cycle.x, e.clientY - cycle.y) < CYCLE_RADIUS_PX &&
|
||||
downTs - cycle.at < CYCLE_WINDOW_MS;
|
||||
if (hit?.element.hasAttribute("data-hf-group")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -105,6 +131,18 @@ export function usePreviewInteraction({
|
||||
applyDomSelection(child ?? hit);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
hit &&
|
||||
!hasStackCycleAtSpot &&
|
||||
!hit.element.hasAttribute("data-composition-src") &&
|
||||
!hit.element.hasAttribute("data-composition-file")
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
cycleRef.current = null;
|
||||
applyDomSelection(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
@@ -119,10 +157,16 @@ export function usePreviewInteraction({
|
||||
if (e.shiftKey) {
|
||||
// Additive selection — no cycling
|
||||
cycleRef.current = null;
|
||||
const nextSelection = await resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
||||
preferClipAncestor: options?.preferClipAncestor ?? false,
|
||||
});
|
||||
if (!nextSelection) return;
|
||||
const nextSelection =
|
||||
(await resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
||||
preferClipAncestor: options?.preferClipAncestor ?? false,
|
||||
})) ??
|
||||
options?.hoverSelection ??
|
||||
null;
|
||||
if (!nextSelection) {
|
||||
resumeIfNothingSelected();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyDomSelection(nextSelection, { additive: true });
|
||||
@@ -156,9 +200,11 @@ export function usePreviewInteraction({
|
||||
activeGroupElement: null,
|
||||
});
|
||||
}
|
||||
nextSelection = nextSelection ?? options?.hoverSelection ?? null;
|
||||
if (!nextSelection) {
|
||||
cycleRef.current = null;
|
||||
applyDomSelection(null, { revealPanel: false });
|
||||
resumeIfNothingSelected();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -180,6 +226,7 @@ export function usePreviewInteraction({
|
||||
captionEditMode,
|
||||
compositionLoading,
|
||||
onClickToSource,
|
||||
pausePreviewPlayback,
|
||||
resolveAllDomSelectionsFromPreviewPoint,
|
||||
resolveDomSelectionFromPreviewPoint,
|
||||
setActiveGroupElement,
|
||||
@@ -225,14 +272,8 @@ export function usePreviewInteraction({
|
||||
);
|
||||
|
||||
const handleDomManualDragStart = useCallback(() => {
|
||||
const pausedTime = pauseStudioPreviewPlayback(previewIframeRef.current);
|
||||
const playerStore = usePlayerStore.getState();
|
||||
playerStore.setIsPlaying(false);
|
||||
if (pausedTime != null) {
|
||||
playerStore.setCurrentTime(pausedTime);
|
||||
liveTime.notify(pausedTime);
|
||||
}
|
||||
}, [previewIframeRef]);
|
||||
pausePreviewPlayback();
|
||||
}, [pausePreviewPlayback]);
|
||||
|
||||
return {
|
||||
handlePreviewCanvasMouseDown,
|
||||
|
||||
Reference in New Issue
Block a user