diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx
index 84dfd588e18..e6bf085f9ff 100644
--- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.test.tsx
@@ -19,6 +19,11 @@ function RegisterModK({ handler }: { handler: () => void }) {
return null
}
+function RegisterModKOutsideEditable({ handler }: { handler: () => void }) {
+ useRegisterGlobalCommands([{ id: 'search', shortcut: 'Mod+K', handler, allowInEditable: false }])
+ return null
+}
+
let container: HTMLDivElement
let root: Root
@@ -87,3 +92,45 @@ describe('GlobalCommandsProvider owned-shortcut yielding', () => {
expect(handler).toHaveBeenCalledTimes(1)
})
})
+
+describe('GlobalCommandsProvider editable guard', () => {
+ it('skips a non-editable command when focus is in an input', () => {
+ const handler = vi.fn()
+ mount(
+
+
+
+
+ )
+ ;(container.querySelector('input') as HTMLInputElement).focus()
+ pressModK()
+ expect(handler).not.toHaveBeenCalled()
+ })
+
+ it('skips a non-editable command when focus is in a contenteditable element', () => {
+ const handler = vi.fn()
+ mount(
+
+
+
+
+ )
+ ;(container.querySelector('[contenteditable]') as HTMLElement).focus()
+ pressModK()
+ expect(handler).toHaveBeenCalledTimes(0)
+ })
+
+ it('fires a non-editable command when focus is in a contenteditable="false" element', () => {
+ const handler = vi.fn()
+ mount(
+
+
+ {/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for a read-only editor */}
+
+
+ )
+ ;(container.querySelector('[contenteditable]') as HTMLElement).focus()
+ pressModK()
+ expect(handler).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
index 039cda318a7..b03ce328735 100644
--- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx
@@ -93,6 +93,18 @@ function shortcutSignature(parsed: ParsedShortcut, isMac: boolean): string {
return `${parsed.key}|${+ctrl}|${+meta}|${+!!parsed.shift}|${+!!parsed.alt}`
}
+/**
+ * Whether `element` is an editable region for the purposes of the editable guard.
+ * `contenteditable="false"` (e.g. a rich-text editor in read-only mode) is not editable,
+ * so commands with `allowInEditable: false` still fire while such an element has focus.
+ */
+function isEditableElement(element: Element | null): boolean {
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) return true
+ if (!(element instanceof HTMLElement)) return false
+ const contentEditable = element.getAttribute('contenteditable')
+ return contentEditable !== null && contentEditable.toLowerCase() !== 'false'
+}
+
/**
* Whether the focused element (or an ancestor) declares it owns `parsed` via a comma-separated
* `data-owned-shortcuts` attribute (e.g. a rich-text editor that binds `Mod+K` to links). Such a
@@ -141,14 +153,7 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
if (e.isComposing) return
for (const [, cmd] of registryRef.current) {
- if (!cmd.allowInEditable) {
- const ae = document.activeElement
- const isEditable =
- ae instanceof HTMLInputElement ||
- ae instanceof HTMLTextAreaElement ||
- ae?.hasAttribute('contenteditable')
- if (isEditable) continue
- }
+ if (!cmd.allowInEditable && isEditableElement(document.activeElement)) continue
if (matchesShortcut(e, cmd.parsed)) {
if (focusedElementOwnsShortcut(cmd.parsed, isMac)) continue
diff --git a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
index 24a6eead3ad..4aabc11bc9e 100644
--- a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
@@ -17,6 +17,7 @@ export type CommandId =
| 'clear-terminal-console'
| 'focus-toolbar-search'
| 'fit-to-view'
+ | 'toggle-sidebar'
/**
* Static metadata for a global command.
@@ -92,6 +93,11 @@ export const COMMAND_DEFINITIONS: Record = {
shortcut: 'Mod+Shift+F',
allowInEditable: false,
},
+ 'toggle-sidebar': {
+ id: 'toggle-sidebar',
+ shortcut: 'Mod+B',
+ allowInEditable: false,
+ },
}
/**
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index 6929727cc15..66a04dfd0d4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -120,18 +120,20 @@ export function SidebarTooltip({
label,
enabled,
side = 'right',
+ shortcut,
}: {
children: React.ReactElement
label: string
enabled: boolean
side?: 'right' | 'bottom'
+ shortcut?: string
}) {
if (!enabled) return children
return (
{children}
- {label}
+ {shortcut ? {label} : {label}
}
)
@@ -1234,6 +1236,12 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
handleCreateWorkflow()
},
},
+ {
+ id: 'toggle-sidebar',
+ handler: () => {
+ toggleCollapsed()
+ },
+ },
])
)
@@ -1284,7 +1292,12 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
isCollapsed={isCollapsed}
onExpandSidebar={toggleCollapsed}
/>
-
+