mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 22:59:14 +00:00
[fix] Stop widget scroll events from being intercepted by LiteGraph
This commit is contained in:
@@ -16,6 +16,7 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { usePreserveWidgetScroll } from '@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
|
||||
@@ -24,6 +25,9 @@ import { electronAPI, isElectron } from './utils/envUtil'
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
|
||||
// Preserve native scrolling in Vue widgets
|
||||
usePreserveWidgetScroll()
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
const PRIMEVUE_SCROLLABLE_CLASSES = new Set([
|
||||
'p-select-option',
|
||||
'p-dropdown-item'
|
||||
])
|
||||
|
||||
const PRIMEVUE_SCROLLABLE_CONTAINERS = [
|
||||
'.p-select',
|
||||
'.p-multiselect',
|
||||
'.p-treeselect',
|
||||
'.p-select-dropdown',
|
||||
'.p-dropdown-panel'
|
||||
].join(', ')
|
||||
|
||||
/**
|
||||
* Check if an element should handle wheel events natively (scrolling)
|
||||
* instead of letting them bubble to canvas zoom
|
||||
*/
|
||||
function isScrollableElement(target: Element): boolean {
|
||||
// Check common scrollable elements
|
||||
const tagName = target.tagName.toLowerCase()
|
||||
if (tagName === 'textarea' || tagName === 'select' || tagName === 'input') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check PrimeVue select options and other dropdown elements
|
||||
for (const className of target.classList) {
|
||||
if (PRIMEVUE_SCROLLABLE_CLASSES.has(className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (target.closest(PRIMEVUE_SCROLLABLE_CONTAINERS)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for elements with scrollable overflow
|
||||
const computedStyle = window.getComputedStyle(target)
|
||||
const overflowY = computedStyle.overflowY
|
||||
if (overflowY === 'scroll' || overflowY === 'auto') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if element has scrollable content
|
||||
if (target.scrollHeight > target.clientHeight && overflowY !== 'hidden') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* App-level composable that preserves native scrolling behavior in widgets
|
||||
* by preventing wheel events from bubbling to the canvas zoom handler.
|
||||
* Call once at the app level to enable native scrolling in textareas, selects, etc.
|
||||
*/
|
||||
export function usePreserveWidgetScroll() {
|
||||
useEventListener(
|
||||
window,
|
||||
'wheel',
|
||||
(event: WheelEvent) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
isScrollableElement(event.target)
|
||||
) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
},
|
||||
{ capture: true, passive: false }
|
||||
)
|
||||
}
|
||||
117
tests-ui/tests/composables/usePreserveWidgetScroll.test.ts
Normal file
117
tests-ui/tests/composables/usePreserveWidgetScroll.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock VueUse
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useEventListener: vi.fn()
|
||||
}))
|
||||
|
||||
describe('usePreserveWidgetScroll', () => {
|
||||
let mockUseEventListener: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseEventListener = vi.mocked(useEventListener)
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should set up window wheel event listener with capture and passive options', async () => {
|
||||
const { usePreserveWidgetScroll } = await import(
|
||||
'@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||
)
|
||||
|
||||
usePreserveWidgetScroll()
|
||||
|
||||
expect(mockUseEventListener).toHaveBeenCalledWith(
|
||||
window,
|
||||
'wheel',
|
||||
expect.any(Function),
|
||||
{ capture: true, passive: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('should call stopPropagation on textarea wheel events', async () => {
|
||||
let wheelHandler: (event: WheelEvent) => void
|
||||
mockUseEventListener.mockImplementation((_target, _event, handler) => {
|
||||
wheelHandler = handler
|
||||
})
|
||||
|
||||
const { usePreserveWidgetScroll } = await import(
|
||||
'@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||
)
|
||||
usePreserveWidgetScroll()
|
||||
|
||||
// Create real DOM textarea element and dispatch wheel event
|
||||
const textarea = document.createElement('textarea')
|
||||
document.body.appendChild(textarea)
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', { bubbles: true })
|
||||
const stopPropagation = vi.fn()
|
||||
wheelEvent.stopPropagation = stopPropagation
|
||||
|
||||
Object.defineProperty(wheelEvent, 'target', { value: textarea })
|
||||
|
||||
wheelHandler!(wheelEvent)
|
||||
|
||||
expect(stopPropagation).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call stopPropagation on non-scrollable elements', async () => {
|
||||
let wheelHandler: (event: WheelEvent) => void
|
||||
mockUseEventListener.mockImplementation((_target, _event, handler) => {
|
||||
wheelHandler = handler
|
||||
})
|
||||
|
||||
const { usePreserveWidgetScroll } = await import(
|
||||
'@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||
)
|
||||
usePreserveWidgetScroll()
|
||||
|
||||
// Create regular div element
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', { bubbles: true })
|
||||
const stopPropagation = vi.fn()
|
||||
wheelEvent.stopPropagation = stopPropagation
|
||||
|
||||
Object.defineProperty(wheelEvent, 'target', { value: div })
|
||||
|
||||
wheelHandler!(wheelEvent)
|
||||
|
||||
expect(stopPropagation).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle PrimeVue select dropdown elements', async () => {
|
||||
let wheelHandler: (event: WheelEvent) => void
|
||||
mockUseEventListener.mockImplementation((_target, _event, handler) => {
|
||||
wheelHandler = handler
|
||||
})
|
||||
|
||||
const { usePreserveWidgetScroll } = await import(
|
||||
'@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||
)
|
||||
usePreserveWidgetScroll()
|
||||
|
||||
// Create element with PrimeVue select option class
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('p-select-option')
|
||||
document.body.appendChild(div)
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', { bubbles: true })
|
||||
const stopPropagation = vi.fn()
|
||||
wheelEvent.stopPropagation = stopPropagation
|
||||
|
||||
Object.defineProperty(wheelEvent, 'target', { value: div })
|
||||
|
||||
wheelHandler!(wheelEvent)
|
||||
|
||||
expect(stopPropagation).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user