@samitouri / QOS-React / commits / 937d262f55

React events: keyboard press, types, tests (#15314)

* Add HoverProps type * Add more Hover event module tests * Add more Press event module tests * Change default longPress delay from 1000 to 500 * Rename dispatchPressEvent -> dispatchEvent * Consolidate state updates in Press event module * Add keyboard support for Press events * Add FocusProps type and unit tests

Nicolas Gallagher committed Apr 4, 2019 at 08:55 UTC 937d262f557fd3332576be0da60adf5f94c545d9
6 files changed +584 -241
packages/react-events/src/Focus.js
+23 -10
@@ -10,10 +10,12 @@
10 import type {EventResponderContext} from 'events/EventTypes';
11 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
12
13 -const targetEventTypes = [
14 - {name: 'focus', passive: true, capture: true},
15 - {name: 'blur', passive: true, capture: true},
16 -];
13 +type FocusProps = {
14 + disabled: boolean,
15 + onBlur: (e: FocusEvent) => void,
16 + onFocus: (e: FocusEvent) => void,
17 + onFocusChange: boolean => void,
18 +};
19
20 type FocusState = {
21 isFocused: boolean,
@@ -27,6 +29,11 @@ type FocusEvent = {|
29 type: FocusEventType,
30 |};
31
32 +const targetEventTypes = [
33 + {name: 'focus', passive: true, capture: true},
34 + {name: 'blur', passive: true, capture: true},
35 +];
36 +
37 function createFocusEvent(
38 type: FocusEventType,
39 target: Element | Document,
@@ -39,7 +46,10 @@ function createFocusEvent(
46 };
47 }
48
42 -function dispatchFocusInEvents(context: EventResponderContext, props: Object) {
49 +function dispatchFocusInEvents(
50 + context: EventResponderContext,
51 + props: FocusProps,
52 +) {
53 const {event, eventTarget} = context;
54 if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
55 return;
@@ -53,19 +63,22 @@ function dispatchFocusInEvents(context: EventResponderContext, props: Object) {
63 context.dispatchEvent(syntheticEvent, {discrete: true});
64 }
65 if (props.onFocusChange) {
56 - const focusChangeEventListener = () => {
66 + const listener = () => {
67 props.onFocusChange(true);
68 };
69 const syntheticEvent = createFocusEvent(
70 'focuschange',
71 eventTarget,
62 - focusChangeEventListener,
72 + listener,
73 );
74 context.dispatchEvent(syntheticEvent, {discrete: true});
75 }
76 }
77
68 -function dispatchFocusOutEvents(context: EventResponderContext, props: Object) {
78 +function dispatchFocusOutEvents(
79 + context: EventResponderContext,
80 + props: FocusProps,
81 +) {
82 const {event, eventTarget} = context;
83 if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
84 return;
@@ -75,13 +88,13 @@ function dispatchFocusOutEvents(context: EventResponderContext, props: Object) {
88 context.dispatchEvent(syntheticEvent, {discrete: true});
89 }
90 if (props.onFocusChange) {
78 - const focusChangeEventListener = () => {
91 + const listener = () => {
92 props.onFocusChange(false);
93 };
94 const syntheticEvent = createFocusEvent(
95 'focuschange',
96 eventTarget,
84 - focusChangeEventListener,
97 + listener,
98 );
99 context.dispatchEvent(syntheticEvent, {discrete: true});
100 }
packages/react-events/src/Hover.js
+41 -24
@@ -10,12 +10,14 @@
10 import type {EventResponderContext} from 'events/EventTypes';
11 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
12
13 -const targetEventTypes = [
14 - 'pointerover',
15 - 'pointermove',
16 - 'pointerout',
17 - 'pointercancel',
18 -];
13 +type HoverProps = {
14 + disabled: boolean,
15 + delayHoverEnd: number,
16 + delayHoverStart: number,
17 + onHoverChange: boolean => void,
18 + onHoverEnd: (e: HoverEvent) => void,
19 + onHoverStart: (e: HoverEvent) => void,
20 +};
21
22 type HoverState = {
23 isHovered: boolean,
@@ -31,6 +33,21 @@ type HoverEvent = {|
33 type: HoverEventType,
34 |};
35
36 +// const DEFAULT_HOVER_END_DELAY_MS = 0;
37 +// const DEFAULT_HOVER_START_DELAY_MS = 0;
38 +
39 +const targetEventTypes = [
40 + 'pointerover',
41 + 'pointermove',
42 + 'pointerout',
43 + 'pointercancel',
44 +];
45 +
46 +// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
47 +if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
48 + targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
49 +}
50 +
51 function createHoverEvent(
52 type: HoverEventType,
53 target: Element | Document,
@@ -43,16 +60,9 @@ function createHoverEvent(
60 };
61 }
62
46 -// In the case we don't have PointerEvents (Safari), we listen to touch events
47 -// too
48 -if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
49 - targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
50 -}
51 -
63 function dispatchHoverStartEvents(
64 context: EventResponderContext,
54 - props: Object,
55 - state: HoverState,
65 + props: HoverProps,
66 ): void {
67 const {event, eventTarget} = context;
68 if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
@@ -67,19 +77,22 @@ function dispatchHoverStartEvents(
77 context.dispatchEvent(syntheticEvent, {discrete: true});
78 }
79 if (props.onHoverChange) {
70 - const hoverChangeEventListener = () => {
80 + const listener = () => {
81 props.onHoverChange(true);
82 };
83 const syntheticEvent = createHoverEvent(
84 'hoverchange',
85 eventTarget,
76 - hoverChangeEventListener,
86 + listener,
87 );
88 context.dispatchEvent(syntheticEvent, {discrete: true});
89 }
90 }
91
82 -function dispatchHoverEndEvents(context: EventResponderContext, props: Object) {
92 +function dispatchHoverEndEvents(
93 + context: EventResponderContext,
94 + props: HoverProps,
95 +) {
96 const {event, eventTarget} = context;
97 if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
98 return;
@@ -93,13 +106,13 @@ function dispatchHoverEndEvents(context: EventResponderContext, props: Object) {
106 context.dispatchEvent(syntheticEvent, {discrete: true});
107 }
108 if (props.onHoverChange) {
96 - const hoverChangeEventListener = () => {
109 + const listener = () => {
110 props.onHoverChange(false);
111 };
112 const syntheticEvent = createHoverEvent(
113 'hoverchange',
114 eventTarget,
102 - hoverChangeEventListener,
115 + listener,
116 );
117 context.dispatchEvent(syntheticEvent, {discrete: true});
118 }
@@ -116,18 +129,22 @@ const HoverResponder = {
129 },
130 handleEvent(
131 context: EventResponderContext,
119 - props: Object,
132 + props: HoverProps,
133 state: HoverState,
134 ): void {
135 const {eventType, eventTarget, event} = context;
136
137 switch (eventType) {
125 - case 'touchstart':
126 - // Touch devices don't have hover support
138 + /**
139 + * Prevent hover events when touch is being used.
140 + */
141 + case 'touchstart': {
142 if (!state.isTouched) {
143 state.isTouched = true;
144 }
145 break;
146 + }
147 +
148 case 'pointerover':
149 case 'mouseover': {
150 if (
@@ -148,7 +165,7 @@ const HoverResponder = {
165 state.isInHitSlop = true;
166 return;
167 }
151 - dispatchHoverStartEvents(context, props, state);
168 + dispatchHoverStartEvents(context, props);
169 state.isHovered = true;
170 }
171 break;
@@ -172,7 +189,7 @@ const HoverResponder = {
189 (event: any).y,
190 )
191 ) {
175 - dispatchHoverStartEvents(context, props, state);
192 + dispatchHoverStartEvents(context, props);
193 state.isHovered = true;
194 state.isInHitSlop = false;
195 }
packages/react-events/src/Press.js
+164 -139
@@ -10,27 +10,6 @@
10 import type {EventResponderContext} from 'events/EventTypes';
11 import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
12
13 -// const DEFAULT_PRESS_DELAY_MS = 0;
14 -// const DEFAULT_PRESS_END_DELAY_MS = 0;
15 -// const DEFAULT_PRESS_START_DELAY_MS = 0;
16 -const DEFAULT_LONG_PRESS_DELAY_MS = 1000;
17 -
18 -const targetEventTypes = [
19 - {name: 'click', passive: false},
20 - {name: 'keydown', passive: false},
21 - 'pointerdown',
22 - 'pointercancel',
23 - 'contextmenu',
24 -];
25 -const rootEventTypes = [{name: 'pointerup', passive: false}, 'scroll'];
26 -
27 -// In the case we don't have PointerEvents (Safari), we listen to touch events
28 -// too
29 -if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
30 - targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel');
31 - rootEventTypes.push({name: 'mouseup', passive: false});
32 -}
33 -
13 type PressProps = {
14 disabled: boolean,
15 delayLongPress: number,
@@ -70,6 +49,30 @@ type PressEvent = {|
49 type: PressEventType,
50 |};
51
52 +// const DEFAULT_PRESS_DELAY_MS = 0;
53 +// const DEFAULT_PRESS_END_DELAY_MS = 0;
54 +// const DEFAULT_PRESS_START_DELAY_MS = 0;
55 +const DEFAULT_LONG_PRESS_DELAY_MS = 500;
56 +
57 +const targetEventTypes = [
58 + {name: 'click', passive: false},
59 + {name: 'keydown', passive: false},
60 + 'pointerdown',
61 + 'pointercancel',
62 + 'contextmenu',
63 +];
64 +const rootEventTypes = [
65 + {name: 'keyup', passive: false},
66 + {name: 'pointerup', passive: false},
67 + 'scroll',
68 +];
69 +
70 +// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
71 +if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
72 + targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel');
73 + rootEventTypes.push({name: 'mouseup', passive: false});
74 +}
75 +
76 function createPressEvent(
77 type: PressEventType,
78 target: Element | Document,
@@ -82,7 +85,7 @@ function createPressEvent(
85 };
86 }
87
85 -function dispatchPressEvent(
88 +function dispatchEvent(
89 context: EventResponderContext,
90 state: PressState,
91 name: PressEventType,
@@ -93,23 +96,40 @@ function dispatchPressEvent(
96 context.dispatchEvent(syntheticEvent, {discrete: true});
97 }
98
99 +function dispatchPressChangeEvent(
100 + context: EventResponderContext,
101 + props: PressProps,
102 + state: PressState,
103 +): void {
104 + const listener = () => {
105 + props.onPressChange(state.isPressed);
106 + };
107 + dispatchEvent(context, state, 'presschange', listener);
108 +}
109 +
110 +function dispatchLongPressChangeEvent(
111 + context: EventResponderContext,
112 + props: PressProps,
113 + state: PressState,
114 +): void {
115 + const listener = () => {
116 + props.onLongPressChange(state.isLongPressed);
117 + };
118 + dispatchEvent(context, state, 'longpresschange', listener);
119 +}
120 +
121 function dispatchPressStartEvents(
122 context: EventResponderContext,
123 props: PressProps,
124 state: PressState,
125 ): void {
101 - function dispatchPressChangeEvent(bool) {
102 - const pressChangeEventListener = () => {
103 - props.onPressChange(bool);
104 - };
105 - dispatchPressEvent(context, state, 'presschange', pressChangeEventListener);
106 - }
126 + state.isPressed = true;
127
128 if (props.onPressStart) {
109 - dispatchPressEvent(context, state, 'pressstart', props.onPressStart);
129 + dispatchEvent(context, state, 'pressstart', props.onPressStart);
130 }
131 if (props.onPressChange) {
112 - dispatchPressChangeEvent(true);
132 + dispatchPressChangeEvent(context, props, state);
133 }
134 if ((props.onLongPress || props.onLongPressChange) && !state.isLongPressed) {
135 const delayLongPress = calculateDelayMS(
@@ -125,31 +145,18 @@ function dispatchPressStartEvents(
145 state.longPressTimeout = null;
146
147 if (props.onLongPress) {
128 - const longPressEventListener = e => {
148 + const listener = e => {
149 props.onLongPress(e);
150 // TODO address this again at some point
151 // if (e.nativeEvent.defaultPrevented) {
152 // state.defaultPrevented = true;
153 // }
154 };
135 - dispatchPressEvent(
136 - context,
137 - state,
138 - 'longpress',
139 - longPressEventListener,
140 - );
155 + dispatchEvent(context, state, 'longpress', listener);
156 }
157
158 if (props.onLongPressChange) {
144 - const longPressChangeEventListener = () => {
145 - props.onLongPressChange(true);
146 - };
147 - dispatchPressEvent(
148 - context,
149 - state,
150 - 'longpresschange',
151 - longPressChangeEventListener,
152 - );
159 + dispatchLongPressChangeEvent(context, props, state);
160 }
161 }),
162 delayLongPress,
@@ -167,24 +174,21 @@ function dispatchPressEndEvents(
174 state.longPressTimeout = null;
175 }
176 if (props.onPressEnd) {
170 - dispatchPressEvent(context, state, 'pressend', props.onPressEnd);
177 + dispatchEvent(context, state, 'pressend', props.onPressEnd);
178 }
172 - if (props.onPressChange) {
173 - const pressChangeEventListener = () => {
174 - props.onPressChange(false);
175 - };
176 - dispatchPressEvent(context, state, 'presschange', pressChangeEventListener);
179 +
180 + if (state.isPressed) {
181 + state.isPressed = false;
182 + if (props.onPressChange) {
183 + dispatchPressChangeEvent(context, props, state);
184 + }
185 }
178 - if (props.onLongPressChange && state.isLongPressed) {
179 - const longPressChangeEventListener = () => {
180 - props.onLongPressChange(false);
181 - };
182 - dispatchPressEvent(
183 - context,
184 - state,
185 - 'longpresschange',
186 - longPressChangeEventListener,
187 - );
186 +
187 + if (state.isLongPressed) {
188 + state.isLongPressed = false;
189 + if (props.onLongPressChange) {
190 + dispatchLongPressChangeEvent(context, props, state);
191 + }
192 }
193 }
194
@@ -223,15 +227,74 @@ const PressResponder = {
227 const {eventTarget, eventType, event} = context;
228
229 switch (eventType) {
226 - case 'keydown': {
230 + /**
231 + * Respond to pointer events and fall back to mouse.
232 + */
233 + case 'pointerdown':
234 + case 'mousedown': {
235 if (
228 - !props.onPress ||
229 - context.isTargetOwned(eventTarget) ||
230 - !isValidKeyPress((event: any).key)
236 + !state.isPressed &&
237 + !context.isTargetOwned(eventTarget) &&
238 + !state.shouldSkipMouseAfterTouch
239 ) {
232 - return;
240 + if (
241 + (event: any).pointerType === 'mouse' ||
242 + eventType === 'mousedown'
243 + ) {
244 + if (
245 + // Ignore right- and middle-clicks
246 + event.button === 1 ||
247 + event.button === 2 ||
248 + // Ignore pressing on hit slop area with mouse
249 + context.isPositionWithinTouchHitTarget(
250 + (event: any).x,
251 + (event: any).y,
252 + )
253 + ) {
254 + return;
255 + }
256 + }
257 + state.pressTarget = eventTarget;
258 + dispatchPressStartEvents(context, props, state);
259 + context.addRootEventTypes(rootEventTypes);
260 }
234 - dispatchPressEvent(context, state, 'press', props.onPress);
261 + break;
262 + }
263 + case 'pointerup':
264 + case 'mouseup': {
265 + if (state.isPressed) {
266 + if (state.shouldSkipMouseAfterTouch) {
267 + state.shouldSkipMouseAfterTouch = false;
268 + return;
269 + }
270 +
271 + const wasLongPressed = state.isLongPressed;
272 +
273 + dispatchPressEndEvents(context, props, state);
274 +
275 + if (state.pressTarget !== null && props.onPress) {
276 + if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
277 + if (
278 + !(
279 + wasLongPressed &&
280 + props.onLongPressShouldCancelPress &&
281 + props.onLongPressShouldCancelPress()
282 + )
283 + ) {
284 + const listener = e => {
285 + props.onPress(e);
286 + // TODO address this again at some point
287 + // if (e.nativeEvent.defaultPrevented) {
288 + // state.defaultPrevented = true;
289 + // }
290 + };
291 + dispatchEvent(context, state, 'press', listener);
292 + }
293 + }
294 + }
295 + context.removeRootEventTypes(rootEventTypes);
296 + }
297 + state.isAnchorTouched = false;
298 break;
299 }
300
@@ -239,7 +302,7 @@ const PressResponder = {
302 * Touch event implementations are only needed for Safari, which lacks
303 * support for pointer events.
304 */
242 - case 'touchstart':
305 + case 'touchstart': {
306 if (!state.isPressed && !context.isTargetOwned(eventTarget)) {
307 // We bail out of polyfilling anchor tags, given the same heuristics
308 // explained above in regards to needing to use click events.
@@ -249,21 +312,21 @@ const PressResponder = {
312 }
313 state.pressTarget = eventTarget;
314 dispatchPressStartEvents(context, props, state);
252 - state.isPressed = true;
315 context.addRootEventTypes(rootEventTypes);
316 }
255 -
317 break;
318 + }
319 case 'touchend': {
320 if (state.isAnchorTouched) {
321 + state.isAnchorTouched = false;
322 return;
323 }
324 if (state.isPressed) {
325 + const wasLongPressed = state.isLongPressed;
326 +
327 dispatchPressEndEvents(context, props, state);
263 - if (
264 - eventType !== 'touchcancel' &&
265 - (props.onPress || props.onLongPress)
266 - ) {
328 +
329 + if (eventType !== 'touchcancel' && props.onPress) {
330 // Find if the X/Y of the end touch is still that of the original target
331 const changedTouch = (event: any).changedTouches[0];
332 const doc = (eventTarget: any).ownerDocument;
@@ -276,19 +339,16 @@ const PressResponder = {
339 context.isTargetWithinEventComponent(target)
340 ) {
341 if (
279 - props.onPress &&
342 !(
281 - state.isLongPressed &&
343 + wasLongPressed &&
344 props.onLongPressShouldCancelPress &&
345 props.onLongPressShouldCancelPress()
346 )
347 ) {
286 - dispatchPressEvent(context, state, 'press', props.onPress);
348 + dispatchEvent(context, state, 'press', props.onPress);
349 }
350 }
351 }
290 - state.isPressed = false;
291 - state.isLongPressed = false;
352 state.shouldSkipMouseAfterTouch = true;
353 context.removeRootEventTypes(rootEventTypes);
354 }
@@ -296,93 +356,58 @@ const PressResponder = {
356 }
357
358 /**
299 - * Respond to pointer events and fall back to mouse.
359 + * Keyboard interaction support
360 + * TODO: determine UX for metaKey + validKeyPress interactions
361 */
301 - case 'pointerdown':
302 - case 'mousedown': {
362 + case 'keydown': {
363 if (
364 !state.isPressed &&
365 + !state.isLongPressed &&
366 !context.isTargetOwned(eventTarget) &&
306 - !state.shouldSkipMouseAfterTouch
367 + isValidKeyPress((event: any).key)
368 ) {
308 - if (
309 - (event: any).pointerType === 'mouse' ||
310 - eventType === 'mousedown'
311 - ) {
312 - // Ignore if we are pressing on hit slop area with mouse
313 - if (
314 - context.isPositionWithinTouchHitTarget(
315 - (event: any).x,
316 - (event: any).y,
317 - )
318 - ) {
319 - return;
320 - }
321 - // Ignore middle- and right-clicks
322 - if (event.button === 2 || event.button === 1) {
323 - return;
324 - }
369 + // Prevent spacebar press from scrolling the window
370 + if ((event: any).key === ' ') {
371 + (event: any).preventDefault();
372 }
373 state.pressTarget = eventTarget;
374 dispatchPressStartEvents(context, props, state);
328 - state.isPressed = true;
375 context.addRootEventTypes(rootEventTypes);
376 }
377 break;
378 }
333 - case 'pointerup':
334 - case 'mouseup': {
335 - if (state.isPressed) {
336 - if (state.shouldSkipMouseAfterTouch) {
337 - state.shouldSkipMouseAfterTouch = false;
338 - return;
339 - }
379 + case 'keyup': {
380 + if (state.isPressed && isValidKeyPress((event: any).key)) {
381 + const wasLongPressed = state.isLongPressed;
382 dispatchPressEndEvents(context, props, state);
341 - if (
342 - state.pressTarget !== null &&
343 - (props.onPress || props.onLongPress)
344 - ) {
345 - if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
346 - if (
347 - props.onPress &&
348 - !(
349 - state.isLongPressed &&
350 - props.onLongPressShouldCancelPress &&
351 - props.onLongPressShouldCancelPress()
352 - )
353 - ) {
354 - const pressEventListener = e => {
355 - props.onPress(e);
356 - // TODO address this again at some point
357 - // if (e.nativeEvent.defaultPrevented) {
358 - // state.defaultPrevented = true;
359 - // }
360 - };
361 - dispatchPressEvent(context, state, 'press', pressEventListener);
362 - }
383 + if (state.pressTarget !== null && props.onPress) {
384 + if (
385 + !(
386 + wasLongPressed &&
387 + props.onLongPressShouldCancelPress &&
388 + props.onLongPressShouldCancelPress()
389 + )
390 + ) {
391 + dispatchEvent(context, state, 'press', props.onPress);
392 }
393 }
365 - state.isPressed = false;
366 - state.isLongPressed = false;
394 context.removeRootEventTypes(rootEventTypes);
395 }
369 - state.isAnchorTouched = false;
396 break;
397 }
398
373 - case 'scroll':
374 - case 'touchcancel':
399 case 'contextmenu':
376 - case 'pointercancel': {
400 + case 'pointercancel':
401 + case 'scroll':
402 + case 'touchcancel': {
403 if (state.isPressed) {
404 state.shouldSkipMouseAfterTouch = false;
405 dispatchPressEndEvents(context, props, state);
380 - state.isPressed = false;
381 - state.isLongPressed = false;
406 context.removeRootEventTypes(rootEventTypes);
407 }
408 break;
409 }
410 +
411 case 'click': {
412 if (state.defaultPrevented) {
413 (event: any).preventDefault();
packages/react-events/src/__tests__/Focus-test.internal.js new
+111
@@ -0,0 +1,111 @@
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 Focus;
16 +
17 +const createFocusEvent = type => {
18 + const event = document.createEvent('Event');
19 + event.initEvent(type, true, true);
20 + return event;
21 +};
22 +
23 +describe('Focus event responder', () => {
24 + let container;
25 +
26 + beforeEach(() => {
27 + jest.resetModules();
28 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
29 + ReactFeatureFlags.enableEventAPI = true;
30 + React = require('react');
31 + ReactDOM = require('react-dom');
32 + Focus = require('react-events/focus');
33 +
34 + container = document.createElement('div');
35 + document.body.appendChild(container);
36 + });
37 +
38 + afterEach(() => {
39 + document.body.removeChild(container);
40 + container = null;
41 + });
42 +
43 + describe('onBlur', () => {
44 + let onBlur, ref;
45 +
46 + beforeEach(() => {
47 + onBlur = jest.fn();
48 + ref = React.createRef();
49 + const element = (
50 + <Focus onBlur={onBlur}>
51 + <div ref={ref} />
52 + </Focus>
53 + );
54 + ReactDOM.render(element, container);
55 + });
56 +
57 + it('is called after "blur" event', () => {
58 + ref.current.dispatchEvent(createFocusEvent('focus'));
59 + ref.current.dispatchEvent(createFocusEvent('blur'));
60 + expect(onBlur).toHaveBeenCalledTimes(1);
61 + });
62 + });
63 +
64 + describe('onFocus', () => {
65 + let onFocus, ref;
66 +
67 + beforeEach(() => {
68 + onFocus = jest.fn();
69 + ref = React.createRef();
70 + const element = (
71 + <Focus onFocus={onFocus}>
72 + <div ref={ref} />
73 + </Focus>
74 + );
75 + ReactDOM.render(element, container);
76 + });
77 +
78 + it('is called after "focus" event', () => {
79 + ref.current.dispatchEvent(createFocusEvent('focus'));
80 + expect(onFocus).toHaveBeenCalledTimes(1);
81 + });
82 + });
83 +
84 + describe('onFocusChange', () => {
85 + let onFocusChange, ref;
86 +
87 + beforeEach(() => {
88 + onFocusChange = jest.fn();
89 + ref = React.createRef();
90 + const element = (
91 + <Focus onFocusChange={onFocusChange}>
92 + <div ref={ref} />
93 + </Focus>
94 + );
95 + ReactDOM.render(element, container);
96 + });
97 +
98 + it('is called after "blur" and "focus" events', () => {
99 + ref.current.dispatchEvent(createFocusEvent('focus'));
100 + expect(onFocusChange).toHaveBeenCalledTimes(1);
101 + expect(onFocusChange).toHaveBeenCalledWith(true);
102 + ref.current.dispatchEvent(createFocusEvent('blur'));
103 + expect(onFocusChange).toHaveBeenCalledTimes(2);
104 + expect(onFocusChange).toHaveBeenCalledWith(false);
105 + });
106 + });
107 +
108 + it('expect displayName to show up for event component', () => {
109 + expect(Focus.displayName).toBe('Focus');
110 + });
111 +});
packages/react-events/src/__tests__/Hover-test.internal.js
+145 -54
@@ -14,6 +14,12 @@ let ReactFeatureFlags;
14 let ReactDOM;
15 let Hover;
16
17 +const createPointerEvent = type => {
18 + const event = document.createEvent('Event');
19 + event.initEvent(type, true, true);
20 + return event;
21 +};
22 +
23 describe('Hover event responder', () => {
24 let container;
25
@@ -34,69 +40,154 @@ describe('Hover event responder', () => {
40 container = null;
41 });
42
37 - it('should support onHover', () => {
38 - let divRef = React.createRef();
39 - let events = [];
40 -
41 - function handleOnHover(e) {
42 - if (e) {
43 - events.push('hover in');
44 - } else {
45 - events.push('hover out');
46 - }
47 - }
48 -
49 - function Component() {
50 - return (
51 - <Hover onHoverChange={handleOnHover}>
52 - <div ref={divRef}>Hover me!</div>
43 + describe('onHoverStart', () => {
44 + let onHoverStart, ref;
45 +
46 + beforeEach(() => {
47 + onHoverStart = jest.fn();
48 + ref = React.createRef();
49 + const element = (
50 + <Hover onHoverStart={onHoverStart}>
51 + <div ref={ref} />
52 </Hover>
53 );
55 - }
56 -
57 - ReactDOM.render(<Component />, container);
58 -
59 - const mouseOverEvent = document.createEvent('Event');
60 - mouseOverEvent.initEvent('mouseover', true, true);
61 - divRef.current.dispatchEvent(mouseOverEvent);
62 -
63 - const mouseOutEvent = document.createEvent('Event');
64 - mouseOutEvent.initEvent('mouseout', true, true);
65 - divRef.current.dispatchEvent(mouseOutEvent);
66 -
67 - expect(events).toEqual(['hover in', 'hover out']);
54 + ReactDOM.render(element, container);
55 + });
56 +
57 + it('is called after "pointerover" event', () => {
58 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
59 + expect(onHoverStart).toHaveBeenCalledTimes(1);
60 + });
61 +
62 + it('is not called if "pointerover" pointerType is touch', () => {
63 + const event = createPointerEvent('pointerover');
64 + event.pointerType = 'touch';
65 + ref.current.dispatchEvent(event);
66 + expect(onHoverStart).not.toBeCalled();
67 + });
68 +
69 + it('ignores browser emulated "mouseover" event', () => {
70 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
71 + ref.current.dispatchEvent(createPointerEvent('mouseover'));
72 + expect(onHoverStart).toHaveBeenCalledTimes(1);
73 + });
74 +
75 + // No PointerEvent fallbacks
76 + it('is called after "mouseover" event', () => {
77 + ref.current.dispatchEvent(createPointerEvent('mouseover'));
78 + expect(onHoverStart).toHaveBeenCalledTimes(1);
79 + });
80 + it('is not called after "touchstart"', () => {
81 + ref.current.dispatchEvent(createPointerEvent('touchstart'));
82 + ref.current.dispatchEvent(createPointerEvent('touchend'));
83 + ref.current.dispatchEvent(createPointerEvent('mouseover'));
84 + expect(onHoverStart).not.toBeCalled();
85 + });
86 +
87 + // TODO: complete delayHoverStart tests
88 + // describe('delayHoverStart', () => {});
89 });
90
70 - it('should support onHoverStart and onHoverEnd', () => {
71 - let divRef = React.createRef();
72 - let events = [];
73 -
74 - function handleOnHoverStart() {
75 - events.push('onHoverStart');
76 - }
91 + describe('onHoverChange', () => {
92 + let onHoverChange, ref;
93
78 - function handleOnHoverEnd() {
79 - events.push('onHoverEnd');
80 - }
81 -
82 - function Component() {
83 - return (
84 - <Hover onHoverStart={handleOnHoverStart} onHoverEnd={handleOnHoverEnd}>
85 - <div ref={divRef}>Hover me!</div>
94 + beforeEach(() => {
95 + onHoverChange = jest.fn();
96 + ref = React.createRef();
97 + const element = (
98 + <Hover onHoverChange={onHoverChange}>
99 + <div ref={ref} />
100 </Hover>
101 );
88 - }
89 -
90 - ReactDOM.render(<Component />, container);
102 + ReactDOM.render(element, container);
103 + });
104 +
105 + it('is called after "pointerover" and "pointerout" events', () => {
106 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
107 + expect(onHoverChange).toHaveBeenCalledTimes(1);
108 + expect(onHoverChange).toHaveBeenCalledWith(true);
109 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
110 + expect(onHoverChange).toHaveBeenCalledTimes(2);
111 + expect(onHoverChange).toHaveBeenCalledWith(false);
112 + });
113 +
114 + // No PointerEvent fallbacks
115 + it('is called after "mouseover" and "mouseout" events', () => {
116 + ref.current.dispatchEvent(createPointerEvent('mouseover'));
117 + expect(onHoverChange).toHaveBeenCalledTimes(1);
118 + expect(onHoverChange).toHaveBeenCalledWith(true);
119 + ref.current.dispatchEvent(createPointerEvent('mouseout'));
120 + expect(onHoverChange).toHaveBeenCalledTimes(2);
121 + expect(onHoverChange).toHaveBeenCalledWith(false);
122 + });
123 + });
124
92 - const mouseOverEvent = document.createEvent('Event');
93 - mouseOverEvent.initEvent('mouseover', true, true);
94 - divRef.current.dispatchEvent(mouseOverEvent);
125 + describe('onHoverEnd', () => {
126 + let onHoverEnd, ref;
127
96 - const mouseOutEvent = document.createEvent('Event');
97 - mouseOutEvent.initEvent('mouseout', true, true);
98 - divRef.current.dispatchEvent(mouseOutEvent);
128 + beforeEach(() => {
129 + onHoverEnd = jest.fn();
130 + ref = React.createRef();
131 + const element = (
132 + <Hover onHoverEnd={onHoverEnd}>
133 + <div ref={ref} />
134 + </Hover>
135 + );
136 + ReactDOM.render(element, container);
137 + });
138 +
139 + it('is called after "pointerout" event', () => {
140 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
141 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
142 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
143 + });
144 +
145 + it('is not called if "pointerover" pointerType is touch', () => {
146 + const event = createPointerEvent('pointerover');
147 + event.pointerType = 'touch';
148 + ref.current.dispatchEvent(event);
149 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
150 + expect(onHoverEnd).not.toBeCalled();
151 + });
152 +
153 + it('ignores browser emulated "mouseout" event', () => {
154 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
155 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
156 + ref.current.dispatchEvent(createPointerEvent('mouseout'));
157 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
158 + });
159 +
160 + it('is called after "pointercancel" event', () => {
161 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
162 + ref.current.dispatchEvent(createPointerEvent('pointercancel'));
163 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
164 + });
165 +
166 + it('is not called again after "pointercancel" event if it follows "pointerout"', () => {
167 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
168 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
169 + ref.current.dispatchEvent(createPointerEvent('pointercancel'));
170 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
171 + });
172 +
173 + // No PointerEvent fallbacks
174 + it('is called after "mouseout" event', () => {
175 + ref.current.dispatchEvent(createPointerEvent('mouseover'));
176 + ref.current.dispatchEvent(createPointerEvent('mouseout'));
177 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
178 + });
179 + it('is not called after "touchend"', () => {
180 + ref.current.dispatchEvent(createPointerEvent('touchstart'));
181 + ref.current.dispatchEvent(createPointerEvent('touchend'));
182 + ref.current.dispatchEvent(createPointerEvent('mouseout'));
183 + expect(onHoverEnd).not.toBeCalled();
184 + });
185 +
186 + // TODO: complete delayHoverStart tests
187 + // describe('delayHoverEnd', () => {});
188 + });
189
100 - expect(events).toEqual(['onHoverStart', 'onHoverEnd']);
190 + it('expect displayName to show up for event component', () => {
191 + expect(Hover.displayName).toBe('Hover');
192 });
193 });
packages/react-events/src/__tests__/Press-test.internal.js
+100 -14
@@ -14,7 +14,7 @@ let ReactFeatureFlags;
14 let ReactDOM;
15 let Press;
16
17 -const DEFAULT_LONG_PRESS_DELAY = 1000;
17 +const DEFAULT_LONG_PRESS_DELAY = 500;
18
19 const createPointerEvent = type => {
20 const event = document.createEvent('Event');
@@ -69,12 +69,31 @@ describe('Event responder: Press', () => {
69 expect(onPressStart).toHaveBeenCalledTimes(1);
70 });
71
72 - it('ignores emulated "mousedown" event', () => {
72 + it('ignores browser emulated "mousedown" event', () => {
73 ref.current.dispatchEvent(createPointerEvent('pointerdown'));
74 ref.current.dispatchEvent(createPointerEvent('mousedown'));
75 expect(onPressStart).toHaveBeenCalledTimes(1);
76 });
77
78 + it('is called once after "keydown" events for Enter', () => {
79 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
80 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
81 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
82 + expect(onPressStart).toHaveBeenCalledTimes(1);
83 + });
84 +
85 + it('is called once after "keydown" events for Spacebar', () => {
86 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
87 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
88 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
89 + expect(onPressStart).toHaveBeenCalledTimes(1);
90 + });
91 +
92 + it('is not called after "keydown" for other keys', () => {
93 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'a'}));
94 + expect(onPressStart).not.toBeCalled();
95 + });
96 +
97 // No PointerEvent fallbacks
98 it('is called after "mousedown" event', () => {
99 ref.current.dispatchEvent(createPointerEvent('mousedown'));
@@ -109,20 +128,37 @@ describe('Event responder: Press', () => {
128 expect(onPressEnd).toHaveBeenCalledTimes(1);
129 });
130
112 - it('ignores emulated "mouseup" event', () => {
131 + it('ignores browser emulated "mouseup" event', () => {
132 ref.current.dispatchEvent(createPointerEvent('touchstart'));
133 ref.current.dispatchEvent(createPointerEvent('touchend'));
134 ref.current.dispatchEvent(createPointerEvent('mouseup'));
135 expect(onPressEnd).toHaveBeenCalledTimes(1);
136 });
137
138 + it('is called after "keyup" event for Enter', () => {
139 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
140 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
141 + expect(onPressEnd).toHaveBeenCalledTimes(1);
142 + });
143 +
144 + it('is called after "keyup" event for Spacebar', () => {
145 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
146 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '}));
147 + expect(onPressEnd).toHaveBeenCalledTimes(1);
148 + });
149 +
150 + it('is not called after "keyup" event for other keys', () => {
151 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
152 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'a'}));
153 + expect(onPressEnd).not.toBeCalled();
154 + });
155 +
156 // No PointerEvent fallbacks
157 it('is called after "mouseup" event', () => {
158 ref.current.dispatchEvent(createPointerEvent('mousedown'));
159 ref.current.dispatchEvent(createPointerEvent('mouseup'));
160 expect(onPressEnd).toHaveBeenCalledTimes(1);
161 });
125 -
162 it('is called after "touchend" event', () => {
163 ref.current.dispatchEvent(createPointerEvent('touchstart'));
164 ref.current.dispatchEvent(createPointerEvent('touchend'));
@@ -155,6 +191,33 @@ describe('Event responder: Press', () => {
191 expect(onPressChange).toHaveBeenCalledTimes(2);
192 expect(onPressChange).toHaveBeenCalledWith(false);
193 });
194 +
195 + it('is called after valid "keydown" and "keyup" events', () => {
196 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
197 + expect(onPressChange).toHaveBeenCalledTimes(1);
198 + expect(onPressChange).toHaveBeenCalledWith(true);
199 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
200 + expect(onPressChange).toHaveBeenCalledTimes(2);
201 + expect(onPressChange).toHaveBeenCalledWith(false);
202 + });
203 +
204 + // No PointerEvent fallbacks
205 + it('is called after "mousedown" and "mouseup" events', () => {
206 + ref.current.dispatchEvent(createPointerEvent('mousedown'));
207 + expect(onPressChange).toHaveBeenCalledTimes(1);
208 + expect(onPressChange).toHaveBeenCalledWith(true);
209 + ref.current.dispatchEvent(createPointerEvent('mouseup'));
210 + expect(onPressChange).toHaveBeenCalledTimes(2);
211 + expect(onPressChange).toHaveBeenCalledWith(false);
212 + });
213 + it('is called after "touchstart" and "touchend" events', () => {
214 + ref.current.dispatchEvent(createPointerEvent('touchstart'));
215 + expect(onPressChange).toHaveBeenCalledTimes(1);
216 + expect(onPressChange).toHaveBeenCalledWith(true);
217 + ref.current.dispatchEvent(createPointerEvent('touchend'));
218 + expect(onPressChange).toHaveBeenCalledTimes(2);
219 + expect(onPressChange).toHaveBeenCalledWith(false);
220 + });
221 });
222
223 describe('onPress', () => {
@@ -176,6 +239,20 @@ describe('Event responder: Press', () => {
239 ref.current.dispatchEvent(createPointerEvent('pointerup'));
240 expect(onPress).toHaveBeenCalledTimes(1);
241 });
242 +
243 + it('is called after valid "keyup" event', () => {
244 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
245 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
246 + expect(onPress).toHaveBeenCalledTimes(1);
247 + });
248 +
249 + // No PointerEvent fallbacks
250 + // TODO: jsdom missing APIs
251 + //it('is called after "touchend" event', () => {
252 + //ref.current.dispatchEvent(createPointerEvent('touchstart'));
253 + //ref.current.dispatchEvent(createPointerEvent('touchend'));
254 + //expect(onPress).toHaveBeenCalledTimes(1);
255 + //});
256 });
257
258 describe('onLongPress', () => {
@@ -192,7 +269,7 @@ describe('Event responder: Press', () => {
269 ReactDOM.render(element, container);
270 });
271
195 - it('is called if press lasts default delay', () => {
272 + it('is called if "pointerdown" lasts default delay', () => {
273 ref.current.dispatchEvent(createPointerEvent('pointerdown'));
274 jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
275 expect(onLongPress).not.toBeCalled();
@@ -200,7 +277,7 @@ describe('Event responder: Press', () => {
277 expect(onLongPress).toHaveBeenCalledTimes(1);
278 });
279
203 - it('is not called if press is released before delay', () => {
280 + it('is not called if "pointerup" is dispatched before delay', () => {
281 ref.current.dispatchEvent(createPointerEvent('pointerdown'));
282 jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
283 ref.current.dispatchEvent(createPointerEvent('pointerup'));
@@ -208,6 +285,22 @@ describe('Event responder: Press', () => {
285 expect(onLongPress).not.toBeCalled();
286 });
287
288 + it('is called if valid "keydown" lasts default delay', () => {
289 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
290 + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
291 + expect(onLongPress).not.toBeCalled();
292 + jest.advanceTimersByTime(1);
293 + expect(onLongPress).toHaveBeenCalledTimes(1);
294 + });
295 +
296 + it('is not called if valid "keyup" is dispatched before delay', () => {
297 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
298 + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
299 + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
300 + jest.advanceTimersByTime(1);
301 + expect(onLongPress).not.toBeCalled();
302 + });
303 +
304 describe('delayLongPress', () => {
305 it('can be configured', () => {
306 const element = (
@@ -339,7 +432,7 @@ describe('Event responder: Press', () => {
432
433 describe('nested responders', () => {
434 it('dispatch events in the correct order', () => {
342 - let events = [];
435 + const events = [];
436 const ref = React.createRef();
437 const createEventHandler = msg => () => {
438 events.push(msg);
@@ -385,13 +478,6 @@ describe('Event responder: Press', () => {
478 'outer: onPressChange',
479 'outer: onPress',
480 ]);
388 -
389 - events = [];
390 - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
391 - // TODO update this test once we have a form of stopPropagation in
392 - // the responder system again. This test had to be updated because
393 - // we have removed stopPropagation() from synthetic events.
394 - expect(events).toEqual(['keydown', 'inner: onPress', 'outer: onPress']);
481 });
482 });
483