├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src └── use-wasd.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # OS config files 2 | .DS_Store 3 | 4 | # Visual Studio Code 5 | .vscode/* 6 | !.vscode/settings.json 7 | !.vscode/tasks.json 8 | !.vscode/launch.json 9 | !.vscode/extensions.json 10 | !.vscode/*.code-snippets 11 | 12 | # Local History for Visual Studio Code 13 | .history/ 14 | 15 | # Built Visual Studio Code Extensions 16 | *.vsix 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 19 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 20 | .idea 21 | 22 | # dependencies 23 | /node_modules 24 | 25 | # debug 26 | npm-debug.log* 27 | 28 | # bundling 29 | .parcel-cache 30 | dist 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-wad 2 | 3 | [![Version](https://img.shields.io/npm/v/use-wasd?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/use-wasd) 4 | [![Build Size](https://img.shields.io/bundlephobia/minzip/use-wasd?label=bundle%20size&style=flat&colorA=000000&colorB=000000)](<[https://bundlephobia.com/result?p=use-wasd](https://bundlephobia.com/package/use-wasd@2.0.1)>) 5 | [![Downloads](https://img.shields.io/npm/dt/use-wasd.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/use-wasd) 6 | 7 | Easy and agnostic react hook to handle keys and key-combinations on your keyboard. 8 | 9 | ```bash 10 | npm install use-wasd 11 | ``` 12 | 13 | This hook returns an object with the keys and combos and their pressed state. 14 | 15 | ```js 16 | import useWASD from "use-wasd"; 17 | 18 | export default function App() { 19 | const keyboard = useWASD(); 20 | 21 | return ( 22 |
 23 |       {JSON.stringify(keyboard)}
 24 |     
