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:
@@ -27,6 +27,18 @@ import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export interface ApplyDomSelectionOptions {
|
||||
revealPanel?: boolean;
|
||||
additive?: boolean;
|
||||
preserveGroup?: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveDomSelectionOptions {
|
||||
preferClipAncestor?: boolean;
|
||||
skipSourceProbe?: boolean;
|
||||
activeGroupElement?: HTMLElement | null;
|
||||
}
|
||||
|
||||
export interface UseDomSelectionParams {
|
||||
projectId: string | null;
|
||||
activeCompPath: string | null;
|
||||
@@ -61,29 +73,17 @@ export interface UseDomSelectionReturn {
|
||||
// Callbacks
|
||||
applyDomSelection: (
|
||||
selection: DomEditSelection | null,
|
||||
options?: {
|
||||
revealPanel?: boolean;
|
||||
additive?: boolean;
|
||||
preserveGroup?: boolean;
|
||||
},
|
||||
options?: ApplyDomSelectionOptions,
|
||||
) => void;
|
||||
clearDomSelection: () => void;
|
||||
buildDomSelectionFromTarget: (
|
||||
target: HTMLElement,
|
||||
options?: {
|
||||
preferClipAncestor?: boolean;
|
||||
skipSourceProbe?: boolean;
|
||||
activeGroupElement?: HTMLElement | null;
|
||||
},
|
||||
options?: ResolveDomSelectionOptions,
|
||||
) => Promise<DomEditSelection | null>;
|
||||
resolveDomSelectionFromPreviewPoint: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
options?: {
|
||||
preferClipAncestor?: boolean;
|
||||
skipSourceProbe?: boolean;
|
||||
activeGroupElement?: HTMLElement | null;
|
||||
},
|
||||
options?: ResolveDomSelectionOptions,
|
||||
) => Promise<DomEditSelection | null>;
|
||||
resolveAllDomSelectionsFromPreviewPoint: (
|
||||
clientX: number,
|
||||
@@ -130,6 +130,7 @@ export function useDomSelection({
|
||||
const domEditGroupSelectionsRef = useRef<DomEditSelection[]>(domEditGroupSelections);
|
||||
const domEditHoverSelectionRef = useRef<DomEditSelection | null>(domEditHoverSelection);
|
||||
const activeGroupElementRef = useRef<HTMLElement | null>(activeGroupElement);
|
||||
const compositionIdentityRef = useRef({ activeCompPath, projectId });
|
||||
|
||||
// Keep refs in sync with state
|
||||
domEditSelectionRef.current = domEditSelection;
|
||||
@@ -230,6 +231,8 @@ export function useDomSelection({
|
||||
// the user isn't left with an out-of-scope element selected.
|
||||
const setActiveGroupElement = useCallback(
|
||||
(el: HTMLElement | null) => {
|
||||
if (activeGroupElementRef.current === el) return;
|
||||
activeGroupElementRef.current = el;
|
||||
setActiveGroupElementState(el);
|
||||
applyDomSelection(null, { revealPanel: false });
|
||||
},
|
||||
@@ -454,6 +457,17 @@ export function useDomSelection({
|
||||
updateDomEditHoverSelection(null);
|
||||
}, [activeCompPath, projectId, previewIframe, refreshKey, updateDomEditHoverSelection]);
|
||||
|
||||
// Clear committed selection only when the composition identity actually changes.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
const previous = compositionIdentityRef.current;
|
||||
if (previous.activeCompPath === activeCompPath && previous.projectId === projectId) return;
|
||||
compositionIdentityRef.current = { activeCompPath, projectId };
|
||||
activeGroupElementRef.current = null;
|
||||
setActiveGroupElementState(null);
|
||||
applyDomSelection(null, { revealPanel: false });
|
||||
}, [activeCompPath, projectId, applyDomSelection]);
|
||||
|
||||
// Clear hover conditionally (caption mode, matches selection, disconnected element)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user