From 9ad1d18cdefaf94c3569d8223244475a92c310dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 8 Jun 2026 13:42:33 -0400 Subject: [PATCH] fix(studio): sync child overlays during parent element drag When dragging a parent element's overlay, the RAF loop was fully paused to prevent flickering on the dragged element. This caused child element overlays to freeze in place since they rely on the RAF loop to measure their positions from the iframe DOM. Split the pause into two modes: rafPausedRef (full pause, used for group drags where all overlay items are manually managed) and rafSelectionOnlyPausedRef (skips only the selected element's rect update while letting hover and group rects continue measuring). Single-element drag/resize/rotate now uses the selection-only pause, so children, hover targets, and other overlays follow the parent's movement in real time. --- .../src/components/editor/DomEditOverlay.tsx | 3 ++ .../editor/domEditOverlayStartGesture.ts | 2 +- .../editor/useDomEditOverlayGestures.ts | 6 ++-- .../editor/useDomEditOverlayRects.ts | 30 +++++++++++-------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/studio/src/components/editor/DomEditOverlay.tsx b/packages/studio/src/components/editor/DomEditOverlay.tsx index ec289fc78..247aaacb1 100644 --- a/packages/studio/src/components/editor/DomEditOverlay.tsx +++ b/packages/studio/src/components/editor/DomEditOverlay.tsx @@ -98,6 +98,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({ const suppressNextOverlayMouseDownRef = useRef(false); const snapGuidesRef = useRef(null); const rafPausedRef = useRef(false); + const rafSelectionOnlyPausedRef = useRef(false); const selectionRef = useRef(selection); selectionRef.current = selection; @@ -142,6 +143,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({ groupSelectionsRef, hoverSelectionRef, rafPausedRef, + rafSelectionOnlyPausedRef, }); const [compRect, setCompRect] = useState({ @@ -199,6 +201,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({ groupGestureRef, blockedMoveRef, rafPausedRef, + rafSelectionOnlyPausedRef, suppressNextBoxClickRef, setOverlayRect, setGroupOverlayItems, diff --git a/packages/studio/src/components/editor/domEditOverlayStartGesture.ts b/packages/studio/src/components/editor/domEditOverlayStartGesture.ts index 8b3dc4775..e6336c2a4 100644 --- a/packages/studio/src/components/editor/domEditOverlayStartGesture.ts +++ b/packages/studio/src/components/editor/domEditOverlayStartGesture.ts @@ -154,7 +154,7 @@ export function startGesture( e.preventDefault(); e.stopPropagation(); e.currentTarget.setPointerCapture(e.pointerId); - opts.rafPausedRef.current = true; + opts.rafSelectionOnlyPausedRef.current = true; opts.gestureRef.current = { kind, mode, diff --git a/packages/studio/src/components/editor/useDomEditOverlayGestures.ts b/packages/studio/src/components/editor/useDomEditOverlayGestures.ts index a43a49653..f2d7ba38e 100644 --- a/packages/studio/src/components/editor/useDomEditOverlayGestures.ts +++ b/packages/studio/src/components/editor/useDomEditOverlayGestures.ts @@ -65,6 +65,7 @@ export type UseDomEditOverlayGesturesOptions = { groupGestureRef: RefObject; blockedMoveRef: RefObject; rafPausedRef: RefObject; + rafSelectionOnlyPausedRef: RefObject; suppressNextBoxClickRef: RefObject; setOverlayRect: (next: OverlayRect | null) => void; setGroupOverlayItems: (next: GroupOverlayItem[]) => void; @@ -391,11 +392,11 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu if (!g || !sel) { opts.gestureRef.current = null; - opts.rafPausedRef.current = false; + opts.rafSelectionOnlyPausedRef.current = false; return; } opts.gestureRef.current = null; - opts.rafPausedRef.current = false; + opts.rafSelectionOnlyPausedRef.current = false; const movedDistance = Math.hypot(e.clientX - g.startX, e.clientY - g.startY); if (g.kind === "drag" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) { @@ -522,6 +523,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu opts.groupGestureRef.current = null; opts.gestureRef.current = null; opts.rafPausedRef.current = false; + opts.rafSelectionOnlyPausedRef.current = false; }; return { startGesture, startGroupDrag, onPointerMove, onPointerUp, clearPointerState }; diff --git a/packages/studio/src/components/editor/useDomEditOverlayRects.ts b/packages/studio/src/components/editor/useDomEditOverlayRects.ts index 1bda0197d..80506fd05 100644 --- a/packages/studio/src/components/editor/useDomEditOverlayRects.ts +++ b/packages/studio/src/components/editor/useDomEditOverlayRects.ts @@ -25,6 +25,8 @@ interface UseDomEditOverlayRectsOptions { groupSelectionsRef: RefObject; hoverSelectionRef: RefObject; rafPausedRef: RefObject; + /** When true, the RAF loop skips only the main selection rect — hover and group rects keep updating. */ + rafSelectionOnlyPausedRef?: RefObject; } interface UseDomEditOverlayRectsResult { @@ -47,6 +49,7 @@ export function useDomEditOverlayRects({ groupSelectionsRef, hoverSelectionRef, rafPausedRef, + rafSelectionOnlyPausedRef, }: UseDomEditOverlayRectsOptions): UseDomEditOverlayRectsResult { const [overlayRect, setOverlayRectState] = useState(null); const [hoverRect, setHoverRectState] = useState(null); @@ -104,6 +107,7 @@ export function useDomEditOverlayRects({ frame = requestAnimationFrame(update); if (rafPausedRef.current) return; + const selectionOnlyPaused = rafSelectionOnlyPausedRef?.current ?? false; const sel = selectionRef.current; const iframe = iframeRef.current; const overlayEl = overlayRef.current; @@ -124,21 +128,23 @@ export function useDomEditOverlayRects({ return; } - if (sel) { - const el = resolveElementForOverlay( - doc, - sel, - activeCompositionPathRef.current, - resolvedElementRef as ResolvedElementRef, - ); - if (el && isElementVisibleForOverlay(el)) { - setOverlayRect(toOverlayRect(overlayEl, iframe, el)); + if (!selectionOnlyPaused) { + if (sel) { + const el = resolveElementForOverlay( + doc, + sel, + activeCompositionPathRef.current, + resolvedElementRef as ResolvedElementRef, + ); + if (el && isElementVisibleForOverlay(el)) { + setOverlayRect(toOverlayRect(overlayEl, iframe, el)); + } else { + setOverlayRect(null); + } } else { + resolvedElementRef.current = null; setOverlayRect(null); } - } else { - resolvedElementRef.current = null; - setOverlayRect(null); } const group = groupSelectionsRef.current;