React events: consolidate logic of Hover event component (#15450)
Minor refactor of Hover and additional regression coverage.
Nicolas Gallagher committed
Apr 19, 2019 at 09:51 UTC
cdfce1ad23813046137c9d396f03afca5a31716d
2 files changed
+91
-86
packages/react-events/src/Hover.js
+77
-84
@@ -12,6 +12,11 @@ import type {
12
ReactResponderContext,
13
} from 'shared/ReactTypes';
14
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
+import {
16
+ getEventPointerType,
17
+ getEventCurrentTarget,
18
+ isEventPositionWithinTouchHitTarget,
19
+} from './utils';
20
21
const CAPTURE_PHASE = 2;
22
@@ -31,11 +36,11 @@ type HoverState = {
36
hoverTarget: null | Element | Document,
37
isActiveHovered: boolean,
38
isHovered: boolean,
34
- isInHitSlop: boolean,
39
+ isOverTouchHitTarget: boolean,
40
isTouched: boolean,
41
hoverStartTimeout: null | Symbol,
42
hoverEndTimeout: null | Symbol,
38
- skipMouseAfterPointer: boolean,
43
+ ignoreEmulatedMouseEvents: boolean,
44
};
45
46
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
@@ -181,9 +186,9 @@ function dispatchHoverEndEvents(
186
dispatchHoverChangeEvent(context, props, state);
187
}
188
184
- state.isInHitSlop = false;
189
+ state.isOverTouchHitTarget = false;
190
state.hoverTarget = null;
186
- state.skipMouseAfterPointer = false;
191
+ state.ignoreEmulatedMouseEvents = false;
192
state.isTouched = false;
193
};
194
@@ -219,17 +224,25 @@ function unmountResponder(
224
}
225
}
226
227
+function isEmulatedMouseEvent(event, state) {
228
+ const {type} = event;
229
+ return (
230
+ state.ignoreEmulatedMouseEvents &&
231
+ (type === 'mousemove' || type === 'mouseover' || type === 'mouseout')
232
+ );
233
+}
234
+
235
const HoverResponder = {
236
targetEventTypes,
237
createInitialState() {
238
return {
239
isActiveHovered: false,
240
isHovered: false,
228
- isInHitSlop: false,
241
+ isOverTouchHitTarget: false,
242
isTouched: false,
243
hoverStartTimeout: null,
244
hoverEndTimeout: null,
232
- skipMouseAfterPointer: false,
245
+ ignoreEmulatedMouseEvents: false,
246
};
247
},
248
onEvent(
@@ -238,112 +251,92 @@ const HoverResponder = {
251
props: HoverProps,
252
state: HoverState,
253
): boolean {
241
- const {type, phase, target} = event;
242
- const nativeEvent: any = event.nativeEvent;
254
+ const {type} = event;
255
256
// Hover doesn't handle capture target events at this point
245
- if (phase === CAPTURE_PHASE) {
257
+ if (event.phase === CAPTURE_PHASE) {
258
return false;
259
}
248
- switch (type) {
249
- /**
250
- * Prevent hover events when touch is being used.
251
- */
252
- case 'touchstart': {
253
- if (!state.isTouched) {
254
- state.isTouched = true;
255
- }
256
- break;
257
- }
258
- case 'touchcancel':
259
- case 'touchend': {
260
- if (state.isTouched) {
261
- state.isTouched = false;
262
- }
263
- break;
264
- }
260
261
+ const pointerType = getEventPointerType(event);
262
+
263
+ switch (type) {
264
+ // START
265
case 'pointerover':
267
- case 'mouseover': {
268
- if (!state.isHovered && !state.isTouched) {
269
- if (nativeEvent.pointerType === 'touch') {
266
+ case 'mouseover':
267
+ case 'touchstart': {
268
+ if (!state.isHovered) {
269
+ // Prevent hover events for touch
270
+ if (state.isTouched || pointerType === 'touch') {
271
state.isTouched = true;
272
return false;
273
}
273
- if (type === 'pointerover') {
274
- state.skipMouseAfterPointer = true;
274
+
275
+ // Prevent hover events for emulated events
276
+ if (isEmulatedMouseEvent(event, state)) {
277
+ return false;
278
}
276
- if (
277
- context.isPositionWithinTouchHitTarget(
278
- target.ownerDocument,
279
- nativeEvent.x,
280
- nativeEvent.y,
281
- )
282
- ) {
283
- state.isInHitSlop = true;
279
+
280
+ if (isEventPositionWithinTouchHitTarget(event, context)) {
281
+ state.isOverTouchHitTarget = true;
282
return false;
283
}
286
- state.hoverTarget = target;
284
+ state.hoverTarget = getEventCurrentTarget(event, context);
285
+ state.ignoreEmulatedMouseEvents = true;
286
dispatchHoverStartEvents(event, context, props, state);
287
}
289
- break;
290
- }
291
- case 'pointerout':
292
- case 'mouseout': {
293
- if (state.isHovered && !state.isTouched) {
294
- dispatchHoverEndEvents(event, context, props, state);
295
- }
296
- break;
288
+ return false;
289
}
290
291
+ // MOVE
292
case 'pointermove':
293
case 'mousemove': {
301
- if (type === 'mousemove' && state.skipMouseAfterPointer === true) {
302
- return false;
303
- }
304
-
305
- if (state.isHovered && !state.isTouched) {
306
- if (state.isInHitSlop) {
307
- if (
308
- !context.isPositionWithinTouchHitTarget(
309
- target.ownerDocument,
310
- nativeEvent.x,
311
- nativeEvent.y,
312
- )
313
- ) {
314
- dispatchHoverStartEvents(event, context, props, state);
315
- state.isInHitSlop = false;
316
- }
317
- } else if (state.isHovered) {
318
- if (
319
- context.isPositionWithinTouchHitTarget(
320
- target.ownerDocument,
321
- nativeEvent.x,
322
- nativeEvent.y,
323
- )
324
- ) {
325
- dispatchHoverEndEvents(event, context, props, state);
326
- state.isInHitSlop = true;
294
+ if (state.isHovered && !isEmulatedMouseEvent(event, state)) {
295
+ if (state.isHovered) {
296
+ if (state.isOverTouchHitTarget) {
297
+ // If we were moving over the TouchHitTarget and have now moved
298
+ // over the Responder target
299
+ if (!isEventPositionWithinTouchHitTarget(event, context)) {
300
+ dispatchHoverStartEvents(event, context, props, state);
301
+ state.isOverTouchHitTarget = false;
302
+ }
303
} else {
328
- if (props.onHoverMove) {
329
- const syntheticEvent = createHoverEvent('hovermove', target);
330
- context.dispatchEvent(syntheticEvent, props.onHoverMove, {
331
- discrete: false,
332
- });
304
+ // If we were moving over the Responder target and have now moved
305
+ // over the TouchHitTarget
306
+ if (isEventPositionWithinTouchHitTarget(event, context)) {
307
+ dispatchHoverEndEvents(event, context, props, state);
308
+ state.isOverTouchHitTarget = true;
309
+ } else {
310
+ if (props.onHoverMove && state.hoverTarget !== null) {
311
+ const syntheticEvent = createHoverEvent(
312
+ 'hovermove',
313
+ state.hoverTarget,
314
+ );
315
+ context.dispatchEvent(syntheticEvent, props.onHoverMove, {
316
+ discrete: false,
317
+ });
318
+ }
319
}
320
}
321
}
322
}
337
- break;
323
+ return false;
324
}
325
340
- case 'pointercancel': {
341
- if (state.isHovered && !state.isTouched) {
326
+ // END
327
+ case 'pointerout':
328
+ case 'pointercancel':
329
+ case 'mouseout':
330
+ case 'touchcancel':
331
+ case 'touchend': {
332
+ if (state.isHovered) {
333
dispatchHoverEndEvents(event, context, props, state);
343
- state.hoverTarget = null;
334
+ state.ignoreEmulatedMouseEvents = false;
335
+ }
336
+ if (state.isTouched) {
337
state.isTouched = false;
338
}
346
- break;
339
+ return false;
340
}
341
}
342
return false;
packages/react-events/src/__tests__/Hover-test.internal.js
+14
-2
@@ -65,12 +65,24 @@ describe('Hover event responder', () => {
65
});
66
67
it('is not called if "pointerover" pointerType is touch', () => {
68
- const event = createPointerEvent('pointerover');
69
- event.pointerType = 'touch';
68
+ const event = createPointerEvent('pointerover', {pointerType: 'touch'});
69
ref.current.dispatchEvent(event);
70
expect(onHoverStart).not.toBeCalled();
71
});
72
73
+ it('is called if valid "pointerover" follows touch', () => {
74
+ ref.current.dispatchEvent(
75
+ createPointerEvent('pointerover', {pointerType: 'touch'}),
76
+ );
77
+ ref.current.dispatchEvent(
78
+ createPointerEvent('pointerout', {pointerType: 'touch'}),
79
+ );
80
+ ref.current.dispatchEvent(
81
+ createPointerEvent('pointerover', {pointerType: 'mouse'}),
82
+ );
83
+ expect(onHoverStart).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
it('ignores browser emulated "mouseover" event', () => {
87
ref.current.dispatchEvent(createPointerEvent('pointerover'));
88
ref.current.dispatchEvent(createPointerEvent('mouseover'));