Files
ComfyUI_frontend/src/platform/keybindings/raisedSurfaceStore.ts
Glary-Bot 16dc701f4c fix: prevent escape from exiting subgraph while a context menu is open
Pressing Escape while a right-click context menu is open inside a
subgraph used to fire the global Comfy.Graph.ExitSubgraph keybinding,
exiting the subgraph while leaving the menu open. The keybinding service
only suppressed Escape when a Pinia dialog was open, so other raised
surfaces leaked the event to window-level handlers.

Introduce a raisedSurfaceStore that tracks open popovers, context menus,
and top-level modals as a single source of truth, and consult it from
the keybinding service alongside the existing dialog check. The legacy
LiteGraph ContextMenu now closes itself on Escape (mirroring its
existing outside-pointerdown handler) and reports open/close lifecycle
via document events so the store stays in sync without monkey patches.
NodeContextMenu registers itself via the new useRaisedSurface composable.

Fixes the bug demonstrated in the attached screencast.
2026-05-15 20:39:05 +00:00

73 lines
1.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { computed, onScopeDispose, ref, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
/**
* Tracks open "raised surfaces" — popovers, context menus, and top-level
* modals — that should suppress global keybindings while they are open.
*
* UX axiom: standard keybindings work in every context EXCEPT when a raised
* surface is open. Consumers register/unregister surfaces here; the keybinding
* service consults {@link isAnyOpen} as its single source of truth.
*/
type RaisedSurfaceKind = 'context-menu' | 'popover' | 'modal'
interface RaisedSurfaceEntry {
id: symbol
kind: RaisedSurfaceKind
}
export const useRaisedSurfaceStore = defineStore('raisedSurface', () => {
const stack = ref<RaisedSurfaceEntry[]>([])
const isAnyOpen = computed(() => stack.value.length > 0)
function open(kind: RaisedSurfaceKind): symbol {
const id = Symbol(kind)
stack.value.push({ id, kind })
return id
}
function close(id: symbol): void {
const index = stack.value.findIndex((entry) => entry.id === id)
if (index !== -1) stack.value.splice(index, 1)
}
return { stack, isAnyOpen, open, close }
})
/**
* Bind a surface's reactive open-state to the raised-surface registry.
*
* @example
* const isOpen = ref(false)
* useRaisedSurface('context-menu', isOpen)
*/
export function useRaisedSurface(
kind: RaisedSurfaceKind,
isOpen: MaybeRefOrGetter<boolean>
): void {
const store = useRaisedSurfaceStore()
let id: symbol | null = null
function release() {
if (id !== null) {
store.close(id)
id = null
}
}
watch(
() => toValue(isOpen),
(open) => {
if (open && id === null) {
id = store.open(kind)
} else if (!open) {
release()
}
},
{ immediate: true, flush: 'sync' }
)
onScopeDispose(release)
}