From 1ae409d2c75741cceb1ce0bf12115cfffefb9c91 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Wed, 17 Apr 2019 11:33:32 -0700 Subject: [PATCH] React events: fix nested Hover components error (#15428) * Add failing test for nested Hover * Fix error caused by nested Hover event components --- packages/react-events/src/Focus.js | 1 + packages/react-events/src/Hover.js | 40 ++++++----- .../src/__tests__/Focus-test.internal.js | 2 +- .../src/__tests__/Hover-test.internal.js | 68 ++++++++++++++++++- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js index 929828d0382e4..a5532a0667165 100644 --- a/packages/react-events/src/Focus.js +++ b/packages/react-events/src/Focus.js @@ -20,6 +20,7 @@ type FocusProps = { onBlur: (e: FocusEvent) => void, onFocus: (e: FocusEvent) => void, onFocusChange: boolean => void, + stopPropagation: boolean, }; type FocusState = { diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js index d10e6a78e8d2d..d5257db270826 100644 --- a/packages/react-events/src/Hover.js +++ b/packages/react-events/src/Hover.js @@ -23,6 +23,8 @@ type HoverProps = { onHoverEnd: (e: HoverEvent) => void, onHoverMove: (e: HoverEvent) => void, onHoverStart: (e: HoverEvent) => void, + preventDefault: boolean, + stopPropagation: boolean, }; type HoverState = { @@ -178,6 +180,11 @@ function dispatchHoverEndEvents( if (props.onHoverChange) { dispatchHoverChangeEvent(context, props, state); } + + state.isInHitSlop = false; + state.hoverTarget = null; + state.skipMouseAfterPointer = false; + state.isTouched = false; }; if (state.isActiveHovered) { @@ -231,7 +238,8 @@ const HoverResponder = { props: HoverProps, state: HoverState, ): boolean { - const {type, phase, target, nativeEvent} = event; + const {type, phase, target} = event; + const nativeEvent: any = event.nativeEvent; // Hover doesn't handle capture target events at this point if (phase === CAPTURE_PHASE) { @@ -247,11 +255,18 @@ const HoverResponder = { } break; } + case 'touchcancel': + case 'touchend': { + if (state.isTouched) { + state.isTouched = false; + } + break; + } case 'pointerover': case 'mouseover': { if (!state.isHovered && !state.isTouched) { - if ((nativeEvent: any).pointerType === 'touch') { + if (nativeEvent.pointerType === 'touch') { state.isTouched = true; return false; } @@ -261,8 +276,8 @@ const HoverResponder = { if ( context.isPositionWithinTouchHitTarget( target.ownerDocument, - (nativeEvent: any).x, - (nativeEvent: any).y, + nativeEvent.x, + nativeEvent.y, ) ) { state.isInHitSlop = true; @@ -278,10 +293,6 @@ const HoverResponder = { if (state.isHovered && !state.isTouched) { dispatchHoverEndEvents(event, context, props, state); } - state.isInHitSlop = false; - state.hoverTarget = null; - state.isTouched = false; - state.skipMouseAfterPointer = false; break; } @@ -296,8 +307,8 @@ const HoverResponder = { if ( !context.isPositionWithinTouchHitTarget( target.ownerDocument, - (nativeEvent: any).x, - (nativeEvent: any).y, + nativeEvent.x, + nativeEvent.y, ) ) { dispatchHoverStartEvents(event, context, props, state); @@ -307,18 +318,15 @@ const HoverResponder = { if ( context.isPositionWithinTouchHitTarget( target.ownerDocument, - (nativeEvent: any).x, - (nativeEvent: any).y, + nativeEvent.x, + nativeEvent.y, ) ) { dispatchHoverEndEvents(event, context, props, state); state.isInHitSlop = true; } else { if (props.onHoverMove) { - const syntheticEvent = createHoverEvent( - 'hovermove', - event.target, - ); + const syntheticEvent = createHoverEvent('hovermove', target); context.dispatchEvent(syntheticEvent, props.onHoverMove, { discrete: false, }); diff --git a/packages/react-events/src/__tests__/Focus-test.internal.js b/packages/react-events/src/__tests__/Focus-test.internal.js index bcda95d2083c8..9e7131f7b6b2b 100644 --- a/packages/react-events/src/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/__tests__/Focus-test.internal.js @@ -106,7 +106,7 @@ describe('Focus event responder', () => { }); describe('nested Focus components', () => { - it('does not propagate events by default', () => { + it('do not propagate events by default', () => { const events = []; const innerRef = React.createRef(); const outerRef = React.createRef(); diff --git a/packages/react-events/src/__tests__/Hover-test.internal.js b/packages/react-events/src/__tests__/Hover-test.internal.js index 151631eaa1f97..cdd8ea3532861 100644 --- a/packages/react-events/src/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/__tests__/Hover-test.internal.js @@ -14,9 +14,14 @@ let ReactFeatureFlags; let ReactDOM; let Hover; -const createPointerEvent = type => { - const event = document.createEvent('Event'); - event.initEvent(type, true, true); +const createPointerEvent = (type, data) => { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent(type, true, true); + if (data != null) { + Object.entries(data).forEach(([key, value]) => { + event[key] = value; + }); + } return event; }; @@ -361,6 +366,63 @@ describe('Hover event responder', () => { }); }); + describe('nested Hover components', () => { + it('do not propagate events by default', () => { + const events = []; + const innerRef = React.createRef(); + const outerRef = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ +
+ +
+
+ ); + + ReactDOM.render(element, container); + + outerRef.current.dispatchEvent(createPointerEvent('pointerover')); + outerRef.current.dispatchEvent( + createPointerEvent('pointerout', {relatedTarget: innerRef.current}), + ); + innerRef.current.dispatchEvent(createPointerEvent('pointerover')); + innerRef.current.dispatchEvent( + createPointerEvent('pointerout', {relatedTarget: outerRef.current}), + ); + outerRef.current.dispatchEvent( + createPointerEvent('pointerover', {relatedTarget: innerRef.current}), + ); + outerRef.current.dispatchEvent(createPointerEvent('pointerout')); + // TODO: correct result should include commented events + expect(events).toEqual([ + 'outer: onHoverStart', + 'outer: onHoverChange', + // 'outer: onHoverEnd', + // 'outer: onHoverChange', + 'inner: onHoverStart', + 'inner: onHoverChange', + 'inner: onHoverEnd', + 'inner: onHoverChange', + // 'outer: onHoverStart', + // 'outer: onHoverChange', + 'outer: onHoverEnd', + 'outer: onHoverChange', + ]); + }); + }); + it('expect displayName to show up for event component', () => { expect(Hover.displayName).toBe('Hover'); });