Skip to content

Commit

Permalink
React events: add onHoverMove support (#15388)
Browse files Browse the repository at this point in the history
  • Loading branch information
necolas authored Apr 12, 2019
1 parent cdfb06e commit 4e59d4f
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 22 deletions.
40 changes: 35 additions & 5 deletions packages/react-events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ const TextField = (props) => (

```js
// Types
type FocusEvent = {}
type FocusEvent = {
type: 'blur' | 'focus' | 'focuschange'
}
```

### disabled: boolean
Expand Down Expand Up @@ -76,7 +78,10 @@ const Link = (props) => (

```js
// Types
type HoverEvent = {}
type HoverEvent = {
pointerType: 'mouse',
type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange'
}
```

### delayHoverEnd: number
Expand All @@ -103,12 +108,25 @@ Called when the element changes hover state (i.e., after `onHoverStart` and
Called once the element is no longer hovered. It will be cancelled if the
pointer leaves the element before the `delayHoverStart` threshold is exceeded.

### onHoverMove: (e: HoverEvent) => void

Called when the pointer moves within the hit bounds of the element. `onHoverMove` is
called immediately and doesn't wait for delayed `onHoverStart`.

### onHoverStart: (e: HoverEvent) => void

Called once the element is hovered. It will not be called if the pointer leaves
the element before the `delayHoverStart` threshold is exceeded. And it will not
be called more than once before `onHoverEnd` is called.

### preventDefault: boolean = true

Whether to `preventDefault()` native events.

### stopPropagation: boolean = true

Whether to `stopPropagation()` native events.


## Press

Expand Down Expand Up @@ -145,7 +163,10 @@ const Button = (props) => (

```js
// Types
type PressEvent = {}
type PressEvent = {
pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard',
type: 'press' | 'pressstart' | 'pressend' | 'presschange' | 'pressmove' | 'longpress' | 'longpresschange'
}

type PressOffset = {
top: number,
Expand Down Expand Up @@ -210,8 +231,9 @@ called during a press.

### onPressMove: (e: PressEvent) => void

Called when an active press moves within the hit bounds of the element. Never
called for keyboard-initiated press events.
Called when a press moves within the hit bounds of the element. `onPressMove` is
called immediately and doesn't wait for delayed `onPressStart`. Never called for
keyboard-initiated press events.

### onPressStart: (e: PressEvent) => void

Expand All @@ -225,3 +247,11 @@ Defines how far the pointer (while held down) may move outside the bounds of the
element before it is deactivated. Once deactivated, the pointer (still held
down) can be moved back within the bounds of the element to reactivate it.
Ensure you pass in a constant to reduce memory allocations.

### preventDefault: boolean = true

Whether to `preventDefault()` native events.

### stopPropagation: boolean = true

Whether to `stopPropagation()` native events.
48 changes: 35 additions & 13 deletions packages/react-events/src/Hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type HoverProps = {
delayHoverStart: number,
onHoverChange: boolean => void,
onHoverEnd: (e: HoverEvent) => void,
onHoverMove: (e: HoverEvent) => void,
onHoverStart: (e: HoverEvent) => void,
};

Expand All @@ -29,9 +30,10 @@ type HoverState = {
isTouched: boolean,
hoverStartTimeout: null | Symbol,
hoverEndTimeout: null | Symbol,
skipMouseAfterPointer: boolean,
};

type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';

type HoverEvent = {|
listener: HoverEvent => void,
Expand All @@ -51,7 +53,7 @@ const targetEventTypes = [

// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
targetEventTypes.push('touchstart', 'mouseover', 'mousemove', 'mouseout');
}

function createHoverEvent(
Expand Down Expand Up @@ -200,6 +202,7 @@ const HoverResponder = {
isTouched: false,
hoverStartTimeout: null,
hoverEndTimeout: null,
skipMouseAfterPointer: false,
};
},
onEvent(
Expand Down Expand Up @@ -228,6 +231,9 @@ const HoverResponder = {
state.isTouched = true;
return;
}
if (type === 'pointerover') {
state.skipMouseAfterPointer = true;
}
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
Expand All @@ -249,10 +255,16 @@ const HoverResponder = {
}
state.isInHitSlop = false;
state.isTouched = false;
state.skipMouseAfterPointer = false;
break;
}

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

if (state.isHovered && !state.isTouched) {
if (state.isInHitSlop) {
if (
Expand All @@ -265,16 +277,26 @@ const HoverResponder = {
dispatchHoverStartEvents(event, context, props, state);
state.isInHitSlop = false;
}
} else if (
state.isHovered &&
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
dispatchHoverEndEvents(event, context, props, state);
state.isInHitSlop = true;
} else if (state.isHovered) {
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
dispatchHoverEndEvents(event, context, props, state);
state.isInHitSlop = true;
} else {
if (props.onHoverMove) {
const syntheticEvent = createHoverEvent(
'hovermove',
event.target,
props.onHoverMove,
);
context.dispatchEvent(syntheticEvent, {discrete: false});
}
}
}
}
break;
Expand Down
15 changes: 11 additions & 4 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {
ReactResponderEvent,
ReactResponderContext,
ReactResponderDispatchEventOptions,
} from 'shared/ReactTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';

Expand Down Expand Up @@ -130,13 +131,17 @@ function dispatchEvent(
state: PressState,
name: PressEventType,
listener: (e: Object) => void,
options?: ReactResponderDispatchEventOptions,
): void {
const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType;
const syntheticEvent = createPressEvent(name, target, listener, pointerType);
context.dispatchEvent(syntheticEvent, {
discrete: true,
});
context.dispatchEvent(
syntheticEvent,
options || {
discrete: true,
},
);
state.didDispatchEvent = true;
}

Expand Down Expand Up @@ -489,7 +494,9 @@ const PressResponder = {
if (isPressWithinResponderRegion(nativeEvent, state)) {
state.isPressWithinResponderRegion = true;
if (props.onPressMove) {
dispatchEvent(context, state, 'pressmove', props.onPressMove);
dispatchEvent(context, state, 'pressmove', props.onPressMove, {
discrete: false,
});
}
} else {
state.isPressWithinResponderRegion = false;
Expand Down
30 changes: 30 additions & 0 deletions packages/react-events/src/__tests__/Hover-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,36 @@ describe('Hover event responder', () => {
});
});

describe('onHoverMove', () => {
it('is called after "pointermove"', () => {
const onHoverMove = jest.fn();
const ref = React.createRef();
const element = (
<Hover onHoverMove={onHoverMove}>
<div ref={ref} />
</Hover>
);
ReactDOM.render(element, container);

ref.current.getBoundingClientRect = () => ({
top: 50,
left: 50,
bottom: 500,
right: 500,
});
ref.current.dispatchEvent(createPointerEvent('pointerover'));
ref.current.dispatchEvent(
createPointerEvent('pointermove', {pointerType: 'mouse'}),
);
ref.current.dispatchEvent(createPointerEvent('touchmove'));
ref.current.dispatchEvent(createPointerEvent('mousemove'));
expect(onHoverMove).toHaveBeenCalledTimes(1);
expect(onHoverMove).toHaveBeenCalledWith(
expect.objectContaining({type: 'hovermove'}),
);
});
});

it('expect displayName to show up for event component', () => {
expect(Hover.displayName).toBe('Hover');
});
Expand Down

0 comments on commit 4e59d4f

Please sign in to comment.