@samitouri / QOS-React-2 / commits / e0472709c8

[Flare] Adds Keyboard event responder (#16204)

Dominic Gannaway committed Jul 25, 2019 at 17:47 UTC e0472709c81f53b333feafb5442319d6d25dda4b
6 files changed +390
packages/react-events/keyboard.js new
+12
@@ -0,0 +1,12 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +module.exports = require('./src/dom/Keyboard');
packages/react-events/npm/keyboard.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-keyboard.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-keyboard.development.js');
7 +}
packages/react-events/package.json
+1
@@ -20,6 +20,7 @@
20 "scroll.js",
21 "focus-scope.js",
22 "input.js",
23 + "keyboard.js",
24 "build-info.json",
25 "cjs/",
26 "umd/"
packages/react-events/src/dom/Keyboard.js new
+206
@@ -0,0 +1,206 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {
11 + ReactDOMResponderEvent,
12 + ReactDOMResponderContext,
13 +} from 'shared/ReactDOMTypes';
14 +
15 +import React from 'react';
16 +import {DiscreteEvent} from 'shared/ReactTypes';
17 +
18 +type KeyboardEventType = 'keydown' | 'keyup';
19 +
20 +type KeyboardListenerProps = {|
21 + onKeyDown: (e: KeyboardEvent) => void,
22 + onKeyUp: (e: KeyboardEvent) => void,
23 +|};
24 +
25 +type KeyboardResponderProps = {
26 + disabled: boolean,
27 +};
28 +
29 +type KeyboardEvent = {|
30 + altKey: boolean,
31 + ctrlKey: boolean,
32 + isComposing: boolean,
33 + key: string,
34 + location: number,
35 + metaKey: boolean,
36 + repeat: boolean,
37 + shiftKey: boolean,
38 + target: Element | Document,
39 + type: KeyboardEventType,
40 + timeStamp: number,
41 +|};
42 +
43 +const targetEventTypes = ['keydown', 'keyup'];
44 +
45 +/**
46 + * Normalization of deprecated HTML5 `key` values
47 + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
48 + */
49 +const normalizeKey = {
50 + Esc: 'Escape',
51 + Spacebar: ' ',
52 + Left: 'ArrowLeft',
53 + Up: 'ArrowUp',
54 + Right: 'ArrowRight',
55 + Down: 'ArrowDown',
56 + Del: 'Delete',
57 + Win: 'OS',
58 + Menu: 'ContextMenu',
59 + Apps: 'ContextMenu',
60 + Scroll: 'ScrollLock',
61 + MozPrintableKey: 'Unidentified',
62 +};
63 +
64 +/**
65 + * Translation from legacy `keyCode` to HTML5 `key`
66 + * Only special keys supported, all others depend on keyboard layout or browser
67 + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
68 + */
69 +const translateToKey = {
70 + '8': 'Backspace',
71 + '9': 'Tab',
72 + '12': 'Clear',
73 + '13': 'Enter',
74 + '16': 'Shift',
75 + '17': 'Control',
76 + '18': 'Alt',
77 + '19': 'Pause',
78 + '20': 'CapsLock',
79 + '27': 'Escape',
80 + '32': ' ',
81 + '33': 'PageUp',
82 + '34': 'PageDown',
83 + '35': 'End',
84 + '36': 'Home',
85 + '37': 'ArrowLeft',
86 + '38': 'ArrowUp',
87 + '39': 'ArrowRight',
88 + '40': 'ArrowDown',
89 + '45': 'Insert',
90 + '46': 'Delete',
91 + '112': 'F1',
92 + '113': 'F2',
93 + '114': 'F3',
94 + '115': 'F4',
95 + '116': 'F5',
96 + '117': 'F6',
97 + '118': 'F7',
98 + '119': 'F8',
99 + '120': 'F9',
100 + '121': 'F10',
101 + '122': 'F11',
102 + '123': 'F12',
103 + '144': 'NumLock',
104 + '145': 'ScrollLock',
105 + '224': 'Meta',
106 +};
107 +
108 +function getEventKey(nativeEvent): string {
109 + const nativeKey = nativeEvent.key;
110 + if (nativeKey) {
111 + // Normalize inconsistent values reported by browsers due to
112 + // implementations of a working draft specification.
113 +
114 + // FireFox implements `key` but returns `MozPrintableKey` for all
115 + // printable characters (normalized to `Unidentified`), ignore it.
116 + const key = normalizeKey[nativeKey] || nativeKey;
117 + if (key !== 'Unidentified') {
118 + return key;
119 + }
120 + }
121 + return translateToKey[nativeEvent.keyCode] || 'Unidentified';
122 +}
123 +
124 +function createKeyboardEvent(
125 + event: ReactDOMResponderEvent,
126 + context: ReactDOMResponderContext,
127 + type: KeyboardEventType,
128 + target: Document | Element,
129 +): KeyboardEvent {
130 + const nativeEvent = (event: any).nativeEvent;
131 + const {
132 + altKey,
133 + ctrlKey,
134 + isComposing,
135 + location,
136 + metaKey,
137 + repeat,
138 + shiftKey,
139 + } = nativeEvent;
140 +
141 + return {
142 + altKey,
143 + ctrlKey,
144 + isComposing,
145 + key: getEventKey(nativeEvent),
146 + location,
147 + metaKey,
148 + repeat,
149 + shiftKey,
150 + target,
151 + timeStamp: context.getTimeStamp(),
152 + type,
153 + };
154 +}
155 +
156 +function dispatchKeyboardEvent(
157 + eventPropName: string,
158 + event: ReactDOMResponderEvent,
159 + context: ReactDOMResponderContext,
160 + type: KeyboardEventType,
161 + target: Element | Document,
162 +): void {
163 + const syntheticEvent = createKeyboardEvent(event, context, type, target);
164 + context.dispatchEvent(eventPropName, syntheticEvent, DiscreteEvent);
165 +}
166 +
167 +const keyboardResponderImpl = {
168 + targetEventTypes,
169 + onEvent(
170 + event: ReactDOMResponderEvent,
171 + context: ReactDOMResponderContext,
172 + props: KeyboardResponderProps,
173 + ): void {
174 + const {responderTarget, type} = event;
175 +
176 + if (props.disabled) {
177 + return;
178 + }
179 + if (type === 'keydown') {
180 + dispatchKeyboardEvent(
181 + 'onKeyDown',
182 + event,
183 + context,
184 + 'keydown',
185 + ((responderTarget: any): Element | Document),
186 + );
187 + } else if (type === 'keyup') {
188 + dispatchKeyboardEvent(
189 + 'onKeyUp',
190 + event,
191 + context,
192 + 'keyup',
193 + ((responderTarget: any): Element | Document),
194 + );
195 + }
196 + },
197 +};
198 +
199 +export const KeyboardResponder = React.unstable_createResponder(
200 + 'Keyboard',
201 + keyboardResponderImpl,
202 +);
203 +
204 +export function useKeyboardListener(props: KeyboardListenerProps): void {
205 + React.unstable_useListener(KeyboardResponder, props);
206 +}
packages/react-events/src/dom/__tests__/Keyboard-test.internal.js new
+149
@@ -0,0 +1,149 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +let KeyboardResponder;
16 +let useKeyboardListener;
17 +
18 +const createEvent = (type, data) => {
19 + const event = document.createEvent('CustomEvent');
20 + event.initCustomEvent(type, true, true);
21 + if (data != null) {
22 + Object.entries(data).forEach(([key, value]) => {
23 + event[key] = value;
24 + });
25 + }
26 + return event;
27 +};
28 +
29 +describe('Keyboard event responder', () => {
30 + let container;
31 +
32 + beforeEach(() => {
33 + jest.resetModules();
34 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
35 + ReactFeatureFlags.enableFlareAPI = true;
36 + React = require('react');
37 + ReactDOM = require('react-dom');
38 + KeyboardResponder = require('react-events/keyboard').KeyboardResponder;
39 + useKeyboardListener = require('react-events/keyboard').useKeyboardListener;
40 +
41 + container = document.createElement('div');
42 + document.body.appendChild(container);
43 + });
44 +
45 + afterEach(() => {
46 + ReactDOM.render(null, container);
47 + document.body.removeChild(container);
48 + container = null;
49 + });
50 +
51 + describe('disabled', () => {
52 + let onKeyDown, onKeyUp, ref;
53 +
54 + beforeEach(() => {
55 + onKeyDown = jest.fn();
56 + onKeyUp = jest.fn();
57 + ref = React.createRef();
58 + const Component = () => {
59 + useKeyboardListener({
60 + onKeyDown,
61 + onKeyUp,
62 + });
63 + return (
64 + <div ref={ref} responders={<KeyboardResponder disabled={true} />} />
65 + );
66 + };
67 + ReactDOM.render(<Component />, container);
68 + });
69 +
70 + it('prevents custom events being dispatched', () => {
71 + ref.current.dispatchEvent(createEvent('scroll'));
72 + expect(onKeyDown).not.toBeCalled();
73 + expect(onKeyUp).not.toBeCalled();
74 + });
75 + });
76 +
77 + describe('onKeyDown', () => {
78 + let onKeyDown, ref;
79 +
80 + beforeEach(() => {
81 + onKeyDown = jest.fn();
82 + ref = React.createRef();
83 + const Component = () => {
84 + useKeyboardListener({
85 + onKeyDown,
86 + });
87 + return <div ref={ref} responders={<KeyboardResponder />} />;
88 + };
89 + ReactDOM.render(<Component />, container);
90 + });
91 +
92 + it('is called after "keydown" event', () => {
93 + ref.current.dispatchEvent(
94 + new KeyboardEvent('keydown', {
95 + bubbles: true,
96 + cancelable: true,
97 + key: 'Q',
98 + }),
99 + );
100 + expect(onKeyDown).toHaveBeenCalledTimes(1);
101 + expect(onKeyDown).toHaveBeenCalledWith(
102 + expect.objectContaining({key: 'Q', type: 'keydown'}),
103 + );
104 + });
105 + });
106 +
107 + describe('onKeyUp', () => {
108 + let onKeyDown, onKeyUp, ref;
109 +
110 + beforeEach(() => {
111 + onKeyDown = jest.fn();
112 + onKeyUp = jest.fn();
113 + ref = React.createRef();
114 + const Component = () => {
115 + useKeyboardListener({
116 + onKeyDown,
117 + onKeyUp,
118 + });
119 + return <div ref={ref} responders={<KeyboardResponder />} />;
120 + };
121 + ReactDOM.render(<Component />, container);
122 + });
123 +
124 + it('is called after "keydown" event', () => {
125 + ref.current.dispatchEvent(
126 + new KeyboardEvent('keydown', {
127 + bubbles: true,
128 + cancelable: true,
129 + key: 'Q',
130 + }),
131 + );
132 + ref.current.dispatchEvent(
133 + new KeyboardEvent('keyup', {
134 + bubbles: true,
135 + cancelable: true,
136 + key: 'Q',
137 + }),
138 + );
139 + expect(onKeyDown).toHaveBeenCalledTimes(1);
140 + expect(onKeyDown).toHaveBeenCalledWith(
141 + expect.objectContaining({key: 'Q', type: 'keydown'}),
142 + );
143 + expect(onKeyUp).toHaveBeenCalledTimes(1);
144 + expect(onKeyUp).toHaveBeenCalledWith(
145 + expect.objectContaining({key: 'Q', type: 'keyup'}),
146 + );
147 + });
148 + });
149 +});
scripts/rollup/bundles.js
+15
@@ -538,6 +538,21 @@ const bundles = [
538 global: 'ReactEventsScroll',
539 externals: ['react'],
540 },
541 +
542 + {
543 + bundleTypes: [
544 + UMD_DEV,
545 + UMD_PROD,
546 + NODE_DEV,
547 + NODE_PROD,
548 + FB_WWW_DEV,
549 + FB_WWW_PROD,
550 + ],
551 + moduleType: NON_FIBER_RENDERER,
552 + entry: 'react-events/keyboard',
553 + global: 'ReactEventsKeyboard',
554 + externals: ['react'],
555 + },
556 ];
557
558 // Based on deep-freeze by substack (public domain)