mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
fix: disable global keybindings while a modal dialog is open
Gate keybindHandler on dialogStore.dialogStack so background shortcuts no longer fire when a modal (e.g. Templates) is open. Keybindings whose event target is inside the dialog still execute, preserving dialog-scoped shortcuts. Fixes FE-642
This commit is contained in:
117
src/platform/keybindings/keybindingService.dialog.test.ts
Normal file
117
src/platform/keybindings/keybindingService.dialog.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { markRaw, reactive } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { DialogInstance } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
function createTestDialogInstance(
|
||||
key: string,
|
||||
overrides: Partial<DialogInstance> = {}
|
||||
): DialogInstance {
|
||||
return {
|
||||
key,
|
||||
visible: true,
|
||||
component: markRaw({ template: '<div />' }),
|
||||
contentProps: {},
|
||||
dialogComponentProps: {},
|
||||
priority: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => {
|
||||
const dialogStack = reactive<DialogInstance[]>([])
|
||||
return {
|
||||
useDialogStore: () => ({ dialogStack })
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: null
|
||||
}
|
||||
}))
|
||||
|
||||
describe('keybindingService - dialog gate', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let mockCommandExecute: ReturnType<typeof useCommandStore>['execute']
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
mockCommandExecute = vi.fn()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.dialogStack.length = 0
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
function createKeyboardEvent(
|
||||
key: string,
|
||||
target: HTMLElement = document.body
|
||||
): KeyboardEvent {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [target])
|
||||
return event
|
||||
}
|
||||
|
||||
it('executes a global keybinding when no dialog is open', async () => {
|
||||
const event = createKeyboardEvent('w')
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith(
|
||||
'Workspace.ToggleSidebarTab.workflows'
|
||||
)
|
||||
})
|
||||
|
||||
it('does NOT execute a global keybinding while a dialog is open', async () => {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.dialogStack.push(createTestDialogInstance('templates-dialog'))
|
||||
|
||||
const event = createKeyboardEvent('w')
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still executes a keybinding whose target lives inside the open dialog', async () => {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.dialogStack.push(createTestDialogInstance('templates-dialog'))
|
||||
|
||||
const dialog = document.createElement('div')
|
||||
dialog.setAttribute('role', 'dialog')
|
||||
const inner = document.createElement('button')
|
||||
dialog.appendChild(inner)
|
||||
document.body.appendChild(dialog)
|
||||
|
||||
try {
|
||||
const event = createKeyboardEvent('w', inner)
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith(
|
||||
'Workspace.ToggleSidebarTab.workflows'
|
||||
)
|
||||
} finally {
|
||||
document.body.removeChild(dialog)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -108,7 +108,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute Escape keybinding with modifiers regardless of dialog state', async () => {
|
||||
it('should NOT execute Escape keybinding with modifiers when a dialog is open', async () => {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.dialogStack.push(createTestDialogInstance('test-dialog'))
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
const event = createKeyboardEvent('Escape', { ctrlKey: true })
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Test.CtrlEscape')
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should verify Escape keybinding exists in CORE_KEYBINDINGS', () => {
|
||||
|
||||
@@ -55,6 +55,18 @@ export function useKeybindingService() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block global keybindings from triggering background actions while a
|
||||
* modal dialog is open. Keybindings whose event target lives inside an
|
||||
* open dialog still fire, so dialog-scoped shortcuts keep working.
|
||||
*/
|
||||
if (dialogStore.dialogStack.length > 0) {
|
||||
const inDialog = target.closest?.('[role="dialog"]') != null
|
||||
if (!inDialog) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const runCommandIds = new Set([
|
||||
'Comfy.QueuePrompt',
|
||||
|
||||
Reference in New Issue
Block a user