[fix] do not drag on right-click + fix refs (#5784)

## Summary

Fixes drag handling logic.

## Changes

Only check for drag on left-click.

Adds handler logic for following pointer events:
1. drag termination
2. context menu
3. pointer cancel

Adds tests.

Consolidates cleanup tasks.

## Screenshots

Fixed State:

Ignore first failed drag, browser window didn't have context.


https://github.com/user-attachments/assets/00ec685a-1ef7-4102-b19b-4cdb9b201d22

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5784-fix-do-not-drag-on-right-click-fix-refs-27a6d73d3650812ea797fccf14022568)
by [Unito](https://www.unito.io)
This commit is contained in:
Arjan Singh
2025-09-26 12:05:36 -07:00
committed by GitHub
parent c96f719f91
commit 62441fa0f9
4 changed files with 344 additions and 41 deletions

View File

@@ -1,4 +1,4 @@
import { type MaybeRefOrGetter, computed, ref, toValue } from 'vue'
import { type MaybeRefOrGetter, computed, onUnmounted, ref, toValue } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
@@ -9,16 +9,27 @@ import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayo
const DRAG_THRESHOLD_PX = 4
export function useNodePointerInteractions(
nodeDataMaybe: MaybeRefOrGetter<VueNodeData>,
nodeDataMaybe: MaybeRefOrGetter<VueNodeData | null>,
onPointerUp: (
event: PointerEvent,
nodeData: VueNodeData,
wasDragging: boolean
) => void
) {
const nodeData = toValue(nodeDataMaybe)
const nodeData = computed(() => {
const value = toValue(nodeDataMaybe)
if (!value) {
console.warn(
'useNodePointerInteractions: nodeDataMaybe resolved to null/undefined'
)
return null
}
return value
})
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeData.id)
// Avoid potential null access during component initialization
const nodeIdComputed = computed(() => nodeData.value?.id ?? '')
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeIdComputed)
// Use canvas interactions for proper wheel event handling and pointer event capture control
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
useCanvasInteractions()
@@ -28,17 +39,21 @@ export function useNodePointerInteractions(
const dragStyle = computed(() => ({
cursor: isDragging.value ? 'grabbing' : 'grab'
}))
const lastX = ref(0)
const lastY = ref(0)
const startPosition = ref({ x: 0, y: 0 })
const handlePointerDown = (event: PointerEvent) => {
if (!nodeData) {
if (!nodeData.value) {
console.warn(
'LGraphNode: nodeData is null/undefined in handlePointerDown'
)
return
}
// Only start drag on left-click (button 0)
if (event.button !== 0) {
return
}
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
@@ -52,8 +67,7 @@ export function useNodePointerInteractions(
layoutStore.isDraggingVueNodes.value = true
startDrag(event)
lastY.value = event.clientY
lastX.value = event.clientX
startPosition.value = { x: event.clientX, y: event.clientY }
}
const handlePointerMove = (event: PointerEvent) => {
@@ -62,13 +76,42 @@ export function useNodePointerInteractions(
}
}
/**
* Centralized cleanup function for drag state
* Ensures consistent cleanup across all drag termination scenarios
*/
const cleanupDragState = () => {
isDragging.value = false
layoutStore.isDraggingVueNodes.value = false
}
/**
* Safely ends drag operation with proper error handling
* @param event - PointerEvent to end the drag with
*/
const safeDragEnd = async (event: PointerEvent): Promise<void> => {
try {
await endDrag(event)
} catch (error) {
console.error('Error during endDrag:', error)
} finally {
cleanupDragState()
}
}
/**
* Common drag termination handler with fallback cleanup
*/
const handleDragTermination = (event: PointerEvent, errorContext: string) => {
safeDragEnd(event).catch((error) => {
console.error(`Failed to complete ${errorContext}:`, error)
cleanupDragState() // Fallback cleanup
})
}
const handlePointerUp = (event: PointerEvent) => {
if (isDragging.value) {
isDragging.value = false
void endDrag(event)
// Clear Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = false
handleDragTermination(event, 'drag end')
}
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
@@ -78,16 +121,52 @@ export function useNodePointerInteractions(
}
// Emit node-click for selection handling in GraphCanvas
const dx = event.clientX - lastX.value
const dy = event.clientY - lastY.value
const dx = event.clientX - startPosition.value.x
const dy = event.clientY - startPosition.value.y
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
onPointerUp(event, nodeData, wasDragging)
if (!nodeData?.value) return
onPointerUp(event, nodeData.value, wasDragging)
}
/**
* Handles pointer cancellation events (e.g., touch cancelled by browser)
* Ensures drag state is properly cleaned up when pointer interaction is interrupted
*/
const handlePointerCancel = (event: PointerEvent) => {
if (!isDragging.value) return
handleDragTermination(event, 'drag cancellation')
}
/**
* Handles right-click during drag operations
* Cancels the current drag to prevent context menu from appearing while dragging
*/
const handleContextMenu = (event: MouseEvent) => {
if (!isDragging.value) return
event.preventDefault()
// Simply cleanup state without calling endDrag to avoid synthetic event creation
cleanupDragState()
}
// Cleanup on unmount to prevent resource leaks
onUnmounted(() => {
if (!isDragging.value) return
cleanupDragState()
})
const pointerHandlers = {
onPointerdown: handlePointerDown,
onPointermove: handlePointerMove,
onPointerup: handlePointerUp,
onPointercancel: handlePointerCancel,
onContextmenu: handleContextMenu
}
return {
isDragging,
dragStyle,
handlePointerMove,
handlePointerDown,
handlePointerUp
pointerHandlers
}
}