Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/clean-cups-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/hotkeys': patch
---

fix: detect ignored input elements across browser realms
28 changes: 16 additions & 12 deletions packages/hotkeys/src/manager.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,36 @@ export function getDefaultIgnoreInputs(parsedHotkey: ParsedHotkey): boolean {
* Mod+S and Escape fire when the user has tabbed to a form button.
*/
export function isInputElement(element: EventTarget | null): boolean {
if (!element) {
if (
!element ||
!('tagName' in element) ||
typeof element.tagName !== 'string'
) {
return false
}

if (element instanceof HTMLInputElement) {
const type = element.type.toLowerCase()
// Do not use instanceof here: elements from another realm (for example an
// iframe) have different DOM constructors than the current global object.
const tagName = element.tagName.toLowerCase()

if (tagName === 'input') {
const type =
'type' in element && typeof element.type === 'string'
? element.type.toLowerCase()
: ''
if (type === 'button' || type === 'submit' || type === 'reset') {
return false
}
return true
}

if (
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement
) {
if (tagName === 'textarea' || tagName === 'select') {
return true
}

// Check for contenteditable elements (includes "true", "", "plaintext-only",
// and inherited contenteditable from ancestor elements)
if (element instanceof HTMLElement && element.isContentEditable) {
return true
}

return false
return 'isContentEditable' in element && element.isContentEditable === true
}

/**
Expand Down
29 changes: 29 additions & 0 deletions packages/hotkeys/tests/manager.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,35 @@ describe('manager.utils', () => {
const div = document.createElement('div')
expect(isInputElement(div)).toBe(false)
})

it('should not rely on current-realm DOM constructors', () => {
const input = document.createElement('input')
input.type = 'text'
const button = document.createElement('input')
button.type = 'button'
const textarea = document.createElement('textarea')
const select = document.createElement('select')
const editable = document.createElement('div')
editable.contentEditable = 'true'
const regular = document.createElement('div')

vi.stubGlobal('HTMLInputElement', class {})
vi.stubGlobal('HTMLTextAreaElement', class {})
vi.stubGlobal('HTMLSelectElement', class {})
vi.stubGlobal('HTMLElement', class {})

try {
expect(input).not.toBeInstanceOf(HTMLInputElement)
expect(isInputElement(input)).toBe(true)
expect(isInputElement(button)).toBe(false)
expect(isInputElement(textarea)).toBe(true)
expect(isInputElement(select)).toBe(true)
expect(isInputElement(editable)).toBe(true)
expect(isInputElement(regular)).toBe(false)
} finally {
vi.unstubAllGlobals()
}
})
})

describe('getActiveElementForListenerTarget', () => {
Expand Down