From 0139987c7b2c295ce155bc97083f0e138d07cc65 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:49:13 -0400 Subject: [PATCH] Respect form fields inside open shadow roots --- src/index.ts | 15 ++++++------ test/test.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index fede7e6..4d26e10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,11 +21,12 @@ const sequenceTracker = new SequenceTracker({ function keyDownHandler(event: KeyboardEvent) { if (event.defaultPrevented) return - if (!(event.target instanceof Node)) return - if (isFormField(event.target)) { - const target = event.target as HTMLElement - if (!target.id) return - if (!target.ownerDocument.querySelector(`[data-hotkey-scope="${target.id}"]`)) return + const target = event.composedPath()[0] ?? event.target + if (!(target instanceof Node)) return + if (isFormField(target)) { + const field = target as HTMLElement + if (!field.id) return + if (!field.ownerDocument.querySelector(`[data-hotkey-scope="${field.id}"]`)) return } // If the user presses a hotkey that doesn't exist in the Trie, @@ -39,7 +40,7 @@ function keyDownHandler(event: KeyboardEvent) { currentTriePosition = newTriePosition if (newTriePosition instanceof Leaf) { - const target = event.target as HTMLElement + const field = target as HTMLElement let shouldFire = false let elementToFire const formField = isFormField(target) @@ -47,7 +48,7 @@ function keyDownHandler(event: KeyboardEvent) { for (let i = newTriePosition.children.length - 1; i >= 0; i -= 1) { elementToFire = newTriePosition.children[i] const scope = elementToFire.getAttribute('data-hotkey-scope') - if ((!formField && !scope) || (formField && target.id === scope)) { + if ((!formField && !scope) || (formField && field.id === scope)) { shouldFire = true break } diff --git a/test/test.js b/test/test.js index d26d497..7362799 100644 --- a/test/test.js +++ b/test/test.js @@ -77,6 +77,75 @@ describe('hotkey', function () { expect(elementsActivated).toEqual([]) }) + it.each(['', '', '', '
'])( + "doesn't trigger global hotkeys from a shadow root form field: %s", + function (html) { + setHTML('') + const root = document.getElementById('host').attachShadow({mode: 'open'}) + root.innerHTML = html + const event = new KeyboardEvent('keydown', {bubbles: true, composed: true, cancelable: true, key: 'b'}) + + root.firstElementChild.dispatchEvent(event) + + expect(elementsActivated).toEqual([]) + expect(event.defaultPrevented).toBe(false) + } + ) + + it("doesn't trigger global hotkeys from a nested shadow root form field", function () { + setHTML('') + const root = document.getElementById('host').attachShadow({mode: 'open'}) + const innerHost = document.createElement('div') + root.append(innerHost) + const innerRoot = innerHost.attachShadow({mode: 'open'}) + innerRoot.innerHTML = '' + + innerRoot.firstElementChild.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, composed: true, key: 'b'})) + + expect(elementsActivated).toEqual([]) + }) + + it('allows a scoped hotkey from a shadow root form field', function () { + setHTML(` + + + `) + const root = document.getElementById('host').attachShadow({mode: 'open'}) + root.innerHTML = '' + const event = new KeyboardEvent('keydown', {bubbles: true, composed: true, cancelable: true, key: 'b'}) + + root.firstElementChild.dispatchEvent(event) + + expect(elementsActivated).toEqual(['button1']) + expect(event.defaultPrevented).toBe(true) + }) + + it("doesn't fall back to a global hotkey from a shadow root form field with another scope", function () { + setHTML(` + + + `) + const root = document.getElementById('host').attachShadow({mode: 'open'}) + root.innerHTML = '' + + root.firstElementChild.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, composed: true, key: 'b'})) + + expect(elementsActivated).toEqual([]) + }) + + it.each(['', ''])( + 'allows global hotkeys from a shadow root non-editable control: %s', + function (html) { + setHTML('') + const root = document.getElementById('host').attachShadow({mode: 'open'}) + root.innerHTML = html + + root.firstElementChild.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, composed: true, key: 'b'})) + + expect(elementsActivated).toEqual(['button1']) + } + ) + it('triggers when user is focused on a file input', function () { setHTML(`