diff --git a/packages/frontend/@n8n/design-system/src/components/N8nCommandBar/CommandBar.vue b/packages/frontend/@n8n/design-system/src/components/N8nCommandBar/CommandBar.vue
index 4ed933cd9d0..8867446d85e 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nCommandBar/CommandBar.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nCommandBar/CommandBar.vue
@@ -152,20 +152,22 @@ const scrollSelectedIntoView = () => {
});
};
-// Watch instead of imperative open so parents can open via the `open` model too
+// Watch instead of imperative open/close so parents can drive the `open` model too
watch(isOpen, async (open) => {
- if (!open) return;
- selectedIndex.value = 0;
- inputValue.value = '';
- await nextTick();
- inputRef.value?.focus();
+ if (open) {
+ selectedIndex.value = 0;
+ inputValue.value = '';
+ await nextTick();
+ inputRef.value?.focus();
+ } else {
+ selectedIndex.value = -1;
+ inputValue.value = '';
+ currentParentId.value = null;
+ }
});
const closeCommandBar = () => {
isOpen.value = false;
- selectedIndex.value = -1;
- inputValue.value = '';
- currentParentId.value = null;
};
const navigateToChildren = (item: CommandBarItem) => {
diff --git a/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.test.ts b/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.test.ts
index 053457168a1..2f1f7044b67 100644
--- a/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.test.ts
+++ b/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.test.ts
@@ -6,6 +6,7 @@ import { type MockedStore, mockedStore } from '@/__tests__/utils';
import { defaultSettings } from '@/__tests__/defaults';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { commandBarEventBus } from '@/features/shared/commandBar/commandBar.eventBus';
+import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
import AppCommandBar from './AppCommandBar.vue';
vi.mock('@/app/utils/rbac/permissions', () => ({
@@ -65,6 +66,21 @@ describe('AppCommandBar', () => {
await waitFor(() => expect(getByTestId('command-bar-stub').dataset.open).toBe('true'));
});
+ it('closes the command bar on Cmd+F and refocuses the canvas', async () => {
+ const emitSpy = vi.spyOn(canvasEventBus, 'emit');
+ const { getByTestId } = renderComponent({ pinia });
+
+ commandBarEventBus.emit('open:request');
+ await waitFor(() => expect(getByTestId('command-bar-stub').dataset.open).toBe('true'));
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'f', metaKey: true, cancelable: true }),
+ );
+
+ await waitFor(() => expect(getByTestId('command-bar-stub').dataset.open).toBe('false'));
+ await waitFor(() => expect(emitSpy).toHaveBeenCalledWith('focus'));
+ });
+
it('does not render the command bar in canvas-only mode', () => {
settingsStore.settings = { ...defaultSettings, canvasOnly: true };
diff --git a/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.vue b/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.vue
index 3bb24772cef..c4bee198777 100644
--- a/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.vue
+++ b/packages/frontend/editor-ui/src/app/components/app/AppCommandBar.vue
@@ -1,6 +1,7 @@
diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts b/packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts
index 8138f3c18f5..58c9d0248d6 100644
--- a/packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts
+++ b/packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts
@@ -280,6 +280,8 @@ export type CanvasEventBusEvents = {
};
'create:sticky': never;
'deprecated:tab-shortcut': never;
+ /** Move keyboard focus back to the canvas element */
+ focus: never;
};
export interface CanvasNodeInjectionData {
diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue
index a3a3d281bee..2c8acbcd026 100644
--- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue
+++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue
@@ -233,6 +233,7 @@ const {
addSelectedNodes,
removeSelectedNodes,
viewportRef,
+ vueFlowRef,
fitView,
fitBounds,
zoomIn,
@@ -1673,6 +1674,10 @@ function onRequestSetConnectionsOnInit(connections: IConnections) {
pendingConnections = connections;
}
+function onFocusCanvas() {
+ vueFlowRef.value?.focus();
+}
+
onMounted(() => {
props.eventBus.on('fitView', onFitView);
props.eventBus.on('fitView:onNodesInit', onRequestFitViewOnInit);
@@ -1680,6 +1685,7 @@ onMounted(() => {
props.eventBus.on('nodes:select', onSelectNodes);
props.eventBus.on('nodes:selectAll', onSelectAllNodes);
props.eventBus.on('tidyUp', onTidyUp);
+ props.eventBus.on('focus', onFocusCanvas);
window.addEventListener('blur', onWindowBlur);
document.addEventListener('visibilitychange', onVisibilityChange);
});
@@ -1691,6 +1697,7 @@ onUnmounted(() => {
props.eventBus.off('nodes:select', onSelectNodes);
props.eventBus.off('nodes:selectAll', onSelectAllNodes);
props.eventBus.off('tidyUp', onTidyUp);
+ props.eventBus.off('focus', onFocusCanvas);
window.removeEventListener('blur', onWindowBlur);
document.removeEventListener('visibilitychange', onVisibilityChange);
});
@@ -1794,6 +1801,7 @@ defineExpose({
:pan-activation-key-code="panningKeyCode"
:disable-keyboard-a11y="true"
:delete-key-code="null"
+ tabindex="-1"
data-test-id="canvas"
@connect-start="onConnectStart"
@connect="onConnect"