[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)