React events: add onHoverMove support (#15388)

This commit is contained in:
Nicolas Gallagher
2019-04-11 18:59:05 -07:00
committed by GitHub
parent cdfb06e38b
commit 4e59d4f5d2
4 changed files with 111 additions and 22 deletions
+35 -5
View File
@@ -28,7 +28,9 @@ const TextField = (props) => (
```js
// Types
type FocusEvent = {}
type FocusEvent = {
type: 'blur' | 'focus' | 'focuschange'
}
```
### disabled: boolean
@@ -76,7 +78,10 @@ const Link = (props) => (
```js
// Types
type HoverEvent = {}
type HoverEvent = {
pointerType: 'mouse',
type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange'
}
```
### delayHoverEnd: number
@@ -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
@@ -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,
@@ -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
@@ -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.
+35 -13
View File
@@ -19,6 +19,7 @@ type HoverProps = {
delayHoverStart: number,
onHoverChange: boolean => void,
onHoverEnd: (e: HoverEvent) => void,
onHoverMove: (e: HoverEvent) => void,
onHoverStart: (e: HoverEvent) => void,
};
@@ -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,
@@ -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(
@@ -200,6 +202,7 @@ const HoverResponder = {
isTouched: false,
hoverStartTimeout: null,
hoverEndTimeout: null,
skipMouseAfterPointer: false,
};
},
onEvent(
@@ -228,6 +231,9 @@ const HoverResponder = {
state.isTouched = true;
return;
}
if (type === 'pointerover') {
state.skipMouseAfterPointer = true;
}
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
@@ -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 (
@@ -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;
+11 -4
View File
@@ -10,6 +10,7 @@
import type {
ReactResponderEvent,
ReactResponderContext,
ReactResponderDispatchEventOptions,
} from 'shared/ReactTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
@@ -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;
}
@@ -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;
@@ -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');
});