New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add W3C KeyboardEvent.key values support. #10

Closed
wants to merge 9 commits into
base: master
from
Copy path View file
@@ -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
@@ -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
@@ -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
Copy path View file
@@ -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,
@@ -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;
}

@@ -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,
};
@@ -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);
@@ -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);
@@ -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) });
}
};
};
Copy path View file
@@ -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.
*/
@@ -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();
}
@@ -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} />);
@@ -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} />);
@@ -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);
}
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.