Skip to content

Commit 4e19471

Browse files
comfy-pr-botchristian-byrneConnor ByrneDrJKL
authored
[backport core/1.51] fix: keep Ctrl/Cmd shortcuts from falling through to the browser (#15478)
Backport of #15066 to `core/1.51` Automatically created by backport workflow. Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Connor Byrne <c.byrne@comfy.org> Co-authored-by: Alexander Brown <drjkl@comfy.org>
1 parent bc5dc48 commit 4e19471

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

browser_tests/tests/defaultKeybindings.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,33 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
236236
await comfyPage.keyboard.press('Escape')
237237
})
238238

239+
for (const { label, key } of [
240+
{ label: 'Ctrl+s', key: 'Control+s' },
241+
{ label: 'Cmd+s', key: 'Meta+s' }
242+
]) {
243+
test(`'${label}' does not fall through to the browser while a dialog is open`, async ({
244+
comfyPage
245+
}) => {
246+
await comfyPage.keyboard.press('Control+s')
247+
await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
248+
249+
await comfyPage.page.evaluate(() => {
250+
window.TestCommand = false
251+
window.addEventListener('keydown', (event: KeyboardEvent) => {
252+
if (event.key === 's') window.TestCommand = event.defaultPrevented
253+
})
254+
})
255+
256+
await comfyPage.keyboard.press(key)
257+
258+
await expect
259+
.poll(() => comfyPage.page.evaluate(() => window.TestCommand))
260+
.toBe(true)
261+
262+
await comfyPage.keyboard.press('Escape')
263+
})
264+
}
265+
239266
test("'Ctrl+o' triggers open workflow", async ({ comfyPage }) => {
240267
// Ctrl+o calls app.ui.loadFile() which clicks a hidden file input.
241268
// Detect the file input click via an event listener.

src/platform/keybindings/keybindingService.dialog.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,15 @@ describe('keybindingService - dialog gate', () => {
5858

5959
function createKeyboardEvent(
6060
key: string,
61-
target: HTMLElement = document.body
61+
target: HTMLElement = document.body,
62+
modifiers: { ctrlKey?: boolean; metaKey?: boolean } = {}
6263
): KeyboardEvent {
6364
const event = new KeyboardEvent('keydown', {
6465
key,
6566
bubbles: true,
66-
cancelable: true
67+
cancelable: true,
68+
...modifiers
6769
})
68-
event.preventDefault = vi.fn()
6970
event.composedPath = vi.fn(() => [target])
7071
return event
7172
}
@@ -104,7 +105,7 @@ describe('keybindingService - dialog gate', () => {
104105
await keybindingService.keybindHandler(event)
105106

106107
expect(mockCommandExecute).not.toHaveBeenCalled()
107-
expect(event.preventDefault).not.toHaveBeenCalled()
108+
expect(event.defaultPrevented).toBe(false)
108109
} finally {
109110
document.body.removeChild(dialog)
110111
}
@@ -121,7 +122,7 @@ describe('keybindingService - dialog gate', () => {
121122
await keybindingService.keybindHandler(event)
122123

123124
expect(mockCommandExecute).not.toHaveBeenCalled()
124-
expect(event.preventDefault).not.toHaveBeenCalled()
125+
expect(event.defaultPrevented).toBe(false)
125126
} finally {
126127
document.body.removeChild(dialog)
127128
}
@@ -138,12 +139,32 @@ describe('keybindingService - dialog gate', () => {
138139
await keybindingService.keybindHandler(event)
139140

140141
expect(mockCommandExecute).not.toHaveBeenCalled()
141-
expect(event.preventDefault).not.toHaveBeenCalled()
142+
expect(event.defaultPrevented).toBe(false)
142143
} finally {
143144
document.body.removeChild(dialog)
144145
}
145146
})
146147

148+
it.for([
149+
{ label: 'Ctrl+S', modifiers: { ctrlKey: true } },
150+
{ label: 'Meta+S', modifiers: { metaKey: true } }
151+
] as {
152+
label: string
153+
modifiers: { ctrlKey?: boolean; metaKey?: boolean }
154+
}[])(
155+
'still suppresses the browser default for $label while a dialog is open',
156+
async ({ modifiers }) => {
157+
const dialogStore = useDialogStore()
158+
dialogStore.dialogStack.push(createTestDialogInstance('templates-dialog'))
159+
160+
const event = createKeyboardEvent('s', document.body, modifiers)
161+
await keybindingService.keybindHandler(event)
162+
163+
expect(mockCommandExecute).not.toHaveBeenCalled()
164+
expect(event.defaultPrevented).toBe(true)
165+
}
166+
)
167+
147168
it('executes a global keybinding while a reka popover is open', async () => {
148169
const popper = document.createElement('div')
149170
popper.setAttribute('data-reka-popper-content-wrapper', '')

src/platform/keybindings/keybindingService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export function useKeybindingService() {
5151
}
5252
}
5353
if (isModalOpen(dialogStore.dialogStack.length)) {
54+
// Bare keys still have to reach inputs inside the dialog.
55+
if (keyCombo.ctrl) {
56+
event.preventDefault()
57+
}
5458
return
5559
}
5660

0 commit comments

Comments
 (0)