From db503bf739fee78184eec78771891aa182e5c914 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 6 Mar 2026 23:17:58 +1300 Subject: [PATCH] fix: prevent FloatingNodeHeader from intercepting node drag events on Windows Move pointer-events-auto from the header's outer container to the inner content bar so invisible header area doesn't block events reaching nodes. Switch custom drag handler from mouse events to pointer events for consistency with React Flow. Fix pointer capture leak in VideoStitchNode that could break subsequent drags. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/FloatingNodeHeader.tsx | 26 +++++++++++---------- src/components/nodes/VideoStitchNode.tsx | 7 +++++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/nodes/FloatingNodeHeader.tsx b/src/components/nodes/FloatingNodeHeader.tsx index 8da8f732..9326f95c 100644 --- a/src/components/nodes/FloatingNodeHeader.tsx +++ b/src/components/nodes/FloatingNodeHeader.tsx @@ -209,7 +209,7 @@ export function FloatingNodeHeader({ const { setNodes, getNodes, getViewport } = useReactFlow(); const isDraggingRef = useRef(false); - const handleHeaderMouseDown = useCallback((e: React.MouseEvent) => { + const handleHeaderPointerDown = useCallback((e: React.PointerEvent) => { // Don't drag from interactive elements if ((e.target as HTMLElement).closest('.nodrag, button, input, textarea, a')) return; if (e.button !== 0) return; @@ -241,7 +241,7 @@ export function FloatingNodeHeader({ isDraggingRef.current = false; - const handleMouseMove = (e: MouseEvent) => { + const handlePointerMove = (e: PointerEvent) => { const { zoom } = getViewport(); const dx = (e.clientX - startX) / zoom; const dy = (e.clientY - startY) / zoom; @@ -262,30 +262,32 @@ export function FloatingNodeHeader({ } }; - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + const handlePointerUp = () => { + document.removeEventListener('pointermove', handlePointerMove); + document.removeEventListener('pointerup', handlePointerUp); isDraggingRef.current = false; }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('pointermove', handlePointerMove); + document.addEventListener('pointerup', handlePointerUp); }, [id, getNodes, getViewport, setNodes]); return (
setIsHeaderHovered(true)} - onMouseLeave={() => setIsHeaderHovered(false)} - onMouseDown={handleHeaderMouseDown} > -
+
setIsHeaderHovered(true)} + onMouseLeave={() => setIsHeaderHovered(false)} + onPointerDown={handleHeaderPointerDown} + > {/* Title Section */}
{titlePrefix} diff --git a/src/components/nodes/VideoStitchNode.tsx b/src/components/nodes/VideoStitchNode.tsx index aac0ebd5..ff969545 100644 --- a/src/components/nodes/VideoStitchNode.tsx +++ b/src/components/nodes/VideoStitchNode.tsx @@ -251,7 +251,12 @@ export function VideoStitchNode({ id, data, selected }: NodeProps { + const handlePointerUp = useCallback((e: React.PointerEvent) => { + // Always release pointer capture to prevent capture leak + try { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + } catch { /* element may have been removed */ } + if (!draggedClipId || !hoverClipId || draggedClipId === hoverClipId) { setDraggedClipId(null); setHoverClipId(null);