Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
<GlobalCommandsProvider>
<RegisterModKOutsideEditable handler={handler} />
<input type='text' />
</GlobalCommandsProvider>
)
;(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(
<GlobalCommandsProvider>
<RegisterModKOutsideEditable handler={handler} />
<div contentEditable />
</GlobalCommandsProvider>
)
;(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(
<GlobalCommandsProvider>
<RegisterModKOutsideEditable handler={handler} />
{/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for a read-only editor */}
<div contentEditable={false} tabIndex={0} />
</GlobalCommandsProvider>
)
;(container.querySelector('[contenteditable]') as HTMLElement).focus()
pressModK()
expect(handler).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type CommandId =
| 'clear-terminal-console'
| 'focus-toolbar-search'
| 'fit-to-view'
| 'toggle-sidebar'

/**
* Static metadata for a global command.
Expand Down Expand Up @@ -92,6 +93,11 @@ export const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
shortcut: 'Mod+Shift+F',
allowInEditable: false,
},
'toggle-sidebar': {
id: 'toggle-sidebar',
shortcut: 'Mod+B',
allowInEditable: false,
},
Comment thread
waleedlatif1 marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Content side={side}>
<p>{label}</p>
{shortcut ? <Tooltip.Shortcut keys={shortcut}>{label}</Tooltip.Shortcut> : <p>{label}</p>}
</Tooltip.Content>
</Tooltip.Root>
)
Expand Down Expand Up @@ -1234,6 +1236,12 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
handleCreateWorkflow()
},
},
{
id: 'toggle-sidebar',
handler: () => {
toggleCollapsed()
},
},
])
)

Expand Down Expand Up @@ -1284,7 +1292,12 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
isCollapsed={isCollapsed}
onExpandSidebar={toggleCollapsed}
/>
<SidebarTooltip label='Collapse sidebar' enabled={!isCollapsed} side='bottom'>
<SidebarTooltip
label='Collapse sidebar'
enabled={!isCollapsed}
side='bottom'
shortcut={isMac ? '⌘B' : 'Ctrl+B'}
>
<button
type='button'
onClick={toggleCollapsed}
Expand Down
Loading
Loading