From 4f46fb00f0128b78c991c5f8e9e14804662f5a0b Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 4 Apr 2026 03:31:46 +0500 Subject: [PATCH] Fix: Apply 'once' logic only on hotkey match --- src/hotkey.ts | 16 +++++++++--- tests/hotkey.spec.js | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/hotkey.ts b/src/hotkey.ts index ac03df2..3c1f3c0 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -78,17 +78,27 @@ export function registerHotkey( target = document.querySelector(target) ?? error(`No element found for selector '${target}'`); } - return listen(target, eventName, function (this: EventTarget, e: KeyboardEvent) { - if (!(options as HotkeyEventListenerOptions)?.trusted || e.isTrusted) { + const require_trusted = (options as HotkeyEventListenerOptions)?.trusted; + const once = (options as HotkeyEventListenerOptions)?.once; + + const native_options = options && typeof options === "object" + ? { capture: options.capture, passive: options.passive } + : options; + + const unregister = listen(target, eventName, function (this: EventTarget, e: KeyboardEvent) { + if (!require_trusted || e.isTrusted) { if (!(e.target as HTMLElement)?.closest("[data-hotkey-ignore]")) { if (info.code === e.code.toUpperCase()) { if (control_keys.every(n => info[n as keyof Hotkey] === e[n as keyof KeyboardEvent])) { + once && unregister(); handler.call(this, e); } } } } - } as EventListener, options); + } as EventListener, native_options); + + return unregister; } function describe(hotkey: string): Hotkey { diff --git a/tests/hotkey.spec.js b/tests/hotkey.spec.js index fcf605b..f3aa8a7 100644 --- a/tests/hotkey.spec.js +++ b/tests/hotkey.spec.js @@ -339,3 +339,63 @@ test("should not trigger on untrusted events when 'trusted' option is true", asy const triggered = await page.evaluate(() => window.hotkeyTriggered); expect(triggered).toBe(false); }); + +test("should not be consumed by non-matching keys when 'once' option is set", async ({ page }) => { + await page.evaluate(() => { + window.hotkeyCount = 0; + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyCount++; + }, "keydown", { once: true }); + }); + + await page.locator("#text").focus(); + + await page.keyboard.press("A"); + await page.keyboard.press("B"); + + await page.keyboard.press("Control+k"); + + const count = await page.evaluate(() => window.hotkeyCount); + expect(count).toBe(1); +}); + +test("should trigger only once with 'once' option", async ({ page }) => { + await page.evaluate(() => { + window.hotkeyCount = 0; + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyCount++; + }, "keydown", { once: true }); + }); + + await page.locator("#text").focus(); + + // Press the hotkey twice + await page.keyboard.press("Control+k"); + await page.keyboard.press("Control+k"); + + const count = await page.evaluate(() => window.hotkeyCount); + expect(count).toBe(1); +}); + +test("should trigger multiple times without 'once' option", async ({ page }) => { + await page.evaluate(() => { + window.hotkeyCount = 0; + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyCount++; + }); + }); + + await page.locator("#text").focus(); + + await page.keyboard.press("Control+k"); + await page.keyboard.press("Control+k"); + + const count = await page.evaluate(() => window.hotkeyCount); + expect(count).toBe(2); +});