Skip to content

Commit 9bdee33

Browse files
Support Pointer Events
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
1 parent fa8e678 commit 9bdee33

File tree

15 files changed

+424
-57
lines changed

15 files changed

+424
-57
lines changed

fixtures/dom/src/components/Header.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class Header extends React.Component {
6464
<option value="/event-pooling">Event Pooling</option>
6565
<option value="/custom-elements">Custom Elements</option>
6666
<option value="/media-events">Media Events</option>
67+
<option value="/pointer-events">Pointer Events</option>
6768
</select>
6869
</label>
6970
<label htmlFor="react_version">

fixtures/dom/src/components/fixtures/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ErrorHandling from './error-handling';
1111
import EventPooling from './event-pooling';
1212
import CustomElementFixtures from './custom-elements';
1313
import MediaEventsFixtures from './media-events';
14+
import PointerEventsFixtures from './pointer-events';
1415

1516
const React = window.React;
1617

@@ -46,6 +47,8 @@ function FixturesPage() {
4647
return <CustomElementFixtures />;
4748
case '/media-events':
4849
return <MediaEventsFixtures />;
50+
case '/pointer-events':
51+
return <PointerEventsFixtures />;
4952
default:
5053
return <p>Please select a test fixture.</p>;
5154
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const React = window.React;
2+
3+
class DrawBox extends React.Component {
4+
state = {
5+
isDrawing: false,
6+
isCapturing: false,
7+
isOver: false,
8+
x: 0,
9+
y: 0,
10+
};
11+
12+
el = React.createRef();
13+
14+
onDown = event => {
15+
this.setState({
16+
isDrawing: true,
17+
...this.extractRelativeCoordinates(event),
18+
});
19+
20+
this.el.current.setPointerCapture(event.pointerId);
21+
};
22+
23+
onMove = event => {
24+
if (!this.state.isDrawing) {
25+
return;
26+
}
27+
28+
const nextState = this.extractRelativeCoordinates(event);
29+
30+
const ctx = this.el.current.getContext('2d');
31+
ctx.beginPath();
32+
ctx.moveTo(this.state.x, this.state.y);
33+
ctx.lineTo(nextState.x, nextState.y);
34+
ctx.stroke();
35+
ctx.closePath();
36+
37+
this.setState(nextState);
38+
};
39+
40+
onUp = event => {
41+
this.setState({
42+
isDrawing: false,
43+
});
44+
};
45+
46+
onOver = event => {
47+
this.setState({isOver: true});
48+
};
49+
50+
onOut = event => {
51+
this.setState({isOver: false});
52+
};
53+
54+
onGotCapture = event => {
55+
this.setState({isCapturing: true});
56+
};
57+
58+
onLostCapture = event => {
59+
this.setState({isCapturing: false});
60+
};
61+
62+
extractRelativeCoordinates = event => {
63+
const rect = this.el.current.getBoundingClientRect();
64+
65+
return {
66+
x: event.clientX - rect.left,
67+
y: event.clientY - rect.top,
68+
};
69+
};
70+
71+
render() {
72+
const {isOver, isCapturing} = this.state;
73+
74+
const boxStyle = {
75+
border: `1px solid ${isCapturing ? 'blue' : isOver ? 'red' : '#d9d9d9'}`,
76+
margin: '10px 0 20px',
77+
touchAction: 'none',
78+
};
79+
80+
return (
81+
<canvas
82+
ref={this.el}
83+
width={300}
84+
height={300}
85+
style={boxStyle}
86+
onPointerDown={this.onDown}
87+
onPointerMove={this.onMove}
88+
onPointerUp={this.onUp}
89+
onPointerCancel={this.onUp}
90+
onPointerOver={this.onOver}
91+
onPointerOut={this.onOut}
92+
onGotPointerCapture={this.onGotCapture}
93+
onLostPointerCapture={this.onLostCapture}
94+
/>
95+
);
96+
}
97+
}
98+
99+
export default DrawBox;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import TestCase from '../../TestCase';
2+
import DrawBox from './draw-box';
3+
4+
const React = window.React;
5+
6+
class Drawing extends React.Component {
7+
render() {
8+
return (
9+
<TestCase title="Drawing" description="">
10+
<TestCase.Steps>
11+
<li>Draw on the canvas below with any pointer tool</li>
12+
</TestCase.Steps>
13+
14+
<TestCase.ExpectedResult>
15+
You should see strokes as you move the pointer tool. While your
16+
pointer tool is over the canvas, it should also have a red border.
17+
While drawing, the canvas must have a blue border to signalize that a
18+
pointer capture was received.
19+
</TestCase.ExpectedResult>
20+
21+
<DrawBox />
22+
</TestCase>
23+
);
24+
}
25+
}
26+
27+
export default Drawing;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const React = window.React;
2+
3+
class DrawBox extends React.Component {
4+
render() {
5+
const boxStyle = {
6+
border: `1px solid #d9d9d9`,
7+
margin: '10px 0 20px',
8+
padding: '20px 20px',
9+
touchAction: 'none',
10+
};
11+
12+
const obstacleStyle = {
13+
border: `1px solid #d9d9d9`,
14+
width: '25%',
15+
height: '200px',
16+
margin: '12.5%',
17+
display: 'inline-block',
18+
};
19+
20+
return (
21+
<div
22+
style={boxStyle}
23+
onPointerOver={this.props.onOver}
24+
onPointerOut={this.props.onOut}
25+
onPointerEnter={this.props.onEnter}
26+
onPointerLeave={this.props.onLeave}>
27+
<div style={obstacleStyle} />
28+
<div style={obstacleStyle} />
29+
</div>
30+
);
31+
}
32+
}
33+
34+
export default DrawBox;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import TestCase from '../../TestCase';
2+
import HoverBox from './hover-box';
3+
4+
const React = window.React;
5+
6+
class Hover extends React.Component {
7+
state = {
8+
overs: 0,
9+
outs: 0,
10+
enters: 0,
11+
leaves: 0,
12+
};
13+
14+
onOver = () => {
15+
this.setState({overs: this.state.overs + 1});
16+
};
17+
18+
onOut = () => {
19+
this.setState({outs: this.state.outs + 1});
20+
};
21+
22+
onEnter = () => {
23+
this.setState({enters: this.state.enters + 1});
24+
};
25+
26+
onLeave = () => {
27+
this.setState({leaves: this.state.leaves + 1});
28+
};
29+
30+
render() {
31+
const {overs, outs, enters, leaves} = this.state;
32+
33+
return (
34+
<TestCase title="Hover" description="">
35+
<TestCase.Steps>
36+
<li>Hover over the above box and the obstacles</li>
37+
</TestCase.Steps>
38+
39+
<TestCase.ExpectedResult>
40+
Overs and outs should increase when moving over the obstacles but
41+
enters and leaves should not.
42+
</TestCase.ExpectedResult>
43+
44+
<HoverBox
45+
onOver={this.onOver}
46+
onOut={this.onOut}
47+
onEnter={this.onEnter}
48+
onLeave={this.onLeave}
49+
/>
50+
51+
<p>
52+
Pointer Overs: <b>{overs}</b> <br />
53+
Pointer Outs: <b>{outs}</b> <br />
54+
Pointer Enters: <b>{enters}</b> <br />
55+
Pointer Leaves: <b>{leaves}</b> <br />
56+
</p>
57+
</TestCase>
58+
);
59+
}
60+
}
61+
62+
export default Hover;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import FixtureSet from '../../FixtureSet';
2+
import Drawing from './drawing';
3+
import Hover from './hover';
4+
5+
const React = window.React;
6+
7+
class PointerEvents extends React.Component {
8+
render() {
9+
return (
10+
<FixtureSet title="Pointer Events" description="">
11+
<Drawing />
12+
<Hover />
13+
</FixtureSet>
14+
);
15+
}
16+
}
17+
18+
export default PointerEvents;

packages/events/EventPluginUtils.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,27 @@ export const injection = {
3333
export function isEndish(topLevelType) {
3434
return (
3535
topLevelType === 'topMouseUp' ||
36+
topLevelType === 'topPointerUp' ||
37+
topLevelType === 'topPointerCancel' ||
3638
topLevelType === 'topTouchEnd' ||
3739
topLevelType === 'topTouchCancel'
3840
);
3941
}
4042

4143
export function isMoveish(topLevelType) {
42-
return topLevelType === 'topMouseMove' || topLevelType === 'topTouchMove';
44+
return (
45+
topLevelType === 'topMouseMove' ||
46+
topLevelType === 'topPointerMove' ||
47+
topLevelType === 'topTouchMove'
48+
);
4349
}
50+
4451
export function isStartish(topLevelType) {
45-
return topLevelType === 'topMouseDown' || topLevelType === 'topTouchStart';
52+
return (
53+
topLevelType === 'topMouseDown' ||
54+
topLevelType === 'topPointerDown' ||
55+
topLevelType === 'topTouchStart'
56+
);
4657
}
4758

4859
let validateEventDispatches;

packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Array [
3535
"ended",
3636
"error",
3737
"focus",
38+
"gotPointerCapture",
3839
"input",
3940
"invalid",
4041
"keyDown",
@@ -44,6 +45,7 @@ Array [
4445
"loadStart",
4546
"loadedData",
4647
"loadedMetadata",
48+
"lostPointerCapture",
4749
"mouseDown",
4850
"mouseEnter",
4951
"mouseLeave",
@@ -55,6 +57,14 @@ Array [
5557
"pause",
5658
"play",
5759
"playing",
60+
"pointerCancel",
61+
"pointerDown",
62+
"pointerEnter",
63+
"pointerLeave",
64+
"pointerMove",
65+
"pointerOut",
66+
"pointerOver",
67+
"pointerUp",
5868
"progress",
5969
"rateChange",
6070
"reset",
@@ -112,6 +122,7 @@ Array [
112122
"ended",
113123
"error",
114124
"focus",
125+
"gotPointerCapture",
115126
"input",
116127
"keyDown",
117128
"keyPress",
@@ -120,6 +131,7 @@ Array [
120131
"loadStart",
121132
"loadedData",
122133
"loadedMetadata",
134+
"lostPointerCapture",
123135
"mouseDown",
124136
"mouseMove",
125137
"mouseOut",
@@ -129,6 +141,14 @@ Array [
129141
"pause",
130142
"play",
131143
"playing",
144+
"pointerCancel",
145+
"pointerDown",
146+
"pointerEnter",
147+
"pointerLeave",
148+
"pointerMove",
149+
"pointerOut",
150+
"pointerOver",
151+
"pointerUp",
132152
"progress",
133153
"rateChange",
134154
"scroll",

packages/react-dom/src/events/BrowserEventConstants.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ export const topLevelTypes = {
6161
topTouchStart: 'touchstart',
6262
topTransitionEnd: getVendorPrefixedEventName('transitionend'),
6363
topWheel: 'wheel',
64+
topPointerUp: 'pointerup',
65+
topPointerDown: 'pointerdown',
66+
topPointerCancel: 'pointercancel',
67+
topPointerOver: 'pointerover',
68+
topPointerOut: 'pointerout',
69+
topPointerMove: 'pointermove',
70+
topGotPointerCapture: 'gotpointercapture',
71+
topLostPointerCapture: 'lostpointercapture',
72+
topPointerEnter: 'pointerenter',
73+
topPointerLeave: 'pointerleave',
6474
};
6575

6676
// There are so many media events, it makes sense to just

0 commit comments

Comments
 (0)