@samitouri / QOS-React-2 / commits / 07a02fb80d

[react-events] Refactor unit tests for Hover (#16320)

**Problem** The existing responders listen to pointer events by default and add fallback events if PointerEvent is not supported. However, this complicates the responders and makes it easy to create a problematic unit test environment. jsdom doesn't support PointerEvent, which means that the responders end up listening to pointer events *and* fallback events in unit tests. This isn't a direct problem in production environments, because no browser will fire pointer events if they aren't supported. But in the unit test environment, we often dispatch event sequences taken from browsers that support pointer events. This means that what we're often testing is actually a (complex) scenario that cannot even occur in production: a responder that is listening to and receives both pointer events and fallback events. Not only does this risk making responders more complicated to implement but it could also hide bugs in implementations. **Response** Implement the responders so that they're only listening to *either* pointer events *or* fallback events, never both. This should make the default pointer events implementations significantly simpler and easier to test, as well as free to rely on the complete PointerEvents API. In the future it should also make DCE easier for target environments that are known to support PointerEvents, as we can use build tools with an equivalent of the runtime check. The fallback events (touch and mouse) need to coexist and be resilient to browser emulated events. Our unit tests should express a suite of high-level interactions that can be run in environments with and without PointerEvents support.

Nicolas Gallagher committed Aug 9, 2019 at 10:53 UTC 07a02fb80d99710443f23e30122bc1ffa8ea4f40
4 files changed +670 -532
packages/react-events/src/dom/Hover.js
+111 -96
@@ -10,6 +10,7 @@
10 import type {
11 ReactDOMResponderEvent,
12 ReactDOMResponderContext,
13 + PointerType,
14 } from 'shared/ReactDOMTypes';
15 import type {ReactEventResponderListener} from 'shared/ReactTypes';
16
@@ -29,15 +30,14 @@ type HoverState = {
30 hoverTarget: null | Element | Document,
31 isActiveHovered: boolean,
32 isHovered: boolean,
32 - isTouched: boolean,
33 - hoverStartTimeout: null | number,
34 - hoverEndTimeout: null | number,
35 - ignoreEmulatedMouseEvents: boolean,
33 + isTouched?: boolean,
34 + ignoreEmulatedMouseEvents?: boolean,
35 };
36
37 type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
38
39 type HoverEvent = {|
40 + pointerType: PointerType,
41 target: Element | Document,
42 type: HoverEventType,
43 timeStamp: number,
@@ -51,17 +51,8 @@ type HoverEvent = {|
51 y: null | number,
52 |};
53
54 -const targetEventTypes = [
55 - 'pointerover',
56 - 'pointermove',
57 - 'pointerout',
58 - 'pointercancel',
59 -];
60 -
61 -// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
62 -if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
63 - targetEventTypes.push('touchstart', 'mouseover', 'mousemove', 'mouseout');
64 -}
54 +const hasPointerEvents =
55 + typeof window !== 'undefined' && window.PointerEvent != null;
56
57 function isFunction(obj): boolean {
58 return typeof obj === 'function';
@@ -79,13 +70,16 @@ function createHoverEvent(
70 let pageY = null;
71 let screenX = null;
72 let screenY = null;
73 + let pointerType = '';
74
75 if (event) {
76 const nativeEvent = (event.nativeEvent: any);
77 + pointerType = event.pointerType;
78 ({clientX, clientY, pageX, pageY, screenX, screenY} = nativeEvent);
79 }
80
81 return {
82 + pointerType,
83 target,
84 type,
85 timeStamp: context.getTimeStamp(),
@@ -131,11 +125,6 @@ function dispatchHoverStartEvents(
125
126 state.isHovered = true;
127
134 - if (state.hoverEndTimeout !== null) {
135 - context.clearTimeout(state.hoverEndTimeout);
136 - state.hoverEndTimeout = null;
137 - }
138 -
128 if (!state.isActiveHovered) {
129 state.isActiveHovered = true;
130 const onHoverStart = props.onHoverStart;
@@ -152,6 +141,20 @@ function dispatchHoverStartEvents(
141 }
142 }
143
144 +function dispatchHoverMoveEvent(event, context, props, state) {
145 + const target = state.hoverTarget;
146 + const onHoverMove = props.onHoverMove;
147 + if (isFunction(onHoverMove)) {
148 + const syntheticEvent = createHoverEvent(
149 + event,
150 + context,
151 + 'hovermove',
152 + ((target: any): Element | Document),
153 + );
154 + context.dispatchEvent(syntheticEvent, onHoverMove, UserBlockingEvent);
155 + }
156 +}
157 +
158 function dispatchHoverEndEvents(
159 event: null | ReactDOMResponderEvent,
160 context: ReactDOMResponderContext,
@@ -170,11 +173,6 @@ function dispatchHoverEndEvents(
173
174 state.isHovered = false;
175
173 - if (state.hoverStartTimeout !== null) {
174 - context.clearTimeout(state.hoverStartTimeout);
175 - state.hoverStartTimeout = null;
176 - }
177 -
176 if (state.isActiveHovered) {
177 state.isActiveHovered = false;
178 const onHoverEnd = props.onHoverEnd;
@@ -189,7 +187,6 @@ function dispatchHoverEndEvents(
187 }
188 dispatchHoverChangeEvent(event, context, props, state);
189 state.hoverTarget = null;
192 - state.ignoreEmulatedMouseEvents = false;
190 state.isTouched = false;
191 }
192 }
@@ -204,24 +201,17 @@ function unmountResponder(
201 }
202 }
203
207 -function isEmulatedMouseEvent(event, state) {
208 - const {type} = event;
209 - return (
210 - state.ignoreEmulatedMouseEvents &&
211 - (type === 'mousemove' || type === 'mouseover' || type === 'mouseout')
212 - );
213 -}
214 -
204 const hoverResponderImpl = {
216 - targetEventTypes,
205 + targetEventTypes: [
206 + 'pointerover',
207 + 'pointermove',
208 + 'pointerout',
209 + 'pointercancel',
210 + ],
211 getInitialState() {
212 return {
213 isActiveHovered: false,
214 isHovered: false,
221 - isTouched: false,
222 - hoverStartTimeout: null,
223 - hoverEndTimeout: null,
224 - ignoreEmulatedMouseEvents: false,
215 };
216 },
217 allowMultipleHostChildren: false,
@@ -237,95 +227,120 @@ const hoverResponderImpl = {
227 if (props.disabled) {
228 if (state.isHovered) {
229 dispatchHoverEndEvents(event, context, props, state);
240 - state.ignoreEmulatedMouseEvents = false;
241 - }
242 - if (state.isTouched) {
243 - state.isTouched = false;
230 }
231 return;
232 }
233
234 switch (type) {
235 // START
250 - case 'pointerover':
251 - case 'mouseover':
252 - case 'touchstart': {
253 - if (!state.isHovered) {
254 - // Prevent hover events for touch
255 - if (state.isTouched || pointerType === 'touch') {
256 - state.isTouched = true;
257 - return;
258 - }
259 -
260 - // Prevent hover events for emulated events
261 - if (isEmulatedMouseEvent(event, state)) {
262 - return;
263 - }
236 + case 'pointerover': {
237 + if (!state.isHovered && pointerType !== 'touch') {
238 state.hoverTarget = event.responderTarget;
265 - state.ignoreEmulatedMouseEvents = true;
239 dispatchHoverStartEvents(event, context, props, state);
240 }
268 - return;
241 + break;
242 }
243
244 // MOVE
272 - case 'pointermove':
273 - case 'mousemove': {
274 - if (state.isHovered && !isEmulatedMouseEvent(event, state)) {
275 - const onHoverMove = props.onHoverMove;
276 - if (state.hoverTarget !== null && isFunction(onHoverMove)) {
277 - const syntheticEvent = createHoverEvent(
278 - event,
279 - context,
280 - 'hovermove',
281 - state.hoverTarget,
282 - );
283 - context.dispatchEvent(
284 - syntheticEvent,
285 - onHoverMove,
286 - UserBlockingEvent,
287 - );
288 - }
245 + case 'pointermove': {
246 + if (state.isHovered && state.hoverTarget !== null) {
247 + dispatchHoverMoveEvent(event, context, props, state);
248 }
290 - return;
249 + break;
250 }
251
252 // END
253 case 'pointerout':
295 - case 'pointercancel':
296 - case 'mouseout':
297 - case 'touchcancel':
298 - case 'touchend': {
254 + case 'pointercancel': {
255 if (state.isHovered) {
256 dispatchHoverEndEvents(event, context, props, state);
301 - state.ignoreEmulatedMouseEvents = false;
257 }
303 - if (state.isTouched) {
304 - state.isTouched = false;
305 - }
306 - return;
258 + break;
259 }
260 }
261 },
310 - onUnmount(
311 - context: ReactDOMResponderContext,
312 - props: HoverProps,
313 - state: HoverState,
314 - ) {
315 - unmountResponder(context, props, state);
262 + onUnmount: unmountResponder,
263 + onOwnershipChange: unmountResponder,
264 +};
265 +
266 +const hoverResponderFallbackImpl = {
267 + targetEventTypes: ['mouseover', 'mousemove', 'mouseout', 'touchstart'],
268 + getInitialState() {
269 + return {
270 + isActiveHovered: false,
271 + isHovered: false,
272 + isTouched: false,
273 + ignoreEmulatedMouseEvents: false,
274 + };
275 },
317 - onOwnershipChange(
276 + allowMultipleHostChildren: false,
277 + allowEventHooks: true,
278 + onEvent(
279 + event: ReactDOMResponderEvent,
280 context: ReactDOMResponderContext,
281 props: HoverProps,
282 state: HoverState,
321 - ) {
322 - unmountResponder(context, props, state);
283 + ): void {
284 + const {type} = event;
285 +
286 + if (props.disabled) {
287 + if (state.isHovered) {
288 + dispatchHoverEndEvents(event, context, props, state);
289 + state.ignoreEmulatedMouseEvents = false;
290 + }
291 + state.isTouched = false;
292 + return;
293 + }
294 +
295 + switch (type) {
296 + // START
297 + case 'mouseover': {
298 + if (!state.isHovered && !state.ignoreEmulatedMouseEvents) {
299 + state.hoverTarget = event.responderTarget;
300 + dispatchHoverStartEvents(event, context, props, state);
301 + }
302 + break;
303 + }
304 +
305 + // MOVE
306 + case 'mousemove': {
307 + if (
308 + state.isHovered &&
309 + state.hoverTarget !== null &&
310 + !state.ignoreEmulatedMouseEvents
311 + ) {
312 + dispatchHoverMoveEvent(event, context, props, state);
313 + } else if (!state.isHovered && type === 'mousemove') {
314 + state.ignoreEmulatedMouseEvents = false;
315 + state.isTouched = false;
316 + }
317 + break;
318 + }
319 +
320 + // END
321 + case 'mouseout': {
322 + if (state.isHovered) {
323 + dispatchHoverEndEvents(event, context, props, state);
324 + }
325 + break;
326 + }
327 +
328 + case 'touchstart': {
329 + if (!state.isHovered) {
330 + state.isTouched = true;
331 + state.ignoreEmulatedMouseEvents = true;
332 + }
333 + break;
334 + }
335 + }
336 },
337 + onUnmount: unmountResponder,
338 + onOwnershipChange: unmountResponder,
339 };
340
341 export const HoverResponder = React.unstable_createResponder(
342 'Hover',
328 - hoverResponderImpl,
343 + hasPointerEvents ? hoverResponderImpl : hoverResponderFallbackImpl,
344 );
345
346 export function useHoverResponder(
packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js
+132 -164
@@ -9,24 +9,16 @@
9
10 'use strict';
11
12 +import {createEvent, platform, setPointerEvent} from '../test-utils';
13 +
14 let React;
15 let ReactFeatureFlags;
16 let ReactDOM;
17 let useContextMenuResponder;
18
17 -function createEvent(type, data) {
18 - const event = document.createEvent('CustomEvent');
19 - event.initCustomEvent(type, true, true);
20 - if (data != null) {
21 - Object.entries(data).forEach(([key, value]) => {
22 - event[key] = value;
23 - });
24 - }
25 - return event;
26 -}
27 -
28 -function init(hasPointerEvents) {
29 - global.PointerEvents = hasPointerEvents ? function() {} : undefined;
19 +function initializeModules(hasPointerEvents) {
20 + setPointerEvent(hasPointerEvents);
21 + jest.resetModules();
22 ReactFeatureFlags = require('shared/ReactFeatureFlags');
23 ReactFeatureFlags.enableFlareAPI = true;
24 React = require('react');
@@ -35,29 +27,6 @@ function init(hasPointerEvents) {
27 .useContextMenuResponder;
28 }
29
38 -const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
39 -function setPlatform(platform: 'mac' | 'windows') {
40 - jest.resetModules();
41 - switch (platform) {
42 - case 'mac': {
43 - platformGetter.mockReturnValue('MacIntel');
44 - break;
45 - }
46 - case 'windows': {
47 - platformGetter.mockReturnValue('Win32');
48 - break;
49 - }
50 - default: {
51 - break;
52 - }
53 - }
54 - init();
55 -}
56 -
57 -function clearPlatform() {
58 - platformGetter.mockClear();
59 -}
60 -
30 function dispatchContextMenuEvents(ref, options) {
31 const preventDefault = options.preventDefault || function() {};
32 const variant = (options.variant: 'mouse' | 'touch' | 'modified');
@@ -76,7 +45,7 @@ function dispatchContextMenuEvents(ref, options) {
45 createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
46 );
47 dispatchEvent(createEvent('mousedown', {button: 0}));
79 - if (global.navigator.platform === 'MacIntel') {
48 + if (platform.get() === 'mac') {
49 dispatchEvent(
50 createEvent('contextmenu', {button: 0, ctrlKey: true, preventDefault}),
51 );
@@ -97,144 +66,143 @@ function dispatchContextMenuEvents(ref, options) {
66 }
67
68 const forcePointerEvents = true;
69 +const table = [[forcePointerEvents], [!forcePointerEvents]];
70 +
71 +describe.each(table)('ContextMenu responder', hasPointerEvents => {
72 + let container;
73 +
74 + beforeEach(() => {
75 + initializeModules(hasPointerEvents);
76 + container = document.createElement('div');
77 + document.body.appendChild(container);
78 + });
79 +
80 + afterEach(() => {
81 + ReactDOM.render(null, container);
82 + document.body.removeChild(container);
83 + container = null;
84 + });
85 +
86 + describe('all platforms', () => {
87 + it('mouse right-click', () => {
88 + const onContextMenu = jest.fn();
89 + const preventDefault = jest.fn();
90 + const ref = React.createRef();
91 + const Component = () => {
92 + const listener = useContextMenuResponder({onContextMenu});
93 + return <div ref={ref} listeners={listener} />;
94 + };
95 + ReactDOM.render(<Component />, container);
96 +
97 + dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
98 + expect(preventDefault).toHaveBeenCalledTimes(1);
99 + expect(onContextMenu).toHaveBeenCalledTimes(1);
100 + expect(onContextMenu).toHaveBeenCalledWith(
101 + expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
102 + );
103 + });
104 +
105 + it('touch long-press', () => {
106 + const onContextMenu = jest.fn();
107 + const preventDefault = jest.fn();
108 + const ref = React.createRef();
109 + const Component = () => {
110 + const listener = useContextMenuResponder({onContextMenu});
111 + return <div ref={ref} listeners={listener} />;
112 + };
113 + ReactDOM.render(<Component />, container);
114 +
115 + dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
116 + expect(preventDefault).toHaveBeenCalledTimes(1);
117 + expect(onContextMenu).toHaveBeenCalledTimes(1);
118 + expect(onContextMenu).toHaveBeenCalledWith(
119 + expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
120 + );
121 + });
122 +
123 + it('"disabled" is true', () => {
124 + const onContextMenu = jest.fn();
125 + const ref = React.createRef();
126 + const Component = () => {
127 + const listener = useContextMenuResponder({
128 + onContextMenu,
129 + disabled: true,
130 + });
131 + return <div ref={ref} listeners={listener} />;
132 + };
133 + ReactDOM.render(<Component />, container);
134 +
135 + dispatchContextMenuEvents(ref, 'mouse');
136 + expect(onContextMenu).toHaveBeenCalledTimes(0);
137 + });
138
101 -describe.each([[forcePointerEvents], [!forcePointerEvents]])(
102 - 'ContextMenu responder',
103 - hasPointerEvents => {
104 - let container;
139 + it('"preventDefault" is false', () => {
140 + const preventDefault = jest.fn();
141 + const onContextMenu = jest.fn();
142 + const ref = React.createRef();
143 + const Component = () => {
144 + const listener = useContextMenuResponder({
145 + onContextMenu,
146 + preventDefault: false,
147 + });
148 + return <div ref={ref} listeners={listener} />;
149 + };
150 + ReactDOM.render(<Component />, container);
151 +
152 + dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
153 + expect(preventDefault).toHaveBeenCalledTimes(0);
154 + expect(onContextMenu).toHaveBeenCalledTimes(1);
155 + });
156 + });
157
158 + describe('mac platform', () => {
159 beforeEach(() => {
160 + platform.set('mac');
161 jest.resetModules();
108 - init(hasPointerEvents);
109 - container = document.createElement('div');
110 - document.body.appendChild(container);
162 });
163
164 afterEach(() => {
114 - ReactDOM.render(null, container);
115 - document.body.removeChild(container);
116 - container = null;
165 + platform.clear();
166 });
167
119 - describe('all platforms', () => {
120 - it('mouse right-click', () => {
121 - const onContextMenu = jest.fn();
122 - const preventDefault = jest.fn();
123 - const ref = React.createRef();
124 - const Component = () => {
125 - const listener = useContextMenuResponder({onContextMenu});
126 - return <div ref={ref} listeners={listener} />;
127 - };
128 - ReactDOM.render(<Component />, container);
129 -
130 - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
131 - expect(preventDefault).toHaveBeenCalledTimes(1);
132 - expect(onContextMenu).toHaveBeenCalledTimes(1);
133 - expect(onContextMenu).toHaveBeenCalledWith(
134 - expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
135 - );
136 - });
137 -
138 - it('touch long-press', () => {
139 - const onContextMenu = jest.fn();
140 - const preventDefault = jest.fn();
141 - const ref = React.createRef();
142 - const Component = () => {
143 - const listener = useContextMenuResponder({onContextMenu});
144 - return <div ref={ref} listeners={listener} />;
145 - };
146 - ReactDOM.render(<Component />, container);
147 -
148 - dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
149 - expect(preventDefault).toHaveBeenCalledTimes(1);
150 - expect(onContextMenu).toHaveBeenCalledTimes(1);
151 - expect(onContextMenu).toHaveBeenCalledWith(
152 - expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
153 - );
154 - });
155 -
156 - it('"disabled" is true', () => {
157 - const onContextMenu = jest.fn();
158 - const ref = React.createRef();
159 - const Component = () => {
160 - const listener = useContextMenuResponder({
161 - onContextMenu,
162 - disabled: true,
163 - });
164 - return <div ref={ref} listeners={listener} />;
165 - };
166 - ReactDOM.render(<Component />, container);
167 -
168 - dispatchContextMenuEvents(ref, 'mouse');
169 - expect(onContextMenu).toHaveBeenCalledTimes(0);
170 - });
171 -
172 - it('"preventDefault" is false', () => {
173 - const preventDefault = jest.fn();
174 - const onContextMenu = jest.fn();
175 - const ref = React.createRef();
176 - const Component = () => {
177 - const listener = useContextMenuResponder({
178 - onContextMenu,
179 - preventDefault: false,
180 - });
181 - return <div ref={ref} listeners={listener} />;
182 - };
183 - ReactDOM.render(<Component />, container);
184 -
185 - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
186 - expect(preventDefault).toHaveBeenCalledTimes(0);
187 - expect(onContextMenu).toHaveBeenCalledTimes(1);
188 - });
168 + it('mouse modified left-click', () => {
169 + const onContextMenu = jest.fn();
170 + const ref = React.createRef();
171 + const Component = () => {
172 + const listener = useContextMenuResponder({onContextMenu});
173 + return <div ref={ref} listeners={listener} />;
174 + };
175 + ReactDOM.render(<Component />, container);
176 +
177 + dispatchContextMenuEvents(ref, {variant: 'modified'});
178 + expect(onContextMenu).toHaveBeenCalledTimes(1);
179 + expect(onContextMenu).toHaveBeenCalledWith(
180 + expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
181 + );
182 });
183 + });
184
191 - describe('mac platform', () => {
192 - beforeEach(() => {
193 - setPlatform('mac');
194 - });
195 -
196 - afterEach(() => {
197 - clearPlatform();
198 - });
199 -
200 - it('mouse modified left-click', () => {
201 - const onContextMenu = jest.fn();
202 - const ref = React.createRef();
203 - const Component = () => {
204 - const listener = useContextMenuResponder({onContextMenu});
205 - return <div ref={ref} listeners={listener} />;
206 - };
207 - ReactDOM.render(<Component />, container);
208 -
209 - dispatchContextMenuEvents(ref, {variant: 'modified'});
210 - expect(onContextMenu).toHaveBeenCalledTimes(1);
211 - expect(onContextMenu).toHaveBeenCalledWith(
212 - expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
213 - );
214 - });
185 + describe('windows platform', () => {
186 + beforeEach(() => {
187 + platform.set('windows');
188 + jest.resetModules();
189 + });
190 +
191 + afterEach(() => {
192 + platform.clear();
193 });
194
217 - describe('windows platform', () => {
218 - beforeEach(() => {
219 - setPlatform('windows');
220 - });
221 -
222 - afterEach(() => {
223 - clearPlatform();
224 - });
225 -
226 - it('mouse modified left-click', () => {
227 - const onContextMenu = jest.fn();
228 - const ref = React.createRef();
229 - const Component = () => {
230 - const listener = useContextMenuResponder({onContextMenu});
231 - return <div ref={ref} listeners={listener} />;
232 - };
233 - ReactDOM.render(<Component />, container);
234 -
235 - dispatchContextMenuEvents(ref, {variant: 'modified'});
236 - expect(onContextMenu).toHaveBeenCalledTimes(0);
237 - });
195 + it('mouse modified left-click', () => {
196 + const onContextMenu = jest.fn();
197 + const ref = React.createRef();
198 + const Component = () => {
199 + const listener = useContextMenuResponder({onContextMenu});
200 + return <div ref={ref} listeners={listener} />;
201 + };
202 + ReactDOM.render(<Component />, container);
203 +
204 + dispatchContextMenuEvents(ref, {variant: 'modified'});
205 + expect(onContextMenu).toHaveBeenCalledTimes(0);
206 });
239 - },
240 -);
207 + });
208 +});
packages/react-events/src/dom/__tests__/Hover-test.internal.js
+116 -272
@@ -9,50 +9,41 @@
9
10 'use strict';
11
12 +import {
13 + dispatchPointerCancel,
14 + dispatchPointerHoverEnter,
15 + dispatchPointerHoverExit,
16 + dispatchPointerHoverMove,
17 + dispatchTouchTap,
18 + setPointerEvent,
19 +} from '../test-utils';
20 +
21 let React;
22 let ReactFeatureFlags;
23 let ReactDOM;
15 -let TestUtils;
16 -let Scheduler;
24 let HoverResponder;
25 let useHoverResponder;
26
20 -const createEvent = (type, data) => {
21 - const event = document.createEvent('CustomEvent');
22 - event.initCustomEvent(type, true, true);
23 - if (data != null) {
24 - Object.entries(data).forEach(([key, value]) => {
25 - event[key] = value;
26 - });
27 - }
28 - return event;
29 -};
30 -
31 -function createTouchEvent(type, id, data) {
32 - return createEvent(type, {
33 - changedTouches: [
34 - {
35 - ...data,
36 - identifier: id,
37 - },
38 - ],
39 - });
27 +function initializeModules(hasPointerEvents) {
28 + jest.resetModules();
29 + setPointerEvent(hasPointerEvents);
30 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
31 + ReactFeatureFlags.enableFlareAPI = true;
32 + ReactFeatureFlags.enableUserBlockingEvents = true;
33 + React = require('react');
34 + ReactDOM = require('react-dom');
35 + HoverResponder = require('react-events/hover').HoverResponder;
36 + useHoverResponder = require('react-events/hover').useHoverResponder;
37 }
38
42 -describe('Hover event responder', () => {
39 +const forcePointerEvents = true;
40 +const table = [[forcePointerEvents], [!forcePointerEvents]];
41 +
42 +describe.each(table)('Hover responder', hasPointerEvents => {
43 let container;
44
45 beforeEach(() => {
46 - jest.resetModules();
47 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
48 - ReactFeatureFlags.enableFlareAPI = true;
49 - ReactFeatureFlags.enableUserBlockingEvents = true;
50 - React = require('react');
51 - ReactDOM = require('react-dom');
52 - TestUtils = require('react-dom/test-utils');
53 - Scheduler = require('scheduler');
54 - HoverResponder = require('react-events/hover').HoverResponder;
55 - useHoverResponder = require('react-events/hover').useHoverResponder;
46 + initializeModules(hasPointerEvents);
47 container = document.createElement('div');
48 document.body.appendChild(container);
49 });
@@ -64,17 +55,21 @@ describe('Hover event responder', () => {
55 });
56
57 describe('disabled', () => {
67 - let onHoverStart, onHoverEnd, ref;
58 + let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref;
59
60 beforeEach(() => {
61 + onHoverChange = jest.fn();
62 onHoverStart = jest.fn();
63 + onHoverMove = jest.fn();
64 onHoverEnd = jest.fn();
65 ref = React.createRef();
66 const Component = () => {
67 const listener = useHoverResponder({
68 disabled: true,
76 - onHoverStart: onHoverStart,
77 - onHoverEnd: onHoverEnd,
69 + onHoverChange,
70 + onHoverStart,
71 + onHoverMove,
72 + onHoverEnd,
73 });
74 return <div ref={ref} listeners={listener} />;
75 };
@@ -82,9 +77,11 @@ describe('Hover event responder', () => {
77 });
78
79 it('prevents custom events being dispatched', () => {
85 - ref.current.dispatchEvent(createEvent('pointerover'));
86 - ref.current.dispatchEvent(createEvent('pointerout'));
80 + dispatchPointerHoverEnter(ref);
81 + dispatchPointerHoverExit(ref);
82 + expect(onHoverChange).not.toBeCalled();
83 expect(onHoverStart).not.toBeCalled();
84 + expect(onHoverMove).not.toBeCalled();
85 expect(onHoverEnd).not.toBeCalled();
86 });
87 });
@@ -104,68 +101,21 @@ describe('Hover event responder', () => {
101 ReactDOM.render(<Component />, container);
102 });
103
107 - it('is called after "pointerover" event', () => {
108 - ref.current.dispatchEvent(createEvent('pointerover'));
104 + it('is called for mouse pointers', () => {
105 + dispatchPointerHoverEnter(ref);
106 expect(onHoverStart).toHaveBeenCalledTimes(1);
107 });
108
112 - it('is not called if "pointerover" pointerType is touch', () => {
113 - const event = createEvent('pointerover', {pointerType: 'touch'});
114 - ref.current.dispatchEvent(event);
109 + it('is not called for touch pointers', () => {
110 + dispatchTouchTap(ref);
111 expect(onHoverStart).not.toBeCalled();
112 });
113
118 - it('is called if valid "pointerover" follows touch', () => {
119 - ref.current.dispatchEvent(
120 - createEvent('pointerover', {pointerType: 'touch'}),
121 - );
122 - ref.current.dispatchEvent(
123 - createEvent('pointerout', {pointerType: 'touch'}),
124 - );
125 - ref.current.dispatchEvent(
126 - createEvent('pointerover', {pointerType: 'mouse'}),
127 - );
128 - expect(onHoverStart).toHaveBeenCalledTimes(1);
129 - });
130 -
131 - it('ignores browser emulated "mouseover" event', () => {
132 - ref.current.dispatchEvent(createEvent('pointerover'));
133 - ref.current.dispatchEvent(
134 - createEvent('mouseover', {
135 - button: 0,
136 - }),
137 - );
114 + it('is called if a mouse pointer is used after a touch pointer', () => {
115 + dispatchTouchTap(ref);
116 + dispatchPointerHoverEnter(ref);
117 expect(onHoverStart).toHaveBeenCalledTimes(1);
118 });
140 -
141 - // No PointerEvent fallbacks
142 - it('is called after "mouseover" event', () => {
143 - ref.current.dispatchEvent(
144 - createEvent('mouseover', {
145 - button: 0,
146 - }),
147 - );
148 - expect(onHoverStart).toHaveBeenCalledTimes(1);
149 - });
150 -
151 - it('is not called after "touchstart"', () => {
152 - ref.current.dispatchEvent(
153 - createTouchEvent('touchstart', 0, {
154 - target: ref.current,
155 - }),
156 - );
157 - ref.current.dispatchEvent(
158 - createTouchEvent('touchend', 0, {
159 - target: ref.current,
160 - }),
161 - );
162 - ref.current.dispatchEvent(
163 - createEvent('mouseover', {
164 - button: 0,
165 - }),
166 - );
167 - expect(onHoverStart).not.toBeCalled();
168 - });
119 });
120
121 describe('onHoverChange', () => {
@@ -183,58 +133,18 @@ describe('Hover event responder', () => {
133 ReactDOM.render(<Component />, container);
134 });
135
186 - it('is called after "pointerover" and "pointerout" events', () => {
187 - ref.current.dispatchEvent(createEvent('pointerover'));
188 - expect(onHoverChange).toHaveBeenCalledTimes(1);
189 - expect(onHoverChange).toHaveBeenCalledWith(true);
190 - ref.current.dispatchEvent(createEvent('pointerout'));
191 - expect(onHoverChange).toHaveBeenCalledTimes(2);
192 - expect(onHoverChange).toHaveBeenCalledWith(false);
193 - });
194 -
195 - // No PointerEvent fallbacks
196 - it('is called after "mouseover" and "mouseout" events', () => {
197 - ref.current.dispatchEvent(createEvent('mouseover'));
136 + it('is called for mouse pointers', () => {
137 + dispatchPointerHoverEnter(ref);
138 expect(onHoverChange).toHaveBeenCalledTimes(1);
139 expect(onHoverChange).toHaveBeenCalledWith(true);
200 - ref.current.dispatchEvent(createEvent('mouseout'));
140 + dispatchPointerHoverExit(ref);
141 expect(onHoverChange).toHaveBeenCalledTimes(2);
142 expect(onHoverChange).toHaveBeenCalledWith(false);
143 });
144
205 - it('should be user-blocking but not discrete', async () => {
206 - const {act} = TestUtils;
207 - const {useState} = React;
208 -
209 - const newContainer = document.createElement('div');
210 - document.body.appendChild(newContainer);
211 - const root = ReactDOM.unstable_createRoot(newContainer);
212 -
213 - const target = React.createRef(null);
214 - function Foo() {
215 - const [isHover, setHover] = useState(false);
216 - const listener = useHoverResponder({
217 - onHoverChange: setHover,
218 - });
219 - return (
220 - <div ref={target} listeners={listener}>
221 - {isHover ? 'hovered' : 'not hovered'}
222 - </div>
223 - );
224 - }
225 -
226 - await act(async () => {
227 - root.render(<Foo />);
228 - });
229 - expect(newContainer.textContent).toEqual('not hovered');
230 -
231 - await act(async () => {
232 - target.current.dispatchEvent(createEvent('mouseover'));
233 -
234 - // 3s should be enough to expire the updates
235 - Scheduler.unstable_advanceTime(3000);
236 - expect(newContainer.textContent).toEqual('hovered');
237 - });
145 + it('is not called for touch pointers', () => {
146 + dispatchTouchTap(ref);
147 + expect(onHoverChange).not.toBeCalled();
148 });
149 });
150
@@ -253,64 +163,35 @@ describe('Hover event responder', () => {
163 ReactDOM.render(<Component />, container);
164 });
165
256 - it('is called after "pointerout" event', () => {
257 - ref.current.dispatchEvent(createEvent('pointerover'));
258 - ref.current.dispatchEvent(createEvent('pointerout'));
166 + it('is called for mouse pointers', () => {
167 + dispatchPointerHoverEnter(ref);
168 + dispatchPointerHoverExit(ref);
169 expect(onHoverEnd).toHaveBeenCalledTimes(1);
170 });
171
262 - it('is not called if "pointerover" pointerType is touch', () => {
263 - const event = createEvent('pointerover');
264 - event.pointerType = 'touch';
265 - ref.current.dispatchEvent(event);
266 - ref.current.dispatchEvent(createEvent('pointerout'));
267 - expect(onHoverEnd).not.toBeCalled();
268 - });
269 -
270 - it('ignores browser emulated "mouseout" event', () => {
271 - ref.current.dispatchEvent(createEvent('pointerover'));
272 - ref.current.dispatchEvent(createEvent('pointerout'));
273 - ref.current.dispatchEvent(createEvent('mouseout'));
274 - expect(onHoverEnd).toHaveBeenCalledTimes(1);
275 - });
276 -
277 - it('is called after "pointercancel" event', () => {
278 - ref.current.dispatchEvent(createEvent('pointerover'));
279 - ref.current.dispatchEvent(createEvent('pointercancel'));
280 - expect(onHoverEnd).toHaveBeenCalledTimes(1);
281 - });
282 -
283 - it('is not called again after "pointercancel" event if it follows "pointerout"', () => {
284 - ref.current.dispatchEvent(createEvent('pointerover'));
285 - ref.current.dispatchEvent(createEvent('pointerout'));
286 - ref.current.dispatchEvent(createEvent('pointercancel'));
287 - expect(onHoverEnd).toHaveBeenCalledTimes(1);
288 - });
172 + if (hasPointerEvents) {
173 + it('is called once for cancelled mouse pointers', () => {
174 + dispatchPointerHoverEnter(ref);
175 + dispatchPointerCancel(ref);
176 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
177 +
178 + // only called once if cancel follows exit
179 + onHoverEnd.mockReset();
180 + dispatchPointerHoverEnter(ref);
181 + dispatchPointerHoverExit(ref);
182 + dispatchPointerCancel(ref);
183 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
184 + });
185 + }
186
290 - // No PointerEvent fallbacks
291 - it('is called after "mouseout" event', () => {
292 - ref.current.dispatchEvent(createEvent('mouseover'));
293 - ref.current.dispatchEvent(createEvent('mouseout'));
294 - expect(onHoverEnd).toHaveBeenCalledTimes(1);
295 - });
296 - it('is not called after "touchend"', () => {
297 - ref.current.dispatchEvent(
298 - createTouchEvent('touchstart', 0, {
299 - target: ref.current,
300 - }),
301 - );
302 - ref.current.dispatchEvent(
303 - createTouchEvent('touchend', 0, {
304 - target: ref.current,
305 - }),
306 - );
307 - ref.current.dispatchEvent(createEvent('mouseout'));
187 + it('is not called for touch pointers', () => {
188 + dispatchTouchTap(ref);
189 expect(onHoverEnd).not.toBeCalled();
190 });
191 });
192
193 describe('onHoverMove', () => {
313 - it('is called after "pointermove"', () => {
194 + it('is called after the active pointer moves"', () => {
195 const onHoverMove = jest.fn();
196 const ref = React.createRef();
197 const Component = () => {
@@ -320,20 +201,9 @@ describe('Hover event responder', () => {
201 return <div ref={ref} listeners={listener} />;
202 };
203 ReactDOM.render(<Component />, container);
323 -
324 - ref.current.getBoundingClientRect = () => ({
325 - top: 50,
326 - left: 50,
327 - bottom: 500,
328 - right: 500,
329 - });
330 - ref.current.dispatchEvent(createEvent('pointerover'));
331 - ref.current.dispatchEvent(
332 - createEvent('pointermove', {pointerType: 'mouse'}),
333 - );
334 - ref.current.dispatchEvent(createEvent('touchmove'));
335 - ref.current.dispatchEvent(createEvent('mousemove'));
336 - expect(onHoverMove).toHaveBeenCalledTimes(1);
204 + dispatchPointerHoverEnter(ref);
205 + dispatchPointerHoverMove(ref, {from: {x: 0, y: 0}, to: {x: 1, y: 1}});
206 + expect(onHoverMove).toHaveBeenCalledTimes(2);
207 expect(onHoverMove).toHaveBeenCalledWith(
208 expect.objectContaining({type: 'hovermove'}),
209 );
@@ -372,18 +242,13 @@ describe('Hover event responder', () => {
242 };
243 ReactDOM.render(<Outer />, container);
244
375 - outerRef.current.dispatchEvent(createEvent('pointerover'));
376 - outerRef.current.dispatchEvent(
377 - createEvent('pointerout', {relatedTarget: innerRef.current}),
378 - );
379 - innerRef.current.dispatchEvent(createEvent('pointerover'));
380 - innerRef.current.dispatchEvent(
381 - createEvent('pointerout', {relatedTarget: outerRef.current}),
382 - );
383 - outerRef.current.dispatchEvent(
384 - createEvent('pointerover', {relatedTarget: innerRef.current}),
385 - );
386 - outerRef.current.dispatchEvent(createEvent('pointerout'));
245 + dispatchPointerHoverEnter(outerRef, {relatedTarget: container});
246 + dispatchPointerHoverExit(outerRef, {relatedTarget: innerRef.current});
247 + dispatchPointerHoverEnter(innerRef, {relatedTarget: outerRef.current});
248 + dispatchPointerHoverExit(innerRef, {relatedTarget: outerRef.current});
249 + dispatchPointerHoverEnter(outerRef, {relatedTarget: innerRef.current});
250 + dispatchPointerHoverExit(outerRef, {relatedTarget: container});
251 +
252 expect(events).toEqual([
253 'outer: onHoverStart',
254 'outer: onHoverChange',
@@ -411,10 +276,10 @@ describe('Hover event responder', () => {
276 const eventLog = [];
277 const logEvent = event => {
278 const propertiesWeCareAbout = {
279 + x: event.x,
280 + y: event.y,
281 pageX: event.pageX,
282 pageY: event.pageY,
416 - screenX: event.screenX,
417 - screenY: event.screenY,
283 clientX: event.clientX,
284 clientY: event.clientY,
285 pointerType: event.pointerType,
@@ -435,79 +300,58 @@ describe('Hover event responder', () => {
300 };
301 ReactDOM.render(<Component />, container);
302
438 - ref.current.getBoundingClientRect = () => ({
439 - top: 10,
440 - left: 10,
441 - bottom: 20,
442 - right: 20,
443 - });
303 + dispatchPointerHoverEnter(ref, {x: 10, y: 10});
304 + dispatchPointerHoverMove(ref, {from: {x: 10, y: 10}, to: {x: 20, y: 20}});
305 + dispatchPointerHoverExit(ref, {x: 20, y: 20});
306
445 - ref.current.dispatchEvent(
446 - createEvent('pointerover', {
447 - pointerType: 'mouse',
448 - pageX: 15,
449 - pageY: 16,
450 - screenX: 20,
451 - screenY: 21,
452 - clientX: 30,
453 - clientY: 31,
454 - }),
455 - );
456 - ref.current.dispatchEvent(
457 - createEvent('pointermove', {
458 - pointerType: 'mouse',
459 - pageX: 16,
460 - pageY: 17,
461 - screenX: 21,
462 - screenY: 22,
463 - clientX: 31,
464 - clientY: 32,
465 - }),
466 - );
467 - ref.current.dispatchEvent(
468 - createEvent('pointerout', {
469 - pointerType: 'mouse',
470 - pageX: 17,
471 - pageY: 18,
472 - screenX: 22,
473 - screenY: 23,
474 - clientX: 32,
475 - clientY: 33,
476 - }),
477 - );
307 expect(eventLog).toEqual([
308 {
480 - pageX: 15,
481 - pageY: 16,
482 - screenX: 20,
483 - screenY: 21,
484 - clientX: 30,
485 - clientY: 31,
309 + x: 10,
310 + y: 10,
311 + pageX: 10,
312 + pageY: 10,
313 + clientX: 10,
314 + clientY: 10,
315 target: ref.current,
316 timeStamp: timeStamps[0],
317 type: 'hoverstart',
318 + pointerType: 'mouse',
319 },
320 {
491 - pageX: 16,
492 - pageY: 17,
493 - screenX: 21,
494 - screenY: 22,
495 - clientX: 31,
496 - clientY: 32,
321 + x: 10,
322 + y: 10,
323 + pageX: 10,
324 + pageY: 10,
325 + clientX: 10,
326 + clientY: 10,
327 target: ref.current,
328 timeStamp: timeStamps[1],
329 type: 'hovermove',
330 + pointerType: 'mouse',
331 },
332 {
502 - pageX: 17,
503 - pageY: 18,
504 - screenX: 22,
505 - screenY: 23,
506 - clientX: 32,
507 - clientY: 33,
333 + x: 20,
334 + y: 20,
335 + pageX: 20,
336 + pageY: 20,
337 + clientX: 20,
338 + clientY: 20,
339 target: ref.current,
340 timeStamp: timeStamps[2],
341 + type: 'hovermove',
342 + pointerType: 'mouse',
343 + },
344 + {
345 + x: 20,
346 + y: 20,
347 + pageX: 20,
348 + pageY: 20,
349 + clientX: 20,
350 + clientY: 20,
351 + target: ref.current,
352 + timeStamp: timeStamps[3],
353 type: 'hoverend',
354 + pointerType: 'mouse',
355 },
356 ]);
357 });
packages/react-events/src/dom/test-utils.js new
+311
@@ -0,0 +1,311 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +/* eslint-disable no-unused-vars */
13 +
14 +/**
15 + * Change environment support for PointerEvent.
16 + */
17 +
18 +function hasPointerEvent(bool) {
19 + return global != null && global.PointerEvent != null;
20 +}
21 +
22 +function setPointerEvent(bool) {
23 + global.PointerEvent = bool ? function() {} : undefined;
24 +}
25 +
26 +/**
27 + * Change environment host platform.
28 + */
29 +
30 +const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
31 +
32 +const platform = {
33 + clear() {
34 + platformGetter.mockClear();
35 + },
36 + get() {
37 + return global.navigator.platform === 'MacIntel' ? 'mac' : 'windows';
38 + },
39 + set(name: 'mac' | 'windows') {
40 + switch (name) {
41 + case 'mac': {
42 + platformGetter.mockReturnValue('MacIntel');
43 + break;
44 + }
45 + case 'windows': {
46 + platformGetter.mockReturnValue('Win32');
47 + break;
48 + }
49 + default: {
50 + break;
51 + }
52 + }
53 + },
54 +};
55 +
56 +/**
57 + * Mock native events
58 + */
59 +
60 +function createEvent(type, data) {
61 + const event = document.createEvent('CustomEvent');
62 + event.initCustomEvent(type, true, true);
63 + if (data != null) {
64 + Object.entries(data).forEach(([key, value]) => {
65 + event[key] = value;
66 + });
67 + }
68 + return event;
69 +}
70 +
71 +function createTouchEvent(type, data, id) {
72 + return createEvent(type, {
73 + changedTouches: [
74 + {
75 + ...data,
76 + identifier: id,
77 + },
78 + ],
79 + });
80 +}
81 +
82 +const createKeyboardEvent = (type, data) => {
83 + return new KeyboardEvent(type, {
84 + bubbles: true,
85 + cancelable: true,
86 + ...data,
87 + });
88 +};
89 +
90 +function blur(data) {
91 + return createEvent('blur', data);
92 +}
93 +
94 +function click(data) {
95 + return createEvent('click', data);
96 +}
97 +
98 +function dragstart(data) {
99 + return createEvent('dragstart', data);
100 +}
101 +
102 +function focus(data) {
103 + return createEvent('focus', data);
104 +}
105 +
106 +function gotpointercapture(data) {
107 + return createEvent('gotpointercapture', data);
108 +}
109 +
110 +function lostpointercapture(data) {
111 + return createEvent('lostpointercapture', data);
112 +}
113 +
114 +function pointercancel(data) {
115 + return createEvent('pointercancel', data);
116 +}
117 +
118 +function pointerdown(data) {
119 + return createEvent('pointerdown', data);
120 +}
121 +
122 +function pointerenter(data) {
123 + return createEvent('pointerenter', data);
124 +}
125 +
126 +function pointerleave(data) {
127 + return createEvent('pointerleave', data);
128 +}
129 +
130 +function pointermove(data) {
131 + return createEvent('pointermove', data);
132 +}
133 +
134 +function pointerout(data) {
135 + return createEvent('pointerout', data);
136 +}
137 +
138 +function pointerover(data) {
139 + return createEvent('pointerover', data);
140 +}
141 +
142 +function pointerup(data) {
143 + return createEvent('pointerup', data);
144 +}
145 +
146 +function mousedown(data) {
147 + return createEvent('mousedown', data);
148 +}
149 +
150 +function mouseenter(data) {
151 + return createEvent('mouseenter', data);
152 +}
153 +
154 +function mouseleave(data) {
155 + return createEvent('mouseleave', data);
156 +}
157 +
158 +function mousemove(data) {
159 + return createEvent('mousemove', data);
160 +}
161 +
162 +function mouseout(data) {
163 + return createEvent('mouseout', data);
164 +}
165 +
166 +function mouseover(data) {
167 + return createEvent('mouseover', data);
168 +}
169 +
170 +function mouseup(data) {
171 + return createEvent('mouseup', data);
172 +}
173 +
174 +function touchcancel(data, id) {
175 + return createTouchEvent('touchcancel', data, id);
176 +}
177 +
178 +function touchend(data, id) {
179 + return createTouchEvent('touchend', data, id);
180 +}
181 +
182 +function touchmove(data, id) {
183 + return createTouchEvent('touchmove', data, id);
184 +}
185 +
186 +function touchstart(data, id) {
187 + return createTouchEvent('touchstart', data, id);
188 +}
189 +
190 +/**
191 + * Dispatch high-level event sequences
192 + */
193 +
194 +function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) {
195 + const dispatch = arg => ref.current.dispatchEvent(arg);
196 + const button = -1;
197 + const pointerType = 'mouse';
198 + const event = {
199 + button,
200 + relatedTarget,
201 + clientX: x,
202 + clientY: y,
203 + pageX: x,
204 + pageY: y,
205 + };
206 + if (hasPointerEvent()) {
207 + dispatch(pointerover({pointerType, ...event}));
208 + dispatch(pointerenter({pointerType, ...event}));
209 + }
210 + dispatch(mouseover(event));
211 + dispatch(mouseover(event));
212 +}
213 +
214 +function dispatchPointerHoverMove(ref, {from, to} = {}) {
215 + const dispatch = arg => ref.current.dispatchEvent(arg);
216 + const button = -1;
217 + const pointerId = 1;
218 + const pointerType = 'mouse';
219 + function dispatchMove({x, y}) {
220 + const event = {
221 + button,
222 + clientX: x,
223 + clientY: y,
224 + pageX: x,
225 + pageY: y,
226 + };
227 + if (hasPointerEvent()) {
228 + dispatch(pointermove({pointerId, pointerType, ...event}));
229 + }
230 + dispatch(mousemove(event));
231 + }
232 + dispatchMove({x: from.x, y: from.y});
233 + dispatchMove({x: to.x, y: to.y});
234 +}
235 +
236 +function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) {
237 + const dispatch = arg => ref.current.dispatchEvent(arg);
238 + const button = -1;
239 + const pointerType = 'mouse';
240 + const event = {
241 + button,
242 + relatedTarget,
243 + clientX: x,
244 + clientY: y,
245 + pageX: x,
246 + pageY: y,
247 + };
248 + if (hasPointerEvent()) {
249 + dispatch(pointerout({pointerType, ...event}));
250 + dispatch(pointerleave({pointerType, ...event}));
251 + }
252 + dispatch(mouseout(event));
253 + dispatch(mouseleave(event));
254 +}
255 +
256 +function dispatchPointerCancel(ref, options) {
257 + const dispatchEvent = arg => ref.current.dispatchEvent(arg);
258 + dispatchEvent(pointercancel({pointerType: 'mouse'}));
259 + dispatchEvent(dragstart({pointerType: 'mouse'}));
260 +}
261 +
262 +function dispatchPointerPressDown(ref, {button = 0, pointerType = 'mouse'}) {
263 + const dispatch = arg => ref.current.dispatchEvent(arg);
264 + const pointerId = 1;
265 + if (hasPointerEvent()) {
266 + dispatch(pointerover({pointerId, pointerType, button}));
267 + dispatch(pointerenter({pointerId, pointerType, button}));
268 + dispatch(pointerdown({pointerId, pointerType, button}));
269 + }
270 + dispatch(touchstart(null, pointerId));
271 + if (hasPointerEvent()) {
272 + dispatch(gotpointercapture({pointerId, pointerType, button}));
273 + }
274 +}
275 +
276 +function dispatchPointerPressRelease(ref, {button = 0, pointerType = 'mouse'}) {
277 + const dispatch = arg => ref.current.dispatchEvent(arg);
278 + const pointerId = 1;
279 + if (hasPointerEvent()) {
280 + dispatch(pointerup({pointerId, pointerType, button}));
281 + dispatch(lostpointercapture({pointerId, pointerType, button}));
282 + dispatch(pointerout({pointerId, pointerType, button}));
283 + dispatch(pointerleave({pointerId, pointerType, button}));
284 + }
285 + dispatch(touchend(null, pointerId));
286 + dispatch(mouseover({button}));
287 + dispatch(mousemove({button}));
288 + dispatch(mousedown({button}));
289 + dispatch(focus({button}));
290 + dispatch(mouseup({button}));
291 + dispatch(click({button}));
292 +}
293 +
294 +function dispatchTouchTap(ref) {
295 + dispatchPointerPressDown(ref, {pointerType: 'touch'});
296 + dispatchPointerPressRelease(ref, {pointerType: 'touch'});
297 +}
298 +
299 +module.exports = {
300 + createEvent,
301 + dispatchPointerCancel,
302 + dispatchPointerHoverEnter,
303 + dispatchPointerHoverExit,
304 + dispatchPointerHoverMove,
305 + dispatchPointerPressDown,
306 + dispatchPointerPressRelease,
307 + dispatchTouchTap,
308 + platform,
309 + hasPointerEvent,
310 + setPointerEvent,
311 +};