Skip to content

Commit 2cb078c

Browse files
fix mmb navigation when click starts on Vue nodes (#5921)
## Summary Fixes #5860 by updating Vue node pointer interactions to forward middle mouse button events to canvas instead of handling them locally. ## Review Focus Middle mouse button event detection logic using both `button` property and `buttons` bitmask for cross-browser compatibility. Test coverage for pointer event forwarding behavior. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5921-fix-mmb-navigation-when-click-starts-on-Vue-nodes-2826d73d3650819688eec4600666755d) by [Unito](https://www.unito.io) --------- Co-authored-by: DrJKL <[email protected]>
1 parent dc0b729 commit 2cb078c

File tree

5 files changed

+76
-3
lines changed

5 files changed

+76
-3
lines changed

knip.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const config: KnipConfig = {
2323
project: ['src/**/*.{js,ts}']
2424
}
2525
},
26-
ignoreBinaries: ['python3'],
26+
ignoreBinaries: ['python3', 'stylelint'],
2727
ignoreDependencies: [
2828
// Weird importmap things
2929
'@iconify/json',
@@ -32,7 +32,8 @@ const config: KnipConfig = {
3232
'@primeuix/utils',
3333
'@primevue/icons',
3434
// Dev
35-
'@trivago/prettier-plugin-sort-imports'
35+
'@trivago/prettier-plugin-sort-imports',
36+
'stylelint'
3637
],
3738
ignore: [
3839
// Auto generated manager types

src/base/pointerUtils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Utilities for pointer event handling
3+
*/
4+
5+
/**
6+
* Checks if a pointer or mouse event is a middle button input
7+
* @param event - The pointer or mouse event to check
8+
* @returns true if the event is from the middle button/wheel
9+
*/
10+
export function isMiddlePointerInput(
11+
event: PointerEvent | MouseEvent
12+
): boolean {
13+
if ('button' in event && event.button === 1) {
14+
return true
15+
}
16+
17+
if ('buttons' in event && typeof event.buttons === 'number') {
18+
return event.buttons === 4
19+
}
20+
21+
return false
22+
}

src/renderer/core/canvas/useCanvasInteractions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { computed } from 'vue'
22

3+
import { isMiddlePointerInput } from '@/base/pointerUtils'
34
import { useSettingStore } from '@/platform/settings/settingStore'
45
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
56
import { app } from '@/scripts/app'
@@ -59,6 +60,11 @@ export function useCanvasInteractions() {
5960
* be forwarded to canvas (e.g., space+drag for panning)
6061
*/
6162
const handlePointer = (event: PointerEvent) => {
63+
if (isMiddlePointerInput(event)) {
64+
forwardEventToCanvas(event)
65+
return
66+
}
67+
6268
// Check if canvas exists using established pattern
6369
const canvas = getCanvas()
6470
if (!canvas) return

src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { nextTick, ref } from 'vue'
44
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
55
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
66

7+
const forwardEventToCanvasMock = vi.fn()
8+
79
// Mock the dependencies
810
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
911
useCanvasInteractions: () => ({
10-
forwardEventToCanvas: vi.fn(),
12+
forwardEventToCanvas: forwardEventToCanvasMock,
1113
shouldHandleNodePointerEvents: ref(true)
1214
})
1315
}))
@@ -69,6 +71,7 @@ const createMouseEvent = (
6971
describe('useNodePointerInteractions', () => {
7072
beforeEach(() => {
7173
vi.clearAllMocks()
74+
forwardEventToCanvasMock.mockClear()
7275
})
7376

7477
it('should only start drag on left-click', async () => {
@@ -100,6 +103,34 @@ describe('useNodePointerInteractions', () => {
100103
)
101104
})
102105

106+
it('forwards middle mouse interactions to the canvas', () => {
107+
const mockNodeData = createMockVueNodeData()
108+
const mockOnPointerUp = vi.fn()
109+
110+
const { pointerHandlers } = useNodePointerInteractions(
111+
ref(mockNodeData),
112+
mockOnPointerUp
113+
)
114+
115+
const middlePointerDown = createPointerEvent('pointerdown', { button: 1 })
116+
pointerHandlers.onPointerdown(middlePointerDown)
117+
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerDown)
118+
119+
forwardEventToCanvasMock.mockClear()
120+
121+
const middlePointerMove = createPointerEvent('pointermove', { buttons: 4 })
122+
pointerHandlers.onPointermove(middlePointerMove)
123+
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerMove)
124+
125+
forwardEventToCanvasMock.mockClear()
126+
127+
const middlePointerUp = createPointerEvent('pointerup', { button: 1 })
128+
pointerHandlers.onPointerup(middlePointerUp)
129+
expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerUp)
130+
131+
expect(mockOnPointerUp).not.toHaveBeenCalled()
132+
})
133+
103134
it('should distinguish drag from click based on distance threshold', async () => {
104135
const mockNodeData = createMockVueNodeData()
105136
const mockOnPointerUp = vi.fn()

src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type MaybeRefOrGetter, computed, onUnmounted, ref, toValue } from 'vue'
22

3+
import { isMiddlePointerInput } from '@/base/pointerUtils'
34
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
45
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
56
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
@@ -34,6 +35,12 @@ export function useNodePointerInteractions(
3435
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
3536
useCanvasInteractions()
3637

38+
const forwardMiddlePointerIfNeeded = (event: PointerEvent) => {
39+
if (!isMiddlePointerInput(event)) return false
40+
forwardEventToCanvas(event)
41+
return true
42+
}
43+
3744
// Drag state for styling
3845
const isDragging = ref(false)
3946
const dragStyle = computed(() => {
@@ -52,6 +59,8 @@ export function useNodePointerInteractions(
5259
return
5360
}
5461

62+
if (forwardMiddlePointerIfNeeded(event)) return
63+
5564
const stopNodeDragTarget =
5665
event.target instanceof HTMLElement
5766
? event.target.closest('[data-capture-node="true"]')
@@ -90,6 +99,8 @@ export function useNodePointerInteractions(
9099
}
91100

92101
const handlePointerMove = (event: PointerEvent) => {
102+
if (forwardMiddlePointerIfNeeded(event)) return
103+
93104
if (isDragging.value) {
94105
void handleDrag(event)
95106
}
@@ -129,6 +140,8 @@ export function useNodePointerInteractions(
129140
}
130141

131142
const handlePointerUp = (event: PointerEvent) => {
143+
if (forwardMiddlePointerIfNeeded(event)) return
144+
132145
if (isDragging.value) {
133146
handleDragTermination(event, 'drag end')
134147
}

0 commit comments

Comments
 (0)