Skip to content

Commit ced099a

Browse files
committed
feat(rich-md-editor): let focused editors claim shortcuts from the global command registry
1 parent 8842ad7 commit ced099a

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act, type ReactNode } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockIsMac } = vi.hoisted(() => ({ mockIsMac: vi.fn(() => false) }))
9+
vi.mock('@/lib/core/utils/platform', () => ({ isMacPlatform: mockIsMac }))
10+
vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
11+
12+
import {
13+
GlobalCommandsProvider,
14+
useRegisterGlobalCommands,
15+
} from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
16+
17+
function RegisterModK({ handler }: { handler: () => void }) {
18+
useRegisterGlobalCommands([{ id: 'search', shortcut: 'Mod+K', handler }])
19+
return null
20+
}
21+
22+
let container: HTMLDivElement
23+
let root: Root
24+
25+
function mount(ui: ReactNode) {
26+
act(() => {
27+
root.render(ui)
28+
})
29+
}
30+
31+
/** Non-mac (mocked): `Mod` resolves to Ctrl, so Ctrl+K matches a `Mod+K` shortcut. */
32+
function pressModK() {
33+
window.dispatchEvent(
34+
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true, cancelable: true })
35+
)
36+
}
37+
38+
beforeEach(() => {
39+
container = document.createElement('div')
40+
document.body.appendChild(container)
41+
root = createRoot(container)
42+
})
43+
44+
afterEach(() => {
45+
act(() => root.unmount())
46+
container.remove()
47+
vi.clearAllMocks()
48+
})
49+
50+
describe('GlobalCommandsProvider owned-shortcut yielding', () => {
51+
it('fires a global command when nothing owns the shortcut', () => {
52+
const handler = vi.fn()
53+
mount(
54+
<GlobalCommandsProvider>
55+
<RegisterModK handler={handler} />
56+
</GlobalCommandsProvider>
57+
)
58+
pressModK()
59+
expect(handler).toHaveBeenCalledTimes(1)
60+
})
61+
62+
it('yields the shortcut to a focused element that declares it owns it', () => {
63+
const handler = vi.fn()
64+
mount(
65+
<GlobalCommandsProvider>
66+
<RegisterModK handler={handler} />
67+
{/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for the editor */}
68+
<div data-owned-shortcuts='Mod+K' tabIndex={0} />
69+
</GlobalCommandsProvider>
70+
)
71+
;(container.querySelector('[data-owned-shortcuts]') as HTMLElement).focus()
72+
pressModK()
73+
expect(handler).not.toHaveBeenCalled()
74+
})
75+
76+
it('still fires when the focused element owns only a different shortcut', () => {
77+
const handler = vi.fn()
78+
mount(
79+
<GlobalCommandsProvider>
80+
<RegisterModK handler={handler} />
81+
{/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for the editor */}
82+
<div data-owned-shortcuts='Mod+B' tabIndex={0} />
83+
</GlobalCommandsProvider>
84+
)
85+
;(container.querySelector('[data-owned-shortcuts]') as HTMLElement).focus()
86+
pressModK()
87+
expect(handler).toHaveBeenCalledTimes(1)
88+
})
89+
})

apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ function matchesShortcut(e: KeyboardEvent, parsed: ParsedShortcut): boolean {
8686
)
8787
}
8888

89+
/** Platform-resolved signature of a shortcut, so `Mod+K`, `Cmd+K`, and `Meta+K` compare equal on mac. */
90+
function shortcutSignature(parsed: ParsedShortcut, isMac: boolean): string {
91+
const ctrl = parsed.ctrl || (parsed.mod ? !isMac : false)
92+
const meta = parsed.meta || (parsed.mod ? isMac : false)
93+
return `${parsed.key}|${+ctrl}|${+meta}|${+!!parsed.shift}|${+!!parsed.alt}`
94+
}
95+
96+
/**
97+
* Whether the focused element (or an ancestor) declares it owns `parsed` via a comma-separated
98+
* `data-owned-shortcuts` attribute (e.g. a rich-text editor that binds `Mod+K` to links). Such a
99+
* shortcut is left for that element to handle instead of firing the global command.
100+
*/
101+
function focusedElementOwnsShortcut(parsed: ParsedShortcut, isMac: boolean): boolean {
102+
const active = document.activeElement
103+
const owner = active instanceof HTMLElement ? active.closest('[data-owned-shortcuts]') : null
104+
if (!owner) return false
105+
const target = shortcutSignature(parsed, isMac)
106+
return (owner.getAttribute('data-owned-shortcuts') ?? '')
107+
.split(',')
108+
.map((entry) => entry.trim())
109+
.filter(Boolean)
110+
.some((entry) => shortcutSignature(parseShortcut(entry), isMac) === target)
111+
}
112+
89113
export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
90114
const registryRef = useRef<Map<string, RegistryCommand>>(new Map())
91115
const isMac = useMemo(() => isMacPlatform(), [])
@@ -127,6 +151,8 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
127151
}
128152

129153
if (matchesShortcut(e, cmd.parsed)) {
154+
// A focused rich editor that owns this shortcut (e.g. Mod+K for links) handles it itself.
155+
if (focusedElementOwnsShortcut(cmd.parsed, isMac)) continue
130156
e.preventDefault()
131157
e.stopPropagation()
132158
try {

0 commit comments

Comments
 (0)