From 8d41e3e0800cd28dc37e371cd7a7a4488c603ccf Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 22 May 2026 04:06:02 -0700 Subject: [PATCH] fix: firefox can give invalid drag coordinates causing incorrect drop position (https://bugzilla.mozilla.org/show_bug.cgi?id=1773886) - change to track during drag events --- .../node/useNodeDragToCanvas.test.ts | 86 +++++++++++++++++++ src/composables/node/useNodeDragToCanvas.ts | 15 +++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/composables/node/useNodeDragToCanvas.test.ts b/src/composables/node/useNodeDragToCanvas.test.ts index 2842cb817a..96ad055178 100644 --- a/src/composables/node/useNodeDragToCanvas.test.ts +++ b/src/composables/node/useNodeDragToCanvas.test.ts @@ -456,4 +456,90 @@ describe('useNodeDragToCanvas', () => { expect(dispatchPointerDown(600, 250)).not.toHaveBeenCalled() }) }) + + describe('native drag position tracking', () => { + beforeEach(() => { + mockCanvas.canvas.getBoundingClientRect.mockReturnValue({ + left: 0, + right: 500, + top: 0, + bottom: 500 + }) + mockConvertEventToCanvasOffset.mockReturnValue([300, 300]) + }) + + // happy-dom has no DragEvent constructor; MouseEvent works since the + // handler only reads clientX/clientY. + function fireDrag(x: number, y: number) { + document.dispatchEvent( + new MouseEvent('dragover', { clientX: x, clientY: y, bubbles: true }) + ) + } + + it('should prefer tracked drag position over dragend coordinates', () => { + const { startDrag, setupGlobalListeners, handleNativeDrop } = + useNodeDragToCanvas() + setupGlobalListeners() + startDrag(mockNodeDef, 'native') + + fireDrag(250, 250) + // dragend supplies a bad position (the Firefox bug); the tracked one + // from the last drag event should win. + handleNativeDrop(1505, 102) + + expect(mockConvertEventToCanvasOffset).toHaveBeenCalledWith({ + clientX: 250, + clientY: 250 + }) + }) + + it('should ignore drag events with (0, 0)', () => { + const { startDrag, setupGlobalListeners, handleNativeDrop } = + useNodeDragToCanvas() + setupGlobalListeners() + startDrag(mockNodeDef, 'native') + + fireDrag(250, 250) + fireDrag(0, 0) + handleNativeDrop(1505, 102) + + expect(mockConvertEventToCanvasOffset).toHaveBeenCalledWith({ + clientX: 250, + clientY: 250 + }) + }) + + it('should fall back to dragend coordinates when no drag fired', () => { + const { startDrag, setupGlobalListeners, handleNativeDrop } = + useNodeDragToCanvas() + setupGlobalListeners() + startDrag(mockNodeDef, 'native') + + handleNativeDrop(250, 250) + + expect(mockConvertEventToCanvasOffset).toHaveBeenCalledWith({ + clientX: 250, + clientY: 250 + }) + }) + + it('should clear tracked position between drags', () => { + const { startDrag, setupGlobalListeners, handleNativeDrop } = + useNodeDragToCanvas() + setupGlobalListeners() + + startDrag(mockNodeDef, 'native') + fireDrag(250, 250) + handleNativeDrop(1505, 102) + + // Second drag - no drag events, so we should fall back to args. + startDrag(mockNodeDef, 'native') + handleNativeDrop(300, 300) + + expect(mockConvertEventToCanvasOffset).toHaveBeenLastCalledWith({ + clientX: 300, + clientY: 300 + }) + }) + }) }) diff --git a/src/composables/node/useNodeDragToCanvas.ts b/src/composables/node/useNodeDragToCanvas.ts index 52227b7874..982e16cd11 100644 --- a/src/composables/node/useNodeDragToCanvas.ts +++ b/src/composables/node/useNodeDragToCanvas.ts @@ -10,16 +10,26 @@ const isDragging = ref(false) const draggedNode = shallowRef(null) const cursorPosition = ref({ x: 0, y: 0 }) const dragMode = ref('click') +const lastNativeDragPosition = shallowRef<{ x: number; y: number } | null>(null) let listenersSetup = false function updatePosition(e: PointerEvent) { cursorPosition.value = { x: e.clientX, y: e.clientY } } +// Firefox dragend can report stale clientX/Y and `drag` can fire with +// (0, 0). dragover on the target reliably reports real client coords. +// https://bugzilla.mozilla.org/show_bug.cgi?id=1773886 +function trackNativeDragPosition(e: DragEvent) { + if (e.clientX === 0 && e.clientY === 0) return + lastNativeDragPosition.value = { x: e.clientX, y: e.clientY } +} + function cancelDrag() { isDragging.value = false draggedNode.value = null dragMode.value = 'click' + lastNativeDragPosition.value = null } function isOverCanvas(clientX: number, clientY: number): boolean { @@ -81,6 +91,7 @@ function setupGlobalListeners() { document.addEventListener('pointerdown', blockCommitPointerDown, true) document.addEventListener('pointerup', endDrag, true) document.addEventListener('keydown', handleKeydown) + document.addEventListener('dragover', trackNativeDragPosition) } function cleanupGlobalListeners() { @@ -91,6 +102,7 @@ function cleanupGlobalListeners() { document.removeEventListener('pointerdown', blockCommitPointerDown, true) document.removeEventListener('pointerup', endDrag, true) document.removeEventListener('keydown', handleKeydown) + document.removeEventListener('dragover', trackNativeDragPosition) if (isDragging.value && dragMode.value === 'click') { cancelDrag() @@ -106,8 +118,9 @@ export function useNodeDragToCanvas() { function handleNativeDrop(clientX: number, clientY: number) { if (dragMode.value !== 'native') return + const tracked = lastNativeDragPosition.value try { - addNodeAtPosition(clientX, clientY) + addNodeAtPosition(tracked?.x ?? clientX, tracked?.y ?? clientY) } finally { cancelDrag() }