Skip to content

React events: add onHoverMove support #15388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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