├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── demo ├── components │ ├── app.js │ └── examples │ │ ├── ExampleBox.js │ │ ├── component │ │ └── index.js │ │ ├── decorators │ │ ├── key-handler.js │ │ └── key-toggle-handler.js │ │ └── input │ │ ├── code-explore.js │ │ ├── default.js │ │ └── keypress.js ├── index.html └── main.js ├── lib ├── constants.js ├── index.js ├── key-handle-decorator.js ├── key-handler.js ├── types.js └── utils.js ├── package.json ├── rollup.config.js ├── test ├── components │ ├── helpers │ │ └── triggerKeyEvent.js │ └── key-handler.test.js ├── mocha.opts ├── support │ └── helper.js └── utils.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react", "stage-1"], 3 | "plugins": ["transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/bundle.js 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:flowtype/recommended", 11 | "plugin:prettier/recommended", 12 | "plugin:react/recommended" 13 | ], 14 | "parser": "babel-eslint", 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "experimentalObjectRestSpread": true, 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 6, 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["flowtype", "react"], 24 | "rules": { 25 | "comma-dangle": ["error", "always-multiline"], 26 | "flowtype/no-types-missing-file-annotation": "off", 27 | "indent": ["error", 2], 28 | "linebreak-style": ["error", "unix"], 29 | "quotes": ["error", "single"], 30 | "semi": ["error", "always"] 31 | }, 32 | "settings": { 33 | "react": { 34 | "flowVersion": "0.79", 35 | "version": "16.2" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | 12 | [version] 13 | 0.79.1 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .size-snapshot.json 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | package-lock.json 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | demo/ 3 | lib/ 4 | test/ 5 | .babelrc 6 | .eslintignore 7 | .eslintrc.json 8 | .flowconfig 9 | .size-snapshot.json 10 | .travis.yml 11 | npm-debug.log 12 | rollup.config.js 13 | yarn-error.log 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | demo/index.html 2 | test/mocha.opts 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxBracketSameLine": true, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | - [bugfix] 9 | - [feature] 10 | 11 | ## [1.2.0-beta.3] - 2018-08-30 12 | 13 | - [feature] Add `code` support by [@gforge](https://github.com/gforge) 14 | - [deprecate] `keyCode` prop in favour of `code` by [@gforge](https://github.com/gforge) 15 | - [feature] Allows for `keyValue`, `code` and `keyCode` to come in array format by [@gforge](https://github.com/gforge) 16 | - [breaking] No longer add a `
` wrapper around the `KeyHandler` component, but use `React.Fragment` instead 17 | 18 | ## [1.2.0-beta.2] - 2018-08-21 19 | 20 | - [bugfix] Pass original props to component we are decorating when using one of the decorators 21 | 22 | ## [1.2.0-beta.1] - 2018-08-21 23 | 24 | - [feature] Use rollup to bundle up the code 25 | 26 | ## [1.1.0] - 2018-07-31 27 | 28 | > There's two major bugs in 1.1.0, it is highly recommended to update to 1.2.0 or downgrade to 1.0.1 29 | 30 | - [feature] Add flow support by [@gforge](https://github.com/gforge) 31 | - [bugfix] Fallback from `event.keyCode` to `event.which` for browsers such as FireFox when using the `keypress` event name. [@gforge](https://github.com/gforge) 32 | 33 | ## [1.0.1] - 2017-10-26 34 | 35 | - [feature] Loosen react dependency to support React 16 36 | 37 | ## [1.0.0] - 2017-08-28 38 | 39 | - [breaking] Drop keyName support 40 | - [bugfix] Get rid of deprecation warnings 41 | 42 | ## [0.3.0] - 2016-09-13 43 | 44 | - [feature] Treat content editable elements as inputs by [@jamesfzhang](https://github.com/jamesfzhang). This means the key handle 45 | will be ignored for events triggered from these elements. (Draft.js support) 46 | 47 | ## [0.2.0] - 2016-04-17 48 | 49 | - [feature] Loosen react dependency for future react versions 50 | - [deprecate] `keyName` prop in favour of `keyValue` 51 | - [feature] Added prop types 52 | - [feature] Add support for W3C `KeyboardEvent.key` by [@leocavalcante](https://github.com/leocavalcante) 53 | 54 | ## [0.1.0] - 2016-03-02 55 | 56 | - [feature] Add decorators 57 | - [feature] Ignore key events from form elements 58 | - [feature] Key names 59 | 60 | ## [0.0.4] - 2016-02-27 61 | 62 | - [bugfix] Use right function to remove the event listener 63 | 64 | ## [0.0.2] & [0.0.3] - 2016-02-27 65 | 66 | - [bugfix] Protect from server side rendering errors 67 | 68 | ## [0.0.1] - 2016-02-27 69 | 70 | - Initial implementation 71 | 72 | [unreleased]: https://github.com/ayrton/react-key-handler/compare/v1.1.0...HEAD 73 | [1.2.0-beta.3]: https://github.com/ayrton/react-key-handler/compare/v1.2.0-beta.2...v1.2.0-beta.3 74 | [1.2.0-beta.2]: https://github.com/ayrton/react-key-handler/compare/v1.2.0-beta.1...v1.2.0-beta.2 75 | [1.2.0-beta.1]: https://github.com/ayrton/react-key-handler/compare/v1.1.0...v1.2.0-beta.1 76 | [1.1.0]: https://github.com/ayrton/react-key-handler/compare/v1.0.1...v1.1.0 77 | [1.0.1]: https://github.com/ayrton/react-key-handler/compare/v1.0.0...v1.0.1 78 | [1.0.0]: https://github.com/ayrton/react-key-handler/compare/v0.3.0...v1.0.0 79 | [0.3.0]: https://github.com/ayrton/react-key-handler/compare/v0.2.0...v0.3.0 80 | [0.2.0]: https://github.com/ayrton/react-key-handler/compare/v0.1.0...v0.2.0 81 | [0.1.0]: https://github.com/ayrton/react-key-handler/compare/v0.0.4...v0.1.0 82 | [0.0.4]: https://github.com/ayrton/react-key-handler/compare/v0.0.3...v0.0.4 83 | [0.0.3]: https://github.com/ayrton/react-key-handler/compare/v0.0.2...v0.0.3 84 | [0.0.2]: https://github.com/ayrton/react-key-handler/compare/v0.0.1...v0.0.2 85 | [0.0.1]: https://github.com/ayrton/react-key-handler/commit/8267e3dc7357bb7fb106f5148e6f9cb9f69ed3b5 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-key-handler 🔑 2 | 3 | [](https://www.npmjs.com/package/react-key-handler) [](https://www.npmjs.com/package/react-key-handler) [](https://travis-ci.org/ayrton/react-key-handler) 4 | 5 | React component to handle keyboard events (such as `keyup`, `keydown` & `keypress`). 6 | 7 | ## Testimonials 8 | 9 | >
30 | Press s
to toggle the menu.
31 |
20 | Press s
to open the menu.
21 |
20 | Press s
to toggle the menu.
21 |
= [undefined, undefined, undefined];
17 | const elements = undefCodes
18 | .concat(codes)
19 | .reverse()
20 | .slice(0, 3)
21 | .map((code, i) => {
22 | if (code === undefined) {
23 | return ... ;
24 | }
25 | return (
26 |
27 | {code.code} => {code.value}
28 |
29 | );
30 | });
31 |
32 | return (
33 |
34 |
35 | Input
36 | KeyboardEvent
37 |
38 | .code
39 | and .value
40 | explore:
41 |
42 |
43 |
44 |
45 | Last 3 values (code => value):
46 | {elements}
47 |
48 | );
49 | }
50 |
51 | handleKeyPress = (e: SyntheticKeyboardEvent) => {
52 | const { codes } = this.state;
53 |
54 | codes.push({
55 | // $FlowFixMe
56 | code: e.nativeEvent.code,
57 | value: e.key,
58 | });
59 | if (codes.length > 3) {
60 | codes.shift();
61 | }
62 |
63 | this.setState({ codes });
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/demo/components/examples/input/default.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import React from 'react';
4 | import SyntaxHighlighter from 'react-syntax-highlighter/prism';
5 | import { light } from 'react-syntax-highlighter/styles/prism';
6 |
7 | import { keyToggleHandler, KEYPRESS } from '../../../../lib';
8 | import ExampleBox from '../ExampleBox';
9 |
10 | type Props = {
11 | keyValue: ?string,
12 | };
13 |
14 | function Default({ keyValue }: Props) {
15 | return (
16 |
17 | Input example:
18 |
19 |
20 | Press s
in the following form component and see that the
21 | key handle will be ignored by default.
22 |
23 |
24 |
25 |
26 | {keyValue === 's' && (
27 |
28 | - hello
29 | - world
30 |
31 | )}
32 |
33 | Code:
34 |
35 | {codeString}
36 |
37 |
38 | );
39 | }
40 |
41 | const codeString = `
42 | keyToggleHandler({ keyEventName: KEYPRESS, keyValue: 's' })(
43 | Component,
44 | );
45 | `;
46 |
47 | export default keyToggleHandler({ keyEventName: KEYPRESS, keyValue: 's' })(
48 | Default,
49 | );
50 |
--------------------------------------------------------------------------------
/demo/components/examples/input/keypress.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import React from 'react';
4 | import ExampleBox from '../ExampleBox';
5 | import SyntaxHighlighter from 'react-syntax-highlighter/prism';
6 | import { light } from 'react-syntax-highlighter/styles/prism';
7 |
8 | type State = {
9 | keyValue: ?string,
10 | };
11 |
12 | export default class Keypress extends React.Component<{||}, State> {
13 | state: State = { keyValue: null };
14 |
15 | render() {
16 | return (
17 |
18 | Input onKeyPress example:
19 |
20 |
21 | Press s
in the following form component to toggle the
22 | menu.
23 |
24 |
25 |
26 |
27 | {this.state.keyValue === 's' && (
28 |
29 | - hello
30 | - world
31 |
32 | )}
33 |
34 | Code:
35 |
36 | {codeString}
37 |
38 |
39 | );
40 | }
41 |
42 | handleKeyPress = ({ key }: SyntheticKeyboardEvent) => {
43 | const keyValue = this.state.keyValue === key ? null : key;
44 |
45 | this.setState({ keyValue });
46 | };
47 | }
48 |
49 | const codeString = `
50 | state: State = { keyValue: null };
51 |
52 | render() {
53 | ...
54 |
55 | ...
56 | }
57 |
58 | handleKeyPress = ({ key }) => {
59 | const keyValue = this.state.keyValue === key ? null : key;
60 |
61 | this.setState({ keyValue });
62 | };
63 | `;
64 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | react-key-handler
5 |
6 |
7 |
8 |
9 |
10 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/demo/main.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import App from './components/app';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 |
7 | /**
8 | * Render the application.
9 | */
10 |
11 | const root = document.getElementById('app');
12 | if (!root) throw new Error('No app id in document');
13 |
14 | ReactDOM.render( , root);
15 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | /**
3 | * Key event names.
4 | */
5 |
6 | export const KEYDOWN = 'keydown';
7 | export const KEYPRESS = 'keypress';
8 | export const KEYUP = 'keyup';
9 |
10 | export type KeyConstants = 'keydown' | 'keypress' | 'keyup';
11 |
12 | export const NORMALIZED_KEYS: {
13 | [key: string]: string,
14 | } = {
15 | Esc: 'Escape',
16 | Spacebar: ' ',
17 | Left: 'ArrowLeft',
18 | Up: 'ArrowUp',
19 | Right: 'ArrowRight',
20 | Down: 'ArrowDown',
21 | Del: 'Delete',
22 | Win: 'OS',
23 | Menu: 'ContextMenu',
24 | Apps: 'ContextMenu',
25 | Scroll: 'ScrollLock',
26 | MozPrintableKey: 'Unidentified',
27 | };
28 |
29 | export const KEY_CODE_KEYS: {
30 | [string: string]: string,
31 | } = {
32 | '8': 'Backspace',
33 | '9': 'Tab',
34 | '12': 'Clear',
35 | '13': 'Enter',
36 | '16': 'Shift',
37 | '17': 'Control',
38 | '18': 'Alt',
39 | '19': 'Pause',
40 | '20': 'CapsLock',
41 | '27': 'Escape',
42 | '32': ' ',
43 | '33': 'PageUp',
44 | '34': 'PageDown',
45 | '35': 'End',
46 | '36': 'Home',
47 | '37': 'ArrowLeft',
48 | '38': 'ArrowUp',
49 | '39': 'ArrowRight',
50 | '40': 'ArrowDown',
51 | '45': 'Insert',
52 | '46': 'Delete',
53 | '112': 'F1',
54 | '113': 'F2',
55 | '114': 'F3',
56 | '115': 'F4',
57 | '116': 'F5',
58 | '117': 'F6',
59 | '118': 'F7',
60 | '119': 'F8',
61 | '120': 'F9',
62 | '121': 'F10',
63 | '122': 'F11',
64 | '123': 'F12',
65 | '144': 'NumLock',
66 | '145': 'ScrollLock',
67 | '224': 'Meta',
68 | };
69 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import KeyHandler from './key-handler';
3 |
4 | export default KeyHandler;
5 | export { KEYDOWN, KEYPRESS, KEYUP } from './constants';
6 | export {
7 | default as keyHandleDecorator,
8 | keyToggleHandler,
9 | keyHandler,
10 | } from './key-handle-decorator';
11 |
--------------------------------------------------------------------------------
/lib/key-handle-decorator.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import React from 'react';
4 |
5 | import KeyHandler from './key-handler';
6 | import { type KeyConstants } from './constants';
7 | import { matchesKeyboardEvent, eventKey } from './utils';
8 | import { type primitiveOrArray } from './types';
9 |
10 | export type KeyhandleDecoratorState = {|
11 | keyValue?: ?primitiveOrArray,
12 | keyCode: ?primitiveOrArray,
13 | code: ?primitiveOrArray,
14 | |};
15 |
16 | export type DecoratorProps = {|
17 | keyValue: ?primitiveOrArray,
18 | keyCode: ?primitiveOrArray,
19 | code: ?primitiveOrArray,
20 | keyEventName?: KeyConstants,
21 | |};
22 |
23 | export default function keyHandleDecorator(
24 | matcher?: typeof matchesKeyboardEvent,
25 | ): Function {
26 | return (props?: DecoratorProps & T): Function => {
27 | const { keyValue, keyCode, code, keyEventName } = props || {};
28 |
29 | return Component =>
30 | class KeyHandleDecorator extends React.Component<
31 | T,
32 | KeyhandleDecoratorState,
33 | > {
34 | state: KeyhandleDecoratorState = {
35 | keyCode: null,
36 | keyValue: null,
37 | code: null,
38 | };
39 |
40 | render() {
41 | return (
42 |
43 |
50 |
51 |
52 | );
53 | }
54 |
55 | handleKey = (event: KeyboardEvent): void => {
56 | if (matcher && matcher(event, this.state)) {
57 | this.setState({ keyValue: null, keyCode: null });
58 | return;
59 | }
60 |
61 | this.setState({ keyValue: eventKey(event), keyCode: event.keyCode });
62 | };
63 | };
64 | };
65 | }
66 |
67 | export const keyHandler = keyHandleDecorator();
68 | export const keyToggleHandler = keyHandleDecorator(matchesKeyboardEvent);
69 |
--------------------------------------------------------------------------------
/lib/key-handler.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import { canUseDOM } from 'exenv';
6 |
7 | import { KEYDOWN, KEYPRESS, KEYUP, type KeyConstants } from './constants';
8 | import { type primitiveOrArray } from './types';
9 | import { isInput, matchesKeyboardEvent } from './utils';
10 |
11 | type Props = {|
12 | +keyValue?: ?primitiveOrArray,
13 | +keyCode?: ?primitiveOrArray,
14 | +code?: ?primitiveOrArray,
15 | +keyEventName: KeyConstants,
16 | +onKeyHandle?: (event: KeyboardEvent) => mixed,
17 | |};
18 |
19 | export default class KeyHandler extends React.Component {
20 | static propTypes = {
21 | keyValue: PropTypes.oneOfType([
22 | PropTypes.string,
23 | PropTypes.arrayOf(PropTypes.string),
24 | ]),
25 | keyCode: PropTypes.oneOfType([
26 | PropTypes.number,
27 | PropTypes.arrayOf(PropTypes.number),
28 | ]),
29 | code: PropTypes.oneOfType([
30 | PropTypes.string,
31 | PropTypes.arrayOf(PropTypes.string),
32 | ]),
33 | keyEventName: PropTypes.oneOf([KEYDOWN, KEYPRESS, KEYUP]),
34 | onKeyHandle: PropTypes.func,
35 | };
36 |
37 | static defaultProps = {
38 | keyEventName: KEYUP,
39 | };
40 |
41 | shouldComponentUpdate(): boolean {
42 | return false;
43 | }
44 |
45 | constructor(props: Props) {
46 | super(props);
47 |
48 | /* eslint-disable no-console */
49 | if (props.keyCode) {
50 | console.warn(
51 | 'Warning: Deprecated propType: `keyCode` is deprecated in favour of `code` for `KeyHandler`.',
52 | );
53 | }
54 |
55 | if (!props.keyValue && !props.keyCode && !props.code) {
56 | console.error(
57 | 'Warning: Failed propType: Missing prop `code`, `keyValue` or `keyCode` for `KeyHandler`.',
58 | );
59 | }
60 |
61 | /* eslint-enable */
62 | }
63 |
64 | componentDidMount(): void {
65 | if (!canUseDOM) return;
66 |
67 | window.document.addEventListener(this.props.keyEventName, this.handleKey);
68 | }
69 |
70 | componentWillUnmount(): void {
71 | if (!canUseDOM) return;
72 |
73 | window.document.removeEventListener(
74 | this.props.keyEventName,
75 | this.handleKey,
76 | );
77 | }
78 |
79 | render(): null {
80 | return null;
81 | }
82 |
83 | handleKey = (event: KeyboardEvent): void => {
84 | const { keyValue, keyCode, code, onKeyHandle } = this.props;
85 |
86 | if (!onKeyHandle) {
87 | return;
88 | }
89 |
90 | const { target } = event;
91 |
92 | if (target instanceof window.HTMLElement && isInput(target)) {
93 | return;
94 | }
95 |
96 | if (!matchesKeyboardEvent(event, { keyValue, keyCode, code })) {
97 | return;
98 | }
99 |
100 | onKeyHandle(event);
101 | };
102 | }
103 |
--------------------------------------------------------------------------------
/lib/types.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | export type primitiveOrArray = T | T[];
3 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import {
3 | KEYDOWN,
4 | KEYPRESS,
5 | KEYUP,
6 | KEY_CODE_KEYS,
7 | NORMALIZED_KEYS,
8 | } from './constants';
9 | import { type primitiveOrArray } from './types';
10 |
11 | type KeyboardKey = {|
12 | +keyValue: ?primitiveOrArray,
13 | +keyCode: ?primitiveOrArray,
14 | +code: ?primitiveOrArray,
15 | |};
16 |
17 | /**
18 | * Check if `given` element is an input / textarea form element or acts as one.
19 | */
20 |
21 | export function isInput(element: HTMLElement): boolean {
22 | if (!element) {
23 | return false;
24 | }
25 |
26 | const { tagName } = element;
27 | const editable = isContentEditable(element);
28 |
29 | return tagName === 'INPUT' || tagName === 'TEXTAREA' || editable;
30 | }
31 |
32 | function isContentEditable(element: HTMLElement): boolean {
33 | if (typeof element.getAttribute !== 'function') {
34 | return false;
35 | }
36 |
37 | return !!element.getAttribute('contenteditable');
38 | }
39 |
40 | /**
41 | * Matches an event against a given keyboard key.
42 | */
43 |
44 | function matchesElementOrArray(a: T | T[], b: T) {
45 | if (Array.isArray(a)) {
46 | return a.includes(b);
47 | }
48 | return a === b;
49 | }
50 |
51 | export function matchesKeyboardEvent(
52 | event: KeyboardEvent,
53 | { keyCode, keyValue, code }: KeyboardKey,
54 | ): boolean {
55 | if (!isNullOrUndefined(keyValue)) {
56 | const value = eventKey(event);
57 | if (matchesElementOrArray(keyValue, value)) {
58 | return true;
59 | }
60 | }
61 |
62 | if (!isNullOrUndefined(code)) {
63 | if (matchesElementOrArray(code, event.code)) {
64 | return true;
65 | }
66 | }
67 |
68 | if (!isNullOrUndefined(keyCode)) {
69 | // Firefox handles keyCode through which
70 | const keyCodeTarget = event.keyCode || event.which;
71 | if (matchesElementOrArray(keyCode, keyCodeTarget)) {
72 | return true;
73 | }
74 | }
75 |
76 | return false;
77 | }
78 |
79 | function isNullOrUndefined(value): boolean {
80 | return value === undefined || value === null;
81 | }
82 |
83 | export function eventKey(event: KeyboardEvent): string {
84 | const { key, keyCode, type } = event;
85 |
86 | if (key) {
87 | const normalizedKey = NORMALIZED_KEYS[key] || key;
88 |
89 | if (normalizedKey !== 'Unidentified') {
90 | return normalizedKey;
91 | }
92 | }
93 |
94 | if (type === KEYPRESS) {
95 | const charCode = eventCharCode(event);
96 |
97 | return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
98 | }
99 |
100 | if (type === KEYDOWN || type === KEYUP) {
101 | return KEY_CODE_KEYS[String(keyCode)] || 'Unidentified';
102 | }
103 |
104 | return '';
105 | }
106 |
107 | function eventCharCode(event: KeyboardEvent): number {
108 | let { charCode, keyCode } = event;
109 |
110 | if ('charCode' in event) {
111 | if (charCode === 0 && keyCode === 13) {
112 | return 13;
113 | }
114 | } else {
115 | charCode = keyCode;
116 | }
117 |
118 | if (charCode >= 32 || charCode === 13) {
119 | return charCode;
120 | }
121 |
122 | return 0;
123 | }
124 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-key-handler",
3 | "version": "1.2.0-beta.3",
4 | "description": "React component to handle keyboard events",
5 | "homepage": "https://github.com/ayrton/react-key-handler",
6 | "author": {
7 | "name": "Ayrton De Craene",
8 | "email": "im@ayrton.be",
9 | "url": "https://github.com/ayrton"
10 | },
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/ayrton/react-key-handler.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/ayrton/react-key-handler/issues"
18 | },
19 | "main": "dist/cjs/index.js",
20 | "module": "dist/esm/index.js",
21 | "scripts": {
22 | "build": "npm run build:clean && npm run build:bundle && npm run build:flow",
23 | "build:clean": "rimraf dist",
24 | "build:bundle": "rollup -c",
25 | "build:flow": "flow-copy-source lib dist/cjs",
26 | "demo": "parcel ./demo/index.html",
27 | "demo:build": "parcel build demo/index.html --out-dir demo/dist --public-url=/react-key-handler",
28 | "demo:deploy": "npm run demo:build && gh-pages -d demo/dist",
29 | "prepublish": "npm run build",
30 | "test": "npm run test:lint && npm run test:unit && npm run test:flow",
31 | "test:flow": "flow check",
32 | "test:lint": "eslint demo/ lib/ test/",
33 | "test:unit": "mocha"
34 | },
35 | "keywords": [
36 | "react",
37 | "reactjs",
38 | "key",
39 | "event",
40 | "handler"
41 | ],
42 | "devDependencies": {
43 | "babel-core": "^6.26.3",
44 | "babel-eslint": "^9.0.0",
45 | "babel-loader": "^8.0.0",
46 | "babel-plugin-external-helpers": "^6.22.0",
47 | "babel-plugin-transform-class-properties": "^6.24.1",
48 | "babel-preset-env": "^1.7.0",
49 | "babel-preset-react": "^6.11.0",
50 | "babel-preset-stage-1": "^6.13.0",
51 | "chai": "^4.1.2",
52 | "chai-enzyme": "^1.0.0-beta.1",
53 | "enzyme": "^3.3.0",
54 | "enzyme-adapter-react-16": "^1.1.1",
55 | "eslint": "^5.4.0",
56 | "eslint-config-prettier": "^3.0.1",
57 | "eslint-plugin-prettier": "^2.6.2",
58 | "eslint-plugin-react": "^7.11.1",
59 | "flow-bin": "^0.79.1",
60 | "flow-copy-source": "^2.0.2",
61 | "gh-pages": "^1.2.0",
62 | "jsdom": "^11.12.0",
63 | "mocha": "^5.2.0",
64 | "parcel-bundler": "^1.9.7",
65 | "prettier": "^1.14.2",
66 | "react": "^16.4.2",
67 | "react-dom": "^16.4.2",
68 | "react-syntax-highlighter": "^8.0.1",
69 | "rimraf": "^2.6.2",
70 | "rollup": "^0.63.5",
71 | "rollup-plugin-babel": "^3.0.7",
72 | "rollup-plugin-node-resolve": "^3.3.0",
73 | "rollup-plugin-replace": "^2.0.0",
74 | "rollup-plugin-size-snapshot": "^0.6.1",
75 | "rollup-plugin-terser": "^1.0.1",
76 | "sinon": "^6.1.4"
77 | },
78 | "peerDependencies": {
79 | "react": "^16.2.0-0"
80 | },
81 | "dependencies": {
82 | "eslint-plugin-flowtype": "^2.50.0",
83 | "exenv": "^1.2.0",
84 | "prop-types": "^15.6.2"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import replace from 'rollup-plugin-replace';
3 | import resolve from 'rollup-plugin-node-resolve';
4 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot';
5 | import { terser } from 'rollup-plugin-terser';
6 |
7 | const input = 'lib/index';
8 | const external = ['exenv', 'prop-types', 'react'];
9 |
10 | const globals = {
11 | 'prop-types': 'PropTypes',
12 | exenv: 'exenv',
13 | react: 'React',
14 | };
15 |
16 | const babelOptions = {
17 | babelrc: false,
18 | exclude: '**/node_modules/**',
19 | presets: [
20 | [
21 | 'env',
22 | {
23 | modules: false,
24 | },
25 | ],
26 | 'react',
27 | 'stage-1',
28 | ],
29 | plugins: ['external-helpers', 'transform-class-properties'],
30 | };
31 |
32 | export default [
33 | {
34 | input,
35 | output: {
36 | file: 'dist/umd/index.js',
37 | format: 'umd',
38 | name: 'ReactKeyHandler',
39 | globals,
40 | },
41 | external,
42 | plugins: [
43 | resolve(),
44 | babel(babelOptions),
45 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }),
46 | sizeSnapshot(),
47 | ],
48 | },
49 |
50 | {
51 | input,
52 | output: {
53 | file: 'dist/umd/index.min.js',
54 | format: 'umd',
55 | name: 'ReactKeyHandler',
56 | globals,
57 | },
58 | external,
59 | plugins: [
60 | resolve(),
61 | babel(babelOptions),
62 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }),
63 | sizeSnapshot(),
64 | terser(),
65 | ],
66 | },
67 | {
68 | input,
69 | output: {
70 | file: 'dist/cjs/index.js',
71 | format: 'cjs',
72 | },
73 | external,
74 | plugins: [resolve(), babel(babelOptions), sizeSnapshot()],
75 | },
76 |
77 | {
78 | input,
79 | output: {
80 | file: 'dist/esm/index.js',
81 | format: 'esm',
82 | },
83 | external,
84 | plugins: [resolve(), babel(babelOptions), sizeSnapshot()],
85 | },
86 | ];
87 |
--------------------------------------------------------------------------------
/test/components/helpers/triggerKeyEvent.js:
--------------------------------------------------------------------------------
1 | export default function triggerKeyEvent(
2 | eventName,
3 | keyCode,
4 | keyValue = undefined,
5 | ) {
6 | const event = new window.KeyboardEvent(eventName, { keyCode, key: keyValue });
7 | document.dispatchEvent(event);
8 | }
9 |
--------------------------------------------------------------------------------
/test/components/key-handler.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import sinon from 'sinon';
3 | import { expect } from 'chai';
4 | import { mount, render } from 'enzyme';
5 | import triggerKeyEvent from './helpers/triggerKeyEvent';
6 |
7 | import KeyHandler, { KEYUP, KEYDOWN } from '../../lib';
8 |
9 | const M = 77;
10 | const S = 83;
11 | const ARROW_LEFT = 'ArrowLeft';
12 | const ARROW_RIGHT = 'ArrowRight';
13 |
14 | describe('KeyHandler', () => {
15 | it('renders nothing', () => {
16 | const el = render( );
17 | expect(el).to.be.blank();
18 | });
19 |
20 | it('handles key up events when key value match', () => {
21 | const handler = sinon.spy();
22 | mount( );
23 |
24 | triggerKeyEvent(KEYUP, undefined, ARROW_LEFT);
25 |
26 | expect(handler.calledOnce).to.equal(true);
27 | });
28 |
29 | it('handles more than one key value match', () => {
30 | const handler = sinon.spy();
31 | mount(
32 | ,
33 | );
34 |
35 | triggerKeyEvent(KEYUP, undefined, ARROW_LEFT);
36 | triggerKeyEvent(KEYUP, undefined, ARROW_RIGHT);
37 | triggerKeyEvent(KEYUP, undefined, 's');
38 |
39 | expect(handler.calledTwice).to.equal(true);
40 | });
41 |
42 | it('handles key up events when key code match', () => {
43 | const handler = sinon.spy();
44 | mount( );
45 |
46 | triggerKeyEvent(KEYUP, M);
47 |
48 | expect(handler.calledOnce).to.equal(true);
49 | });
50 |
51 | it('ignores key up events when no key value match', () => {
52 | const handler = sinon.spy();
53 | mount( );
54 |
55 | triggerKeyEvent(KEYUP, undefined, ARROW_RIGHT);
56 |
57 | expect(handler.calledOnce).to.equal(false);
58 | });
59 |
60 | it('ignores key up events when no key code match', () => {
61 | const handler = sinon.spy();
62 | mount( );
63 |
64 | triggerKeyEvent(KEYUP, M);
65 |
66 | expect(handler.calledOnce).to.equal(false);
67 | });
68 |
69 | it('ignores key up events when no key code or no names are passed', () => {
70 | const handler = sinon.spy();
71 | mount( );
72 |
73 | triggerKeyEvent(KEYUP, M);
74 |
75 | expect(handler.calledOnce).to.equal(false);
76 | });
77 |
78 | it('handles key down events', () => {
79 | const handler = sinon.spy();
80 | mount(
81 | ,
82 | );
83 |
84 | triggerKeyEvent(KEYDOWN, M);
85 |
86 | expect(handler.calledOnce).to.equal(true);
87 | });
88 |
89 | // This make little sense - a user will not understand that there is a priority
90 | // the user wil most likely expect that the function gets called if any
91 | // of the matching criteria match.
92 | it.skip('prioritizes key value over code', () => {
93 | const handler = sinon.spy();
94 | mount(
95 | ,
96 | );
97 |
98 | triggerKeyEvent(KEYUP, M);
99 |
100 | expect(handler.calledOnce).to.equal(false);
101 |
102 | triggerKeyEvent(KEYUP, undefined, ARROW_LEFT);
103 |
104 | expect(handler.calledOnce).to.equal(true);
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --recursive
2 | --full-trace
3 | --require babel-core/register
4 | --require ./test/support/helper.js
5 | --timeout 5000
6 | --slow 3000
7 |
--------------------------------------------------------------------------------
/test/support/helper.js:
--------------------------------------------------------------------------------
1 | import Adapter from 'enzyme-adapter-react-16';
2 | import chai from 'chai';
3 | import chaiEnzyme from 'chai-enzyme';
4 | import { JSDOM } from 'jsdom';
5 | import { configure } from 'enzyme';
6 |
7 | const jsdom = new JSDOM('');
8 | const { window } = jsdom;
9 |
10 | function copyProps(src, target) {
11 | const props = Object.getOwnPropertyNames(src)
12 | .filter(prop => typeof target[prop] === 'undefined')
13 | .reduce(
14 | (result, prop) => ({
15 | ...result,
16 | [prop]: Object.getOwnPropertyDescriptor(src, prop),
17 | }),
18 | {},
19 | );
20 | Object.defineProperties(target, props);
21 | }
22 |
23 | global.window = window;
24 | global.document = window.document;
25 |
26 | global.navigator = {
27 | userAgent: 'node.js',
28 | };
29 |
30 | copyProps(window, global);
31 |
32 | configure({ adapter: new Adapter() });
33 | chai.use(chaiEnzyme());
34 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { JSDOM } from 'jsdom';
3 |
4 | import { isInput, matchesKeyboardEvent, eventKey } from '../lib/utils';
5 |
6 | describe('isInput', () => {
7 | it('returns true if the element is an input', () => {
8 | expect(isInput({ tagName: 'INPUT' })).to.be.true;
9 | });
10 |
11 | it('returns true if the element is a textarea', () => {
12 | expect(isInput({ tagName: 'TEXTAREA' })).to.be.true;
13 | });
14 |
15 | it('returns true if the element is contenteditable', () => {
16 | expect(
17 | isInput({
18 | getAttribute: function() {
19 | return true;
20 | },
21 | }),
22 | ).to.be.true;
23 | });
24 |
25 | it('returns false if the element is not contenteditable', () => {
26 | expect(
27 | isInput({
28 | getAttribute: function() {
29 | return false;
30 | },
31 | }),
32 | ).to.be.false;
33 | });
34 |
35 | it('returns false if the element is not an input or textarea', () => {
36 | expect(isInput({ tagName: 'A' })).to.be.false;
37 | });
38 |
39 | it('returns false if the element is falsey', () => {
40 | expect(isInput(null)).to.be.false;
41 | });
42 | });
43 |
44 | const getKeyboardEvent = (
45 | eventName: string,
46 | opts: {| key?: string, keyCode?: number |},
47 | ) => {
48 | const { window: testWindow } = new JSDOM('', { runScripts: 'dangerously' });
49 | return new testWindow.KeyboardEvent(eventName, opts);
50 | };
51 |
52 | describe('matchesKeyboardEvent', () => {
53 | describe('matches KeyboardEvent.key against', () => {
54 | const arrowUpValueEvent = getKeyboardEvent('keyup', { key: 'ArrowUp' });
55 |
56 | it('keyValue Props property', () => {
57 | expect(matchesKeyboardEvent(arrowUpValueEvent, { keyValue: 'ArrowUp' }))
58 | .to.be.true;
59 | expect(matchesKeyboardEvent(arrowUpValueEvent, { keyValue: 'ArrowDown' }))
60 | .to.be.false;
61 | });
62 |
63 | it('keyCode Props property', () => {
64 | expect(matchesKeyboardEvent(arrowUpValueEvent, { keyCode: 38 })).to.be
65 | .false;
66 | expect(matchesKeyboardEvent(arrowUpValueEvent, { keyCode: 40 })).to.be
67 | .false;
68 | });
69 | });
70 |
71 | describe('matches KeyboardEvent.keyCode against', () => {
72 | const arrowUpCodeEvent = getKeyboardEvent('keyup', { keyCode: 38 });
73 |
74 | it('keyValue Props property', () => {
75 | expect(matchesKeyboardEvent(arrowUpCodeEvent, { keyValue: 'ArrowUp' })).to
76 | .be.true;
77 | expect(matchesKeyboardEvent(arrowUpCodeEvent, { keyValue: 'ArrowDown' }))
78 | .to.be.false;
79 | });
80 |
81 | it('keyCode Props property', () => {
82 | expect(matchesKeyboardEvent(arrowUpCodeEvent, { keyCode: 38 })).to.be
83 | .true;
84 | expect(matchesKeyboardEvent(arrowUpCodeEvent, { keyCode: 40 })).to.be
85 | .false;
86 | });
87 | });
88 | });
89 |
90 | describe('eventKey', () => {
91 | it('normalizes keys', () => {
92 | const event = getKeyboardEvent('keyup', { key: 'Esc' });
93 | expect(eventKey(event)).to.equal('Escape');
94 | });
95 |
96 | it('returns valid keys', () => {
97 | const event = getKeyboardEvent('keyup', { key: 'Escape' });
98 | expect(eventKey(event)).to.equal('Escape');
99 | });
100 |
101 | it('returns Unidentified key', () => {
102 | const event = getKeyboardEvent('keyup', { key: 'Unidentified' });
103 | expect(eventKey(event)).to.equal('Unidentified');
104 | });
105 |
106 | it('ignores Unidentified key in favor of key codes', () => {
107 | const event = getKeyboardEvent('keyup', {
108 | key: 'Unidentified',
109 | keyCode: 38,
110 | });
111 | expect(eventKey(event)).to.equal('ArrowUp');
112 | });
113 |
114 | it('translates key codes', () => {
115 | const event = getKeyboardEvent('keyup', { keyCode: 38 });
116 | expect(eventKey(event)).to.equal('ArrowUp');
117 | });
118 |
119 | it('falls back to Unidentified for unknown key code keys', () => {
120 | const event = getKeyboardEvent('keyup', { keyCode: 1337 });
121 | expect(eventKey(event)).to.equal('Unidentified');
122 | });
123 |
124 | it('supports enter on key press', () => {
125 | const event = getKeyboardEvent('keypress', { keyCode: 13 });
126 | expect(eventKey(event)).to.equal('Enter');
127 | });
128 | });
129 |
--------------------------------------------------------------------------------