mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
React events: add onHoverMove support (#15388)
This commit is contained in:
@@ -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.
|
||||
|
||||
Vendored
+35
-13
@@ -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;
|
||||
|
||||
Vendored
+11
-4
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user