25 | ); 26 | } 27 | ``` 28 | 29 | ![Try it yourself.](https://github.com/doemser/dead-simple-react/blob/main/assets/png/use-wasd-try-it-yourself.png?raw=true) 30 | 31 | --- 32 | 33 | ### Table of Content 34 | 35 | - [Options](#options) 36 | - [allow/block](#allowblock) 37 | - [combos](#combos) 38 | - [initialValue](#initialvalue) 39 | - [preventDefault](#preventdefault) 40 | - [ref](#ref) 41 | - [Performance](#performance) 42 | - [Destructuring](#destructuring) 43 | - [Memoization](#memoization) 44 | - [Examples](#examples) 45 | - [Learn](#learn) 46 | 47 | --- 48 | 49 | ## Options 50 | 51 | You can pass an optional `options` object. 52 | 53 | ```js 54 | const options = { allow: ["w", "a", "s", "d"] }; 55 | 56 | export default function App() { 57 | const { w, a ,s ,d } = useWASD(options); 58 | ... 59 | } 60 | ``` 61 | 62 | Available options are: 63 | 64 | - allow 65 | - block 66 | - combos 67 | - initialValue 68 | - preventDefault 69 | - ref 70 | 71 | --- 72 | 73 | ### allow/block 74 | 75 | You can and should explicitly allow or block keys. 76 | 77 | ```js 78 | const options = { 79 | // either 80 | allow: ["w", "shift", "c"], 81 | // or 82 | block: ["c"], 83 | }; 84 | ``` 85 | 86 | > Do not use both. 87 | 88 | --- 89 | 90 | ### combos 91 | 92 | You can define custom combos. 93 | 94 | ```js 95 | const options = { 96 | allow: ["w", "shift", "space"], 97 | combos: { sprint: ["w", "shift"], sprintJump: ["w", "shift", "space"] } 98 | }; 99 | 100 | export default function App() { 101 | const { sprint, sprintJump } = useWASD(options); 102 | ... 103 | } 104 | ``` 105 | 106 | > You don´t need to also allow combos, it´s enough if the keys for the combo are allowed and not blocked. 107 | 108 | ![Try it yourself.](https://github.com/doemser/dead-simple-react/blob/main/assets/png/use-wasd-try-it-yourself.png?raw=true) 109 | 110 | --- 111 | 112 | ### initialValue 113 | 114 | You can initially fill the object. 115 | 116 | ```js 117 | const options = { 118 | initialValue: { w: true, shift: false, sprint: false }, 119 | }; 120 | ``` 121 | 122 | > Note that the `"keydown"` event will always set keys `true`, while the `"keyup"` event will always set to `false`. Initially setting a key to `true` will not reverse the mechanism. 123 | 124 | ![Try it yourself.](https://github.com/doemser/dead-simple-react/blob/main/assets/png/use-wasd-try-it-yourself.png?raw=true) 125 | 126 | --- 127 | 128 | ### preventDefault 129 | 130 | You can call `event.preventDefault()` to prevent default actions for keys. 131 | 132 | ```js 133 | const options = { preventDefault: ["arrowup", "arrowdown"] }; 134 | ``` 135 | 136 | You can also set it to `true` to prevent the default function for every key. 137 | 138 | ```js 139 | const options = { preventDefault: true }; 140 | ``` 141 | 142 | > Be aware that by doing so you can jeopardize the a11y 143 | 144 | ![Try it yourself.](https://github.com/doemser/dead-simple-react/blob/main/assets/png/use-wasd-try-it-yourself.png?raw=true) 145 | 146 | --- 147 | 148 | ### ref 149 | 150 | By default the EventListener will be added to the `document`, if you want it to be added to another element, you can pass it as `ref`. 151 | 152 | ```js 153 | export default function App() { 154 | const ref = useRef(); 155 | const keyboard = useWASD({...options, ref}); 156 | ... 157 | } 158 | ``` 159 | 160 | ![Try it yourself.](https://github.com/doemser/dead-simple-react/blob/main/assets/png/use-wasd-try-it-yourself.png?raw=true) 161 | 162 | --- 163 | 164 | ## Performance 165 | 166 | ### Destructuring 167 | 168 | > We recommend destructuring the object returned by useWASD. 169 | 170 | ```diff 171 | 172 | export default function App() { 173 | - const keyboard = useWASD(); 174 | + const { w, a ,s ,d } = useWASD(); 175 | ... 176 | } 177 | ``` 178 | 179 | ### Memoization 180 | 181 | > We recommend memoizing the options object. 182 | 183 | Here are 3 common examples of passing the options object: 184 | 185 | 1. Declare it outside the Component. 186 | 187 | ```js 188 | const options = {...}; 189 | 190 | export default function App() { 191 | const keyboard = useWASD(options); 192 | ... 193 | } 194 | ``` 195 | 196 | 2. Using useMemo hook. 197 | 198 | ```js 199 | 200 | export default function App() { 201 | const options = useMemo(() => ({...}), []); 202 | const keyboard = useWASD(options); 203 | ... 204 | } 205 | ``` 206 | 207 | 3. Using useRef hook. 208 | 209 | ```js 210 | 211 | export default function App() { 212 | const options = useRef({...}); 213 | const keyboard = useWASD(options.current); 214 | ... 215 | } 216 | ``` 217 | 218 | Do not pass the object directly into the hook, this would cause unnecessary rerenders. 219 | 220 | ```js 221 | 222 | export default function App() { 223 | const keyboard = useWASD({...}); 224 | ... 225 | } 226 | ``` 227 | 228 | --- 229 | 230 | ## Examples 231 | 232 | [Basic Example](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-basic) 233 | 234 | [combos Example](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-combos) 235 | 236 | [initialValue Example](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-initial-value) 237 | 238 | [preventDefault Example](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-prevent-default) 239 | 240 | [ref Example](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-ref) 241 | 242 | --- 243 | 244 | ## Learn 245 | 246 | [useWASD vanilla source](https://codesandbox.io/s/github/doemser/dead-simple-react/tree/main/examples/use-wasd/use-wasd-vanilla) 247 | 248 | > if you are familiar with typescript, you can also find the source code on [github](https://github.com/doemser/use-wasd/blob/main/src/use-wasd.ts). 249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-wasd", 3 | "version": "2.0.6", 4 | "license": "MIT", 5 | "description": "Easy and agnostic react hook to handle keys and key-combinations on your keyboard.", 6 | "keywords": [ 7 | "react", 8 | "hook", 9 | "keyboard", 10 | "track", 11 | "keys", 12 | "shortcuts", 13 | "wasd", 14 | "combos", 15 | "combination", 16 | "gaming" 17 | ], 18 | "files": [ 19 | "dist" 20 | ], 21 | "author": "doemser", 22 | "main": "dist/use-wasd.js", 23 | "source": "src/use-wasd.ts", 24 | "types": "dist/types.d.ts", 25 | "module": "dist/esm/use-wasd.js", 26 | "peerDependencies": { 27 | "react": ">=18", 28 | "react-dom": ">=18" 29 | }, 30 | "devDependencies": { 31 | "@parcel/packager-ts": "^2.7.0", 32 | "@parcel/transformer-typescript-types": "^2.7.0", 33 | "@types/react": "^18.0.21", 34 | "@types/react-dom": "^18.0.6", 35 | "parcel": "^2.7.0", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "react-scripts": "^5.0.1", 39 | "typescript": "4.7" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com:doemser/use-wasd.git" 44 | }, 45 | "homepage": "http://doemser.de", 46 | "scripts": { 47 | "test": "react-scripts test --env=jsdom", 48 | "build": "parcel build", 49 | "dev": "parcel watch", 50 | "release": "parcel build && npm publish" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/use-wasd.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef, useState } from "react"; 2 | 3 | export interface UseWASDOptions { 4 | allowed?: string[]; 5 | blocked?: string[]; 6 | combos?: Record; 7 | initialValue?: Record; 8 | preventDefault?: boolean | string[]; 9 | ref?: MutableRefObject; 10 | } 11 | 12 | function shouldTrack(options: UseWASDOptions, key: string) { 13 | const allowAll = !options.allowed; 14 | const isBlocked = options.blocked?.includes(key); 15 | const isAllowed = allowAll || options.allowed?.includes(key); 16 | 17 | return !(isBlocked || !isAllowed); 18 | } 19 | 20 | function shouldPreventDefault( 21 | preventDefault: UseWASDOptions["preventDefault"], 22 | key: string 23 | ) { 24 | if (Array.isArray(preventDefault)) { 25 | return preventDefault?.includes(key); 26 | } 27 | return Boolean(preventDefault); 28 | } 29 | 30 | const initialValue = {}; 31 | 32 | export default function useWASD(options: UseWASDOptions = initialValue) { 33 | const [keyboard, setKeyboard] = useState( 34 | options.initialValue || initialValue 35 | ); 36 | 37 | const keys = useRef({ ...keyboard }); 38 | 39 | useEffect(() => { 40 | function update() { 41 | const matchingCombos = options.combos 42 | ? Object.entries(options.combos).reduce( 43 | (previousValue, [name, characters]) => ({ 44 | ...previousValue, 45 | [name]: characters.every((character) => keys.current[character]), 46 | }), 47 | {} 48 | ) 49 | : {}; 50 | setKeyboard({ ...keys.current, ...matchingCombos }); 51 | } 52 | 53 | function handleDown(event: KeyboardEvent) { 54 | const key = event.key.toLowerCase().trim() || "space"; 55 | if (shouldPreventDefault(options.preventDefault, key)) { 56 | event.preventDefault(); 57 | } 58 | if (shouldTrack(options, key)) { 59 | keys.current[key] = true; 60 | update(); 61 | } 62 | } 63 | 64 | function handleUp(event: KeyboardEvent) { 65 | const key = event.key.toLowerCase().trim() || "space"; 66 | if (shouldPreventDefault(options.preventDefault, key)) { 67 | event.preventDefault(); 68 | } 69 | if (shouldTrack(options, key)) { 70 | keys.current[key] = false; 71 | update(); 72 | } 73 | } 74 | 75 | const context = options.ref?.current ?? document; 76 | 77 | context.addEventListener("keydown", handleDown); 78 | context.addEventListener("keyup", handleUp); 79 | 80 | return () => { 81 | context.removeEventListener("keydown", handleDown); 82 | context.removeEventListener("keyup", handleUp); 83 | }; 84 | }, [options]); 85 | 86 | return keyboard; 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "alwaysStrict": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "importHelpers": true, 10 | "isolatedModules": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noEmit": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": false, 20 | "target": "es6", 21 | "baseUrl": ".", 22 | "typeRoots": ["./node_modules/@types"], 23 | "incremental": true 24 | }, 25 | "exclude": ["node_modules"], 26 | "include": ["**/*.ts", "**/*.tsx"] 27 | } 28 | --------------------------------------------------------------------------------