feat(swipe): Touch support (for MSFT Surface and others), natural scrolling detection for Mac OS X

Bonus - you can test touch events in Chrome using device mode: https://developer.chrome.com/devtools/docs/device-mode
This commit is contained in:
Ben Gotow 2016-02-22 11:12:49 -08:00
parent 7a873cac58
commit 4b6617b272

View file

@ -1,5 +1,6 @@
import React, {Component, PropTypes} from 'react';
import _ from 'underscore';
import {exec} from 'child_process';
const Phase = {
// No wheel events received yet, container is inactive.
@ -15,6 +16,25 @@ const Phase = {
Settling: 'settling',
}
let SwipeInverted = false;
if (process.platform === 'darwin') {
exec("defaults read -g com.apple.swipescrolldirection", (err, stdout)=> {
if (err !== null) {
return;
}
if (stdout.toString().trim() === '1') {
SwipeInverted = true;
}
});
} else if (process.platform === 'win32') {
// Currently does not matter because we don't support trackpad gestures on Win.
// It appears there's a config key called FlipFlopWheel which we might have to
// check, but it also looks like disabling natural scroll on Win32 only changes
// vertical, not horizontal, behavior.
}
export default class SwipeContainer extends Component {
static displayName = 'SwipeContainer';
@ -29,6 +49,7 @@ export default class SwipeContainer extends Component {
constructor(props) {
super(props);
this.tracking = false;
this.trackingTouchIdentifier = null;
this.phase = Phase.None;
this.fired = false;
this.state = {
@ -44,11 +65,6 @@ export default class SwipeContainer extends Component {
window.addEventListener('scroll-touch-end', this._onScrollTouchEnd);
}
// componentWillReceiveProps(nextProps) {
// if (nextProps.children !== this.props.children) {
// this.setState({currentX: 0, velocity: 0, targetX: 0, settling: false});
// }
// }
componentDidUpdate() {
if (this.phase === Phase.Settling) {
window.requestAnimationFrame(()=> {
@ -69,11 +85,21 @@ export default class SwipeContainer extends Component {
}
_onWheel = (e)=> {
let velocity = e.deltaX / 3;
if (SwipeInverted) {
velocity = -velocity;
}
this._onDragWithVelocity(velocity);
if (this.phase === Phase.GestureConfirmed) {
e.preventDefault();
}
}
_onDragWithVelocity = (velocity)=> {
if ((this.tracking === false) || !this._isEnabled()) {
return;
}
const velocity = e.deltaX / 3;
const velocityConfirmsGesture = Math.abs(velocity) > 3;
if (this.phase === Phase.None) {
@ -84,10 +110,6 @@ export default class SwipeContainer extends Component {
this.phase = Phase.GestureConfirmed;
}
if (this.phase === Phase.GestureConfirmed) {
e.preventDefault();
}
let {fullDistance, thresholdDistance} = this.state;
if (fullDistance === 'unknown') {
@ -99,13 +121,13 @@ export default class SwipeContainer extends Component {
const estimatedSettleX = currentX + velocity * 8;
let targetX = 0;
if (this.props.onSwipeLeft && (estimatedSettleX > thresholdDistance)) {
if (this.props.onSwipeRight && (estimatedSettleX > thresholdDistance)) {
targetX = fullDistance;
}
if (this.props.onSwipeRight && (estimatedSettleX < -thresholdDistance)) {
if (this.props.onSwipeLeft && (estimatedSettleX < -thresholdDistance)) {
targetX = -fullDistance;
}
this.setState({thresholdDistance, fullDistance, velocity, currentX, targetX})
this.setState({thresholdDistance, fullDistance, velocity, currentX, targetX});
}
_onScrollTouchBegin = ()=> {
@ -121,6 +143,56 @@ export default class SwipeContainer extends Component {
}
}
_onTouchStart = (e)=> {
if ((this.trackingTouchIdentifier === null) && (e.targetTouches.length > 0)) {
const touch = e.targetTouches.item(0);
this.trackingTouchIdentifier = touch.identifier;
this.trackingTouchX = touch.clientX;
this._onScrollTouchBegin();
}
}
_onTouchMove = (e)=> {
if (this.trackingTouchIdentifier === null) {
return;
}
if (e.cancelable === false) {
// Chrome has already started interpreting these touch events as a scroll.
// We can no longer call preventDefault to make them ours.
return;
}
let trackingTouch = null;
for (let ii = 0; ii < e.changedTouches.length; ii++) {
const touch = e.changedTouches.item(ii);
if (touch.identifier === this.trackingTouchIdentifier) {
trackingTouch = touch;
break;
}
}
if (trackingTouch !== null) {
const velocity = (trackingTouch.clientX - this.trackingTouchX);
this.trackingTouchX = trackingTouch.clientX;
this._onDragWithVelocity(velocity);
if (this.phase === Phase.GestureConfirmed) {
e.preventDefault();
}
}
}
_onTouchEnd = (e)=> {
if (this.trackingTouchIdentifier === null) {
return;
}
for (let ii = 0; ii < e.changedTouches.length; ii++) {
if (e.changedTouches.item(ii).identifier === this.trackingTouchIdentifier) {
this.trackingTouchIdentifier = null;
this._onScrollTouchEnd();
break;
}
}
}
_settle() {
const {currentX, targetX} = this.state;
let {velocity} = this.state;
@ -149,10 +221,10 @@ export default class SwipeContainer extends Component {
if (shouldFire) {
this.fired = true;
if (targetX < 0) {
if (targetX > 0) {
this.props.onSwipeRight();
}
if (targetX > 0) {
if (targetX < 0) {
this.props.onSwipeLeft();
}
}
@ -182,25 +254,30 @@ export default class SwipeContainer extends Component {
const backingStyles = {top: 0, bottom: 0, position: 'absolute'};
let backingClass = 'swipe-backing';
if (currentX > 0) {
if (currentX < 0) {
backingClass += ' ' + this.props.onSwipeLeftClass;
backingStyles.right = 0;
backingStyles.width = currentX + 1;
if (targetX > 0) {
backingClass += ' confirmed';
}
} else if (currentX < 0) {
backingClass += ' ' + this.props.onSwipeRightClass;
backingStyles.left = 0;
backingStyles.width = -currentX + 1;
if (targetX < 0) {
backingClass += ' confirmed';
}
} else if (currentX > 0) {
backingClass += ' ' + this.props.onSwipeRightClass;
backingStyles.left = 0;
backingStyles.width = currentX + 1;
if (targetX > 0) {
backingClass += ' confirmed';
}
}
return (
<div onWheel={this._onWheel} {...otherProps}>
<div onWheel={this._onWheel}
onTouchStart={this._onTouchStart}
onTouchMove={this._onTouchMove}
onTouchEnd={this._onTouchEnd}
onTouchCancel={this._onTouchEnd}
{...otherProps}>
<div style={backingStyles} className={backingClass}></div>
<div style={{transform: 'translate3d(' + -currentX + 'px, 0, 0)'}}>
<div style={{transform: 'translate3d(' + currentX + 'px, 0, 0)'}}>
{this.props.children}
</div>
</div>