-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui-hooks): adding useHotkeys (#696)
- Loading branch information
Showing
4 changed files
with
545 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { renderHook } from "@testing-library/react"; | ||
import { describe, expect, it, vi } from "vitest"; | ||
|
||
import { shouldFireEvent, useHotkeys } from "../useHotkeys"; | ||
|
||
const dispatchEvent = (data: any) => { | ||
const event = new KeyboardEvent("keydown", data); | ||
document.documentElement.dispatchEvent(event); | ||
}; | ||
|
||
describe("useHotkey", () => { | ||
it("should listen to document events", () => { | ||
const handler = vi.fn(); | ||
renderHook(() => useHotkeys([["shift+ctrl+S", handler]])); | ||
dispatchEvent({ shiftKey: true, ctrlKey: true, key: "S" }); | ||
expect(handler).toHaveBeenCalled(); | ||
}); | ||
|
||
it("should not fire when keys mismatch", () => { | ||
const handler = vi.fn(); | ||
renderHook(() => useHotkeys([["alt+L", handler]])); | ||
dispatchEvent({ metaKey: true, key: "L" }); | ||
expect(handler).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should not fire when event is no exact match", () => { | ||
const handler = vi.fn(); | ||
renderHook(() => useHotkeys([["mod+P", handler]])); | ||
dispatchEvent({ metaKey: true, altKey: true, key: "P" }); | ||
expect(handler).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe("shouldFireEvent", () => { | ||
it("should return true if event target is not an HTML element", () => { | ||
const event = { target: null } as KeyboardEvent; | ||
expect(shouldFireEvent(event, [])).toBe(true); | ||
}); | ||
|
||
it("should return true if event target is an HTML element and not content editable", () => { | ||
const event = { | ||
target: document.createElement("div"), | ||
} as unknown as KeyboardEvent; | ||
expect(shouldFireEvent(event, [])).toBe(true); | ||
}); | ||
|
||
it("should return false if event target is an ignored tag", () => { | ||
const input = document.createElement("input"); | ||
const event = { target: input } as unknown as KeyboardEvent; | ||
expect(shouldFireEvent(event, ["INPUT"])).toBe(false); | ||
}); | ||
|
||
it("should return true if event target is content editable and triggerOnContentEditable is true", () => { | ||
const div = document.createElement("div"); | ||
div.contentEditable = "true"; | ||
const event = { target: div } as unknown as KeyboardEvent; | ||
expect(shouldFireEvent(event, [], true)).toBe(true); | ||
}); | ||
}); |
326 changes: 326 additions & 0 deletions
326
packages/ui-hooks/src/hooks/__tests__/utilities.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,326 @@ | ||
import { describe, expect, it, vi } from "vitest"; | ||
|
||
import { getHotkeyHandler, getHotkeyMatcher, parseHotkey } from "../utilities"; | ||
import type { HotkeyItemOptions } from "../utilities"; | ||
|
||
describe("@mantine/hooks/use-hot-key/parse-hotkey", () => { | ||
it("should parse hotkey correctly", () => { | ||
expect(parseHotkey("meta+S")).toMatchObject({ | ||
alt: false, | ||
ctrl: false, | ||
meta: true, | ||
mod: false, | ||
shift: false, | ||
key: "s", | ||
}); | ||
|
||
expect(parseHotkey("alt+shift+L")).toMatchObject({ | ||
alt: true, | ||
ctrl: false, | ||
meta: false, | ||
mod: false, | ||
shift: true, | ||
key: "l", | ||
}); | ||
|
||
expect(parseHotkey("mod+K")).toMatchObject({ | ||
alt: false, | ||
ctrl: false, | ||
meta: false, | ||
mod: true, | ||
shift: false, | ||
key: "k", | ||
}); | ||
|
||
expect(parseHotkey("ctrl+shift+alt+K")).toMatchObject({ | ||
alt: true, | ||
ctrl: true, | ||
meta: false, | ||
mod: false, | ||
shift: true, | ||
key: "k", | ||
}); | ||
|
||
expect(parseHotkey("mod+S+A")).toMatchObject({ | ||
alt: false, | ||
ctrl: false, | ||
meta: false, | ||
mod: true, | ||
shift: false, | ||
key: "s", | ||
}); | ||
}); | ||
|
||
it("should detect exact hotkey", () => { | ||
expect( | ||
getHotkeyMatcher("ctrl+alt+I")( | ||
new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
altKey: true, | ||
key: "I", | ||
}), | ||
), | ||
).toBe(true); | ||
|
||
expect( | ||
getHotkeyMatcher("mod+E")( | ||
new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "E", | ||
}), | ||
), | ||
).toBe(true); | ||
|
||
expect( | ||
getHotkeyMatcher("mod+E")( | ||
new KeyboardEvent("keydown", { | ||
metaKey: true, | ||
key: "E", | ||
}), | ||
), | ||
).toBe(true); | ||
|
||
expect( | ||
getHotkeyMatcher("mod+S")( | ||
new KeyboardEvent("keydown", { | ||
metaKey: true, | ||
key: "E", | ||
}), | ||
), | ||
).toBe(false); | ||
|
||
expect( | ||
getHotkeyMatcher("mod+S")( | ||
new KeyboardEvent("keydown", { | ||
shiftKey: true, | ||
metaKey: true, | ||
key: "E", | ||
}), | ||
), | ||
).toBe(false); | ||
|
||
expect( | ||
getHotkeyMatcher("mod+S")( | ||
new KeyboardEvent("keydown", { | ||
metaKey: true, | ||
shiftKey: true, | ||
key: "E", | ||
}), | ||
), | ||
).toBe(false); | ||
|
||
expect( | ||
getHotkeyMatcher("shift+alt+O")( | ||
new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
altKey: true, | ||
shiftKey: true, | ||
key: "O", | ||
}), | ||
), | ||
).toBe(false); | ||
}); | ||
}); | ||
|
||
//---------------------------------------------- | ||
|
||
describe("parseHotkey", () => { | ||
it("should parse a hotkey string into a Hotkey object", () => { | ||
expect(parseHotkey("ctrl+alt+shift+a")).toEqual({ | ||
ctrl: true, | ||
alt: true, | ||
shift: true, | ||
mod: false, | ||
meta: false, | ||
key: "a", | ||
}); | ||
|
||
expect(parseHotkey("mod+b")).toEqual({ | ||
ctrl: false, | ||
alt: false, | ||
shift: false, | ||
mod: true, | ||
meta: false, | ||
key: "b", | ||
}); | ||
|
||
expect(parseHotkey("meta+c")).toEqual({ | ||
ctrl: false, | ||
alt: false, | ||
shift: false, | ||
mod: false, | ||
meta: true, | ||
key: "c", | ||
}); | ||
}); | ||
|
||
it("should handle hotkeys without a free key", () => { | ||
expect(parseHotkey("ctrl+alt+shift")).toEqual({ | ||
ctrl: true, | ||
alt: true, | ||
shift: true, | ||
mod: false, | ||
meta: false, | ||
key: undefined, | ||
}); | ||
}); | ||
}); | ||
|
||
describe("getHotkeyMatcher", () => { | ||
it("should match the correct hotkey event", () => { | ||
const matcher = getHotkeyMatcher("ctrl+alt+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
altKey: true, | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(true); | ||
}); | ||
|
||
it("should not match an incorrect hotkey event", () => { | ||
const matcher = getHotkeyMatcher("ctrl+alt+b"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
altKey: true, | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(false); | ||
}); | ||
|
||
it("should not match if mod key is expected but neither ctrl nor meta is pressed", () => { | ||
const matcher = getHotkeyMatcher("mod+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(false); | ||
}); | ||
|
||
it("should match if mod key is expected and ctrl is pressed", () => { | ||
const matcher = getHotkeyMatcher("mod+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(true); | ||
}); | ||
|
||
it("should match if mod key is expected and meta is pressed", () => { | ||
const matcher = getHotkeyMatcher("mod+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
metaKey: true, | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(true); | ||
}); | ||
|
||
it("should not match if meta key is expected but not pressed", () => { | ||
const matcher = getHotkeyMatcher("meta+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(false); | ||
}); | ||
|
||
it("should match if meta key is expected and pressed", () => { | ||
const matcher = getHotkeyMatcher("meta+a"); | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
metaKey: true, | ||
key: "a", | ||
}); | ||
|
||
expect(matcher(event)).toBe(true); | ||
}); | ||
}); | ||
|
||
describe("getHotkeyHandler", () => { | ||
it("should call the correct handler for a matching hotkey", () => { | ||
const handler = vi.fn(); | ||
const hotkeys: [string, (event: any) => void, HotkeyItemOptions?][] = [ | ||
["ctrl+a", handler], | ||
]; | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "a", | ||
}); | ||
|
||
const hotkeyHandler = getHotkeyHandler(hotkeys); | ||
hotkeyHandler(event); | ||
|
||
expect(handler).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("should prevent default if option is set", () => { | ||
const handler = vi.fn(); | ||
const hotkeys: [string, (event: any) => void, HotkeyItemOptions?][] = [ | ||
["ctrl+a", handler, { preventDefault: true }], | ||
]; | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "a", | ||
}); | ||
|
||
const preventDefaultSpy = vi.spyOn(event, "preventDefault"); | ||
|
||
const hotkeyHandler = getHotkeyHandler(hotkeys); | ||
hotkeyHandler(event); | ||
|
||
expect(preventDefaultSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("should not prevent default if option is not set", () => { | ||
const handler = vi.fn(); | ||
const hotkeys: [string, (event: any) => void, HotkeyItemOptions?][] = [ | ||
["ctrl+a", handler, { preventDefault: false }], | ||
]; | ||
|
||
const event = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "a", | ||
}); | ||
|
||
const preventDefaultSpy = vi.spyOn(event, "preventDefault"); | ||
|
||
const hotkeyHandler = getHotkeyHandler(hotkeys); | ||
hotkeyHandler(event); | ||
|
||
expect(preventDefaultSpy).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should handle events with nativeEvent correctly", () => { | ||
const handler = vi.fn(); | ||
const hotkeys: [string, (event: any) => void, HotkeyItemOptions?][] = [ | ||
["ctrl+a", handler], | ||
]; | ||
|
||
const nativeEvent = new KeyboardEvent("keydown", { | ||
ctrlKey: true, | ||
key: "a", | ||
}); | ||
|
||
const event = { | ||
nativeEvent, | ||
preventDefault: vi.fn(), | ||
}; | ||
|
||
const hotkeyHandler = getHotkeyHandler(hotkeys); | ||
hotkeyHandler(event as unknown as React.KeyboardEvent<HTMLElement>); | ||
|
||
expect(handler).toHaveBeenCalledTimes(1); | ||
expect(event.preventDefault).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
Oops, something went wrong.