@samitouri / QOS-React-2 / commits / 4e59d4f5d2

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 });