├── .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 | [](https://travis-ci.org/haldarmahesh/use-key-hook)
2 | [](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 |
--------------------------------------------------------------------------------