React events: add onHoverMove support (#15388)
Nicolas Gallagher committed
Apr 11, 2019 at 18:59 UTC
4e59d4f5d26a620a2c6e8804a589b227481a80aa
4 files changed
+111
-22
packages/react-events/README.md
+35
-5
@@ -28,7 +28,9 @@ const TextField = (props) => (
28
29
```js
30
// Types
31
-type FocusEvent = {}
31
+type FocusEvent = {
32
+ type: 'blur' | 'focus' | 'focuschange'
33
+}
34
```
35
36
### disabled: boolean
@@ -76,7 +78,10 @@ const Link = (props) => (
78
79
```js
80
// Types
79
-type HoverEvent = {}
81
+type HoverEvent = {
82
+ pointerType: 'mouse',
83
+ type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange'
84
+}
85
```
86
87
### delayHoverEnd: number
@@ -103,12 +108,25 @@ Called when the element changes hover state (i.e., after `onHoverStart` and
108
Called once the element is no longer hovered. It will be cancelled if the
109
pointer leaves the element before the `delayHoverStart` threshold is exceeded.
110
111
+### onHoverMove: (e: HoverEvent) => void
112
+
113
+Called when the pointer moves within the hit bounds of the element. `onHoverMove` is
114
+called immediately and doesn't wait for delayed `onHoverStart`.
115
+
116
### onHoverStart: (e: HoverEvent) => void
117
118
Called once the element is hovered. It will not be called if the pointer leaves
119
the element before the `delayHoverStart` threshold is exceeded. And it will not
120
be called more than once before `onHoverEnd` is called.
121
122
+### preventDefault: boolean = true
123
+
124
+Whether to `preventDefault()` native events.
125
+
126
+### stopPropagation: boolean = true
127
+
128
+Whether to `stopPropagation()` native events.
129
+
130
131
## Press
132
@@ -145,7 +163,10 @@ const Button = (props) => (
163
164
```js
165
// Types
148
-type PressEvent = {}
166
+type PressEvent = {
167
+ pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard',
168
+ type: 'press' | 'pressstart' | 'pressend' | 'presschange' | 'pressmove' | 'longpress' | 'longpresschange'
169
+}
170
171
type PressOffset = {
172
top: number,
@@ -210,8 +231,9 @@ called during a press.
231
232
### onPressMove: (e: PressEvent) => void
233
213
-Called when an active press moves within the hit bounds of the element. Never
214
-called for keyboard-initiated press events.
234
+Called when a press moves within the hit bounds of the element. `onPressMove` is
235
+called immediately and doesn't wait for delayed `onPressStart`. Never called for
236
+keyboard-initiated press events.
237
238
### onPressStart: (e: PressEvent) => void
239
@@ -225,3 +247,11 @@ Defines how far the pointer (while held down) may move outside the bounds of the
247
element before it is deactivated. Once deactivated, the pointer (still held
248
down) can be moved back within the bounds of the element to reactivate it.
249
Ensure you pass in a constant to reduce memory allocations.
250
+
251
+### preventDefault: boolean = true
252
+
253
+Whether to `preventDefault()` native events.
254
+
255
+### stopPropagation: boolean = true
256
+
257
+Whether to `stopPropagation()` native events.
packages/react-events/src/Hover.js
+35
-13
@@ -19,6 +19,7 @@ type HoverProps = {
19
delayHoverStart: number,
20
onHoverChange: boolean => void,
21
onHoverEnd: (e: HoverEvent) => void,
22
+ onHoverMove: (e: HoverEvent) => void,
23
onHoverStart: (e: HoverEvent) => void,
24
};
25
@@ -29,9 +30,10 @@ type HoverState = {
30
isTouched: boolean,
31
hoverStartTimeout: null | Symbol,
32
hoverEndTimeout: null | Symbol,
33
+ skipMouseAfterPointer: boolean,
34
};
35
34
-type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
36
+type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
37
38
type HoverEvent = {|
39
listener: HoverEvent => void,
@@ -51,7 +53,7 @@ const targetEventTypes = [
53
54
// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
55
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
54
- targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
56
+ targetEventTypes.push('touchstart', 'mouseover', 'mousemove', 'mouseout');
57
}
58
59
function createHoverEvent(
@@ -200,6 +202,7 @@ const HoverResponder = {
202
isTouched: false,
203
hoverStartTimeout: null,
204
hoverEndTimeout: null,
205
+ skipMouseAfterPointer: false,
206
};
207
},
208
onEvent(
@@ -228,6 +231,9 @@ const HoverResponder = {
231
state.isTouched = true;
232
return;
233
}
234
+ if (type === 'pointerover') {
235
+ state.skipMouseAfterPointer = true;
236
+ }
237
if (
238
context.isPositionWithinTouchHitTarget(
239
target.ownerDocument,
@@ -249,10 +255,16 @@ const HoverResponder = {
255
}
256
state.isInHitSlop = false;
257
state.isTouched = false;
258
+ state.skipMouseAfterPointer = false;
259
break;
260
}
261
255
- case 'pointermove': {
262
+ case 'pointermove':
263
+ case 'mousemove': {
264
+ if (type === 'mousemove' && state.skipMouseAfterPointer === true) {
265
+ return;
266
+ }
267
+
268
if (state.isHovered && !state.isTouched) {
269
if (state.isInHitSlop) {
270
if (
@@ -265,16 +277,26 @@ const HoverResponder = {
277
dispatchHoverStartEvents(event, context, props, state);
278
state.isInHitSlop = false;
279
}
268
- } else if (
269
- state.isHovered &&
270
- context.isPositionWithinTouchHitTarget(
271
- target.ownerDocument,
272
- (nativeEvent: any).x,
273
- (nativeEvent: any).y,
274
- )
275
- ) {
276
- dispatchHoverEndEvents(event, context, props, state);
277
- state.isInHitSlop = true;
280
+ } else if (state.isHovered) {
281
+ if (
282
+ context.isPositionWithinTouchHitTarget(
283
+ target.ownerDocument,
284
+ (nativeEvent: any).x,
285
+ (nativeEvent: any).y,
286
+ )
287
+ ) {
288
+ dispatchHoverEndEvents(event, context, props, state);
289
+ state.isInHitSlop = true;
290
+ } else {
291
+ if (props.onHoverMove) {
292
+ const syntheticEvent = createHoverEvent(
293
+ 'hovermove',
294
+ event.target,
295
+ props.onHoverMove,
296
+ );
297
+ context.dispatchEvent(syntheticEvent, {discrete: false});
298
+ }
299
+ }
300
}
301
}
302
break;
packages/react-events/src/Press.js
+11
-4
@@ -10,6 +10,7 @@
10
import type {
11
ReactResponderEvent,
12
ReactResponderContext,
13
+ ReactResponderDispatchEventOptions,
14
} from 'shared/ReactTypes';
15
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
16
@@ -130,13 +131,17 @@ function dispatchEvent(
131
state: PressState,
132
name: PressEventType,
133
listener: (e: Object) => void,
134
+ options?: ReactResponderDispatchEventOptions,
135
): void {
136
const target = ((state.pressTarget: any): Element | Document);
137
const pointerType = state.pointerType;
138
const syntheticEvent = createPressEvent(name, target, listener, pointerType);
137
- context.dispatchEvent(syntheticEvent, {
138
- discrete: true,
139
- });
139
+ context.dispatchEvent(
140
+ syntheticEvent,
141
+ options || {
142
+ discrete: true,
143
+ },
144
+ );
145
state.didDispatchEvent = true;
146
}
147
@@ -489,7 +494,9 @@ const PressResponder = {
494
if (isPressWithinResponderRegion(nativeEvent, state)) {
495
state.isPressWithinResponderRegion = true;
496
if (props.onPressMove) {
492
- dispatchEvent(context, state, 'pressmove', props.onPressMove);
497
+ dispatchEvent(context, state, 'pressmove', props.onPressMove, {
498
+ discrete: false,
499
+ });
500
}
501
} else {
502
state.isPressWithinResponderRegion = false;
packages/react-events/src/__tests__/Hover-test.internal.js
+30
@@ -331,6 +331,36 @@ describe('Hover event responder', () => {
331
});
332
});
333
334
+ describe('onHoverMove', () => {
335
+ it('is called after "pointermove"', () => {
336
+ const onHoverMove = jest.fn();
337
+ const ref = React.createRef();
338
+ const element = (
339
+ <Hover onHoverMove={onHoverMove}>
340
+ <div ref={ref} />
341
+ </Hover>
342
+ );
343
+ ReactDOM.render(element, container);
344
+
345
+ ref.current.getBoundingClientRect = () => ({
346
+ top: 50,
347
+ left: 50,
348
+ bottom: 500,
349
+ right: 500,
350
+ });
351
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
352
+ ref.current.dispatchEvent(
353
+ createPointerEvent('pointermove', {pointerType: 'mouse'}),
354
+ );
355
+ ref.current.dispatchEvent(createPointerEvent('touchmove'));
356
+ ref.current.dispatchEvent(createPointerEvent('mousemove'));
357
+ expect(onHoverMove).toHaveBeenCalledTimes(1);
358
+ expect(onHoverMove).toHaveBeenCalledWith(
359
+ expect.objectContaining({type: 'hovermove'}),
360
+ );
361
+ });
362
+ });
363
+
364
it('expect displayName to show up for event component', () => {
365
expect(Hover.displayName).toBe('Hover');
366
});