[react-events] Adds preventKeys support to Keyboard responder (#16642)
Dominic Gannaway committed
Sep 4, 2019 at 01:38 UTC
af032764a9f244633eb7ff1a3205d2ec6073b501
2 files changed
+174
-3
packages/react-events/src/dom/Keyboard.js
+46
-3
@@ -22,6 +22,7 @@ type KeyboardProps = {
22
disabled: boolean,
23
onKeyDown: (e: KeyboardEvent) => void,
24
onKeyUp: (e: KeyboardEvent) => void,
25
+ preventKeys: Array<string>,
26
};
27
28
type KeyboardEvent = {|
@@ -36,9 +37,12 @@ type KeyboardEvent = {|
37
target: Element | Document,
38
type: KeyboardEventType,
39
timeStamp: number,
40
+ defaultPrevented: boolean,
41
|};
42
41
-const targetEventTypes = ['keydown', 'keyup'];
43
+const isArray = Array.isArray;
44
+const targetEventTypes = ['keydown_active', 'keyup'];
45
+const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
46
47
/**
48
* Normalization of deprecated HTML5 `key` values
@@ -107,7 +111,7 @@ function isFunction(obj): boolean {
111
return typeof obj === 'function';
112
}
113
110
-function getEventKey(nativeEvent): string {
114
+function getEventKey(nativeEvent: Object): string {
115
const nativeKey = nativeEvent.key;
116
if (nativeKey) {
117
// Normalize inconsistent values reported by browsers due to
@@ -128,6 +132,7 @@ function createKeyboardEvent(
132
context: ReactDOMResponderContext,
133
type: KeyboardEventType,
134
target: Document | Element,
135
+ defaultPrevented: boolean,
136
): KeyboardEvent {
137
const nativeEvent = (event: any).nativeEvent;
138
const {
@@ -143,6 +148,7 @@ function createKeyboardEvent(
148
return {
149
altKey,
150
ctrlKey,
151
+ defaultPrevented,
152
isComposing,
153
key: getEventKey(nativeEvent),
154
location,
@@ -161,8 +167,15 @@ function dispatchKeyboardEvent(
167
context: ReactDOMResponderContext,
168
type: KeyboardEventType,
169
target: Element | Document,
170
+ defaultPrevented: boolean,
171
): void {
165
- const syntheticEvent = createKeyboardEvent(event, context, type, target);
172
+ const syntheticEvent = createKeyboardEvent(
173
+ event,
174
+ context,
175
+ type,
176
+ target,
177
+ defaultPrevented,
178
+ );
179
context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
180
}
181
@@ -174,11 +187,39 @@ const keyboardResponderImpl = {
187
props: KeyboardProps,
188
): void {
189
const {responderTarget, type} = event;
190
+ const nativeEvent: any = event.nativeEvent;
191
192
if (props.disabled) {
193
return;
194
}
195
+ let defaultPrevented = nativeEvent.defaultPrevented === true;
196
if (type === 'keydown') {
197
+ const preventKeys = props.preventKeys;
198
+ if (!defaultPrevented && isArray(preventKeys)) {
199
+ preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
200
+ const preventKey = preventKeys[i];
201
+ let key = preventKey;
202
+
203
+ if (isArray(preventKey)) {
204
+ key = preventKey[0];
205
+ const config = ((preventKey[1]: any): Object);
206
+ for (let s = 0; s < modifiers.length; s++) {
207
+ const modifier = modifiers[s];
208
+ if (
209
+ (config[modifier] && !nativeEvent[modifier]) ||
210
+ (!config[modifier] && nativeEvent[modifier])
211
+ ) {
212
+ continue preventKeyLoop;
213
+ }
214
+ }
215
+ }
216
+ if (key === getEventKey(nativeEvent)) {
217
+ defaultPrevented = true;
218
+ nativeEvent.preventDefault();
219
+ break;
220
+ }
221
+ }
222
+ }
223
const onKeyDown = props.onKeyDown;
224
if (isFunction(onKeyDown)) {
225
dispatchKeyboardEvent(
@@ -187,6 +228,7 @@ const keyboardResponderImpl = {
228
context,
229
'keydown',
230
((responderTarget: any): Element | Document),
231
+ defaultPrevented,
232
);
233
}
234
} else if (type === 'keyup') {
@@ -198,6 +240,7 @@ const keyboardResponderImpl = {
240
context,
241
'keyup',
242
((responderTarget: any): Element | Document),
243
+ defaultPrevented,
244
);
245
}
246
}
packages/react-events/src/dom/__tests__/Keyboard-test.internal.js
+128
@@ -92,6 +92,134 @@ describe('Keyboard event responder', () => {
92
});
93
});
94
95
+ describe('preventKeys', () => {
96
+ it('onKeyDown is default prevented', () => {
97
+ const onKeyDown = jest.fn();
98
+ const ref = React.createRef();
99
+ const Component = () => {
100
+ const listener = useKeyboard({
101
+ onKeyDown,
102
+ preventKeys: ['Tab'],
103
+ });
104
+ return <div ref={ref} listeners={listener} />;
105
+ };
106
+ ReactDOM.render(<Component />, container);
107
+
108
+ const preventDefault = jest.fn();
109
+ const target = createEventTarget(ref.current);
110
+ target.keydown({key: 'Tab', preventDefault});
111
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
112
+ expect(preventDefault).toBeCalled();
113
+ expect(onKeyDown).toHaveBeenCalledWith(
114
+ expect.objectContaining({
115
+ key: 'Tab',
116
+ type: 'keydown',
117
+ defaultPrevented: true,
118
+ }),
119
+ );
120
+ });
121
+
122
+ it('onKeyDown is default prevented (falsy modifier keys)', () => {
123
+ let onKeyDown = jest.fn();
124
+ let ref = React.createRef();
125
+ let Component = () => {
126
+ const listener = useKeyboard({
127
+ onKeyDown,
128
+ preventKeys: [['Tab', {metaKey: false}]],
129
+ });
130
+ return <div ref={ref} listeners={listener} />;
131
+ };
132
+ ReactDOM.render(<Component />, container);
133
+
134
+ let preventDefault = jest.fn();
135
+ let target = createEventTarget(ref.current);
136
+ target.keydown({key: 'Tab', preventDefault, metaKey: true});
137
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
138
+ expect(preventDefault).not.toBeCalled();
139
+ expect(onKeyDown).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ key: 'Tab',
142
+ type: 'keydown',
143
+ defaultPrevented: false,
144
+ }),
145
+ );
146
+
147
+ onKeyDown = jest.fn();
148
+ ref = React.createRef();
149
+ Component = () => {
150
+ const listener = useKeyboard({
151
+ onKeyDown,
152
+ preventKeys: [['Tab', {metaKey: true}]],
153
+ });
154
+ return <div ref={ref} listeners={listener} />;
155
+ };
156
+ ReactDOM.render(<Component />, container);
157
+
158
+ preventDefault = jest.fn();
159
+ target = createEventTarget(ref.current);
160
+ target.keydown({key: 'Tab', preventDefault, metaKey: false});
161
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
162
+ expect(preventDefault).not.toBeCalled();
163
+ expect(onKeyDown).toHaveBeenCalledWith(
164
+ expect.objectContaining({
165
+ key: 'Tab',
166
+ type: 'keydown',
167
+ defaultPrevented: false,
168
+ }),
169
+ );
170
+ });
171
+
172
+ it('onKeyDown is default prevented (truthy modifier keys)', () => {
173
+ let onKeyDown = jest.fn();
174
+ let ref = React.createRef();
175
+ let Component = () => {
176
+ const listener = useKeyboard({
177
+ onKeyDown,
178
+ preventKeys: [['Tab', {metaKey: true}]],
179
+ });
180
+ return <div ref={ref} listeners={listener} />;
181
+ };
182
+ ReactDOM.render(<Component />, container);
183
+
184
+ let preventDefault = jest.fn();
185
+ let target = createEventTarget(ref.current);
186
+ target.keydown({key: 'Tab', preventDefault, metaKey: true});
187
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
188
+ expect(preventDefault).toBeCalled();
189
+ expect(onKeyDown).toHaveBeenCalledWith(
190
+ expect.objectContaining({
191
+ key: 'Tab',
192
+ type: 'keydown',
193
+ defaultPrevented: true,
194
+ }),
195
+ );
196
+
197
+ onKeyDown = jest.fn();
198
+ ref = React.createRef();
199
+ Component = () => {
200
+ const listener = useKeyboard({
201
+ onKeyDown,
202
+ preventKeys: [['Tab', {metaKey: false}]],
203
+ });
204
+ return <div ref={ref} listeners={listener} />;
205
+ };
206
+ ReactDOM.render(<Component />, container);
207
+
208
+ preventDefault = jest.fn();
209
+ target = createEventTarget(ref.current);
210
+ target.keydown({key: 'Tab', preventDefault, metaKey: false});
211
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
212
+ expect(preventDefault).toBeCalled();
213
+ expect(onKeyDown).toHaveBeenCalledWith(
214
+ expect.objectContaining({
215
+ key: 'Tab',
216
+ type: 'keydown',
217
+ defaultPrevented: true,
218
+ }),
219
+ );
220
+ });
221
+ });
222
+
223
describe('onKeyUp', () => {
224
let onKeyDown, onKeyUp, ref;
225