Skip to content

Commit

Permalink
React events: consolidate logic of Hover event component (#15450)
Browse files Browse the repository at this point in the history
Minor refactor of Hover and additional regression coverage.
  • Loading branch information
necolas committed Apr 19, 2019
1 parent 5857c89 commit cdfce1a
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 86 deletions.
161 changes: 77 additions & 84 deletions packages/react-events/src/Hover.js
Expand Up @@ -12,6 +12,11 @@ import type {
ReactResponderContext,
} from 'shared/ReactTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
import {
getEventPointerType,
getEventCurrentTarget,
isEventPositionWithinTouchHitTarget,
} from './utils';

const CAPTURE_PHASE = 2;

Expand All @@ -31,11 +36,11 @@ type HoverState = {
hoverTarget: null | Element | Document,
isActiveHovered: boolean,
isHovered: boolean,
isInHitSlop: boolean,
isOverTouchHitTarget: boolean,
isTouched: boolean,
hoverStartTimeout: null | Symbol,
hoverEndTimeout: null | Symbol,
skipMouseAfterPointer: boolean,
ignoreEmulatedMouseEvents: boolean,
};

type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
Expand Down Expand Up @@ -181,9 +186,9 @@ function dispatchHoverEndEvents(
dispatchHoverChangeEvent(context, props, state);
}

state.isInHitSlop = false;
state.isOverTouchHitTarget = false;
state.hoverTarget = null;
state.skipMouseAfterPointer = false;
state.ignoreEmulatedMouseEvents = false;
state.isTouched = false;
};

Expand Down Expand Up @@ -219,17 +224,25 @@ function unmountResponder(
}
}

function isEmulatedMouseEvent(event, state) {
const {type} = event;
return (
state.ignoreEmulatedMouseEvents &&
(type === 'mousemove' || type === 'mouseover' || type === 'mouseout')
);
}

const HoverResponder = {
targetEventTypes,
createInitialState() {
return {
isActiveHovered: false,
isHovered: false,
isInHitSlop: false,
isOverTouchHitTarget: false,
isTouched: false,
hoverStartTimeout: null,
hoverEndTimeout: null,
skipMouseAfterPointer: false,
ignoreEmulatedMouseEvents: false,
};
},
onEvent(
Expand All @@ -238,112 +251,92 @@ const HoverResponder = {
props: HoverProps,
state: HoverState,
): boolean {
const {type, phase, target} = event;
const nativeEvent: any = event.nativeEvent;
const {type} = event;

// Hover doesn't handle capture target events at this point
if (phase === CAPTURE_PHASE) {
if (event.phase === CAPTURE_PHASE) {
return false;
}
switch (type) {
/**
* Prevent hover events when touch is being used.
*/
case 'touchstart': {
if (!state.isTouched) {
state.isTouched = true;
}
break;
}
case 'touchcancel':
case 'touchend': {
if (state.isTouched) {
state.isTouched = false;
}
break;
}

const pointerType = getEventPointerType(event);

switch (type) {
// START
case 'pointerover':
case 'mouseover': {
if (!state.isHovered && !state.isTouched) {
if (nativeEvent.pointerType === 'touch') {
case 'mouseover':
case 'touchstart': {
if (!state.isHovered) {
// Prevent hover events for touch
if (state.isTouched || pointerType === 'touch') {
state.isTouched = true;
return false;
}
if (type === 'pointerover') {
state.skipMouseAfterPointer = true;

// Prevent hover events for emulated events
if (isEmulatedMouseEvent(event, state)) {
return false;
}
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
nativeEvent.x,
nativeEvent.y,
)
) {
state.isInHitSlop = true;

if (isEventPositionWithinTouchHitTarget(event, context)) {
state.isOverTouchHitTarget = true;
return false;
}
state.hoverTarget = target;
state.hoverTarget = getEventCurrentTarget(event, context);
state.ignoreEmulatedMouseEvents = true;
dispatchHoverStartEvents(event, context, props, state);
}
break;
}
case 'pointerout':
case 'mouseout': {
if (state.isHovered && !state.isTouched) {
dispatchHoverEndEvents(event, context, props, state);
}
break;
return false;
}

// MOVE
case 'pointermove':
case 'mousemove': {
if (type === 'mousemove' && state.skipMouseAfterPointer === true) {
return false;
}

if (state.isHovered && !state.isTouched) {
if (state.isInHitSlop) {
if (
!context.isPositionWithinTouchHitTarget(
target.ownerDocument,
nativeEvent.x,
nativeEvent.y,
)
) {
dispatchHoverStartEvents(event, context, props, state);
state.isInHitSlop = false;
}
} else if (state.isHovered) {
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
nativeEvent.x,
nativeEvent.y,
)
) {
dispatchHoverEndEvents(event, context, props, state);
state.isInHitSlop = true;
if (state.isHovered && !isEmulatedMouseEvent(event, state)) {
if (state.isHovered) {
if (state.isOverTouchHitTarget) {
// If we were moving over the TouchHitTarget and have now moved
// over the Responder target
if (!isEventPositionWithinTouchHitTarget(event, context)) {
dispatchHoverStartEvents(event, context, props, state);
state.isOverTouchHitTarget = false;
}
} else {
if (props.onHoverMove) {
const syntheticEvent = createHoverEvent('hovermove', target);
context.dispatchEvent(syntheticEvent, props.onHoverMove, {
discrete: false,
});
// If we were moving over the Responder target and have now moved
// over the TouchHitTarget
if (isEventPositionWithinTouchHitTarget(event, context)) {
dispatchHoverEndEvents(event, context, props, state);
state.isOverTouchHitTarget = true;
} else {
if (props.onHoverMove && state.hoverTarget !== null) {
const syntheticEvent = createHoverEvent(
'hovermove',
state.hoverTarget,
);
context.dispatchEvent(syntheticEvent, props.onHoverMove, {
discrete: false,
});
}
}
}
}
}
break;
return false;
}

case 'pointercancel': {
if (state.isHovered && !state.isTouched) {
// END
case 'pointerout':
case 'pointercancel':
case 'mouseout':
case 'touchcancel':
case 'touchend': {
if (state.isHovered) {
dispatchHoverEndEvents(event, context, props, state);
state.hoverTarget = null;
state.ignoreEmulatedMouseEvents = false;
}
if (state.isTouched) {
state.isTouched = false;
}
break;
return false;
}
}
return false;
Expand Down
16 changes: 14 additions & 2 deletions packages/react-events/src/__tests__/Hover-test.internal.js
Expand Up @@ -65,12 +65,24 @@ describe('Hover event responder', () => {
});

it('is not called if "pointerover" pointerType is touch', () => {
const event = createPointerEvent('pointerover');
event.pointerType = 'touch';
const event = createPointerEvent('pointerover', {pointerType: 'touch'});
ref.current.dispatchEvent(event);
expect(onHoverStart).not.toBeCalled();
});

it('is called if valid "pointerover" follows touch', () => {
ref.current.dispatchEvent(
createPointerEvent('pointerover', {pointerType: 'touch'}),
);
ref.current.dispatchEvent(
createPointerEvent('pointerout', {pointerType: 'touch'}),
);
ref.current.dispatchEvent(
createPointerEvent('pointerover', {pointerType: 'mouse'}),
);
expect(onHoverStart).toHaveBeenCalledTimes(1);
});

it('ignores browser emulated "mouseover" event', () => {
ref.current.dispatchEvent(createPointerEvent('pointerover'));
ref.current.dispatchEvent(createPointerEvent('mouseover'));
Expand Down

0 comments on commit cdfce1a

Please sign in to comment.