diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index fbb2e6e505b39..4a7b1513e66cb 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -64,6 +64,7 @@ class Header extends React.Component { Event Pooling Custom Elements Media Events + Pointer Events diff --git a/fixtures/dom/src/components/fixtures/index.js b/fixtures/dom/src/components/fixtures/index.js index 9b40eb9b1f857..d43a94df3418b 100644 --- a/fixtures/dom/src/components/fixtures/index.js +++ b/fixtures/dom/src/components/fixtures/index.js @@ -11,6 +11,7 @@ import ErrorHandling from './error-handling'; import EventPooling from './event-pooling'; import CustomElementFixtures from './custom-elements'; import MediaEventsFixtures from './media-events'; +import PointerEventsFixtures from './pointer-events'; const React = window.React; @@ -46,6 +47,8 @@ function FixturesPage() { return ; case '/media-events': return ; + case '/pointer-events': + return ; default: return Please select a test fixture.; } diff --git a/fixtures/dom/src/components/fixtures/pointer-events/drag-box.js b/fixtures/dom/src/components/fixtures/pointer-events/drag-box.js new file mode 100644 index 0000000000000..0bbc894ccc0e7 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/pointer-events/drag-box.js @@ -0,0 +1,90 @@ +const React = window.React; + +const CIRCLE_SIZE = 80; + +class DragBox extends React.Component { + state = { + hasCapture: false, + circleLeft: 80, + circleTop: 80, + }; + isDragging = false; + previousLeft = 0; + previousTop = 0; + + onDown = event => { + this.isDragging = true; + event.target.setPointerCapture(event.pointerId); + + // We store the initial coordinates to be able to calculate the changes + // later on. + this.extractPositionDelta(event); + }; + + onMove = event => { + if (!this.isDragging) { + return; + } + const {left, top} = this.extractPositionDelta(event); + + this.setState(({circleLeft, circleTop}) => ({ + circleLeft: circleLeft + left, + circleTop: circleTop + top, + })); + }; + + onUp = event => (this.isDragging = false); + onGotCapture = event => this.setState({hasCapture: true}); + onLostCapture = event => this.setState({hasCapture: false}); + + extractPositionDelta = event => { + const left = event.pageX; + const top = event.pageY; + const delta = { + left: left - this.previousLeft, + top: top - this.previousTop, + }; + this.previousLeft = left; + this.previousTop = top; + return delta; + }; + + render() { + const {hasCapture, circleLeft, circleTop} = this.state; + + const boxStyle = { + border: '1px solid #d9d9d9', + margin: '10px 0 20px', + minHeight: 400, + width: '100%', + position: 'relative', + }; + + const circleStyle = { + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + position: 'absolute', + left: circleLeft, + top: circleTop, + backgroundColor: hasCapture ? 'blue' : 'green', + touchAction: 'none', + }; + + return ( + + + + ); + } +} + +export default DragBox; diff --git a/fixtures/dom/src/components/fixtures/pointer-events/drag.js b/fixtures/dom/src/components/fixtures/pointer-events/drag.js new file mode 100644 index 0000000000000..3c829cc657c77 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/pointer-events/drag.js @@ -0,0 +1,25 @@ +import TestCase from '../../TestCase'; +import DragBox from './drag-box'; + +const React = window.React; + +class Drag extends React.Component { + render() { + return ( + + + Drag the circle below with any pointer tool + + + + While dragging, the circle must have turn blue to indicate that a + pointer capture was received. + + + + + ); + } +} + +export default Drag; diff --git a/fixtures/dom/src/components/fixtures/pointer-events/hover-box.js b/fixtures/dom/src/components/fixtures/pointer-events/hover-box.js new file mode 100644 index 0000000000000..4fa4a07e65b33 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/pointer-events/hover-box.js @@ -0,0 +1,34 @@ +const React = window.React; + +class DrawBox extends React.Component { + render() { + const boxStyle = { + border: '1px solid #d9d9d9', + margin: '10px 0 20px', + padding: '20px 20px', + touchAction: 'none', + }; + + const obstacleStyle = { + border: '1px solid #d9d9d9', + width: '25%', + height: '200px', + margin: '12.5%', + display: 'inline-block', + }; + + return ( + + + + + ); + } +} + +export default DrawBox; diff --git a/fixtures/dom/src/components/fixtures/pointer-events/hover.js b/fixtures/dom/src/components/fixtures/pointer-events/hover.js new file mode 100644 index 0000000000000..f64eff82e3523 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/pointer-events/hover.js @@ -0,0 +1,51 @@ +import TestCase from '../../TestCase'; +import HoverBox from './hover-box'; + +const React = window.React; + +class Hover extends React.Component { + state = { + overs: 0, + outs: 0, + enters: 0, + leaves: 0, + }; + + onOver = () => this.setState({overs: this.state.overs + 1}); + onOut = () => this.setState({outs: this.state.outs + 1}); + onEnter = () => this.setState({enters: this.state.enters + 1}); + onLeave = () => this.setState({leaves: this.state.leaves + 1}); + + render() { + const {overs, outs, enters, leaves} = this.state; + + return ( + + + Hover over the above box and the obstacles + + + + Overs and outs should increase when moving over the obstacles but + enters and leaves should not. + + + + + + Pointer Overs: {overs} + Pointer Outs: {outs} + Pointer Enters: {enters} + Pointer Leaves: {leaves} + + + ); + } +} + +export default Hover; diff --git a/fixtures/dom/src/components/fixtures/pointer-events/index.js b/fixtures/dom/src/components/fixtures/pointer-events/index.js new file mode 100644 index 0000000000000..cad6cdfef1630 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/pointer-events/index.js @@ -0,0 +1,20 @@ +import FixtureSet from '../../FixtureSet'; +import Drag from './drag'; +import Hover from './hover'; + +const React = window.React; + +class PointerEvents extends React.Component { + render() { + return ( + + + + + ); + } +} + +export default PointerEvents; diff --git a/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap b/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap index 95d2593f75285..ff776881ef5b4 100644 --- a/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap +++ b/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap @@ -35,6 +35,7 @@ Array [ "ended", "error", "focus", + "gotPointerCapture", "input", "invalid", "keyDown", @@ -44,6 +45,7 @@ Array [ "loadStart", "loadedData", "loadedMetadata", + "lostPointerCapture", "mouseDown", "mouseEnter", "mouseLeave", @@ -55,6 +57,14 @@ Array [ "pause", "play", "playing", + "pointerCancel", + "pointerDown", + "pointerEnter", + "pointerLeave", + "pointerMove", + "pointerOut", + "pointerOver", + "pointerUp", "progress", "rateChange", "reset", diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index 1898b0f965808..298afec9c9964 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -72,6 +72,9 @@ export const TOP_ENCRYPTED = unsafeCastStringToDOMTopLevelType('encrypted'); export const TOP_ENDED = unsafeCastStringToDOMTopLevelType('ended'); export const TOP_ERROR = unsafeCastStringToDOMTopLevelType('error'); export const TOP_FOCUS = unsafeCastStringToDOMTopLevelType('focus'); +export const TOP_GOT_POINTER_CAPTURE = unsafeCastStringToDOMTopLevelType( + 'gotpointercapture', +); export const TOP_INPUT = unsafeCastStringToDOMTopLevelType('input'); export const TOP_INVALID = unsafeCastStringToDOMTopLevelType('invalid'); export const TOP_KEY_DOWN = unsafeCastStringToDOMTopLevelType('keydown'); @@ -83,6 +86,9 @@ export const TOP_LOADED_DATA = unsafeCastStringToDOMTopLevelType('loadeddata'); export const TOP_LOADED_METADATA = unsafeCastStringToDOMTopLevelType( 'loadedmetadata', ); +export const TOP_LOST_POINTER_CAPTURE = unsafeCastStringToDOMTopLevelType( + 'lostpointercapture', +); export const TOP_MOUSE_DOWN = unsafeCastStringToDOMTopLevelType('mousedown'); export const TOP_MOUSE_MOVE = unsafeCastStringToDOMTopLevelType('mousemove'); export const TOP_MOUSE_OUT = unsafeCastStringToDOMTopLevelType('mouseout'); @@ -92,6 +98,26 @@ export const TOP_PASTE = unsafeCastStringToDOMTopLevelType('paste'); export const TOP_PAUSE = unsafeCastStringToDOMTopLevelType('pause'); export const TOP_PLAY = unsafeCastStringToDOMTopLevelType('play'); export const TOP_PLAYING = unsafeCastStringToDOMTopLevelType('playing'); +export const TOP_POINTER_CANCEL = unsafeCastStringToDOMTopLevelType( + 'pointercancel', +); +export const TOP_POINTER_DOWN = unsafeCastStringToDOMTopLevelType( + 'pointerdown', +); +export const TOP_POINTER_ENTER = unsafeCastStringToDOMTopLevelType( + 'pointerenter', +); +export const TOP_POINTER_LEAVE = unsafeCastStringToDOMTopLevelType( + 'pointerleave', +); +export const TOP_POINTER_MOVE = unsafeCastStringToDOMTopLevelType( + 'pointermove', +); +export const TOP_POINTER_OUT = unsafeCastStringToDOMTopLevelType('pointerout'); +export const TOP_POINTER_OVER = unsafeCastStringToDOMTopLevelType( + 'pointerover', +); +export const TOP_POINTER_UP = unsafeCastStringToDOMTopLevelType('pointerup'); export const TOP_PROGRESS = unsafeCastStringToDOMTopLevelType('progress'); export const TOP_RATE_CHANGE = unsafeCastStringToDOMTopLevelType('ratechange'); export const TOP_RESET = unsafeCastStringToDOMTopLevelType('reset'); diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index 3e15dd8a90cd2..d0ca03e6a3db5 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -7,8 +7,14 @@ import {accumulateEnterLeaveDispatches} from 'events/EventPropagators'; -import {TOP_MOUSE_OUT, TOP_MOUSE_OVER} from './DOMTopLevelEventTypes'; +import { + TOP_MOUSE_OUT, + TOP_MOUSE_OVER, + TOP_POINTER_OUT, + TOP_POINTER_OVER, +} from './DOMTopLevelEventTypes'; import SyntheticMouseEvent from './SyntheticMouseEvent'; +import SyntheticPointerEvent from './SyntheticPointerEvent'; import { getClosestInstanceFromNode, getNodeFromInstance, @@ -23,6 +29,14 @@ const eventTypes = { registrationName: 'onMouseLeave', dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER], }, + pointerEnter: { + registrationName: 'onPointerEnter', + dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER], + }, + pointerLeave: { + registrationName: 'onPointerLeave', + dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER], + }, }; const EnterLeaveEventPlugin = { @@ -41,14 +55,17 @@ const EnterLeaveEventPlugin = { nativeEvent, nativeEventTarget, ) { - if ( - topLevelType === TOP_MOUSE_OVER && - (nativeEvent.relatedTarget || nativeEvent.fromElement) - ) { + const isOverEvent = + topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER; + const isOutEvent = + topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT; + + if (isOverEvent && (nativeEvent.relatedTarget || nativeEvent.fromElement)) { return null; } - if (topLevelType !== TOP_MOUSE_OUT && topLevelType !== TOP_MOUSE_OVER) { - // Must not be a mouse in or mouse out - ignoring. + + if (!isOutEvent && !isOverEvent) { + // Must not be a mouse or pointer in or out - ignoring. return null; } @@ -68,7 +85,7 @@ const EnterLeaveEventPlugin = { let from; let to; - if (topLevelType === TOP_MOUSE_OUT) { + if (isOutEvent) { from = targetInst; const related = nativeEvent.relatedTarget || nativeEvent.toElement; to = related ? getClosestInstanceFromNode(related) : null; @@ -83,26 +100,43 @@ const EnterLeaveEventPlugin = { return null; } + let eventInterface, leaveEventType, enterEventType, eventTypePrefix; + + if (topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_MOUSE_OVER) { + eventInterface = SyntheticMouseEvent; + leaveEventType = eventTypes.mouseLeave; + enterEventType = eventTypes.mouseEnter; + eventTypePrefix = 'mouse'; + } else if ( + topLevelType === TOP_POINTER_OUT || + topLevelType === TOP_POINTER_OVER + ) { + eventInterface = SyntheticPointerEvent; + leaveEventType = eventTypes.pointerLeave; + enterEventType = eventTypes.pointerEnter; + eventTypePrefix = 'pointer'; + } + const fromNode = from == null ? win : getNodeFromInstance(from); const toNode = to == null ? win : getNodeFromInstance(to); - const leave = SyntheticMouseEvent.getPooled( - eventTypes.mouseLeave, + const leave = eventInterface.getPooled( + leaveEventType, from, nativeEvent, nativeEventTarget, ); - leave.type = 'mouseleave'; + leave.type = eventTypePrefix + 'leave'; leave.target = fromNode; leave.relatedTarget = toNode; - const enter = SyntheticMouseEvent.getPooled( - eventTypes.mouseEnter, + const enter = eventInterface.getPooled( + enterEventType, to, nativeEvent, nativeEventTarget, ); - enter.type = 'mouseenter'; + enter.type = eventTypePrefix + 'enter'; enter.target = toNode; enter.relatedTarget = fromNode; diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 193aff57b5528..8473b546fb655 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -29,6 +29,7 @@ import SyntheticClipboardEvent from './SyntheticClipboardEvent'; import SyntheticFocusEvent from './SyntheticFocusEvent'; import SyntheticKeyboardEvent from './SyntheticKeyboardEvent'; import SyntheticMouseEvent from './SyntheticMouseEvent'; +import SyntheticPointerEvent from './SyntheticPointerEvent'; import SyntheticDragEvent from './SyntheticDragEvent'; import SyntheticTouchEvent from './SyntheticTouchEvent'; import SyntheticTransitionEvent from './SyntheticTransitionEvent'; @@ -78,6 +79,9 @@ const interactiveEventTypeNames: Array = [ [DOMTopLevelEventTypes.TOP_PASTE, 'paste'], [DOMTopLevelEventTypes.TOP_PAUSE, 'pause'], [DOMTopLevelEventTypes.TOP_PLAY, 'play'], + [DOMTopLevelEventTypes.TOP_POINTER_CANCEL, 'pointerCancel'], + [DOMTopLevelEventTypes.TOP_POINTER_DOWN, 'pointerDown'], + [DOMTopLevelEventTypes.TOP_POINTER_UP, 'pointerUp'], [DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange'], [DOMTopLevelEventTypes.TOP_RESET, 'reset'], [DOMTopLevelEventTypes.TOP_SEEKED, 'seeked'], @@ -104,14 +108,19 @@ const nonInteractiveEventTypeNames: Array = [ [DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted'], [DOMTopLevelEventTypes.TOP_ENDED, 'ended'], [DOMTopLevelEventTypes.TOP_ERROR, 'error'], + [DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE, 'gotPointerCapture'], [DOMTopLevelEventTypes.TOP_LOAD, 'load'], [DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData'], [DOMTopLevelEventTypes.TOP_LOADED_METADATA, 'loadedMetadata'], [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'], + [DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE, 'lostPointerCapture'], [DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove'], [DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut'], [DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver'], [DOMTopLevelEventTypes.TOP_PLAYING, 'playing'], + [DOMTopLevelEventTypes.TOP_POINTER_MOVE, 'pointerMove'], + [DOMTopLevelEventTypes.TOP_POINTER_OUT, 'pointerOut'], + [DOMTopLevelEventTypes.TOP_POINTER_OVER, 'pointerOver'], [DOMTopLevelEventTypes.TOP_PROGRESS, 'progress'], [DOMTopLevelEventTypes.TOP_SCROLL, 'scroll'], [DOMTopLevelEventTypes.TOP_SEEKING, 'seeking'], @@ -282,6 +291,16 @@ const SimpleEventPlugin: PluginModule & { case DOMTopLevelEventTypes.TOP_PASTE: EventConstructor = SyntheticClipboardEvent; break; + case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE: + case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE: + case DOMTopLevelEventTypes.TOP_POINTER_CANCEL: + case DOMTopLevelEventTypes.TOP_POINTER_DOWN: + case DOMTopLevelEventTypes.TOP_POINTER_MOVE: + case DOMTopLevelEventTypes.TOP_POINTER_OUT: + case DOMTopLevelEventTypes.TOP_POINTER_OVER: + case DOMTopLevelEventTypes.TOP_POINTER_UP: + EventConstructor = SyntheticPointerEvent; + break; default: if (__DEV__) { if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) { diff --git a/packages/react-dom/src/events/SyntheticPointerEvent.js b/packages/react-dom/src/events/SyntheticPointerEvent.js new file mode 100644 index 0000000000000..96a488f18632f --- /dev/null +++ b/packages/react-dom/src/events/SyntheticPointerEvent.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import SyntheticMouseEvent from './SyntheticMouseEvent'; + +/** + * @interface PointerEvent + * @see http://www.w3.org/TR/pointerevents/ + */ +const SyntheticPointerEvent = SyntheticMouseEvent.extend({ + pointerId: null, + width: null, + height: null, + pressure: null, + tiltX: null, + tiltY: null, + pointerType: null, + isPrimary: null, +}); + +export default SyntheticPointerEvent; diff --git a/packages/react-dom/src/events/TapEventPlugin.js b/packages/react-dom/src/events/TapEventPlugin.js index 772e5d405fab2..e1baab05eac4b 100644 --- a/packages/react-dom/src/events/TapEventPlugin.js +++ b/packages/react-dom/src/events/TapEventPlugin.js @@ -15,6 +15,10 @@ import { TOP_MOUSE_DOWN, TOP_MOUSE_MOVE, TOP_MOUSE_UP, + TOP_POINTER_CANCEL, + TOP_POINTER_DOWN, + TOP_POINTER_UP, + TOP_POINTER_MOVE, TOP_TOUCH_CANCEL, TOP_TOUCH_END, TOP_TOUCH_MOVE, @@ -23,14 +27,20 @@ import { import SyntheticUIEvent from './SyntheticUIEvent'; function isStartish(topLevelType) { - return topLevelType === TOP_MOUSE_DOWN || topLevelType === TOP_TOUCH_START; + return ( + topLevelType === TOP_MOUSE_DOWN || + topLevelType === TOP_TOUCH_START || + topLevelType === TOP_POINTER_DOWN + ); } function isEndish(topLevelType) { return ( topLevelType === TOP_MOUSE_UP || - topLevelType === TOP_TOUCH_END || - topLevelType === TOP_TOUCH_CANCEL + topLevelType === TOP_POINTER_CANCEL || + topLevelType === TOP_POINTER_UP || + topLevelType === TOP_TOUCH_CANCEL || + topLevelType === TOP_TOUCH_END ); } @@ -102,8 +112,16 @@ const touchEvents = [ TOP_TOUCH_MOVE, ]; +const pointerEvents = [ + TOP_POINTER_CANCEL, + TOP_POINTER_DOWN, + TOP_POINTER_MOVE, + TOP_POINTER_UP, +]; + const dependencies = [TOP_MOUSE_DOWN, TOP_MOUSE_MOVE, TOP_MOUSE_UP].concat( touchEvents, + pointerEvents, ); const eventTypes = {
Please select a test fixture.
+ Pointer Overs: {overs} + Pointer Outs: {outs} + Pointer Enters: {enters} + Pointer Leaves: {leaves} +