Skip to content

Commit 62441fa

Browse files
authored
[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)
1 parent c96f719 commit 62441fa

File tree

4 files changed

+344
-41
lines changed

4 files changed

+344
-41
lines changed

src/renderer/extensions/vueNodes/components/LGraphNode.vue

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838
},
3939
dragStyle
4040
]"
41-
@pointerdown="handlePointerDown"
42-
@pointermove="handlePointerMove"
43-
@pointerup="handlePointerUp"
41+
v-bind="pointerHandlers"
4442
@wheel="handleWheel"
4543
>
4644
<div class="flex items-center">
@@ -232,13 +230,10 @@ onErrorCaptured((error) => {
232230
233231
// Use layout system for node position and dragging
234232
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id)
235-
const {
236-
handlePointerDown,
237-
handlePointerUp,
238-
handlePointerMove,
239-
isDragging,
240-
dragStyle
241-
} = useNodePointerInteractions(() => nodeData, handleNodeSelect)
233+
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
234+
() => nodeData,
235+
handleNodeSelect
236+
)
242237
243238
onMounted(() => {
244239
if (size.value && transformState?.camera) {
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { nextTick, ref } from 'vue'
3+
4+
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
5+
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
6+
7+
// Mock the dependencies
8+
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
9+
useCanvasInteractions: () => ({
10+
forwardEventToCanvas: vi.fn(),
11+
shouldHandleNodePointerEvents: ref(true)
12+
})
13+
}))
14+
15+
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
16+
useNodeLayout: () => ({
17+
startDrag: vi.fn(),
18+
endDrag: vi.fn().mockResolvedValue(undefined),
19+
handleDrag: vi.fn().mockResolvedValue(undefined)
20+
})
21+
}))
22+
23+
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
24+
layoutStore: {
25+
isDraggingVueNodes: ref(false)
26+
}
27+
}))
28+
29+
const createMockVueNodeData = (
30+
overrides: Partial<VueNodeData> = {}
31+
): VueNodeData => ({
32+
id: 'test-node-123',
33+
title: 'Test Node',
34+
type: 'TestNodeType',
35+
mode: 0,
36+
selected: false,
37+
executing: false,
38+
inputs: [],
39+
outputs: [],
40+
widgets: [],
41+
...overrides
42+
})
43+
44+
const createPointerEvent = (
45+
eventType: string,
46+
overrides: Partial<PointerEventInit> = {}
47+
): PointerEvent => {
48+
return new PointerEvent(eventType, {
49+
pointerId: 1,
50+
button: 0,
51+
clientX: 100,
52+
clientY: 100,
53+
...overrides
54+
})
55+
}
56+
57+
const createMouseEvent = (
58+
eventType: string,
59+
overrides: Partial<MouseEventInit> = {}
60+
): MouseEvent => {
61+
return new MouseEvent(eventType, {
62+
button: 2, // Right click
63+
clientX: 100,
64+
clientY: 100,
65+
...overrides
66+
})
67+
}
68+
69+
describe('useNodePointerInteractions', () => {
70+
let mockNodeData: VueNodeData
71+
let mockOnPointerUp: ReturnType<typeof vi.fn>
72+
73+
beforeEach(() => {
74+
mockNodeData = createMockVueNodeData()
75+
mockOnPointerUp = vi.fn()
76+
vi.clearAllMocks()
77+
})
78+
79+
afterEach(() => {
80+
vi.restoreAllMocks()
81+
})
82+
83+
it('should only start drag on left-click', async () => {
84+
const { pointerHandlers } = useNodePointerInteractions(
85+
ref(mockNodeData),
86+
mockOnPointerUp
87+
)
88+
89+
// Right-click should not start drag
90+
const rightClickEvent = createPointerEvent('pointerdown', { button: 2 })
91+
pointerHandlers.onPointerdown(rightClickEvent)
92+
await nextTick()
93+
94+
expect(mockOnPointerUp).not.toHaveBeenCalled()
95+
96+
// Left-click should start drag and emit callback
97+
const leftClickEvent = createPointerEvent('pointerdown', { button: 0 })
98+
pointerHandlers.onPointerdown(leftClickEvent)
99+
await nextTick()
100+
101+
const pointerUpEvent = createPointerEvent('pointerup')
102+
pointerHandlers.onPointerup(pointerUpEvent)
103+
await nextTick()
104+
105+
expect(mockOnPointerUp).toHaveBeenCalledWith(
106+
pointerUpEvent,
107+
mockNodeData,
108+
false // wasDragging = false (same position)
109+
)
110+
})
111+
112+
it('should distinguish drag from click based on distance threshold', async () => {
113+
const { pointerHandlers } = useNodePointerInteractions(
114+
ref(mockNodeData),
115+
mockOnPointerUp
116+
)
117+
118+
// Test drag (distance > 4px)
119+
pointerHandlers.onPointerdown(
120+
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
121+
)
122+
await nextTick()
123+
124+
const dragUpEvent = createPointerEvent('pointerup', {
125+
clientX: 200,
126+
clientY: 200
127+
})
128+
pointerHandlers.onPointerup(dragUpEvent)
129+
await nextTick()
130+
131+
expect(mockOnPointerUp).toHaveBeenCalledWith(
132+
dragUpEvent,
133+
mockNodeData,
134+
true
135+
)
136+
137+
mockOnPointerUp.mockClear()
138+
139+
// Test click (same position)
140+
const samePos = { clientX: 100, clientY: 100 }
141+
pointerHandlers.onPointerdown(createPointerEvent('pointerdown', samePos))
142+
await nextTick()
143+
144+
const clickUpEvent = createPointerEvent('pointerup', samePos)
145+
pointerHandlers.onPointerup(clickUpEvent)
146+
await nextTick()
147+
148+
expect(mockOnPointerUp).toHaveBeenCalledWith(
149+
clickUpEvent,
150+
mockNodeData,
151+
false
152+
)
153+
})
154+
155+
it('should handle drag termination via cancel and context menu', async () => {
156+
const { pointerHandlers } = useNodePointerInteractions(
157+
ref(mockNodeData),
158+
mockOnPointerUp
159+
)
160+
161+
// Test pointer cancel
162+
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
163+
await nextTick()
164+
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
165+
await nextTick()
166+
167+
// Should not emit callback on cancel
168+
expect(mockOnPointerUp).not.toHaveBeenCalled()
169+
170+
// Test context menu during drag prevents default
171+
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
172+
await nextTick()
173+
174+
const contextMenuEvent = createMouseEvent('contextmenu')
175+
const preventDefaultSpy = vi.spyOn(contextMenuEvent, 'preventDefault')
176+
177+
pointerHandlers.onContextmenu(contextMenuEvent)
178+
await nextTick()
179+
180+
expect(preventDefaultSpy).toHaveBeenCalled()
181+
})
182+
183+
it('should not emit callback when nodeData becomes null', async () => {
184+
const nodeDataRef = ref<VueNodeData | null>(mockNodeData)
185+
const { pointerHandlers } = useNodePointerInteractions(
186+
nodeDataRef,
187+
mockOnPointerUp
188+
)
189+
190+
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
191+
await nextTick()
192+
193+
// Clear nodeData before pointerup
194+
nodeDataRef.value = null
195+
await nextTick()
196+
197+
pointerHandlers.onPointerup(createPointerEvent('pointerup'))
198+
await nextTick()
199+
200+
expect(mockOnPointerUp).not.toHaveBeenCalled()
201+
})
202+
203+
it('should integrate with layout store dragging state', async () => {
204+
const { layoutStore } = await import(
205+
'@/renderer/core/layout/store/layoutStore'
206+
)
207+
const { pointerHandlers } = useNodePointerInteractions(
208+
ref(mockNodeData),
209+
mockOnPointerUp
210+
)
211+
212+
// Start drag
213+
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
214+
await nextTick()
215+
expect(layoutStore.isDraggingVueNodes.value).toBe(true)
216+
217+
// End drag
218+
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
219+
await nextTick()
220+
expect(layoutStore.isDraggingVueNodes.value).toBe(false)
221+
})
222+
})

0 commit comments

Comments
 (0)