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 @@ -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(
Expand All @@ -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).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).toHaveBeenCalledTimes(0)
expect(handler).not.toHaveBeenCalled()
})

it('fires a non-editable command when focus is in a contenteditable="false" element', () => {
Expand All @@ -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)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
Loading