mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(global-commands): use isContentEditable for the editable guard (#5623)
* fix(global-commands): use isContentEditable for the editable guard * chore(lint): keep focusable span in editable-guard test with biome-ignore
This commit is contained in:
@@ -94,6 +94,15 @@ describe('GlobalCommandsProvider owned-shortcut yielding', () => {
|
||||
})
|
||||
|
||||
describe('GlobalCommandsProvider editable guard', () => {
|
||||
/**
|
||||
* jsdom does not implement `isContentEditable`, so stub the browser's computed
|
||||
* editability on the element the test focuses.
|
||||
*/
|
||||
function focusWithEditability(element: HTMLElement, isContentEditable: boolean) {
|
||||
Object.defineProperty(element, 'isContentEditable', { value: isContentEditable })
|
||||
element.focus()
|
||||
}
|
||||
|
||||
it('skips a non-editable command when focus is in an input', () => {
|
||||
const handler = vi.fn()
|
||||
mount(
|
||||
@@ -115,9 +124,25 @@ describe('GlobalCommandsProvider editable guard', () => {
|
||||
<div contentEditable />
|
||||
</GlobalCommandsProvider>
|
||||
)
|
||||
;(container.querySelector('[contenteditable]') as HTMLElement).focus()
|
||||
focusWithEditability(container.querySelector('[contenteditable]') as HTMLElement, true)
|
||||
pressModK()
|
||||
expect(handler).toHaveBeenCalledTimes(0)
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips a non-editable command when focus is on a descendant of a contenteditable root', () => {
|
||||
const handler = vi.fn()
|
||||
mount(
|
||||
<GlobalCommandsProvider>
|
||||
<RegisterModKOutsideEditable handler={handler} />
|
||||
<div contentEditable>
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveTabindex: focusable stand-in for a node view inside the editor */}
|
||||
<span tabIndex={0} />
|
||||
</div>
|
||||
</GlobalCommandsProvider>
|
||||
)
|
||||
focusWithEditability(container.querySelector('span') as HTMLElement, true)
|
||||
pressModK()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fires a non-editable command when focus is in a contenteditable="false" element', () => {
|
||||
@@ -129,7 +154,7 @@ describe('GlobalCommandsProvider editable guard', () => {
|
||||
<div contentEditable={false} tabIndex={0} />
|
||||
</GlobalCommandsProvider>
|
||||
)
|
||||
;(container.querySelector('[contenteditable]') as HTMLElement).focus()
|
||||
focusWithEditability(container.querySelector('[contenteditable]') as HTMLElement, false)
|
||||
pressModK()
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -95,14 +95,13 @@ function shortcutSignature(parsed: ParsedShortcut, isMac: boolean): string {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* `isContentEditable` is the browser's computed editability, so focusable descendants of a
|
||||
* `contenteditable` root count as editable, while `contenteditable="false"` (e.g. a rich-text
|
||||
* editor in read-only mode) does not — commands with `allowInEditable: false` still fire there.
|
||||
*/
|
||||
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'
|
||||
return element instanceof HTMLElement && element.isContentEditable
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user