Compare commits

...

3 Commits

Author SHA1 Message Date
PabloWiedemann
5c36c4a5b7 fix: close node-search modal on Tab instead of cycling focus
The search modal wraps its content in a Reka UI FocusScope, so Tab cycled
focus through the chips/input/list. Combined with the new Tab-toggles-search
keybinding, Tab behaved inconsistently: while the auto-focused input held
focus the global binding was skipped (Tab is reserved by text inputs), so
the modal didn't close until focus had moved off the input.

Intercept Tab at the modal root in the capture phase (runs before
FocusScope) and emit close. Ctrl/Cmd/Alt+Tab are left to the browser.
Auto-focus on the search input is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:35:34 -07:00
PabloWiedemann
7c5c0561ee fix: scope Tab search-box binding to the canvas container
Address accessibility (WCAG 2.1.1) feedback: a global bare Tab binding
would hijack sequential focus navigation across all UI controls. Scope it
to graph-canvas-container (same pattern as Delete, Fit View, etc.) so Tab
only opens the search box when focus is on the canvas, leaving normal Tab
navigation intact everywhere else.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:35:20 -07:00
PabloWiedemann
783cd30d64 feat: set default Tab keybinding for Toggle Search Box
The Toggle Search Box command (double-click node search modal) had no
default keybinding. Bind it to Tab to match the node-search convention of
Houdini, Nuke, and TouchDesigner.

Tab is only intercepted when focus is outside text inputs (the keybinding
handler skips INPUT/TEXTAREA/contenteditable), preserving normal tabbing
while typing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:35:20 -07:00
4 changed files with 57 additions and 1 deletions

View File

@@ -31,6 +31,7 @@
@remove-filter="removeFilter"
@add-node="addNode"
@hover-node="hoveredNodeDef = $event"
@close="closeDialog"
/>
<NodePreviewCard
v-if="hoveredNodeDef && enableNodePreview"

View File

@@ -37,6 +37,7 @@ describe('NodeSearchContent', () => {
const onRemoveFilter =
vi.fn<(f: FuseFilterWithValue<ComfyNodeDefImpl, string>) => void>()
const onAddFilter = vi.fn()
const onClose = vi.fn()
render(NodeSearchContent, {
props: {
filters: [],
@@ -44,6 +45,7 @@ describe('NodeSearchContent', () => {
onHoverNode,
onRemoveFilter,
onAddFilter,
onClose,
...props
},
global: {
@@ -63,7 +65,14 @@ describe('NodeSearchContent', () => {
}
}
})
return { user, onAddNode, onHoverNode, onRemoveFilter, onAddFilter }
return {
user,
onAddNode,
onHoverNode,
onRemoveFilter,
onAddFilter,
onClose
}
}
function mockBookmarks(
@@ -527,6 +536,30 @@ describe('NodeSearchContent', () => {
})
})
describe('close on Tab', () => {
it('should emit close when Tab is pressed from the search input', async () => {
const { user, onClose } = await setupFavorites([
{ name: 'TestNode', display_name: 'Test Node' }
])
await user.click(screen.getByRole('combobox'))
await user.keyboard('{Tab}')
expect(onClose).toHaveBeenCalledTimes(1)
})
it('should not emit close for Ctrl+Tab', async () => {
const { user, onClose } = await setupFavorites([
{ name: 'TestNode', display_name: 'Test Node' }
])
await user.click(screen.getByRole('combobox'))
await user.keyboard('{Control>}{Tab}{/Control}')
expect(onClose).not.toHaveBeenCalled()
})
})
describe('hoverNode emission', () => {
it('should emit hoverNode with the currently selected node', async () => {
const { onHoverNode } = await setupFavorites([

View File

@@ -3,6 +3,7 @@
<div
ref="dialogRef"
class="flex h-[min(80vh,750px)] w-full flex-col overflow-hidden rounded-lg border border-interface-stroke bg-base-background"
@keydown.capture="closeOnTab"
>
<!-- Search input row -->
<NodeSearchInput
@@ -151,8 +152,22 @@ const emit = defineEmits<{
addFilter: [filter: FuseFilterWithValue<ComfyNodeDefImpl, string>]
removeFilter: [filter: FuseFilterWithValue<ComfyNodeDefImpl, string>]
hoverNode: [nodeDef: ComfyNodeDefImpl | null]
close: []
}>()
function closeOnTab(event: KeyboardEvent) {
if (
event.key === 'Tab' &&
!event.ctrlKey &&
!event.metaKey &&
!event.altKey
) {
event.preventDefault()
event.stopPropagation()
emit('close')
}
}
const { t } = useI18n()
const { flags } = useFeatureFlags()
const nodeDefStore = useNodeDefStore()

View File

@@ -36,6 +36,13 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
commandId: 'Workspace.ToggleSidebarTab.workflows'
},
{
combo: {
key: 'Tab'
},
commandId: 'Workspace.SearchBox.Toggle',
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'n'