mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-17 13:38:11 +08:00
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:
parent
7a873cac58
commit
4b6617b272
1 changed files with 103 additions and 26 deletions
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue