mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +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 GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
import { usePreserveWidgetScroll } from '@/renderer/extensions/vueNodes/composables/usePreserveWidgetScroll'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ import { electronAPI, isElectron } from './utils/envUtil'
|
|||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const conflictDetection = useConflictDetection()
|
const conflictDetection = useConflictDetection()
|
||||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||||
|
|
||||||
|
// Preserve native scrolling in Vue widgets
|
||||||
|
usePreserveWidgetScroll()
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
workspaceStore.shiftDown = e.shiftKey
|
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