From f8559c6c13a9bbadd948900732e8d272fe59f13e Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 22 Apr 2023 20:49:57 -0700 Subject: [PATCH] Create useKeyPress --- src/index.ts | 1 + src/use-key-press/README.md | 40 +++++++++++++++ src/use-key-press/index.ts | 1 + src/use-key-press/use-key-press.spec.ts | 13 +++++ src/use-key-press/use-key-press.ts | 65 +++++++++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 src/use-key-press/README.md create mode 100644 src/use-key-press/index.ts create mode 100644 src/use-key-press/use-key-press.spec.ts create mode 100644 src/use-key-press/use-key-press.ts diff --git a/src/index.ts b/src/index.ts index 0dce61f..9faecab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export * from "./use-debounce-effect"; export * from "./use-debounce-state"; export * from "./use-event-listener"; export * from "./use-interval"; +export * from "./use-key-press"; export * from "./use-latest"; export * from "./use-lifetime"; export * from "./use-memoized-callback"; diff --git a/src/use-key-press/README.md b/src/use-key-press/README.md new file mode 100644 index 0000000..df82dd9 --- /dev/null +++ b/src/use-key-press/README.md @@ -0,0 +1,40 @@ +## 🪝 `useKeyPress` + +```ts +function useKeyPress(...keyCodes: KeyCodes[]): boolean; +``` + +Returns `true` if any of the given keys or shortcuts are pressed. The hook expects one or more key codes, which can be: + +- A single key, like `"Space"` +- A combination of keys, like `"Space+W"` +- An array of keys, like `["Space", "W"]` + +Each combination is treated as its own shortcut. If passed more than one key combination, the hook will return `true` if any of the combinations are pressed. + +### 📕 Parameters + +- `keyCodes` - One or more key codes. + +### 📗 Returns + +- Whether any of the given keys or shortcuts are pressed. + +### 📘 Example + +```tsx +function Keyboard() { + const spacePressed = useKeyPress("Space"); + const ctrlAPressed = useKeyPress("LeftControl+A", "RightControl+A"); + + useEffect(() => { + print(`Space pressed: ${spacePressed}`); + }, [spacePressed]); + + useEffect(() => { + print(`Ctrl+A pressed: ${ctrlAPressed}`); + }, [ctrlAPressed]); + + return undefined!; +} +``` diff --git a/src/use-key-press/index.ts b/src/use-key-press/index.ts new file mode 100644 index 0000000..c595813 --- /dev/null +++ b/src/use-key-press/index.ts @@ -0,0 +1 @@ +export * from "./use-key-press"; diff --git a/src/use-key-press/use-key-press.spec.ts b/src/use-key-press/use-key-press.spec.ts new file mode 100644 index 0000000..1524443 --- /dev/null +++ b/src/use-key-press/use-key-press.spec.ts @@ -0,0 +1,13 @@ +/// + +import { renderHook } from "../utils/testez"; +import { useKeyPress } from "./use-key-press"; + +export = () => { + it("should return a boolean", () => { + const { result, unmount } = renderHook(() => useKeyPress("W")); + expect(result.current).to.be.a("boolean"); + expect(result.current).to.equal(false); + unmount(); + }); +}; diff --git a/src/use-key-press/use-key-press.ts b/src/use-key-press/use-key-press.ts new file mode 100644 index 0000000..edecf28 --- /dev/null +++ b/src/use-key-press/use-key-press.ts @@ -0,0 +1,65 @@ +import { InferEnumNames } from "@rbxts/roact"; +import { useMemo, useState } from "@rbxts/roact-hooked"; +import { UserInputService } from "@rbxts/services"; +import { useEventListener } from "../use-event-listener"; + +/** + * A single key code name. + */ +export type KeyCode = InferEnumNames; + +/** + * A single key code or a combination of key codes. + */ +export type KeyCodes = KeyCode | `${KeyCode}+${string}` | KeyCode[]; + +/** + * Returns whether the passed key or shortcut is pressed. The hook expects one + * or more key code, which can be: + * + * - A single key code `"W"` + * - A combination of key codes `"W+Space"` + * - An array of key codes `["W", "Space"]` + * + * Each combination is treated as its own shortcut. If passed more than one + * combination, the hook will return `true` if any of the combinations are + * pressed. + * + * @param keyCodes The key code or combination of key codes to listen for. + * @returns Whether the key or combination of keys is pressed. + */ +export function useKeyPress(...keyCodes: KeyCodes[]) { + const [pressed, setPressed] = useState(false); + + const keys = useMemo(() => { + if (typeIs(keyCodes, "string")) { + return keyCodes.split("+") as KeyCode[]; + } else { + return keyCodes as KeyCode[]; + } + }, keyCodes); + + const keysDown = useMemo(() => { + return new Set(); + }, keyCodes); + + const updatePressed = () => { + setPressed(keys.every((key) => keysDown.has(key))); + }; + + useEventListener(UserInputService.InputBegan, (input, gameProcessed) => { + if (!gameProcessed && keys.includes(input.KeyCode.Name)) { + keysDown.add(input.KeyCode.Name); + updatePressed(); + } + }); + + useEventListener(UserInputService.InputEnded, (input) => { + if (keys.includes(input.KeyCode.Name)) { + keysDown.delete(input.KeyCode.Name); + updatePressed(); + } + }); + + return pressed; +}