Skip to content
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Unless you want absolute flexibility we highly recommend you to use one of the d

### `keyHandler` decorator

The decorator will decorate the given component with a `keyCode` and `keyName`
The decorator will decorate the given component with a `keyValue`, `keyCode` and `keyName`
property.

```jsx
Expand Down Expand Up @@ -60,17 +60,19 @@ The prop types of the `keyHandler` decorator are:

```js
type Props = {
keyValue: ?string,
keyCode: ?number,
keyEventName: ?string,
keyName: ?string,
}
```

* `keyValue` can be any given [W3C keyboard key value](https://www.w3.org/TR/DOM-Level-3-Events-key/)
* `keyCode` can be any given [keyboard code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
* `keyEventName` will default to `'keyup'`
* `keyName` can be any given character

You should either pass a `keyCode` or a `keyName`, not both.
You should either pass a `keyValue`, a `keyCode` or a `keyName`, not both.

### `keyToggleHandler` decorator

Expand Down Expand Up @@ -119,19 +121,21 @@ The prop types of the `KeyHandler` component are:

```js
type Props = {
keyValue: ?string,
keyCode: ?number,
keyEventName: string,
keyName: ?string,
onKeyHandle: Function,
};
```

* `keyValue` can be any given [W3C keyboard key value](https://www.w3.org/TR/DOM-Level-3-Events-key/)
* `keyCode` can be any given [keyboard code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
* `keyEventName` will default to `'keyup'`
* `keyName` can be any given character
* `onKeyHandle` is the function that is being called when key code is handled

You should either pass a `keyCode` or a `keyName`, not both.
You should either pass a `keyValue`, a `keyCode` or a `keyName`, not both.

### Form key handling

Expand Down
37 changes: 19 additions & 18 deletions lib/components/key-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import keyNames from 'keycodes';
import {canUseDOM} from 'exenv';

import {KEYUP} from '../constants';
import {isInput} from '../utils';
import {isInput, keyValues, matchesKeyboardEvent} from '../utils';

/**
* Types.
*/

type Props = {
keyValue: ?string,
keyCode: ?number,
keyEventName: string,
keyName: ?string,
Expand Down Expand Up @@ -56,9 +57,7 @@ export default class KeyHandler extends React.Component {
}

handleKey(event: KeyboardEvent): void {
const keyCode = this.props.keyCode || keyNames(this.props.keyName);

if (event.keyCode !== keyCode) {
if (!matchesKeyboardEvent(event, this.props)) {
return;
}

Expand All @@ -79,12 +78,14 @@ export default class KeyHandler extends React.Component {
*/

type DecoratorProps = {
keyValue: ?string,
keyCode: ?number,
keyName: ?string,
keyEventName: ?string,
}

type State = {
keyValue: ?string,
keyCode: ?number,
keyName: ?string,
};
Expand All @@ -93,10 +94,10 @@ type State = {
* KeyHandler decorators.
*/

export function keyHandler({keyCode, keyName, keyEventName}: DecoratorProps): Function {
export function keyHandler({keyValue, keyCode, keyName, keyEventName}: DecoratorProps): Function {
return (component) => {
return class KeyHandleDecorator extends React.Component {
state: State = { keyCode: null, keyName: null };
state: State = { keyValue: null, keyCode: null, keyName: null };

constructor(props: any) {
super(props);
Expand All @@ -107,30 +108,30 @@ export function keyHandler({keyCode, keyName, keyEventName}: DecoratorProps): Fu
render(): ReactElement {
return (
<div>
<KeyHandler keyCode={keyCode} keyEventName={keyEventName} keyName={keyName} onKeyHandle={this.handleKey} />
<KeyHandler keyValue={keyValue} keyCode={keyCode} keyEventName={keyEventName} keyName={keyName} onKeyHandle={this.handleKey} />

{this.renderDecoratedComponent()}
</div>
);
}

renderDecoratedComponent(): ReactElement {
const {keyCode, keyName} = this.state;
const {keyValue, keyCode, keyName} = this.state;

return React.createElement(component, { ...this.props, keyCode, keyName });
return React.createElement(component, { ...this.props, keyValue, keyCode, keyName });
}

handleKey(event: KeyboardEvent): void {
this.setState({ keyCode: event.keyCode, keyName: keyNames(event.keyCode) });
this.setState({ keyValue: event.key || keyValues(keyNames(event.keyCode)), keyCode: event.keyCode, keyName: keyNames(event.keyCode) });
}
};
};
}

export function keyToggleHandler({keyCode, keyName, keyEventName}: DecoratorProps): Function {
export function keyToggleHandler({keyValue, keyCode, keyName, keyEventName}: DecoratorProps): Function {
return (component) => {
return class KeyHandleDecorator extends React.Component {
state: State = { keyCode: null, keyName: null };
state: State = { keyValue: null, keyCode: null, keyName: null };

constructor(props: any) {
super(props);
Expand All @@ -141,25 +142,25 @@ export function keyToggleHandler({keyCode, keyName, keyEventName}: DecoratorProp
render(): ReactElement {
return (
<div>
<KeyHandler keyCode={keyCode} keyEventName={keyEventName} keyName={keyName} onKeyHandle={this.handleToggleKey} />
<KeyHandler keyValue={keyValue} keyCode={keyCode} keyEventName={keyEventName} keyName={keyName} onKeyHandle={this.handleToggleKey} />

{this.renderDecoratedComponent()}
</div>
);
}

renderDecoratedComponent(): ReactElement {
const {keyCode, keyName} = this.state;
const {keyValue, keyCode, keyName} = this.state;

return React.createElement(component, { ...this.props, keyCode, keyName });
return React.createElement(component, { ...this.props, keyValue, keyCode, keyName });
}

handleToggleKey(event: KeyboardEvent): void {
if (this.state.keyCode === event.keyCode) {
return this.setState({ keyCode: null, keyName: null });
if (!matchesKeyboardEvent(event, this.state)) {
return this.setState({ keyValue: null, keyCode: null, keyName: null });
}

this.setState({ keyCode: event.keyCode, keyName: keyNames(event.keyCode) });
this.setState({ keyValue: event.key || keyValues(keyNames(event.keyCode)), keyCode: event.keyCode, keyName: keyNames(event.keyCode) });
}
};
};
Expand Down
67 changes: 67 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
/* @flow */

import keyNames from 'keycodes';

/**
* Types.
*/

type Props = {
keyValue: ?string,
keyCode: ?number,
keyName: ?string
};

/**
* Check if `given` element is an input or textarea form element.
*/
Expand All @@ -13,3 +25,58 @@ export function isInput(element: HTMLElement): boolean {

return tagName === 'INPUT' || tagName === 'TEXTAREA';
}

/**
* Maps [keycodes](https://www.npmjs.com/package/keycodes) key names to KeyboardEvent.key values.
*/
export function keyValues(keyName: string): string {
const keys = {
ctrl: 'Control',
control: 'Control',
alt: 'Alt',
option: 'Option',
shift: 'Shift',
windows: 'Meta',
command: 'Meta',
esc: 'Escape',
escape: 'Escape',
backspace: 'Backspace',
tab: 'Tab',
enter: 'Enter',
'return': 'Enter',
space: ' ',
pause: 'Pause',
insert: 'Insert',
'delete': 'Delete',
home: 'Home',
end: 'End',
pageup: 'PageUp',
pagedown: 'PageDown',
left: 'ArrowLeft',
up: 'ArrowUp',
right: 'ArrowRight',
down: 'ArrowDown',
capslock: 'CapsLock',
numlock: 'NumLock',
scrolllock: 'ScrollLock',
};

return keys[keyName] || keyName;
}

/**
* Maps keyCodes to KeyboardEvent.key values.
*/
export function keyValueFromCode(keyCode: number): string {
return keyValues(keyNames(keyCode));
}

/**
* Matches a KeyboardEvent against a given Props type by all its possible values.
*/
export function matchesKeyboardEvent(event: KeyboardEvent, {keyValue, keyCode, keyName}: Props): boolean {
const keyVal = keyValue || (keyCode && keyValueFromCode(keyCode)) || (keyName && keyValues(keyName));
const eventKeyVal = event.key || keyValueFromCode(event.keyCode);

return String(eventKeyVal).toLowerCase() === String(keyVal).toLowerCase();
}
33 changes: 31 additions & 2 deletions test/components/key-handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ import KeyHandler, {KEYUP, KEYDOWN} from 'components/key-handler';

const M = 77;
const S = 83;
const ARROW_LEFT = 'ArrowLeft';
const ARROW_RIGHT = 'ArrowRight';

describe('KeyHandler', () => {
it('renders nothing', () => {
const el = render(<KeyHandler />);
expect(el).to.be.blank();
});

it('handles key up events when key value match', () => {
const handler = sinon.spy();
mount(<KeyHandler keyValue={ARROW_LEFT} onKeyHandle={handler} />);

triggerKeyEvent(KEYUP, undefined, ARROW_LEFT);

expect(handler.calledOnce).to.equal(true);
});

it('handles key up events when key code match', () => {
const handler = sinon.spy();
mount(<KeyHandler keyCode={M} onKeyHandle={handler} />);
Expand All @@ -32,6 +43,15 @@ describe('KeyHandler', () => {
expect(handler.calledOnce).to.equal(true);
});

it('ignores key up events when no key value match', () => {
const handler = sinon.spy();
mount(<KeyHandler keyValue={ARROW_LEFT} onKeyHandle={handler} />);

triggerKeyEvent(KEYUP, undefined, ARROW_RIGHT);

expect(handler.calledOnce).to.equal(false);
});

it('ignores key up events when no key code match', () => {
const handler = sinon.spy();
mount(<KeyHandler keyCode={S} onKeyHandle={handler} />);
Expand Down Expand Up @@ -67,9 +87,18 @@ describe('KeyHandler', () => {

expect(handler.calledOnce).to.equal(true);
});

it('prioritizes key value over code/name', () => {
const handler = sinon.spy();
mount(<KeyHandler keyCode={M} keyName="m" keyValue={ARROW_LEFT} onKeyHandle={handler} />);

triggerKeyEvent(KEYUP, S, ARROW_LEFT);

expect(handler.calledOnce).to.equal(true);
});
});

function triggerKeyEvent(eventName, keyCode) {
const event = new window.KeyboardEvent(eventName, { keyCode });
function triggerKeyEvent(eventName, keyCode, keyValue = undefined) {
const event = new window.KeyboardEvent(eventName, { keyCode, key: keyValue });
document.dispatchEvent(event);
}
Loading