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