Browse Source

fix: replace imperative DOM listener with React synthetic event for resize-handle double-click

The aspect-fit double-click feature broke after manual resize because imperative
DOM listeners (useEffect + addEventListener) fall out of sync when React Flow
re-renders resize controls. Switches to a React onDoubleClick handler on a
`display: contents` wrapper, which stays in sync across all re-renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
d9a60f46c7
  1. 34
      src/components/nodes/BaseNode.tsx

34
src/components/nodes/BaseNode.tsx

@ -1,6 +1,6 @@
"use client"; "use client";
import { ReactNode, useCallback, useEffect, useRef } from "react"; import { ReactNode, useCallback } from "react";
import { NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; import { NodeResizer, OnResize, useReactFlow } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions"; import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions";
@ -68,22 +68,13 @@ export function BaseNode({
); );
// Double-click resize handle → aspect-fit to media // Double-click resize handle → aspect-fit to media
const containerRef = useRef<HTMLDivElement>(null); // Uses React synthetic event on a `display: contents` wrapper so it stays
// in sync across React Flow re-renders (imperative DOM listeners break after manual resize).
useEffect(() => { const handleResizeHandleDblClick = useCallback(
if (!aspectFitMedia || !selected) return; async (e: React.MouseEvent) => {
const el = containerRef.current;
if (!el) return;
// Attach to the stable .react-flow__node wrapper (not resize controls directly,
// which get recreated by React Flow on re-render after manual resize)
const nodeWrapper = el.closest(".react-flow__node");
if (!nodeWrapper) return;
const handler = async (e: Event) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (!target.closest(".react-flow__resize-control")) return; if (!target.closest(".react-flow__resize-control")) return;
if (!aspectFitMedia) return;
e.stopPropagation(); e.stopPropagation();
const dims = await getMediaDimensions(aspectFitMedia); const dims = await getMediaDimensions(aspectFitMedia);
@ -123,14 +114,12 @@ export function BaseNode({
return n; return n;
}) })
); );
}; },
[aspectFitMedia, id, fullBleed, getNodes, setNodes]
nodeWrapper.addEventListener("dblclick", handler); );
return () => nodeWrapper.removeEventListener("dblclick", handler);
}, [aspectFitMedia, selected, id, fullBleed, getNodes, setNodes]);
return ( return (
<> <div className="contents" onDoubleClick={handleResizeHandleDblClick}>
<NodeResizer <NodeResizer
isVisible={selected} isVisible={selected}
minWidth={minWidth} minWidth={minWidth}
@ -140,7 +129,6 @@ export function BaseNode({
onResize={handleResize} onResize={handleResize}
/> />
<div <div
ref={containerRef}
className={` className={`
h-full w-full flex flex-col overflow-visible h-full w-full flex flex-col overflow-visible
${fullBleed ? "rounded-lg bg-neutral-800/50 border border-neutral-700/40" : "bg-neutral-800 rounded-lg shadow-lg border"} ${fullBleed ? "rounded-lg bg-neutral-800/50 border border-neutral-700/40" : "bg-neutral-800 rounded-lg shadow-lg border"}
@ -155,6 +143,6 @@ export function BaseNode({
> >
<div className={contentClassName ?? (fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-hidden flex flex-col")}>{children}</div> <div className={contentClassName ?? (fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-hidden flex flex-col")}>{children}</div>
</div> </div>
</> </div>
); );
} }

Loading…
Cancel
Save