Skip to content

Commit cdfce1a

Browse files
authored
React events: consolidate logic of Hover event component (#15450)
Minor refactor of Hover and additional regression coverage.
1 parent 5857c89 commit cdfce1a

File tree

2 files changed

+91
-86
lines changed

2 files changed

+91
-86
lines changed

packages/react-events/src/Hover.js

Lines changed: 77 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import type {
1212
ReactResponderContext,
1313
} from 'shared/ReactTypes';
1414
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15+
import {
16+
getEventPointerType,
17+
getEventCurrentTarget,
18+
isEventPositionWithinTouchHitTarget,
19+
} from './utils';
1520

1621
const CAPTURE_PHASE = 2;
1722

@@ -31,11 +36,11 @@ type HoverState = {
3136
hoverTarget: null | Element | Document,
3237
isActiveHovered: boolean,
3338
isHovered: boolean,
34-
isInHitSlop: boolean,
39+
isOverTouchHitTarget: boolean,
3540
isTouched: boolean,
3641
hoverStartTimeout: null | Symbol,
3742
hoverEndTimeout: null | Symbol,
38-
skipMouseAfterPointer: boolean,
43+
ignoreEmulatedMouseEvents: boolean,
3944
};
4045

4146
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
@@ -181,9 +186,9 @@ function dispatchHoverEndEvents(
181186
dispatchHoverChangeEvent(context, props, state);
182187
}
183188

184-
state.isInHitSlop = false;
189+
state.isOverTouchHitTarget = false;
185190
state.hoverTarget = null;
186-
state.skipMouseAfterPointer = false;
191+
state.ignoreEmulatedMouseEvents = false;
187192
state.isTouched = false;
188193
};
189194

@@ -219,17 +224,25 @@ function unmountResponder(
219224
}
220225
}
221226

227+
function isEmulatedMouseEvent(event, state) {
228+
const {type} = event;
229+
return (
230+
state.ignoreEmulatedMouseEvents &&
231+
(type === 'mousemove' || type === 'mouseover' || type === 'mouseout')
232+
);
233+
}
234+
222235
const HoverResponder = {
223236
targetEventTypes,
224237
createInitialState() {
225238
return {
226239
isActiveHovered: false,
227240
isHovered: false,
228-
isInHitSlop: false,
241+
isOverTouchHitTarget: false,
229242
isTouched: false,
230243
hoverStartTimeout: null,
231244
hoverEndTimeout: null,
232-
skipMouseAfterPointer: false,
245+
ignoreEmulatedMouseEvents: false,
233246
};
234247
},
235248
onEvent(
@@ -238,112 +251,92 @@ const HoverResponder = {
238251
props: HoverProps,
239252
state: HoverState,
240253
): boolean {
241-
const {type, phase, target} = event;
242-
const nativeEvent: any = event.nativeEvent;
254+
const {type} = event;
243255

244256
// Hover doesn't handle capture target events at this point
245-
if (phase === CAPTURE_PHASE) {
257+
if (event.phase === CAPTURE_PHASE) {
246258
return false;
247259
}
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-
}
265260

261+
const pointerType = getEventPointerType(event);
262+
263+
switch (type) {
264+
// START
266265
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') {
270271
state.isTouched = true;
271272
return false;
272273
}
273-
if (type === 'pointerover') {
274-
state.skipMouseAfterPointer = true;
274+
275+
// Prevent hover events for emulated events
276+
if (isEmulatedMouseEvent(event, state)) {
277+
return false;
275278
}
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;
284282
return false;
285283
}
286-
state.hoverTarget = target;
284+
state.hoverTarget = getEventCurrentTarget(event, context);
285+
state.ignoreEmulatedMouseEvents = true;
287286
dispatchHoverStartEvents(event, context, props, state);
288287
}
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;
297289
}
298290

291+
// MOVE
299292
case 'pointermove':
300293
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+
}
327303
} 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+
}
333319
}
334320
}
335321
}
336322
}
337-
break;
323+
return false;
338324
}
339325

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) {
342333
dispatchHoverEndEvents(event, context, props, state);
343-
state.hoverTarget = null;
334+
state.ignoreEmulatedMouseEvents = false;
335+
}
336+
if (state.isTouched) {
344337
state.isTouched = false;
345338
}
346-
break;
339+
return false;
347340
}
348341
}
349342
return false;

packages/react-events/src/__tests__/Hover-test.internal.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,24 @@ describe('Hover event responder', () => {
6565
});
6666

6767
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'});
7069
ref.current.dispatchEvent(event);
7170
expect(onHoverStart).not.toBeCalled();
7271
});
7372

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+
7486
it('ignores browser emulated "mouseover" event', () => {
7587
ref.current.dispatchEvent(createPointerEvent('pointerover'));
7688
ref.current.dispatchEvent(createPointerEvent('mouseover'));

0 commit comments

Comments
 (0)