@samitouri / QOS-React-2 / commits / 87eaa90ef8

[react-events] Keyboard calls preventDefault on 'click' events (#16779)

Make sure to call preventDefault for any 'click' events that follow a 'keydown' event that matches 'preventKeys'

Nicolas Gallagher committed Sep 13, 2019 at 13:19 UTC 87eaa90ef8dabd3d557085586d524016cd946d30
4 files changed +237 -223
packages/react-events/src/dom/Keyboard.js
+34 -12
@@ -16,26 +16,32 @@ import React from 'react';
16 import {DiscreteEvent} from 'shared/ReactTypes';
17 import type {ReactEventResponderListener} from 'shared/ReactTypes';
18
19 -type KeyboardEventType = 'keydown' | 'keyup';
19 +type KeyboardEventType = 'keyboard:keydown' | 'keyboard:keyup';
20
21 -type KeyboardProps = {
21 +type KeyboardProps = {|
22 disabled?: boolean,
23 onKeyDown?: (e: KeyboardEvent) => ?boolean,
24 onKeyUp?: (e: KeyboardEvent) => ?boolean,
25 preventKeys?: PreventKeysArray,
26 -};
26 +|};
27 +
28 +type KeyboardState = {|
29 + defaultPrevented: boolean,
30 + isActive: boolean,
31 +|};
32
33 export type KeyboardEvent = {|
34 altKey: boolean,
35 ctrlKey: boolean,
36 + defaultPrevented: boolean,
37 isComposing: boolean,
38 key: string,
39 metaKey: boolean,
40 + pointerType: 'keyboard',
41 shiftKey: boolean,
42 target: Element | Document,
43 type: KeyboardEventType,
44 timeStamp: number,
38 - defaultPrevented: boolean,
45 |};
46
47 type ModifiersObject = {|
@@ -48,7 +54,7 @@ type ModifiersObject = {|
54 type PreventKeysArray = Array<string | Array<string | ModifiersObject>>;
55
56 const isArray = Array.isArray;
51 -const targetEventTypes = ['keydown_active', 'keyup'];
57 +const targetEventTypes = ['click_active', 'keydown_active', 'keyup'];
58 const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
59
60 /**
@@ -150,6 +156,7 @@ function createKeyboardEvent(
156 isComposing,
157 key: getEventKey(nativeEvent),
158 metaKey,
159 + pointerType: 'keyboard',
160 shiftKey,
161 target: event.target,
162 timeStamp: context.getTimeStamp(),
@@ -182,10 +189,17 @@ function dispatchKeyboardEvent(
189
190 const keyboardResponderImpl = {
191 targetEventTypes,
192 + getInitialState(): KeyboardState {
193 + return {
194 + defaultPrevented: false,
195 + isActive: false,
196 + };
197 + },
198 onEvent(
199 event: ReactDOMResponderEvent,
200 context: ReactDOMResponderContext,
201 props: KeyboardProps,
202 + state: KeyboardState,
203 ): void {
204 const {type} = event;
205 const nativeEvent: any = event.nativeEvent;
@@ -193,10 +207,12 @@ const keyboardResponderImpl = {
207 if (props.disabled) {
208 return;
209 }
196 - let defaultPrevented = nativeEvent.defaultPrevented === true;
210 +
211 if (type === 'keydown') {
212 + state.defaultPrevented = nativeEvent.defaultPrevented === true;
213 +
214 const preventKeys = ((props.preventKeys: any): PreventKeysArray);
199 - if (!defaultPrevented && isArray(preventKeys)) {
215 + if (!state.defaultPrevented && isArray(preventKeys)) {
216 preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
217 const preventKey = preventKeys[i];
218 let key = preventKey;
@@ -216,32 +232,38 @@ const keyboardResponderImpl = {
232 }
233 }
234 }
235 +
236 if (key === getEventKey(nativeEvent)) {
220 - defaultPrevented = true;
237 + state.defaultPrevented = true;
238 nativeEvent.preventDefault();
239 break;
240 }
241 }
242 }
243 + state.isActive = true;
244 const onKeyDown = props.onKeyDown;
245 if (isFunction(onKeyDown)) {
246 dispatchKeyboardEvent(
247 event,
248 ((onKeyDown: any): (e: KeyboardEvent) => ?boolean),
249 context,
232 - 'keydown',
233 - defaultPrevented,
250 + 'keyboard:keydown',
251 + state.defaultPrevented,
252 );
253 }
254 + } else if (type === 'click' && state.isActive && state.defaultPrevented) {
255 + // 'click' occurs before 'keyup' and may need native behavior prevented
256 + nativeEvent.preventDefault();
257 } else if (type === 'keyup') {
258 + state.isActive = false;
259 const onKeyUp = props.onKeyUp;
260 if (isFunction(onKeyUp)) {
261 dispatchKeyboardEvent(
262 event,
263 ((onKeyUp: any): (e: KeyboardEvent) => ?boolean),
264 context,
243 - 'keyup',
244 - defaultPrevented,
265 + 'keyboard:keyup',
266 + state.defaultPrevented,
267 );
268 }
269 }
packages/react-events/src/dom/__tests__/Keyboard-test.internal.js
+201 -205
@@ -25,7 +25,7 @@ function initializeModules(hasPointerEvents) {
25 useKeyboard = require('react-events/keyboard').useKeyboard;
26 }
27
28 -describe('Keyboard event responder', () => {
28 +describe('Keyboard responder', () => {
29 let container;
30
31 beforeEach(() => {
@@ -40,6 +40,71 @@ describe('Keyboard event responder', () => {
40 container = null;
41 });
42
43 + function renderPropagationTest(propagates) {
44 + const onKeyDownInner = jest.fn(() => propagates);
45 + const onKeyDownOuter = jest.fn();
46 + const onKeyUpInner = jest.fn(() => propagates);
47 + const onKeyUpOuter = jest.fn();
48 + const ref = React.createRef();
49 + const Component = () => {
50 + const listenerInner = useKeyboard({
51 + onKeyDown: onKeyDownInner,
52 + onKeyUp: onKeyUpInner,
53 + });
54 + const listenerOuter = useKeyboard({
55 + onKeyDown: onKeyDownOuter,
56 + onKeyUp: onKeyUpOuter,
57 + });
58 + return (
59 + <div listeners={listenerOuter}>
60 + <div ref={ref} listeners={listenerInner} />
61 + </div>
62 + );
63 + };
64 + ReactDOM.render(<Component />, container);
65 + return {
66 + onKeyDownInner,
67 + onKeyDownOuter,
68 + onKeyUpInner,
69 + onKeyUpOuter,
70 + ref,
71 + };
72 + }
73 +
74 + test('propagates event when a callback returns true', () => {
75 + const {
76 + onKeyDownInner,
77 + onKeyDownOuter,
78 + onKeyUpInner,
79 + onKeyUpOuter,
80 + ref,
81 + } = renderPropagationTest(true);
82 + const target = createEventTarget(ref.current);
83 + target.keydown();
84 + expect(onKeyDownInner).toBeCalled();
85 + expect(onKeyDownOuter).toBeCalled();
86 + target.keyup();
87 + expect(onKeyUpInner).toBeCalled();
88 + expect(onKeyUpOuter).toBeCalled();
89 + });
90 +
91 + test('does not propagate event when a callback returns false', () => {
92 + const {
93 + onKeyDownInner,
94 + onKeyDownOuter,
95 + onKeyUpInner,
96 + onKeyUpOuter,
97 + ref,
98 + } = renderPropagationTest(false);
99 + const target = createEventTarget(ref.current);
100 + target.keydown();
101 + expect(onKeyDownInner).toBeCalled();
102 + expect(onKeyDownOuter).not.toBeCalled();
103 + target.keyup();
104 + expect(onKeyUpInner).toBeCalled();
105 + expect(onKeyUpOuter).not.toBeCalled();
106 + });
107 +
108 describe('disabled', () => {
109 let onKeyDown, onKeyUp, ref;
110
@@ -48,17 +113,13 @@ describe('Keyboard event responder', () => {
113 onKeyUp = jest.fn();
114 ref = React.createRef();
115 const Component = () => {
51 - const listener = useKeyboard({
52 - disabled: true,
53 - onKeyDown,
54 - onKeyUp,
55 - });
116 + const listener = useKeyboard({disabled: true, onKeyDown, onKeyUp});
117 return <div ref={ref} listeners={listener} />;
118 };
119 ReactDOM.render(<Component />, container);
120 });
121
61 - it('prevents custom events being dispatched', () => {
122 + test('does not call callbacks', () => {
123 const target = createEventTarget(ref.current);
124 target.keydown();
125 target.keyup();
@@ -74,265 +135,200 @@ describe('Keyboard event responder', () => {
135 onKeyDown = jest.fn();
136 ref = React.createRef();
137 const Component = () => {
77 - const listener = useKeyboard({
78 - onKeyDown,
79 - });
138 + const listener = useKeyboard({onKeyDown});
139 return <div ref={ref} listeners={listener} />;
140 };
141 ReactDOM.render(<Component />, container);
142 });
143
85 - it('is called after "keydown" event', () => {
144 + test('key down', () => {
145 const target = createEventTarget(ref.current);
146 target.keydown({key: 'Q'});
147 expect(onKeyDown).toHaveBeenCalledTimes(1);
148 expect(onKeyDown).toHaveBeenCalledWith(
90 - expect.objectContaining({key: 'Q', type: 'keydown'}),
149 + expect.objectContaining({
150 + altKey: false,
151 + ctrlKey: false,
152 + defaultPrevented: false,
153 + isComposing: false,
154 + key: 'Q',
155 + metaKey: false,
156 + pointerType: 'keyboard',
157 + shiftKey: false,
158 + target: target.node,
159 + timeStamp: expect.any(Number),
160 + type: 'keyboard:keydown',
161 + }),
162 );
163 });
93 - });
164
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();
165 + test('modified key down', () => {
166 const target = createEventTarget(ref.current);
110 - target.keydown({key: 'Tab', preventDefault});
111 - expect(onKeyDown).toHaveBeenCalledTimes(1);
112 - expect(preventDefault).toBeCalled();
167 + target.keydown({
168 + key: 'Q',
169 + altKey: true,
170 + ctrlKey: true,
171 + shiftKey: true,
172 + metaKey: true,
173 + });
174 expect(onKeyDown).toHaveBeenCalledWith(
175 expect.objectContaining({
115 - key: 'Tab',
116 - type: 'keydown',
117 - defaultPrevented: true,
176 + altKey: true,
177 + ctrlKey: true,
178 + defaultPrevented: false,
179 + isComposing: false,
180 + key: 'Q',
181 + metaKey: true,
182 + pointerType: 'keyboard',
183 + shiftKey: true,
184 + target: target.node,
185 + timeStamp: expect.any(Number),
186 + type: 'keyboard:keydown',
187 }),
188 );
189 });
190 + });
191
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 - });
192 + describe('onKeyUp', () => {
193 + let onKeyUp, ref;
194 +
195 + beforeEach(() => {
196 + onKeyUp = jest.fn();
197 + ref = React.createRef();
198 + const Component = () => {
199 + const listener = useKeyboard({onKeyUp});
200 return <div ref={ref} listeners={listener} />;
201 };
202 ReactDOM.render(<Component />, container);
203 + });
204
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(
205 + test('key up', () => {
206 + const target = createEventTarget(ref.current);
207 + target.keydown({key: 'Q'});
208 + target.keyup({key: 'Q'});
209 + expect(onKeyUp).toHaveBeenCalledTimes(1);
210 + expect(onKeyUp).toHaveBeenCalledWith(
211 expect.objectContaining({
141 - key: 'Tab',
142 - type: 'keydown',
212 + altKey: false,
213 + ctrlKey: false,
214 defaultPrevented: false,
215 + isComposing: false,
216 + key: 'Q',
217 + metaKey: false,
218 + pointerType: 'keyboard',
219 + shiftKey: false,
220 + target: target.node,
221 + timeStamp: expect.any(Number),
222 + type: 'keyboard:keyup',
223 }),
224 );
225 + });
226
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(
227 + test('modified key up', () => {
228 + const target = createEventTarget(ref.current);
229 + target.keydown({key: 'Q'});
230 + target.keyup({
231 + key: 'Q',
232 + altKey: true,
233 + ctrlKey: true,
234 + shiftKey: true,
235 + metaKey: true,
236 + });
237 + expect(onKeyUp).toHaveBeenCalledWith(
238 expect.objectContaining({
165 - key: 'Tab',
166 - type: 'keydown',
239 + altKey: true,
240 + ctrlKey: true,
241 defaultPrevented: false,
242 + isComposing: false,
243 + key: 'Q',
244 + metaKey: true,
245 + pointerType: 'keyboard',
246 + shiftKey: true,
247 + target: target.node,
248 + timeStamp: expect.any(Number),
249 + type: 'keyboard:keyup',
250 }),
251 );
252 });
253 + });
254
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 - });
255 + describe('preventKeys', () => {
256 + function render(props) {
257 + const ref = React.createRef();
258 + const Component = () => {
259 + const listener = useKeyboard(props);
260 return <div ref={ref} listeners={listener} />;
261 };
262 ReactDOM.render(<Component />, container);
263 + return ref;
264 + }
265 +
266 + test('key config matches', () => {
267 + const onKeyDown = jest.fn();
268 + const preventDefault = jest.fn();
269 + const preventDefaultClick = jest.fn();
270 + const ref = render({onKeyDown, preventKeys: ['Tab']});
271 +
272 + const target = createEventTarget(ref.current);
273 + target.keydown({key: 'Tab', preventDefault});
274 + target.click({preventDefault: preventDefaultClick});
275
184 - let preventDefault = jest.fn();
185 - let target = createEventTarget(ref.current);
186 - target.keydown({key: 'Tab', preventDefault, metaKey: true});
276 expect(onKeyDown).toHaveBeenCalledTimes(1);
277 expect(preventDefault).toBeCalled();
278 + expect(preventDefaultClick).toBeCalled();
279 expect(onKeyDown).toHaveBeenCalledWith(
280 expect.objectContaining({
191 - key: 'Tab',
192 - type: 'keydown',
281 defaultPrevented: true,
282 + key: 'Tab',
283 + type: 'keyboard:keydown',
284 }),
285 );
286 + });
287
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);
288 + test('key config matches (modifier keys)', () => {
289 + const onKeyDown = jest.fn();
290 + const preventDefault = jest.fn();
291 + const preventDefaultClick = jest.fn();
292 + const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
293 +
294 + const target = createEventTarget(ref.current);
295 + target.keydown({key: 'Tab', preventDefault, shiftKey: true});
296 + target.click({preventDefault: preventDefaultClick, shiftKey: true});
297
208 - preventDefault = jest.fn();
209 - target = createEventTarget(ref.current);
210 - target.keydown({key: 'Tab', preventDefault, metaKey: false});
298 expect(onKeyDown).toHaveBeenCalledTimes(1);
299 expect(preventDefault).toBeCalled();
300 + expect(preventDefaultClick).toBeCalled();
301 expect(onKeyDown).toHaveBeenCalledWith(
302 expect.objectContaining({
215 - key: 'Tab',
216 - type: 'keydown',
303 defaultPrevented: true,
304 + key: 'Tab',
305 + shiftKey: true,
306 + type: 'keyboard:keydown',
307 }),
308 );
309 });
221 - });
310
223 - describe('onKeyUp', () => {
224 - let onKeyDown, onKeyUp, ref;
225 -
226 - beforeEach(() => {
227 - onKeyDown = jest.fn();
228 - onKeyUp = jest.fn();
229 - ref = React.createRef();
230 - const Component = () => {
231 - const listener = useKeyboard({
232 - onKeyDown,
233 - onKeyUp,
234 - });
235 - return <div ref={ref} listeners={listener} />;
236 - };
237 - ReactDOM.render(<Component />, container);
238 - });
311 + test('key config does not match (modifier keys)', () => {
312 + const onKeyDown = jest.fn();
313 + const preventDefault = jest.fn();
314 + const preventDefaultClick = jest.fn();
315 + const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
316
240 - it('is called after "keydown" event', () => {
317 const target = createEventTarget(ref.current);
242 - target.keydown({key: 'Q'});
243 - target.keyup({key: 'Q'});
318 + target.keydown({key: 'Tab', preventDefault, shiftKey: false});
319 + target.click({preventDefault: preventDefaultClick, shiftKey: false});
320 +
321 expect(onKeyDown).toHaveBeenCalledTimes(1);
322 + expect(preventDefault).not.toBeCalled();
323 + expect(preventDefaultClick).not.toBeCalled();
324 expect(onKeyDown).toHaveBeenCalledWith(
246 - expect.objectContaining({key: 'Q', type: 'keydown'}),
247 - );
248 - expect(onKeyUp).toHaveBeenCalledTimes(1);
249 - expect(onKeyUp).toHaveBeenCalledWith(
250 - expect.objectContaining({key: 'Q', type: 'keyup'}),
325 + expect.objectContaining({
326 + defaultPrevented: false,
327 + key: 'Tab',
328 + shiftKey: false,
329 + type: 'keyboard:keydown',
330 + }),
331 );
332 });
333 });
254 -
255 - describe('correctly handles responder propagation', () => {
256 - describe('onKeyDown', () => {
257 - let onKeyDownInner, onKeyDownOuter, ref;
258 -
259 - function renderPropagationTest(propagates) {
260 - onKeyDownInner = jest.fn(() => propagates);
261 - onKeyDownOuter = jest.fn();
262 - ref = React.createRef();
263 - const Component = () => {
264 - const listenerInner = useKeyboard({
265 - onKeyDown: onKeyDownInner,
266 - });
267 - const listenerOuter = useKeyboard({
268 - onKeyDown: onKeyDownOuter,
269 - });
270 - return (
271 - <div listeners={listenerOuter}>
272 - <div ref={ref} listeners={listenerInner} />
273 - </div>
274 - );
275 - };
276 - ReactDOM.render(<Component />, container);
277 - }
278 -
279 - it('propagates when cb returns true', () => {
280 - renderPropagationTest(true);
281 - const target = createEventTarget(ref.current);
282 - target.keydown();
283 - expect(onKeyDownInner).toBeCalled();
284 - expect(onKeyDownOuter).toBeCalled();
285 - });
286 -
287 - it('does not propagate when cb returns false', () => {
288 - renderPropagationTest(false);
289 - const target = createEventTarget(ref.current);
290 - target.keydown();
291 - expect(onKeyDownInner).toBeCalled();
292 - expect(onKeyDownOuter).not.toBeCalled();
293 - });
294 - });
295 -
296 - describe('onKeyUp', () => {
297 - let onKeyUpInner, onKeyUpOuter, ref;
298 -
299 - function renderPropagationTest(propagates) {
300 - onKeyUpInner = jest.fn(() => propagates);
301 - onKeyUpOuter = jest.fn();
302 - ref = React.createRef();
303 - const Component = () => {
304 - const listenerInner = useKeyboard({
305 - onKeyUp: onKeyUpInner,
306 - });
307 - const listenerOuter = useKeyboard({
308 - onKeyUp: onKeyUpOuter,
309 - });
310 - return (
311 - <div listeners={listenerOuter}>
312 - <div ref={ref} listeners={listenerInner} />
313 - </div>
314 - );
315 - };
316 - ReactDOM.render(<Component />, container);
317 - }
318 -
319 - it('propagates when cb returns true', () => {
320 - renderPropagationTest(true);
321 - const target = createEventTarget(ref.current);
322 - target.keydown();
323 - target.keyup();
324 - expect(onKeyUpInner).toBeCalled();
325 - expect(onKeyUpOuter).toBeCalled();
326 - });
327 -
328 - it('does not propagate when cb returns false', () => {
329 - renderPropagationTest(false);
330 - const target = createEventTarget(ref.current);
331 - target.keydown();
332 - target.keyup();
333 - expect(onKeyUpInner).toBeCalled();
334 - expect(onKeyUpOuter).not.toBeCalled();
335 - });
336 - });
337 - });
334 });
packages/react-events/src/dom/testing-library/domEvents.js
+2
@@ -137,6 +137,7 @@ function createKeyboardEvent(
137 {
138 altKey = false,
139 ctrlKey = false,
140 + isComposing = false,
141 key = '',
142 metaKey = false,
143 preventDefault = emptyFunction,
@@ -151,6 +152,7 @@ function createKeyboardEvent(
152 getModifierState(keyArg) {
153 createGetModifierState(keyArg, modifierState);
154 },
155 + isComposing,
156 key,
157 metaKey,
158 preventDefault,
packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
-6
@@ -220,9 +220,6 @@ describe('ReactScope', () => {
220 let target = createEventTarget(ref.current);
221 target.keydown({key: 'Q'});
222 expect(onKeyDown).toHaveBeenCalledTimes(1);
223 - expect(onKeyDown).toHaveBeenCalledWith(
224 - expect.objectContaining({key: 'Q', type: 'keydown'}),
225 - );
223
224 onKeyDown = jest.fn();
225 Component = () => {
@@ -242,9 +239,6 @@ describe('ReactScope', () => {
239 target = createEventTarget(ref.current);
240 target.keydown({key: 'Q'});
241 expect(onKeyDown).toHaveBeenCalledTimes(1);
245 - expect(onKeyDown).toHaveBeenCalledWith(
246 - expect.objectContaining({key: 'Q', type: 'keydown'}),
247 - );
242 });
243 });
244