Skip to content

Commit

Permalink
Support Pointer Events
Browse files Browse the repository at this point in the history
This PR adds support for Pointer Events as discussed in facebook#499. It is
heavily based on previous work in facebook#1389 and will add most pointer events
to the `SimpleEventPlugin` and enter/leave events to
`EnterLeaveEventPlugin`.

I added a new DOM fixture to test all pointer events and make sure my
implementation does indeed work. I tested on Chrome 65 and Firefox 59
without seeing any issues. If you think the fixtures is not necessary
for future changes, I'm happy to remove them as well.

The only open question is if we want to add a polyfill. For the sake of
simplicity, I opted against a polyfill for this PR. However, this work
is compatible with [PEP][] (I've verified this behavior in Safari 11 by
loading PEP in `fixtures/dom/public/index.html`).

[PEP]: https://github.com/jquery/PEP
  • Loading branch information
philipp-spiess committed Mar 31, 2018
1 parent fa8e678 commit 9bdee33
Show file tree
Hide file tree
Showing 15 changed files with 424 additions and 57 deletions.
1 change: 1 addition & 0 deletions fixtures/dom/src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class Header extends React.Component {
<option value="/event-pooling">Event Pooling</option>
<option value="/custom-elements">Custom Elements</option>
<option value="/media-events">Media Events</option>
<option value="/pointer-events">Pointer Events</option>
</select>
</label>
<label htmlFor="react_version">
Expand Down
3 changes: 3 additions & 0 deletions fixtures/dom/src/components/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -46,6 +47,8 @@ function FixturesPage() {
return <CustomElementFixtures />;
case '/media-events':
return <MediaEventsFixtures />;
case '/pointer-events':
return <PointerEventsFixtures />;
default:
return <p>Please select a test fixture.</p>;
}
Expand Down
99 changes: 99 additions & 0 deletions fixtures/dom/src/components/fixtures/pointer-events/draw-box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const React = window.React;

class DrawBox extends React.Component {
state = {
isDrawing: false,
isCapturing: false,
isOver: false,
x: 0,
y: 0,
};

el = React.createRef();

onDown = event => {
this.setState({
isDrawing: true,
...this.extractRelativeCoordinates(event),
});

this.el.current.setPointerCapture(event.pointerId);
};

onMove = event => {
if (!this.state.isDrawing) {
return;
}

const nextState = this.extractRelativeCoordinates(event);

const ctx = this.el.current.getContext('2d');
ctx.beginPath();
ctx.moveTo(this.state.x, this.state.y);
ctx.lineTo(nextState.x, nextState.y);
ctx.stroke();
ctx.closePath();

this.setState(nextState);
};

onUp = event => {
this.setState({
isDrawing: false,
});
};

onOver = event => {
this.setState({isOver: true});
};

onOut = event => {
this.setState({isOver: false});
};

onGotCapture = event => {
this.setState({isCapturing: true});
};

onLostCapture = event => {
this.setState({isCapturing: false});
};

extractRelativeCoordinates = event => {
const rect = this.el.current.getBoundingClientRect();

return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
};

render() {
const {isOver, isCapturing} = this.state;

const boxStyle = {
border: `1px solid ${isCapturing ? 'blue' : isOver ? 'red' : '#d9d9d9'}`,
margin: '10px 0 20px',
touchAction: 'none',
};

return (
<canvas
ref={this.el}
width={300}
height={300}
style={boxStyle}
onPointerDown={this.onDown}
onPointerMove={this.onMove}
onPointerUp={this.onUp}
onPointerCancel={this.onUp}
onPointerOver={this.onOver}
onPointerOut={this.onOut}
onGotPointerCapture={this.onGotCapture}
onLostPointerCapture={this.onLostCapture}
/>
);
}
}

export default DrawBox;
27 changes: 27 additions & 0 deletions fixtures/dom/src/components/fixtures/pointer-events/drawing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import TestCase from '../../TestCase';
import DrawBox from './draw-box';

const React = window.React;

class Drawing extends React.Component {
render() {
return (
<TestCase title="Drawing" description="">
<TestCase.Steps>
<li>Draw on the canvas below with any pointer tool</li>
</TestCase.Steps>

<TestCase.ExpectedResult>
You should see strokes as you move the pointer tool. While your
pointer tool is over the canvas, it should also have a red border.
While drawing, the canvas must have a blue border to signalize that a
pointer capture was received.
</TestCase.ExpectedResult>

<DrawBox />
</TestCase>
);
}
}

export default Drawing;
34 changes: 34 additions & 0 deletions fixtures/dom/src/components/fixtures/pointer-events/hover-box.js
Original file line number Diff line number Diff line change
@@ -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 (
<div
style={boxStyle}
onPointerOver={this.props.onOver}
onPointerOut={this.props.onOut}
onPointerEnter={this.props.onEnter}
onPointerLeave={this.props.onLeave}>
<div style={obstacleStyle} />
<div style={obstacleStyle} />
</div>
);
}
}

