diff --git a/src/hotkey.ts b/src/hotkey.ts index f48f448..566041f 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -44,6 +44,16 @@ interface Hotkey { metaKey: boolean; } +/** + * Extended options for hotkey event listeners. + */ +export interface HotkeyEventListenerOptions extends AddEventListenerOptions { + /** + * If specified, ensures that only trusted events are handled. + */ + trusted?: boolean; +} + /** * Registers a hotkey on the specified target element. * @@ -51,7 +61,7 @@ interface Hotkey { * @param {string} hotkey - The combination of keys for the hotkey, e.g., "Ctrl+Alt+Delete". * @param {(e: KeyboardEvent) => void} handler - The function to be called when the hotkey is triggered. * @param {string} [eventName="keydown"] - The name of the event to listen for (default is "keydown"). - * @param {AddEventListenerOptions | boolean | undefined} [options] - Additional options for the event listener. + * @param {HotkeyEventListenerOptions | boolean | undefined} [options] - Additional options for the event listener. * * @returns {() => void} - A function to unregister the hotkey. */ @@ -60,7 +70,7 @@ export function registerHotkey( hotkey: string, handler: (e: KeyboardEvent) => void, eventName: string = "keydown", - options?: AddEventListenerOptions | boolean | undefined): () => void { + options?: HotkeyEventListenerOptions | boolean | undefined): () => void { const info = describe(hotkey); @@ -69,10 +79,12 @@ export function registerHotkey( } return listen(target, eventName, function (this: EventTarget, e: KeyboardEvent) { - 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])) { - handler.call(this, e); + if (!(options as HotkeyEventListenerOptions)?.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])) { + handler.call(this, e); + } } } } diff --git a/tests/hotkey.spec.js b/tests/hotkey.spec.js index 6fba396..fcf605b 100644 --- a/tests/hotkey.spec.js +++ b/tests/hotkey.spec.js @@ -249,3 +249,93 @@ test("should thrown when target selector is invalid", async ({ page }) => { }); })).rejects.toThrow("No element found for selector '#unknown'"); }); + +test("should only trigger on trusted events when 'trusted' option is set to true", async ({ page }) => { + await page.evaluate(() => { + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyTriggered = true; + }, "keydown", { trusted: true }); + }); + + await page.locator("#text").focus(); + + await page.keyboard.down("Control"); + await page.keyboard.press("K"); + + const triggered = await page.evaluate(() => window.hotkeyTriggered); + expect(triggered).toBe(true); +}); + +test("should trigger on untrusted events when 'trusted' option is not set", async ({ page }) => { + await page.evaluate(() => { + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyTriggered = true; + }); + + const event = new KeyboardEvent("keydown", { + key: "k", + code: "KeyK", + ctrlKey: true, + bubbles: true + }); + + el.dispatchEvent(event); + }); + + await page.locator("#text").focus(); + + const triggered = await page.evaluate(() => window.hotkeyTriggered); + expect(triggered).toBe(true); +}); + +test("should trigger on untrusted events when 'trusted' option is false", async ({ page }) => { + await page.evaluate(() => { + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", () => { + window.hotkeyTriggered = true; + }, "keydown", { trusted: false }); + + const event = new KeyboardEvent("keydown", { + key: "k", + code: "KeyK", + ctrlKey: true, + bubbles: true + }); + + el.dispatchEvent(event); + }); + + await page.locator("#text").focus(); + + const triggered = await page.evaluate(() => window.hotkeyTriggered); + expect(triggered).toBe(true); +}); + +test("should not trigger on untrusted events when 'trusted' option is true", async ({ page }) => { + await page.evaluate(() => { + const el = document.getElementById("text"); + + window.registerHotkey(el, "Ctrl + K", e => { + window.hotkeyTriggered = true; + }, "keydown", { trusted: true }); + + const event = new KeyboardEvent("keydown", { + key: "k", + code: "KeyK", + ctrlKey: true, + bubbles: true + }); + + el.dispatchEvent(event); + }); + + await page.locator("#text").focus(); + + const triggered = await page.evaluate(() => window.hotkeyTriggered); + expect(triggered).toBe(false); +});