diff --git a/packages/react-events/src/dom/Hover.js b/packages/react-events/src/dom/Hover.js
index 3d83313100b3e..df8469c1c86b4 100644
--- a/packages/react-events/src/dom/Hover.js
+++ b/packages/react-events/src/dom/Hover.js
@@ -10,6 +10,7 @@
import type {
ReactDOMResponderEvent,
ReactDOMResponderContext,
+ PointerType,
} from 'shared/ReactDOMTypes';
import type {ReactEventResponderListener} from 'shared/ReactTypes';
@@ -29,15 +30,14 @@ type HoverState = {
hoverTarget: null | Element | Document,
isActiveHovered: boolean,
isHovered: boolean,
- isTouched: boolean,
- hoverStartTimeout: null | number,
- hoverEndTimeout: null | number,
- ignoreEmulatedMouseEvents: boolean,
+ isTouched?: boolean,
+ ignoreEmulatedMouseEvents?: boolean,
};
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
type HoverEvent = {|
+ pointerType: PointerType,
target: Element | Document,
type: HoverEventType,
timeStamp: number,
@@ -51,17 +51,8 @@ type HoverEvent = {|
y: null | number,
|};
-const targetEventTypes = [
- 'pointerover',
- 'pointermove',
- 'pointerout',
- 'pointercancel',
-];
-
-// 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', 'mousemove', 'mouseout');
-}
+const hasPointerEvents =
+ typeof window !== 'undefined' && window.PointerEvent != null;
function isFunction(obj): boolean {
return typeof obj === 'function';
@@ -79,13 +70,16 @@ function createHoverEvent(
let pageY = null;
let screenX = null;
let screenY = null;
+ let pointerType = '';
if (event) {
const nativeEvent = (event.nativeEvent: any);
+ pointerType = event.pointerType;
({clientX, clientY, pageX, pageY, screenX, screenY} = nativeEvent);
}
return {
+ pointerType,
target,
type,
timeStamp: context.getTimeStamp(),
@@ -131,11 +125,6 @@ function dispatchHoverStartEvents(
state.isHovered = true;
- if (state.hoverEndTimeout !== null) {
- context.clearTimeout(state.hoverEndTimeout);
- state.hoverEndTimeout = null;
- }
-
if (!state.isActiveHovered) {
state.isActiveHovered = true;
const onHoverStart = props.onHoverStart;
@@ -152,6 +141,20 @@ function dispatchHoverStartEvents(
}
}
+function dispatchHoverMoveEvent(event, context, props, state) {
+ const target = state.hoverTarget;
+ const onHoverMove = props.onHoverMove;
+ if (isFunction(onHoverMove)) {
+ const syntheticEvent = createHoverEvent(
+ event,
+ context,
+ 'hovermove',
+ ((target: any): Element | Document),
+ );
+ context.dispatchEvent(syntheticEvent, onHoverMove, UserBlockingEvent);
+ }
+}
+
function dispatchHoverEndEvents(
event: null | ReactDOMResponderEvent,
context: ReactDOMResponderContext,
@@ -170,11 +173,6 @@ function dispatchHoverEndEvents(
state.isHovered = false;
- if (state.hoverStartTimeout !== null) {
- context.clearTimeout(state.hoverStartTimeout);
- state.hoverStartTimeout = null;
- }
-
if (state.isActiveHovered) {
state.isActiveHovered = false;
const onHoverEnd = props.onHoverEnd;
@@ -189,7 +187,6 @@ function dispatchHoverEndEvents(
}
dispatchHoverChangeEvent(event, context, props, state);
state.hoverTarget = null;
- state.ignoreEmulatedMouseEvents = false;
state.isTouched = false;
}
}
@@ -204,24 +201,17 @@ function unmountResponder(
}
}
-function isEmulatedMouseEvent(event, state) {
- const {type} = event;
- return (
- state.ignoreEmulatedMouseEvents &&
- (type === 'mousemove' || type === 'mouseover' || type === 'mouseout')
- );
-}
-
const hoverResponderImpl = {
- targetEventTypes,
+ targetEventTypes: [
+ 'pointerover',
+ 'pointermove',
+ 'pointerout',
+ 'pointercancel',
+ ],
getInitialState() {
return {
isActiveHovered: false,
isHovered: false,
- isTouched: false,
- hoverStartTimeout: null,
- hoverEndTimeout: null,
- ignoreEmulatedMouseEvents: false,
};
},
allowMultipleHostChildren: false,
@@ -237,95 +227,120 @@ const hoverResponderImpl = {
if (props.disabled) {
if (state.isHovered) {
dispatchHoverEndEvents(event, context, props, state);
- state.ignoreEmulatedMouseEvents = false;
- }
- if (state.isTouched) {
- state.isTouched = false;
}
return;
}
switch (type) {
// START
- case 'pointerover':
- case 'mouseover':
- case 'touchstart': {
- if (!state.isHovered) {
- // Prevent hover events for touch
- if (state.isTouched || pointerType === 'touch') {
- state.isTouched = true;
- return;
- }
-
- // Prevent hover events for emulated events
- if (isEmulatedMouseEvent(event, state)) {
- return;
- }
+ case 'pointerover': {
+ if (!state.isHovered && pointerType !== 'touch') {
state.hoverTarget = event.responderTarget;
- state.ignoreEmulatedMouseEvents = true;
dispatchHoverStartEvents(event, context, props, state);
}
- return;
+ break;
}
// MOVE
- case 'pointermove':
- case 'mousemove': {
- if (state.isHovered && !isEmulatedMouseEvent(event, state)) {
- const onHoverMove = props.onHoverMove;
- if (state.hoverTarget !== null && isFunction(onHoverMove)) {
- const syntheticEvent = createHoverEvent(
- event,
- context,
- 'hovermove',
- state.hoverTarget,
- );
- context.dispatchEvent(
- syntheticEvent,
- onHoverMove,
- UserBlockingEvent,
- );
- }
+ case 'pointermove': {
+ if (state.isHovered && state.hoverTarget !== null) {
+ dispatchHoverMoveEvent(event, context, props, state);
}
- return;
+ break;
}
// END
case 'pointerout':
- case 'pointercancel':
- case 'mouseout':
- case 'touchcancel':
- case 'touchend': {
+ case 'pointercancel': {
if (state.isHovered) {
dispatchHoverEndEvents(event, context, props, state);
- state.ignoreEmulatedMouseEvents = false;
}
- if (state.isTouched) {
- state.isTouched = false;
- }
- return;
+ break;
}
}
},
- onUnmount(
- context: ReactDOMResponderContext,
- props: HoverProps,
- state: HoverState,
- ) {
- unmountResponder(context, props, state);
+ onUnmount: unmountResponder,
+ onOwnershipChange: unmountResponder,
+};
+
+const hoverResponderFallbackImpl = {
+ targetEventTypes: ['mouseover', 'mousemove', 'mouseout', 'touchstart'],
+ getInitialState() {
+ return {
+ isActiveHovered: false,
+ isHovered: false,
+ isTouched: false,
+ ignoreEmulatedMouseEvents: false,
+ };
},
- onOwnershipChange(
+ allowMultipleHostChildren: false,
+ allowEventHooks: true,
+ onEvent(
+ event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: HoverProps,
state: HoverState,
- ) {
- unmountResponder(context, props, state);
+ ): void {
+ const {type} = event;
+
+ if (props.disabled) {
+ if (state.isHovered) {
+ dispatchHoverEndEvents(event, context, props, state);
+ state.ignoreEmulatedMouseEvents = false;
+ }
+ state.isTouched = false;
+ return;
+ }
+
+ switch (type) {
+ // START
+ case 'mouseover': {
+ if (!state.isHovered && !state.ignoreEmulatedMouseEvents) {
+ state.hoverTarget = event.responderTarget;
+ dispatchHoverStartEvents(event, context, props, state);
+ }
+ break;
+ }
+
+ // MOVE
+ case 'mousemove': {
+ if (
+ state.isHovered &&
+ state.hoverTarget !== null &&
+ !state.ignoreEmulatedMouseEvents
+ ) {
+ dispatchHoverMoveEvent(event, context, props, state);
+ } else if (!state.isHovered && type === 'mousemove') {
+ state.ignoreEmulatedMouseEvents = false;
+ state.isTouched = false;
+ }
+ break;
+ }
+
+ // END
+ case 'mouseout': {
+ if (state.isHovered) {
+ dispatchHoverEndEvents(event, context, props, state);
+ }
+ break;
+ }
+
+ case 'touchstart': {
+ if (!state.isHovered) {
+ state.isTouched = true;
+ state.ignoreEmulatedMouseEvents = true;
+ }
+ break;
+ }
+ }
},
+ onUnmount: unmountResponder,
+ onOwnershipChange: unmountResponder,
};
export const HoverResponder = React.unstable_createResponder(
'Hover',
- hoverResponderImpl,
+ hasPointerEvents ? hoverResponderImpl : hoverResponderFallbackImpl,
);
export function useHoverResponder(
diff --git a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js
index 546378f98f16d..4e03d897bf82d 100644
--- a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js
+++ b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js
@@ -9,24 +9,16 @@
'use strict';
+import {createEvent, platform, setPointerEvent} from '../test-utils';
+
let React;
let ReactFeatureFlags;
let ReactDOM;
let useContextMenuResponder;
-function createEvent(type, data) {
- const event = document.createEvent('CustomEvent');
- event.initCustomEvent(type, true, true);
- if (data != null) {
- Object.entries(data).forEach(([key, value]) => {
- event[key] = value;
- });
- }
- return event;
-}
-
-function init(hasPointerEvents) {
- global.PointerEvents = hasPointerEvents ? function() {} : undefined;
+function initializeModules(hasPointerEvents) {
+ setPointerEvent(hasPointerEvents);
+ jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
@@ -35,29 +27,6 @@ function init(hasPointerEvents) {
.useContextMenuResponder;
}
-const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
-function setPlatform(platform: 'mac' | 'windows') {
- jest.resetModules();
- switch (platform) {
- case 'mac': {
- platformGetter.mockReturnValue('MacIntel');
- break;
- }
- case 'windows': {
- platformGetter.mockReturnValue('Win32');
- break;
- }
- default: {
- break;
- }
- }
- init();
-}
-
-function clearPlatform() {
- platformGetter.mockClear();
-}
-
function dispatchContextMenuEvents(ref, options) {
const preventDefault = options.preventDefault || function() {};
const variant = (options.variant: 'mouse' | 'touch' | 'modified');
@@ -76,7 +45,7 @@ function dispatchContextMenuEvents(ref, options) {
createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
);
dispatchEvent(createEvent('mousedown', {button: 0}));
- if (global.navigator.platform === 'MacIntel') {
+ if (platform.get() === 'mac') {
dispatchEvent(
createEvent('contextmenu', {button: 0, ctrlKey: true, preventDefault}),
);
@@ -97,144 +66,143 @@ function dispatchContextMenuEvents(ref, options) {
}
const forcePointerEvents = true;
+const table = [[forcePointerEvents], [!forcePointerEvents]];
+
+describe.each(table)('ContextMenu responder', hasPointerEvents => {
+ let container;
+
+ beforeEach(() => {
+ initializeModules(hasPointerEvents);
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ ReactDOM.render(null, container);
+ document.body.removeChild(container);
+ container = null;
+ });
+
+ describe('all platforms', () => {
+ it('mouse right-click', () => {
+ const onContextMenu = jest.fn();
+ const preventDefault = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({onContextMenu});
+ return
;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
+ expect(preventDefault).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
+ );
+ });
+
+ it('touch long-press', () => {
+ const onContextMenu = jest.fn();
+ const preventDefault = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({onContextMenu});
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
+ expect(preventDefault).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
+ );
+ });
+
+ it('"disabled" is true', () => {
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({
+ onContextMenu,
+ disabled: true,
+ });
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, 'mouse');
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
+ });
-describe.each([[forcePointerEvents], [!forcePointerEvents]])(
- 'ContextMenu responder',
- hasPointerEvents => {
- let container;
+ it('"preventDefault" is false', () => {
+ const preventDefault = jest.fn();
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({
+ onContextMenu,
+ preventDefault: false,
+ });
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
+ expect(preventDefault).toHaveBeenCalledTimes(0);
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ });
+ });
+ describe('mac platform', () => {
beforeEach(() => {
+ platform.set('mac');
jest.resetModules();
- init(hasPointerEvents);
- container = document.createElement('div');
- document.body.appendChild(container);
});
afterEach(() => {
- ReactDOM.render(null, container);
- document.body.removeChild(container);
- container = null;
+ platform.clear();
});
- describe('all platforms', () => {
- it('mouse right-click', () => {
- const onContextMenu = jest.fn();
- const preventDefault = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({onContextMenu});
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
- expect(preventDefault).toHaveBeenCalledTimes(1);
- expect(onContextMenu).toHaveBeenCalledTimes(1);
- expect(onContextMenu).toHaveBeenCalledWith(
- expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
- );
- });
-
- it('touch long-press', () => {
- const onContextMenu = jest.fn();
- const preventDefault = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({onContextMenu});
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
- expect(preventDefault).toHaveBeenCalledTimes(1);
- expect(onContextMenu).toHaveBeenCalledTimes(1);
- expect(onContextMenu).toHaveBeenCalledWith(
- expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
- );
- });
-
- it('"disabled" is true', () => {
- const onContextMenu = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({
- onContextMenu,
- disabled: true,
- });
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, 'mouse');
- expect(onContextMenu).toHaveBeenCalledTimes(0);
- });
-
- it('"preventDefault" is false', () => {
- const preventDefault = jest.fn();
- const onContextMenu = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({
- onContextMenu,
- preventDefault: false,
- });
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
- expect(preventDefault).toHaveBeenCalledTimes(0);
- expect(onContextMenu).toHaveBeenCalledTimes(1);
- });
+ it('mouse modified left-click', () => {
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({onContextMenu});
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, {variant: 'modified'});
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
+ );
});
+ });
- describe('mac platform', () => {
- beforeEach(() => {
- setPlatform('mac');
- });
-
- afterEach(() => {
- clearPlatform();
- });
-
- it('mouse modified left-click', () => {
- const onContextMenu = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({onContextMenu});
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, {variant: 'modified'});
- expect(onContextMenu).toHaveBeenCalledTimes(1);
- expect(onContextMenu).toHaveBeenCalledWith(
- expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
- );
- });
+ describe('windows platform', () => {
+ beforeEach(() => {
+ platform.set('windows');
+ jest.resetModules();
+ });
+
+ afterEach(() => {
+ platform.clear();
});
- describe('windows platform', () => {
- beforeEach(() => {
- setPlatform('windows');
- });
-
- afterEach(() => {
- clearPlatform();
- });
-
- it('mouse modified left-click', () => {
- const onContextMenu = jest.fn();
- const ref = React.createRef();
- const Component = () => {
- const listener = useContextMenuResponder({onContextMenu});
- return ;
- };
- ReactDOM.render(, container);
-
- dispatchContextMenuEvents(ref, {variant: 'modified'});
- expect(onContextMenu).toHaveBeenCalledTimes(0);
- });
+ it('mouse modified left-click', () => {
+ const onContextMenu = jest.fn();
+ const ref = React.createRef();
+ const Component = () => {
+ const listener = useContextMenuResponder({onContextMenu});
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ dispatchContextMenuEvents(ref, {variant: 'modified'});
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
});
- },
-);
+ });
+});
diff --git a/packages/react-events/src/dom/__tests__/Hover-test.internal.js b/packages/react-events/src/dom/__tests__/Hover-test.internal.js
index 895a8236046e6..73d5106e6fbab 100644
--- a/packages/react-events/src/dom/__tests__/Hover-test.internal.js
+++ b/packages/react-events/src/dom/__tests__/Hover-test.internal.js
@@ -9,50 +9,41 @@
'use strict';
+import {
+ dispatchPointerCancel,
+ dispatchPointerHoverEnter,
+ dispatchPointerHoverExit,
+ dispatchPointerHoverMove,
+ dispatchTouchTap,
+ setPointerEvent,
+} from '../test-utils';
+
let React;
let ReactFeatureFlags;
let ReactDOM;
-let TestUtils;
-let Scheduler;
let HoverResponder;
let useHoverResponder;
-const createEvent = (type, data) => {
- const event = document.createEvent('CustomEvent');
- event.initCustomEvent(type, true, true);
- if (data != null) {
- Object.entries(data).forEach(([key, value]) => {
- event[key] = value;
- });
- }
- return event;
-};
-
-function createTouchEvent(type, id, data) {
- return createEvent(type, {
- changedTouches: [
- {
- ...data,
- identifier: id,
- },
- ],
- });
+function initializeModules(hasPointerEvents) {
+ jest.resetModules();
+ setPointerEvent(hasPointerEvents);
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.enableFlareAPI = true;
+ ReactFeatureFlags.enableUserBlockingEvents = true;
+ React = require('react');
+ ReactDOM = require('react-dom');
+ HoverResponder = require('react-events/hover').HoverResponder;
+ useHoverResponder = require('react-events/hover').useHoverResponder;
}
-describe('Hover event responder', () => {
+const forcePointerEvents = true;
+const table = [[forcePointerEvents], [!forcePointerEvents]];
+
+describe.each(table)('Hover responder', hasPointerEvents => {
let container;
beforeEach(() => {
- jest.resetModules();
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
- ReactFeatureFlags.enableFlareAPI = true;
- ReactFeatureFlags.enableUserBlockingEvents = true;
- React = require('react');
- ReactDOM = require('react-dom');
- TestUtils = require('react-dom/test-utils');
- Scheduler = require('scheduler');
- HoverResponder = require('react-events/hover').HoverResponder;
- useHoverResponder = require('react-events/hover').useHoverResponder;
+ initializeModules(hasPointerEvents);
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -64,17 +55,21 @@ describe('Hover event responder', () => {
});
describe('disabled', () => {
- let onHoverStart, onHoverEnd, ref;
+ let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref;
beforeEach(() => {
+ onHoverChange = jest.fn();
onHoverStart = jest.fn();
+ onHoverMove = jest.fn();
onHoverEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
disabled: true,
- onHoverStart: onHoverStart,
- onHoverEnd: onHoverEnd,
+ onHoverChange,
+ onHoverStart,
+ onHoverMove,
+ onHoverEnd,
});
return ;
};
@@ -82,9 +77,11 @@ describe('Hover event responder', () => {
});
it('prevents custom events being dispatched', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(createEvent('pointerout'));
+ dispatchPointerHoverEnter(ref);
+ dispatchPointerHoverExit(ref);
+ expect(onHoverChange).not.toBeCalled();
expect(onHoverStart).not.toBeCalled();
+ expect(onHoverMove).not.toBeCalled();
expect(onHoverEnd).not.toBeCalled();
});
});
@@ -104,68 +101,21 @@ describe('Hover event responder', () => {
ReactDOM.render(, container);
});
- it('is called after "pointerover" event', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
+ it('is called for mouse pointers', () => {
+ dispatchPointerHoverEnter(ref);
expect(onHoverStart).toHaveBeenCalledTimes(1);
});
- it('is not called if "pointerover" pointerType is touch', () => {
- const event = createEvent('pointerover', {pointerType: 'touch'});
- ref.current.dispatchEvent(event);
+ it('is not called for touch pointers', () => {
+ dispatchTouchTap(ref);
expect(onHoverStart).not.toBeCalled();
});
- it('is called if valid "pointerover" follows touch', () => {
- ref.current.dispatchEvent(
- createEvent('pointerover', {pointerType: 'touch'}),
- );
- ref.current.dispatchEvent(
- createEvent('pointerout', {pointerType: 'touch'}),
- );
- ref.current.dispatchEvent(
- createEvent('pointerover', {pointerType: 'mouse'}),
- );
- expect(onHoverStart).toHaveBeenCalledTimes(1);
- });
-
- it('ignores browser emulated "mouseover" event', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(
- createEvent('mouseover', {
- button: 0,
- }),
- );
+ it('is called if a mouse pointer is used after a touch pointer', () => {
+ dispatchTouchTap(ref);
+ dispatchPointerHoverEnter(ref);
expect(onHoverStart).toHaveBeenCalledTimes(1);
});
-
- // No PointerEvent fallbacks
- it('is called after "mouseover" event', () => {
- ref.current.dispatchEvent(
- createEvent('mouseover', {
- button: 0,
- }),
- );
- expect(onHoverStart).toHaveBeenCalledTimes(1);
- });
-
- it('is not called after "touchstart"', () => {
- ref.current.dispatchEvent(
- createTouchEvent('touchstart', 0, {
- target: ref.current,
- }),
- );
- ref.current.dispatchEvent(
- createTouchEvent('touchend', 0, {
- target: ref.current,
- }),
- );
- ref.current.dispatchEvent(
- createEvent('mouseover', {
- button: 0,
- }),
- );
- expect(onHoverStart).not.toBeCalled();
- });
});
describe('onHoverChange', () => {
@@ -183,58 +133,18 @@ describe('Hover event responder', () => {
ReactDOM.render(, container);
});
- it('is called after "pointerover" and "pointerout" events', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- expect(onHoverChange).toHaveBeenCalledTimes(1);
- expect(onHoverChange).toHaveBeenCalledWith(true);
- ref.current.dispatchEvent(createEvent('pointerout'));
- expect(onHoverChange).toHaveBeenCalledTimes(2);
- expect(onHoverChange).toHaveBeenCalledWith(false);
- });
-
- // No PointerEvent fallbacks
- it('is called after "mouseover" and "mouseout" events', () => {
- ref.current.dispatchEvent(createEvent('mouseover'));
+ it('is called for mouse pointers', () => {
+ dispatchPointerHoverEnter(ref);
expect(onHoverChange).toHaveBeenCalledTimes(1);
expect(onHoverChange).toHaveBeenCalledWith(true);
- ref.current.dispatchEvent(createEvent('mouseout'));
+ dispatchPointerHoverExit(ref);
expect(onHoverChange).toHaveBeenCalledTimes(2);
expect(onHoverChange).toHaveBeenCalledWith(false);
});
- it('should be user-blocking but not discrete', async () => {
- const {act} = TestUtils;
- const {useState} = React;
-
- const newContainer = document.createElement('div');
- document.body.appendChild(newContainer);
- const root = ReactDOM.unstable_createRoot(newContainer);
-
- const target = React.createRef(null);
- function Foo() {
- const [isHover, setHover] = useState(false);
- const listener = useHoverResponder({
- onHoverChange: setHover,
- });
- return (
-
- {isHover ? 'hovered' : 'not hovered'}
-
- );
- }
-
- await act(async () => {
- root.render();
- });
- expect(newContainer.textContent).toEqual('not hovered');
-
- await act(async () => {
- target.current.dispatchEvent(createEvent('mouseover'));
-
- // 3s should be enough to expire the updates
- Scheduler.unstable_advanceTime(3000);
- expect(newContainer.textContent).toEqual('hovered');
- });
+ it('is not called for touch pointers', () => {
+ dispatchTouchTap(ref);
+ expect(onHoverChange).not.toBeCalled();
});
});
@@ -253,64 +163,35 @@ describe('Hover event responder', () => {
ReactDOM.render(, container);
});
- it('is called after "pointerout" event', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(createEvent('pointerout'));
+ it('is called for mouse pointers', () => {
+ dispatchPointerHoverEnter(ref);
+ dispatchPointerHoverExit(ref);
expect(onHoverEnd).toHaveBeenCalledTimes(1);
});
- it('is not called if "pointerover" pointerType is touch', () => {
- const event = createEvent('pointerover');
- event.pointerType = 'touch';
- ref.current.dispatchEvent(event);
- ref.current.dispatchEvent(createEvent('pointerout'));
- expect(onHoverEnd).not.toBeCalled();
- });
-
- it('ignores browser emulated "mouseout" event', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(createEvent('pointerout'));
- ref.current.dispatchEvent(createEvent('mouseout'));
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
- });
-
- it('is called after "pointercancel" event', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(createEvent('pointercancel'));
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
- });
-
- it('is not called again after "pointercancel" event if it follows "pointerout"', () => {
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(createEvent('pointerout'));
- ref.current.dispatchEvent(createEvent('pointercancel'));
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
- });
+ if (hasPointerEvents) {
+ it('is called once for cancelled mouse pointers', () => {
+ dispatchPointerHoverEnter(ref);
+ dispatchPointerCancel(ref);
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+
+ // only called once if cancel follows exit
+ onHoverEnd.mockReset();
+ dispatchPointerHoverEnter(ref);
+ dispatchPointerHoverExit(ref);
+ dispatchPointerCancel(ref);
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+ }
- // No PointerEvent fallbacks
- it('is called after "mouseout" event', () => {
- ref.current.dispatchEvent(createEvent('mouseover'));
- ref.current.dispatchEvent(createEvent('mouseout'));
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
- });
- it('is not called after "touchend"', () => {
- ref.current.dispatchEvent(
- createTouchEvent('touchstart', 0, {
- target: ref.current,
- }),
- );
- ref.current.dispatchEvent(
- createTouchEvent('touchend', 0, {
- target: ref.current,
- }),
- );
- ref.current.dispatchEvent(createEvent('mouseout'));
+ it('is not called for touch pointers', () => {
+ dispatchTouchTap(ref);
expect(onHoverEnd).not.toBeCalled();
});
});
describe('onHoverMove', () => {
- it('is called after "pointermove"', () => {
+ it('is called after the active pointer moves"', () => {
const onHoverMove = jest.fn();
const ref = React.createRef();
const Component = () => {
@@ -320,20 +201,9 @@ describe('Hover event responder', () => {
return ;
};
ReactDOM.render(, container);
-
- ref.current.getBoundingClientRect = () => ({
- top: 50,
- left: 50,
- bottom: 500,
- right: 500,
- });
- ref.current.dispatchEvent(createEvent('pointerover'));
- ref.current.dispatchEvent(
- createEvent('pointermove', {pointerType: 'mouse'}),
- );
- ref.current.dispatchEvent(createEvent('touchmove'));
- ref.current.dispatchEvent(createEvent('mousemove'));
- expect(onHoverMove).toHaveBeenCalledTimes(1);
+ dispatchPointerHoverEnter(ref);
+ dispatchPointerHoverMove(ref, {from: {x: 0, y: 0}, to: {x: 1, y: 1}});
+ expect(onHoverMove).toHaveBeenCalledTimes(2);
expect(onHoverMove).toHaveBeenCalledWith(
expect.objectContaining({type: 'hovermove'}),
);
@@ -372,18 +242,13 @@ describe('Hover event responder', () => {
};
ReactDOM.render(, container);
- outerRef.current.dispatchEvent(createEvent('pointerover'));
- outerRef.current.dispatchEvent(
- createEvent('pointerout', {relatedTarget: innerRef.current}),
- );
- innerRef.current.dispatchEvent(createEvent('pointerover'));
- innerRef.current.dispatchEvent(
- createEvent('pointerout', {relatedTarget: outerRef.current}),
- );
- outerRef.current.dispatchEvent(
- createEvent('pointerover', {relatedTarget: innerRef.current}),
- );
- outerRef.current.dispatchEvent(createEvent('pointerout'));
+ dispatchPointerHoverEnter(outerRef, {relatedTarget: container});
+ dispatchPointerHoverExit(outerRef, {relatedTarget: innerRef.current});
+ dispatchPointerHoverEnter(innerRef, {relatedTarget: outerRef.current});
+ dispatchPointerHoverExit(innerRef, {relatedTarget: outerRef.current});
+ dispatchPointerHoverEnter(outerRef, {relatedTarget: innerRef.current});
+ dispatchPointerHoverExit(outerRef, {relatedTarget: container});
+
expect(events).toEqual([
'outer: onHoverStart',
'outer: onHoverChange',
@@ -411,10 +276,10 @@ describe('Hover event responder', () => {
const eventLog = [];
const logEvent = event => {
const propertiesWeCareAbout = {
+ x: event.x,
+ y: event.y,
pageX: event.pageX,
pageY: event.pageY,
- screenX: event.screenX,
- screenY: event.screenY,
clientX: event.clientX,
clientY: event.clientY,
pointerType: event.pointerType,
@@ -435,79 +300,58 @@ describe('Hover event responder', () => {
};
ReactDOM.render(, container);
- ref.current.getBoundingClientRect = () => ({
- top: 10,
- left: 10,
- bottom: 20,
- right: 20,
- });
+ dispatchPointerHoverEnter(ref, {x: 10, y: 10});
+ dispatchPointerHoverMove(ref, {from: {x: 10, y: 10}, to: {x: 20, y: 20}});
+ dispatchPointerHoverExit(ref, {x: 20, y: 20});
- ref.current.dispatchEvent(
- createEvent('pointerover', {
- pointerType: 'mouse',
- pageX: 15,
- pageY: 16,
- screenX: 20,
- screenY: 21,
- clientX: 30,
- clientY: 31,
- }),
- );
- ref.current.dispatchEvent(
- createEvent('pointermove', {
- pointerType: 'mouse',
- pageX: 16,
- pageY: 17,
- screenX: 21,
- screenY: 22,
- clientX: 31,
- clientY: 32,
- }),
- );
- ref.current.dispatchEvent(
- createEvent('pointerout', {
- pointerType: 'mouse',
- pageX: 17,
- pageY: 18,
- screenX: 22,
- screenY: 23,
- clientX: 32,
- clientY: 33,
- }),
- );
expect(eventLog).toEqual([
{
- pageX: 15,
- pageY: 16,
- screenX: 20,
- screenY: 21,
- clientX: 30,
- clientY: 31,
+ x: 10,
+ y: 10,
+ pageX: 10,
+ pageY: 10,
+ clientX: 10,
+ clientY: 10,
target: ref.current,
timeStamp: timeStamps[0],
type: 'hoverstart',
+ pointerType: 'mouse',
},
{
- pageX: 16,
- pageY: 17,
- screenX: 21,
- screenY: 22,
- clientX: 31,
- clientY: 32,
+ x: 10,
+ y: 10,
+ pageX: 10,
+ pageY: 10,
+ clientX: 10,
+ clientY: 10,
target: ref.current,
timeStamp: timeStamps[1],
type: 'hovermove',
+ pointerType: 'mouse',
},
{
- pageX: 17,
- pageY: 18,
- screenX: 22,
- screenY: 23,
- clientX: 32,
- clientY: 33,
+ x: 20,
+ y: 20,
+ pageX: 20,
+ pageY: 20,
+ clientX: 20,
+ clientY: 20,
target: ref.current,
timeStamp: timeStamps[2],
+ type: 'hovermove',
+ pointerType: 'mouse',
+ },
+ {
+ x: 20,
+ y: 20,
+ pageX: 20,
+ pageY: 20,
+ clientX: 20,
+ clientY: 20,
+ target: ref.current,
+ timeStamp: timeStamps[3],
type: 'hoverend',
+ pointerType: 'mouse',
},
]);
});
diff --git a/packages/react-events/src/dom/test-utils.js b/packages/react-events/src/dom/test-utils.js
new file mode 100644
index 0000000000000..ddf6bf6773a1f
--- /dev/null
+++ b/packages/react-events/src/dom/test-utils.js
@@ -0,0 +1,311 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+/* eslint-disable no-unused-vars */
+
+/**
+ * Change environment support for PointerEvent.
+ */
+
+function hasPointerEvent(bool) {
+ return global != null && global.PointerEvent != null;
+}
+
+function setPointerEvent(bool) {
+ global.PointerEvent = bool ? function() {} : undefined;
+}
+
+/**
+ * Change environment host platform.
+ */
+
+const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
+
+const platform = {
+ clear() {
+ platformGetter.mockClear();
+ },
+ get() {
+ return global.navigator.platform === 'MacIntel' ? 'mac' : 'windows';
+ },
+ set(name: 'mac' | 'windows') {
+ switch (name) {
+ case 'mac': {
+ platformGetter.mockReturnValue('MacIntel');
+ break;
+ }
+ case 'windows': {
+ platformGetter.mockReturnValue('Win32');
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ },
+};
+
+/**
+ * Mock native events
+ */
+
+function createEvent(type, data) {
+ const event = document.createEvent('CustomEvent');
+ event.initCustomEvent(type, true, true);
+ if (data != null) {
+ Object.entries(data).forEach(([key, value]) => {
+ event[key] = value;
+ });
+ }
+ return event;
+}
+
+function createTouchEvent(type, data, id) {
+ return createEvent(type, {
+ changedTouches: [
+ {
+ ...data,
+ identifier: id,
+ },
+ ],
+ });
+}
+
+const createKeyboardEvent = (type, data) => {
+ return new KeyboardEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ ...data,
+ });
+};
+
+function blur(data) {
+ return createEvent('blur', data);
+}
+
+function click(data) {
+ return createEvent('click', data);
+}
+
+function dragstart(data) {
+ return createEvent('dragstart', data);
+}
+
+function focus(data) {
+ return createEvent('focus', data);
+}
+
+function gotpointercapture(data) {
+ return createEvent('gotpointercapture', data);
+}
+
+function lostpointercapture(data) {
+ return createEvent('lostpointercapture', data);
+}
+
+function pointercancel(data) {
+ return createEvent('pointercancel', data);
+}
+
+function pointerdown(data) {
+ return createEvent('pointerdown', data);
+}
+
+function pointerenter(data) {
+ return createEvent('pointerenter', data);
+}
+
+function pointerleave(data) {
+ return createEvent('pointerleave', data);
+}
+
+function pointermove(data) {
+ return createEvent('pointermove', data);
+}
+
+function pointerout(data) {
+ return createEvent('pointerout', data);
+}
+
+function pointerover(data) {
+ return createEvent('pointerover', data);
+}
+
+function pointerup(data) {
+ return createEvent('pointerup', data);
+}
+
+function mousedown(data) {
+ return createEvent('mousedown', data);
+}
+
+function mouseenter(data) {
+ return createEvent('mouseenter', data);
+}
+
+function mouseleave(data) {
+ return createEvent('mouseleave', data);
+}
+
+function mousemove(data) {
+ return createEvent('mousemove', data);
+}
+
+function mouseout(data) {
+ return createEvent('mouseout', data);
+}
+
+function mouseover(data) {
+ return createEvent('mouseover', data);
+}
+
+function mouseup(data) {
+ return createEvent('mouseup', data);
+}
+
+function touchcancel(data, id) {
+ return createTouchEvent('touchcancel', data, id);
+}
+
+function touchend(data, id) {
+ return createTouchEvent('touchend', data, id);
+}
+
+function touchmove(data, id) {
+ return createTouchEvent('touchmove', data, id);
+}
+
+function touchstart(data, id) {
+ return createTouchEvent('touchstart', data, id);
+}
+
+/**
+ * Dispatch high-level event sequences
+ */
+
+function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) {
+ const dispatch = arg => ref.current.dispatchEvent(arg);
+ const button = -1;
+ const pointerType = 'mouse';
+ const event = {
+ button,
+ relatedTarget,
+ clientX: x,
+ clientY: y,
+ pageX: x,
+ pageY: y,
+ };
+ if (hasPointerEvent()) {
+ dispatch(pointerover({pointerType, ...event}));
+ dispatch(pointerenter({pointerType, ...event}));
+ }
+ dispatch(mouseover(event));
+ dispatch(mouseover(event));
+}
+
+function dispatchPointerHoverMove(ref, {from, to} = {}) {
+ const dispatch = arg => ref.current.dispatchEvent(arg);
+ const button = -1;
+ const pointerId = 1;
+ const pointerType = 'mouse';
+ function dispatchMove({x, y}) {
+ const event = {
+ button,
+ clientX: x,
+ clientY: y,
+ pageX: x,
+ pageY: y,
+ };
+ if (hasPointerEvent()) {
+ dispatch(pointermove({pointerId, pointerType, ...event}));
+ }
+ dispatch(mousemove(event));
+ }
+ dispatchMove({x: from.x, y: from.y});
+ dispatchMove({x: to.x, y: to.y});
+}
+
+function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) {
+ const dispatch = arg => ref.current.dispatchEvent(arg);
+ const button = -1;
+ const pointerType = 'mouse';
+ const event = {
+ button,
+ relatedTarget,
+ clientX: x,
+ clientY: y,
+ pageX: x,
+ pageY: y,
+ };
+ if (hasPointerEvent()) {
+ dispatch(pointerout({pointerType, ...event}));
+ dispatch(pointerleave({pointerType, ...event}));
+ }
+ dispatch(mouseout(event));
+ dispatch(mouseleave(event));
+}
+
+function dispatchPointerCancel(ref, options) {
+ const dispatchEvent = arg => ref.current.dispatchEvent(arg);
+ dispatchEvent(pointercancel({pointerType: 'mouse'}));
+ dispatchEvent(dragstart({pointerType: 'mouse'}));
+}
+
+function dispatchPointerPressDown(ref, {button = 0, pointerType = 'mouse'}) {
+ const dispatch = arg => ref.current.dispatchEvent(arg);
+ const pointerId = 1;
+ if (hasPointerEvent()) {
+ dispatch(pointerover({pointerId, pointerType, button}));
+ dispatch(pointerenter({pointerId, pointerType, button}));
+ dispatch(pointerdown({pointerId, pointerType, button}));
+ }
+ dispatch(touchstart(null, pointerId));
+ if (hasPointerEvent()) {
+ dispatch(gotpointercapture({pointerId, pointerType, button}));
+ }
+}
+
+function dispatchPointerPressRelease(ref, {button = 0, pointerType = 'mouse'}) {
+ const dispatch = arg => ref.current.dispatchEvent(arg);
+ const pointerId = 1;
+ if (hasPointerEvent()) {
+ dispatch(pointerup({pointerId, pointerType, button}));
+ dispatch(lostpointercapture({pointerId, pointerType, button}));
+ dispatch(pointerout({pointerId, pointerType, button}));
+ dispatch(pointerleave({pointerId, pointerType, button}));
+ }
+ dispatch(touchend(null, pointerId));
+ dispatch(mouseover({button}));
+ dispatch(mousemove({button}));
+ dispatch(mousedown({button}));
+ dispatch(focus({button}));
+ dispatch(mouseup({button}));
+ dispatch(click({button}));
+}
+
+function dispatchTouchTap(ref) {
+ dispatchPointerPressDown(ref, {pointerType: 'touch'});
+ dispatchPointerPressRelease(ref, {pointerType: 'touch'});
+}
+
+module.exports = {
+ createEvent,
+ dispatchPointerCancel,
+ dispatchPointerHoverEnter,
+ dispatchPointerHoverExit,
+ dispatchPointerHoverMove,
+ dispatchPointerPressDown,
+ dispatchPointerPressRelease,
+ dispatchTouchTap,
+ platform,
+ hasPointerEvent,
+ setPointerEvent,
+};