export default DrawBox;
62 changes: 62 additions & 0 deletions fixtures/dom/src/components/fixtures/pointer-events/hover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 (
<TestCase title="Hover" description="">
<TestCase.Steps>
<li>Hover over the above box and the obstacles</li>
</TestCase.Steps>

<TestCase.ExpectedResult>
Overs and outs should increase when moving over the obstacles but
enters and leaves should not.
</TestCase.ExpectedResult>

<HoverBox
onOver={this.onOver}
onOut={this.onOut}
onEnter={this.onEnter}
onLeave={this.onLeave}
/>

<p>
Pointer Overs: <b>{overs}</b> <br />
Pointer Outs: <b>{outs}</b> <br />
Pointer Enters: <b>{enters}</b> <br />
Pointer Leaves: <b>{leaves}</b> <br />
</p>
</TestCase>
);
}
}

export default Hover;
18 changes: 18 additions & 0 deletions fixtures/dom/src/components/fixtures/pointer-events/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import FixtureSet from '../../FixtureSet';
import Drawing from './drawing';
import Hover from './hover';

const React = window.React;

class PointerEvents extends React.Component {
render() {
return (
<FixtureSet title="Pointer Events" description="">
<Drawing />
<Hover />
</FixtureSet>
);
}
}

export default PointerEvents;
15 changes: 13 additions & 2 deletions packages/events/EventPluginUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,27 @@ export const injection = {
export function isEndish(topLevelType) {
return (
topLevelType === 'topMouseUp' ||
topLevelType === 'topPointerUp' ||
topLevelType === 'topPointerCancel' ||
topLevelType === 'topTouchEnd' ||
topLevelType === 'topTouchCancel'
);
}

export function isMoveish(topLevelType) {
return topLevelType === 'topMouseMove' || topLevelType === 'topTouchMove';
return (
topLevelType === 'topMouseMove' ||
topLevelType === 'topPointerMove' ||
topLevelType === 'topTouchMove'
);
}

export function isStartish(topLevelType) {
return topLevelType === 'topMouseDown' || topLevelType === 'topTouchStart';
return (
topLevelType === 'topMouseDown' ||
topLevelType === 'topPointerDown' ||
topLevelType === 'topTouchStart'
);
}

let validateEventDispatches;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Array [
"ended",
"error",
"focus",
"gotPointerCapture",
"input",
"invalid",
"keyDown",
Expand All @@ -44,6 +45,7 @@ Array [
"loadStart",
"loadedData",
"loadedMetadata",
"lostPointerCapture",
"mouseDown",
"mouseEnter",
"mouseLeave",
Expand All @@ -55,6 +57,14 @@ Array [
"pause",
"play",
"playing",
"pointerCancel",
"pointerDown",
"pointerEnter",
"pointerLeave",
"pointerMove",
"pointerOut",
"pointerOver",
"pointerUp",
"progress",
"rateChange",
"reset",
Expand Down Expand Up @@ -112,6 +122,7 @@ Array [
"ended",
"error",
"focus",
"gotPointerCapture",
"input",
"keyDown",
"keyPress",
Expand All @@ -120,6 +131,7 @@ Array [
"loadStart",
"loadedData",
"loadedMetadata",
"lostPointerCapture",
"mouseDown",
"mouseMove",
"mouseOut",
Expand All @@ -129,6 +141,14 @@ Array [
"pause",
"play",
"playing",
"pointerCancel",
"pointerDown",
"pointerEnter",
"pointerLeave",
"pointerMove",
"pointerOut",
"pointerOver",
"pointerUp",
"progress",
"rateChange",
"scroll",
Expand Down
10 changes: 10 additions & 0 deletions packages/react-dom/src/events/BrowserEventConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export const topLevelTypes = {
topTouchStart: 'touchstart',
topTransitionEnd: getVendorPrefixedEventName('transitionend'),
topWheel: 'wheel',
topPointerUp: 'pointerup',
topPointerDown: 'pointerdown',
topPointerCancel: 'pointercancel',
topPointerOver: 'pointerover',
topPointerOut: 'pointerout',
topPointerMove: 'pointermove',
topGotPointerCapture: 'gotpointercapture',
topLostPointerCapture: 'lostpointercapture',
topPointerEnter: 'pointerenter',
topPointerLeave: 'pointerleave',
};

// There are so many media events, it makes sense to just
Expand Down
Loading

0 comments on commit 9bdee33

Please sign in to comment.