├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── build ├── index.d.ts ├── index.js ├── keys.d.ts └── keys.js ├── dist ├── index.js └── keys.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── index.test.js │ └── keys.test.js ├── index.ts └── keys.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": ["@babel/preset-env", "@babel/preset-react"] 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", // Specifies the ESLint parser 3 | "parserOptions": { 4 | "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features 5 | "sourceType": "module" // Allows for the use of imports 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | ], 10 | "rules": { 11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | # Edit at https://www.gitignore.io/?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # End of https://www.gitignore.io/api/node 83 | # auto generated yarn.lock file removed. 84 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "brace_style": "collapse,preserve-inline" 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: npm run test -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mahesh Haldar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/haldarmahesh/use-key-hook.svg?branch=master)](https://travis-ci.org/haldarmahesh/use-key-hook) 2 | [![npm version](https://badge.fury.io/js/use-key-hook.svg)](https://badge.fury.io/js/use-key-hook) 3 | 4 | # use-key-hook 5 | 6 | This is a React hook that detects all or some keys from keyboard. 7 | 8 | If you want to detect few keys and execute function, you can provide a list of ASCII codes or keys in an array. 9 | 10 | Few examples of use cases: 11 | 12 | - Add keyboard shortcuts in your app 13 | - Close modal on press of escape key 14 | - If it is react music player, control volume and seek video 15 | - Implement next or back on slide show 16 | 17 | ## Installing 18 | 19 | ```bash 20 | npm install use-key-hook 21 | ``` 22 | 23 | ```bash 24 | yarn add use-key-hook 25 | ``` 26 | 27 | ## Demo 28 | 29 | [Demo](http://www.maheshhaldar.com/demo-use-key/) 30 | 31 | ## Usage 32 | 33 | The following definition will only detect and execute provided callback **only** when `A`, `+` or `z` is pressed from keyboard. 34 | 35 | ```javascript 36 | import useKey from 'use-key-hook'; 37 | 38 | function MyComponent = (props) => { 39 | useKey((pressedKey, event) => { 40 | console.log('Detected Key press', pressedKey); 41 | console.log('Get event, if you want more details and preventDefault', event) 42 | }, { 43 | detectKeys: ['A', '+', 122] 44 | }); 45 | }; 46 | ``` 47 | 48 | ## Arguments in useKey 49 | 50 | ### 1) callback _(required)_ 51 | 52 | > type: function 53 | > 54 | > The first argument is callback function which gets executed whenever the keys are pressed. 55 | 56 | ### 2) detectKeys _(optional)_ 57 | 58 | > type: array 59 | > array item type: string | number 60 | > 61 | > The second argument is an object and should contain one key in name of **detectKeys**. 62 | > This has to be an array. 63 | 64 | **When array is empty or not passed** All the keys will be detected and callback will be executed. 65 | 66 | The items in arrays can be **ASCII code** of keys or **characters itself**. 67 | 68 | #### Example values of detectKeys array 69 | 70 | ```js 71 | { 72 | detectKeys: ['A', 69, 27]; 73 | } 74 | ``` 75 | 76 | The above will detect and execute callback only the following keys 77 | 78 | - A maps with item 0 `A` 79 | - Enter key maps with ASCII code is 69 80 | - Escape key maps with numeric ASCII code 27. 81 | 82 | Pressing any other key will not be detected. 83 | 84 | ```js 85 | { 86 | detectKeys: [1, '2']; 87 | } 88 | ``` 89 | 90 | The above will detect when number 2 is pressed only. 91 | Pressing 1, it will not be detected as we passed ASCII code numeric `1` and this is not number `1`. 92 | 93 | Pressing any other key will not be detected. 94 | 95 | ### 3) keyevent _(optional)_ 96 | 97 | > type: string 98 | > 99 | > default: `keydown` 100 | > 101 | > Defines the type of event this hook should capture. 102 | 103 | This parameter is passed in the config object along with `detectKeys`. 104 | 105 | There are 3 type of events options [`keydown`, `keyup`, `keypress`]. 106 | 107 | Example config object: 108 | 109 | ```js 110 | { 111 | detectKeys: [1, '2'], 112 | keyevent: 'keyup' 113 | } 114 | ``` 115 | 116 | ## Contributing 117 | 118 | If you have any new suggestions, new features, bug fixes, etc. please contribute by raising pull request on the [repository](https://github.com/haldarmahesh/use-key-hook). 119 | 120 | If you have any issue with the `use-key-hook`, open an issue on [Github](https://github.com/haldarmahesh/use-key-hook). 121 | -------------------------------------------------------------------------------- /build/index.d.ts: -------------------------------------------------------------------------------- 1 | interface IParamType { 2 | detectKeys: Array; 3 | keyevent: string; 4 | } 5 | declare const useKey: (callback: (currentKeyCode: number, event: Event) => unknown, { detectKeys, keyevent }?: IParamType, { dependencies }?: { 6 | dependencies?: never[] | undefined; 7 | }) => any; 8 | export { useKey }; 9 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.useKey = void 0; 7 | const react_1 = require("react"); 8 | const invariant_1 = __importDefault(require("invariant")); 9 | const keys_js_1 = require("./keys.js"); 10 | const VALID_KEY_EVENTS = ['keydown', 'keyup', 'keypress']; 11 | const useKey = (callback, { detectKeys, keyevent } = { detectKeys: [], keyevent: 'keydown' }, { dependencies = [] } = {}) => { 12 | const isKeyeventValid = VALID_KEY_EVENTS.indexOf(keyevent) > -1; 13 | invariant_1.default(isKeyeventValid, 'keyevent is not valid: ' + keyevent); 14 | invariant_1.default(callback != null, 'callback needs to be defined'); 15 | invariant_1.default(Array.isArray(dependencies), 'dependencies need to be an array'); 16 | let allowedKeys = detectKeys; 17 | if (!Array.isArray(detectKeys)) { 18 | allowedKeys = []; 19 | // eslint-disable-next-line no-console 20 | console.warn('Keys should be array!'); 21 | } 22 | allowedKeys = keys_js_1.convertToAsciiEquivalent(allowedKeys); 23 | const handleEvent = (event) => { 24 | const asciiCode = keys_js_1.getAsciiCode(event); 25 | return keys_js_1.onKeyPress(asciiCode, callback, allowedKeys, event); 26 | }; 27 | react_1.useEffect(() => { 28 | const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); 29 | if (!canUseDOM) { 30 | console.error('Window is not defined'); 31 | return () => { 32 | // returning null 33 | }; 34 | } 35 | window.document.addEventListener(keyevent, handleEvent); 36 | return () => { 37 | window.document.removeEventListener(keyevent, handleEvent); 38 | }; 39 | }, dependencies); 40 | }; 41 | exports.useKey = useKey; 42 | -------------------------------------------------------------------------------- /build/keys.d.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback } from 'react'; 2 | declare const isKeyFromGivenList: (keyCode: number, allowedKeys?: Array) => boolean; 3 | declare const onKeyPress: (currentKeyCode: number, callback: (currentKeyCode: number, event: Event) => unknown, allowedKeys: Array, event: Event) => ReturnType; 4 | declare function getAsciiCode(event: Event): number; 5 | declare function convertToAsciiEquivalent(inputArray: Array): Array; 6 | export { isKeyFromGivenList, onKeyPress, convertToAsciiEquivalent, getAsciiCode }; 7 | -------------------------------------------------------------------------------- /build/keys.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.getAsciiCode = exports.convertToAsciiEquivalent = exports.onKeyPress = exports.isKeyFromGivenList = void 0; 4 | const codeLowerCaseA = 65; 5 | const codeUpperCaseZ = 122; 6 | const isKeyFromGivenList = (keyCode, allowedKeys = []) => { 7 | if (allowedKeys === null || allowedKeys.includes(keyCode) || allowedKeys.length === 0) { 8 | return true; 9 | } 10 | return false; 11 | }; 12 | exports.isKeyFromGivenList = isKeyFromGivenList; 13 | const onKeyPress = (currentKeyCode, callback, allowedKeys, event) => { 14 | if (isKeyFromGivenList(currentKeyCode, allowedKeys)) { 15 | callback(currentKeyCode, event); 16 | } 17 | }; 18 | exports.onKeyPress = onKeyPress; 19 | function getAsciiCode(event) { 20 | let keyCode = event.which; 21 | if (keyCode >= codeLowerCaseA && keyCode <= codeUpperCaseZ) { 22 | keyCode = event.key.charCodeAt(0); 23 | } 24 | return keyCode; 25 | } 26 | exports.getAsciiCode = getAsciiCode; 27 | function convertToAsciiEquivalent(inputArray) { 28 | return inputArray.map((item) => { 29 | const finalVal = item; 30 | if (typeof finalVal === 'string') { 31 | return finalVal.charCodeAt(0); 32 | } 33 | return finalVal; 34 | }); 35 | } 36 | exports.convertToAsciiEquivalent = convertToAsciiEquivalent; 37 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _require = require('react'), 4 | useEffect = _require.useEffect; 5 | 6 | var invariant = require('invariant'); 7 | 8 | var _require2 = require('./keys.js'), 9 | onKeyPress = _require2.onKeyPress, 10 | convertToAsciiEquivalent = _require2.convertToAsciiEquivalent, 11 | getAsciiCode = _require2.getAsciiCode; 12 | 13 | var VALID_KEY_EVENTS = ['keydown', 'keyup', 'keypress']; 14 | 15 | var useKey = function useKey(callback) { 16 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 17 | _ref$detectKeys = _ref.detectKeys, 18 | detectKeys = _ref$detectKeys === void 0 ? [] : _ref$detectKeys, 19 | _ref$keyevent = _ref.keyevent, 20 | keyevent = _ref$keyevent === void 0 ? 'keydown' : _ref$keyevent; 21 | 22 | var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, 23 | _ref2$dependencies = _ref2.dependencies, 24 | dependencies = _ref2$dependencies === void 0 ? [] : _ref2$dependencies; 25 | 26 | var isKeyeventValid = VALID_KEY_EVENTS.indexOf(keyevent) > -1; 27 | invariant(isKeyeventValid, 'keyevent is not valid: ' + keyevent); 28 | invariant(callback != null, 'callback needs to be defined'); 29 | invariant(Array.isArray(dependencies), 'dependencies need to be an array'); 30 | var allowedKeys = detectKeys; 31 | 32 | if (!Array.isArray(detectKeys)) { 33 | allowedKeys = []; // eslint-disable-next-line no-console 34 | 35 | console.warn('Keys should be array!'); 36 | } 37 | 38 | allowedKeys = convertToAsciiEquivalent(allowedKeys); 39 | 40 | var handleEvent = function handleEvent(event) { 41 | var asciiCode = getAsciiCode(event); 42 | return onKeyPress(asciiCode, callback, allowedKeys, event); 43 | }; 44 | 45 | useEffect(function () { 46 | var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); 47 | 48 | if (!canUseDOM) { 49 | console.error('Window is not defined'); 50 | return null; 51 | } 52 | 53 | window.document.addEventListener(keyevent, handleEvent); 54 | return function () { 55 | window.document.removeEventListener(keyevent, handleEvent); 56 | }; 57 | }, dependencies); 58 | }; 59 | 60 | module.exports = useKey; 61 | -------------------------------------------------------------------------------- /dist/keys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var codeLowerCaseA = 65; 4 | var codeUpperCaseZ = 122; 5 | 6 | var isKeyFromGivenList = function isKeyFromGivenList(keyCode) { 7 | var allowedKeys = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 8 | 9 | if (allowedKeys === null || allowedKeys.includes(keyCode) || allowedKeys.length === 0) { 10 | return true; 11 | } 12 | 13 | return false; 14 | }; 15 | 16 | var onKeyPress = function onKeyPress(currentKeyCode, callback, allowedKeys, event) { 17 | if (isKeyFromGivenList(currentKeyCode, allowedKeys)) { 18 | callback(currentKeyCode, event); 19 | } 20 | }; 21 | 22 | function getAsciiCode(event) { 23 | var keyCode = event.which; 24 | 25 | if (keyCode >= codeLowerCaseA && keyCode <= codeUpperCaseZ) { 26 | keyCode = event.key.charCodeAt(0); 27 | } 28 | 29 | return keyCode; 30 | } 31 | 32 | function convertToAsciiEquivalent(inputArray) { 33 | return inputArray.map(function(item) { 34 | var finalVal = item; 35 | 36 | if (typeof item === 'string') { 37 | finalVal = finalVal.charCodeAt(0); 38 | } 39 | 40 | return finalVal; 41 | }); 42 | } 43 | 44 | module.exports = { 45 | isKeyFromGivenList: isKeyFromGivenList, 46 | onKeyPress: onKeyPress, 47 | convertToAsciiEquivalent: convertToAsciiEquivalent, 48 | getAsciiCode: getAsciiCode 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-key-hook", 3 | "version": "1.5.0", 4 | "author": "", 5 | "description": "React hook to handle all the key press.", 6 | "main": "dist/index.js", 7 | "jest": { 8 | "collectCoverage": true, 9 | "coverageReporters": [ 10 | "json", 11 | "html" 12 | ] 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "npm run test && npm run format:fix && npm run lint" 17 | } 18 | }, 19 | "scripts": { 20 | "build:lib": "cross-env NODE_ENV='production' babel src --out-dir dist --ignore '**/__test__/**'", 21 | "test": "npm run build && jest --coverage", 22 | "test-watch": "jest --watchAll --coverage", 23 | "lint": "./node_modules/eslint/bin/eslint.js --ext js,jsx src", 24 | "lint-fix": "./node_modules/eslint/bin/eslint.js --ext js,jsx src --fix", 25 | "format:check": "prettier --config ./.prettierrc --list-different \"src/**/*{.ts,.js,.json,.css,.scss}\"", 26 | "format:fix": "pretty-quick --staged", 27 | "build": "tsc" 28 | }, 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "react": "^16.7.0-alpha.2" 32 | }, 33 | "devDependencies": { 34 | "@types/invariant": "^2.2.34", 35 | "@types/jest": "^26.0.19", 36 | "@types/react": "^17.0.3", 37 | "@typescript-eslint/eslint-plugin": "^4.22.0", 38 | "@typescript-eslint/parser": "^4.22.0", 39 | "cross-env": "^5.2.0", 40 | "eslint": "^7.25.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-plugin-prettier": "^3.4.0", 43 | "husky": "1.1.2", 44 | "jest": "^26.6.3", 45 | "prettier": "^2.2.1", 46 | "pretty-quick": "^1.8.0", 47 | "react": "^16.7.0-alpha.2", 48 | "react-dom": "^16.7.0-alpha.2", 49 | "react-test-renderer": "^16.14.0", 50 | "react-testing-library": "5.9.0", 51 | "regenerator-runtime": "^0.12.1", 52 | "typescript": "^4.1.3", 53 | "@babel/preset-env": "^7.13.15", 54 | "@babel/preset-react": "^7.13.13", 55 | "babel-jest": "^26.6.3" 56 | }, 57 | "files": [ 58 | "dist/index.js", 59 | "dist/keys.js" 60 | ], 61 | "engines": { 62 | "node": ">=8", 63 | "npm": ">=5" 64 | }, 65 | "keywords": [ 66 | "react", 67 | "hook", 68 | "hooks", 69 | "keyboard", 70 | "input" 71 | ], 72 | "dependencies": { 73 | "invariant": "^2.2.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line object-curly-newline 4 | import { render, cleanup, fireEvent } from 'react-testing-library'; 5 | 6 | import { useKey } from '../../build/index'; 7 | 8 | const TestComponent = ({ callback, detectKeys, keyevent }) => { 9 | useKey(callback, { detectKeys, keyevent }); 10 | return
; 11 | }; 12 | 13 | afterEach(cleanup); 14 | 15 | describe('useKey setup', () => { 16 | test('throws error when callback is not defined', () => { 17 | expect(() => render()).toThrowError(); 18 | }); 19 | 20 | test('when keys is not an array it passes with warns', () => { 21 | console.warn = jest.fn(); 22 | render(); 23 | expect(console.warn).toHaveBeenCalledWith('Keys should be array!'); 24 | }); 25 | 26 | test('when the passed keyevent is an invalid event, an error is thrown', () => { 27 | console.warn = jest.fn(); 28 | expect(() => render()).toThrowError(); 29 | }); 30 | }); 31 | 32 | describe('events', () => { 33 | test('it calls the callback with the correct value when the keydown event is fired with the right key', async () => { 34 | const callback = jest.fn(); 35 | const { container } = render(); 36 | const keyDownEvent = new KeyboardEvent('keydown', { 37 | key: 'ArrowUp', 38 | bubbles: true, 39 | which: 38, 40 | code: 'ArrowUp', 41 | }); 42 | fireEvent(container, keyDownEvent); 43 | expect(callback).toHaveBeenCalledWith(38, keyDownEvent); 44 | expect(callback).toHaveBeenCalledTimes(1); 45 | }); 46 | 47 | test('it calls the callback with the correct value when the keyup event is fired with the right key', async () => { 48 | const callback = jest.fn(); 49 | const { container } = render(); 50 | const keyUpEvent = new KeyboardEvent('keyup', { 51 | key: 'ArrowUp', 52 | bubbles: true, 53 | which: 38, 54 | code: 'ArrowUp', 55 | }); 56 | fireEvent(container, keyUpEvent); 57 | expect(callback).toHaveBeenCalledWith(38, keyUpEvent); 58 | expect(callback).toHaveBeenCalledTimes(1); 59 | }); 60 | 61 | test('it calls the callback with the correct value when the keyup event is fired with the right key', async () => { 62 | const callback = jest.fn(); 63 | const { container } = render(); 64 | const keyPressEvent = new KeyboardEvent('keypress', { 65 | key: 'ArrowUp', 66 | bubbles: true, 67 | which: 38, 68 | code: 'ArrowUp', 69 | }); 70 | fireEvent(container, keyPressEvent); 71 | expect(callback).toHaveBeenCalledWith(38, keyPressEvent); 72 | expect(callback).toHaveBeenCalledTimes(1); 73 | }); 74 | 75 | test('it does not call the callback when the key is not in detectKeys', async () => { 76 | const callback = jest.fn(); 77 | const { container } = render(); 78 | const keyDownEvent = new KeyboardEvent('keydown', { 79 | key: 'ArrowUp', 80 | bubbles: true, 81 | which: 38, 82 | code: 'ArrowUp', 83 | }); 84 | fireEvent(container, keyDownEvent); 85 | expect(callback).not.toHaveBeenCalledWith(38, keyDownEvent); 86 | expect(callback).toHaveBeenCalledTimes(0); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/keys.test.js: -------------------------------------------------------------------------------- 1 | import { isKeyFromGivenList, onKeyPress, convertToAsciiEquivalent, getAsciiCode } from '../../build/keys'; 2 | 3 | describe('keys utils', () => { 4 | test('isKeyFromGivenList returns true when current key belong to allowed list', () => { 5 | const allowed = isKeyFromGivenList(12, [34, 12]); 6 | expect(allowed).toBeTruthy(); 7 | }); 8 | test('isKeyFromGivenList returns false when current key belong to allowed list', () => { 9 | const allowed = isKeyFromGivenList(13, [34, 12]); 10 | expect(allowed).toBeFalsy(); 11 | }); 12 | test('isKeyFromGivenList returns true whenallowed list is null', () => { 13 | const allowed = isKeyFromGivenList(13, null); 14 | expect(allowed).toBeTruthy(); 15 | }); 16 | }); 17 | describe('onKeyPress', () => { 18 | test('should call the cb when currentKey is in allowed', () => { 19 | const callback = jest.fn(); 20 | const event = { 21 | keyCode: 12, 22 | }; 23 | onKeyPress(event.keyCode, callback, [12, 34]); 24 | expect(callback).toHaveBeenCalledTimes(1); 25 | }); 26 | test('should not call the cb when currentKey is not in allowed', () => { 27 | const callback = jest.fn(); 28 | const event = { 29 | keyCode: 15, 30 | }; 31 | onKeyPress(event.keyCode, callback, [12, 34]); 32 | expect(callback).not.toHaveBeenCalled(); 33 | }); 34 | test('should call the cb when currentKey matches with character', () => { 35 | const callback = jest.fn(); 36 | const event = { 37 | keyCode: 65, 38 | }; 39 | onKeyPress(event.keyCode, callback, [65, 34]); 40 | expect(callback).toHaveBeenCalled(); 41 | }); 42 | }); 43 | 44 | describe('convertToAsciiEquivalent', () => { 45 | test('should return ascii equivalent array', () => { 46 | const input = ['A']; 47 | const input2 = ['a', ' ']; 48 | const input3 = ['B', 21]; 49 | const input4 = ['Z', 'z', '=', '1', 21]; 50 | const input5 = ['a', 'A']; 51 | expect(convertToAsciiEquivalent(input)[0]).toEqual(65); 52 | expect(convertToAsciiEquivalent(input2)[0]).toEqual(97); 53 | expect(convertToAsciiEquivalent(input2)[1]).toEqual(32); 54 | 55 | expect(convertToAsciiEquivalent(input3)[0]).toEqual(66); 56 | expect(convertToAsciiEquivalent(input3)[1]).toEqual(21); 57 | 58 | expect(convertToAsciiEquivalent(input4)[0]).toEqual(90); 59 | expect(convertToAsciiEquivalent(input4)[1]).toEqual(122); 60 | expect(convertToAsciiEquivalent(input4)[2]).toEqual(61); 61 | expect(convertToAsciiEquivalent(input4)[3]).toEqual(49); 62 | 63 | expect(convertToAsciiEquivalent(input5)[0]).toEqual(97); 64 | expect(convertToAsciiEquivalent(input5)[1]).toEqual(65); 65 | }); 66 | }); 67 | 68 | describe('getAsciiCode', () => { 69 | test('should return the ascii code', () => { 70 | const event1 = { 71 | which: 65, 72 | key: 'A', 73 | }; 74 | const event2 = { 75 | which: 65, 76 | key: 'a', 77 | }; 78 | const event3 = { 79 | which: 90, 80 | key: 'Z', 81 | }; 82 | const event4 = { 83 | which: 90, 84 | key: 'z', 85 | }; 86 | const event5 = { 87 | which: 123, 88 | key: '{', 89 | }; 90 | expect(getAsciiCode(event1)).toEqual(65); 91 | expect(getAsciiCode(event2)).toEqual(97); 92 | expect(getAsciiCode(event3)).toEqual(90); 93 | expect(getAsciiCode(event4)).toEqual(122); 94 | expect(getAsciiCode(event5)).toEqual(123); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect } from 'react'; 2 | 3 | import invariant from 'invariant'; 4 | import { onKeyPress, convertToAsciiEquivalent, getAsciiCode } from './keys.js'; 5 | 6 | const VALID_KEY_EVENTS = ['keydown', 'keyup', 'keypress']; 7 | interface IParamType { 8 | detectKeys: Array; 9 | keyevent: string; 10 | } 11 | const useKey = ( 12 | callback: (currentKeyCode: number, event: Event) => unknown, 13 | { detectKeys, keyevent }: IParamType = { detectKeys: [], keyevent: 'keydown' }, 14 | { dependencies = [] } = {} 15 | ): any => { 16 | const isKeyeventValid = VALID_KEY_EVENTS.indexOf(keyevent) > -1; 17 | 18 | invariant(isKeyeventValid, 'keyevent is not valid: ' + keyevent); 19 | invariant(callback != null, 'callback needs to be defined'); 20 | invariant(Array.isArray(dependencies), 'dependencies need to be an array'); 21 | 22 | let allowedKeys = detectKeys; 23 | 24 | if (!Array.isArray(detectKeys)) { 25 | allowedKeys = []; 26 | // eslint-disable-next-line no-console 27 | console.warn('Keys should be array!'); 28 | } 29 | 30 | allowedKeys = convertToAsciiEquivalent(allowedKeys); 31 | 32 | const handleEvent = (event: Event) => { 33 | const asciiCode = getAsciiCode(event); 34 | return onKeyPress(asciiCode, callback, allowedKeys, event); 35 | }; 36 | 37 | useEffect((): ReturnType => { 38 | const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); 39 | if (!canUseDOM) { 40 | console.error('Window is not defined'); 41 | return (): void => { 42 | // returning null 43 | }; 44 | } 45 | window.document.addEventListener(keyevent, handleEvent); 46 | return () => { 47 | window.document.removeEventListener(keyevent, handleEvent); 48 | }; 49 | }, dependencies); 50 | }; 51 | 52 | export { useKey }; 53 | -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback } from 'react'; 2 | 3 | const codeLowerCaseA = 65; 4 | const codeUpperCaseZ = 122; 5 | const isKeyFromGivenList = (keyCode: number, allowedKeys: Array = []): boolean => { 6 | if (allowedKeys === null || allowedKeys.includes(keyCode) || allowedKeys.length === 0) { 7 | return true; 8 | } 9 | return false; 10 | }; 11 | const onKeyPress = ( 12 | currentKeyCode: number, 13 | callback: (currentKeyCode: number, event: Event) => unknown, 14 | allowedKeys: Array, 15 | event: Event 16 | ): ReturnType => { 17 | if (isKeyFromGivenList(currentKeyCode, allowedKeys)) { 18 | callback(currentKeyCode, event); 19 | } 20 | }; 21 | 22 | function getAsciiCode(event: Event): number { 23 | let keyCode = (event as KeyboardEvent).which; 24 | if (keyCode >= codeLowerCaseA && keyCode <= codeUpperCaseZ) { 25 | keyCode = (event as KeyboardEvent).key.charCodeAt(0); 26 | } 27 | return keyCode; 28 | } 29 | 30 | function convertToAsciiEquivalent(inputArray: Array): Array { 31 | return inputArray.map((item) => { 32 | const finalVal = item; 33 | if (typeof finalVal === 'string') { 34 | return finalVal.charCodeAt(0); 35 | } 36 | return finalVal; 37 | }); 38 | } 39 | 40 | export { isKeyFromGivenList, onKeyPress, convertToAsciiEquivalent, getAsciiCode }; 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "removeComments": false, 9 | "declaration": true, 10 | "outDir": "build", 11 | "lib": ["es6", "DOM"] 12 | }, 13 | "include": ["src", "src/index.ts"], 14 | "exclude": ["node_modules", "**/__tests__/*"] 15 | } 16 | --------------------------------------------------------------------------------