refactor: address review comments - use useMagicKeys and derive button state

- Refactored spacebar forwarding to use useMagicKeys from VueUse instead of raw event listeners
- Added filtering for editable elements (input, textarea, contentEditable)
- Changed panning check in useSlotLinkInteraction to derive button state from event.buttons instead of storing canvas.pointer.isDown statefully
- Updated tests to work with the new useMagicKeys approach using vi.hoisted pattern
This commit is contained in:
Johnpaul
2025-12-31 01:28:27 +01:00
parent f5ac48c5be
commit 2f8f5253b5
3 changed files with 134 additions and 55 deletions

View File

@@ -17,7 +17,26 @@ const mockCanvas = vi.hoisted(() => {
const canvasElement = document.createElement('canvas')
return {
canvas: canvasElement,
processKey: vi.fn()
read_only: false,
dragging_canvas: false,
pointer: { isDown: false }
}
})
// Mock useMagicKeys and useActiveElement from VueUse
// Use vi.hoisted to store refs in an object that's available during mock hoisting
const vueUseMocks = vi.hoisted(() => ({
spaceKey: null as { value: boolean } | null,
activeElement: null as { value: Element | null } | null
}))
vi.mock('@vueuse/core', async () => {
const { ref: vueRef } = await import('vue')
vueUseMocks.spaceKey = vueRef(false)
vueUseMocks.activeElement = vueRef<Element | null>(null)
return {
useMagicKeys: () => ({ space: vueUseMocks.spaceKey }),
useActiveElement: () => vueUseMocks.activeElement
}
})
@@ -342,41 +361,79 @@ describe('useNodePointerInteractions', () => {
)
})
describe('keydown forwarding for spacebar panning', () => {
it('forwards keydown events to canvas.processKey when target is not the canvas', () => {
// Initialize the composable to set up keydown forwarding
useNodePointerInteractions('test-node-123')
// Create a div to simulate a Vue node element
const vueNodeElement = document.createElement('div')
document.body.appendChild(vueNodeElement)
// Dispatch keydown event on the Vue node element (not the canvas)
const keydownEvent = new KeyboardEvent('keydown', {
key: ' ',
bubbles: true
})
vueNodeElement.dispatchEvent(keydownEvent)
// Should forward to canvas.processKey
expect(mockCanvas.processKey).toHaveBeenCalledWith(keydownEvent)
document.body.removeChild(vueNodeElement)
describe('spacebar panning via useMagicKeys', () => {
beforeEach(() => {
mockCanvas.read_only = false
mockCanvas.dragging_canvas = false
vueUseMocks.spaceKey!.value = false
vueUseMocks.activeElement!.value = null
})
it('does not forward keydown events when target is the canvas itself', () => {
it('sets read_only=true when spacebar is pressed on non-canvas element', async () => {
const vueNodeElement = document.createElement('div')
vueUseMocks.activeElement!.value = vueNodeElement
useNodePointerInteractions('test-node-123')
mockCanvas.processKey.mockClear()
// Dispatch keydown event directly on the canvas element
const keydownEvent = new KeyboardEvent('keydown', {
key: ' ',
bubbles: true
})
mockCanvas.canvas.dispatchEvent(keydownEvent)
// Simulate spacebar press
vueUseMocks.spaceKey!.value = true
await nextTick()
// Should NOT forward (canvas handles it directly)
expect(mockCanvas.processKey).not.toHaveBeenCalled()
expect(mockCanvas.read_only).toBe(true)
})
it('resets read_only=false when spacebar is released', async () => {
const vueNodeElement = document.createElement('div')
vueUseMocks.activeElement!.value = vueNodeElement
useNodePointerInteractions('test-node-123')
// Press and release spacebar
vueUseMocks.spaceKey!.value = true
await nextTick()
vueUseMocks.spaceKey!.value = false
await nextTick()
expect(mockCanvas.read_only).toBe(false)
expect(mockCanvas.dragging_canvas).toBe(false)
})
it('does not set read_only when canvas has focus', async () => {
vueUseMocks.activeElement!.value = mockCanvas.canvas
useNodePointerInteractions('test-node-123')
vueUseMocks.spaceKey!.value = true
await nextTick()
// Should NOT change read_only (litegraph handles it directly)
expect(mockCanvas.read_only).toBe(false)
})
it('does not set read_only when input element has focus', async () => {
const inputElement = document.createElement('input')
vueUseMocks.activeElement!.value = inputElement
useNodePointerInteractions('test-node-123')
vueUseMocks.spaceKey!.value = true
await nextTick()
// Should NOT change read_only (avoid interfering with text input)
expect(mockCanvas.read_only).toBe(false)
})
it('does not set read_only when textarea element has focus', async () => {
const textareaElement = document.createElement('textarea')
vueUseMocks.activeElement!.value = textareaElement
useNodePointerInteractions('test-node-123')
vueUseMocks.spaceKey!.value = true
await nextTick()
// Should NOT change read_only (avoid interfering with text input)
expect(mockCanvas.read_only).toBe(false)
})
})
})

View File

@@ -1,6 +1,8 @@
import { onScopeDispose, ref, toValue } from 'vue'
import { onScopeDispose, ref, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useActiveElement, useMagicKeys } from '@vueuse/core'
import { isMiddlePointerInput } from '@/base/pointerUtils'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
@@ -10,29 +12,51 @@ import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
import { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'
import { app } from '@/scripts/app'
// Forward keydown events to litegraph's processKey when Vue nodes have focus
let keydownForwardingInitialized = false
// Forward spacebar key events to litegraph for panning when Vue nodes have focus
let spacebarForwardingInitialized = false
function initKeydownForwarding() {
if (keydownForwardingInitialized) return
keydownForwardingInitialized = true
function isEditableElement(el: Element | null): boolean {
if (!el) return false
const tag = el.tagName.toUpperCase()
if (tag === 'INPUT' || tag === 'TEXTAREA') return true
if (el instanceof HTMLElement && el.isContentEditable) return true
return false
}
document.addEventListener(
'keydown',
(e) => {
const canvas = app.canvas
if (!canvas) return
if (e.target === canvas.canvas) return
canvas.processKey(e)
},
true
)
function initSpacebarForwarding() {
if (spacebarForwardingInitialized) return
spacebarForwardingInitialized = true
const { space } = useMagicKeys()
const activeElement = useActiveElement()
watch(space, (isPressed) => {
const canvas = app.canvas
if (!canvas) return
// Skip if canvas has focus (litegraph handles it) or if in editable element
if (activeElement.value === canvas.canvas) return
if (isEditableElement(activeElement.value!)) return
// Mirror litegraph's processKey behavior for spacebar
if (isPressed) {
canvas.read_only = true
// Set dragging_canvas based on current pointer state
if (canvas.pointer?.isDown) {
canvas.dragging_canvas = true
}
} else {
canvas.read_only = false
canvas.dragging_canvas = false
}
})
}
export function useNodePointerInteractions(
nodeIdRef: MaybeRefOrGetter<string>
) {
initKeydownForwarding()
// Initialize spacebar forwarding for panning when Vue nodes have focus
initSpacebarForwarding()
const { startDrag, endDrag, handleDrag } = useNodeDrag()
// Use canvas interactions for proper wheel event handling and pointer event capture control

View File

@@ -293,9 +293,6 @@ export function useSlotLinkInteraction({
raf.cancel()
dragContext.dispose()
clearCompatible()
if (app.canvas?.pointer) {
app.canvas.pointer.isDown = false
}
}
const updatePointerState = (event: PointerEvent) => {
@@ -414,7 +411,10 @@ export function useSlotLinkInteraction({
if (!pointerSession.matches(event)) return
const canvas = app.canvas
if (canvas?.read_only && canvas.dragging_canvas) {
// When spacebar is held (read_only=true) and left mouse button is down,
// delegate to litegraph's processMouseMove for panning
const isLeftButtonDown = (event.buttons & 1) !== 0
if (canvas?.read_only && isLeftButtonDown) {
canvas.processMouseMove(event)
}
@@ -712,9 +712,7 @@ export function useSlotLinkInteraction({
)
pointerSession.begin(event.pointerId)
if (canvas.pointer) {
canvas.pointer.isDown = true
}
// Update last_mouse so panning delta calculations are correct
canvas.last_mouse = [event.clientX, event.clientY]
toCanvasPointerEvent(event)