@samitouri / QOS-React / commits / cdfce1ad23

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'));