467 lines
14 KiB
JavaScript
467 lines
14 KiB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
|
|
import _objectSpread from "@babel/runtime/helpers/objectSpread";
|
|
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
|
|
/* eslint-disable consistent-this */
|
|
// @inheritedComponent Drawer
|
|
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import ReactDOM from 'react-dom';
|
|
import { polyfill } from 'react-lifecycles-compat';
|
|
import Drawer, { getAnchor, isHorizontal } from '../Drawer/Drawer';
|
|
import { duration } from '../styles/transitions';
|
|
import withTheme from '../styles/withTheme';
|
|
import { getTransitionProps } from '../transitions/utils';
|
|
import SwipeArea from './SwipeArea';
|
|
const Fragment = React.Fragment || 'div'; // This value is closed to what browsers are using internally to
|
|
// trigger a native scroll.
|
|
|
|
const UNCERTAINTY_THRESHOLD = 3; // px
|
|
// We can only have one node at the time claiming ownership for handling the swipe.
|
|
// Otherwise, the UX would be confusing.
|
|
// That's why we use a singleton here.
|
|
|
|
let nodeThatClaimedTheSwipe = null; // Exported for test purposes.
|
|
|
|
export function reset() {
|
|
nodeThatClaimedTheSwipe = null;
|
|
}
|
|
|
|
class SwipeableDrawer extends React.Component {
|
|
constructor(...args) {
|
|
var _temp;
|
|
|
|
return _temp = super(...args), Object.defineProperty(this, "state", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: {}
|
|
}), Object.defineProperty(this, "handleBodyTouchStart", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: event => {
|
|
// We are not supposed to hanlde this touch move.
|
|
if (nodeThatClaimedTheSwipe !== null && nodeThatClaimedTheSwipe !== this) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
disableDiscovery,
|
|
disableSwipeToOpen,
|
|
open,
|
|
swipeAreaWidth
|
|
} = this.props;
|
|
const anchor = getAnchor(this.props);
|
|
const currentX = anchor === 'right' ? document.body.offsetWidth - event.touches[0].pageX : event.touches[0].pageX;
|
|
const currentY = anchor === 'bottom' ? window.innerHeight - event.touches[0].clientY : event.touches[0].clientY;
|
|
|
|
if (!open) {
|
|
if (disableSwipeToOpen) {
|
|
return;
|
|
}
|
|
|
|
if (isHorizontal(this.props)) {
|
|
if (currentX > swipeAreaWidth) {
|
|
return;
|
|
}
|
|
} else if (currentY > swipeAreaWidth) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
nodeThatClaimedTheSwipe = this;
|
|
this.startX = currentX;
|
|
this.startY = currentY;
|
|
this.setState({
|
|
maybeSwiping: true
|
|
});
|
|
|
|
if (!open && this.paper) {
|
|
// the ref may be null when a parent component updates while swiping
|
|
this.setPosition(this.getMaxTranslate() + (disableDiscovery ? 20 : -swipeAreaWidth), {
|
|
changeTransition: false
|
|
});
|
|
}
|
|
|
|
document.body.addEventListener('touchmove', this.handleBodyTouchMove, {
|
|
passive: false
|
|
});
|
|
document.body.addEventListener('touchend', this.handleBodyTouchEnd); // https://plus.google.com/+PaulIrish/posts/KTwfn1Y2238
|
|
|
|
document.body.addEventListener('touchcancel', this.handleBodyTouchEnd);
|
|
}
|
|
}), Object.defineProperty(this, "handleBodyTouchMove", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: event => {
|
|
// the ref may be null when a parent component updates while swiping
|
|
if (!this.paper) return;
|
|
const anchor = getAnchor(this.props);
|
|
const horizontalSwipe = isHorizontal(this.props);
|
|
const currentX = anchor === 'right' ? document.body.offsetWidth - event.touches[0].pageX : event.touches[0].pageX;
|
|
const currentY = anchor === 'bottom' ? window.innerHeight - event.touches[0].clientY : event.touches[0].clientY; // We don't know yet.
|
|
|
|
if (this.isSwiping == null) {
|
|
const dx = Math.abs(currentX - this.startX);
|
|
const dy = Math.abs(currentY - this.startY); // We are likely to be swiping, let's prevent the scroll event on iOS.
|
|
|
|
if (dx > dy) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
const isSwiping = horizontalSwipe ? dx > dy && dx > UNCERTAINTY_THRESHOLD : dy > dx && dy > UNCERTAINTY_THRESHOLD;
|
|
|
|
if (isSwiping === true || (horizontalSwipe ? dy > UNCERTAINTY_THRESHOLD : dx > UNCERTAINTY_THRESHOLD)) {
|
|
this.isSwiping = isSwiping;
|
|
|
|
if (!isSwiping) {
|
|
this.handleBodyTouchEnd(event);
|
|
return;
|
|
} // Shift the starting point.
|
|
|
|
|
|
this.startX = currentX;
|
|
this.startY = currentY; // Compensate for the part of the drawer displayed on touch start.
|
|
|
|
if (!this.props.disableDiscovery && !this.props.open) {
|
|
if (horizontalSwipe) {
|
|
this.startX -= this.props.swipeAreaWidth;
|
|
} else {
|
|
this.startY -= this.props.swipeAreaWidth;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!this.isSwiping) {
|
|
return;
|
|
} // We are swiping, let's prevent the scroll event on iOS.
|
|
|
|
|
|
event.preventDefault();
|
|
this.setPosition(this.getTranslate(horizontalSwipe ? currentX : currentY));
|
|
}
|
|
}), Object.defineProperty(this, "handleBodyTouchEnd", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: event => {
|
|
nodeThatClaimedTheSwipe = null;
|
|
this.removeBodyTouchListeners();
|
|
this.setState({
|
|
maybeSwiping: false
|
|
}); // The swipe wasn't started.
|
|
|
|
if (!this.isSwiping) {
|
|
this.isSwiping = null;
|
|
return;
|
|
}
|
|
|
|
const anchor = getAnchor(this.props);
|
|
let current;
|
|
|
|
if (isHorizontal(this.props)) {
|
|
current = anchor === 'right' ? document.body.offsetWidth - event.changedTouches[0].pageX : event.changedTouches[0].pageX;
|
|
} else {
|
|
current = anchor === 'bottom' ? window.innerHeight - event.changedTouches[0].clientY : event.changedTouches[0].clientY;
|
|
}
|
|
|
|
const translateRatio = this.getTranslate(current) / this.getMaxTranslate(); // We have to open or close after setting swiping to null,
|
|
// because only then CSS transition is enabled.
|
|
|
|
if (translateRatio > 0.5) {
|
|
if (this.isSwiping && !this.props.open) {
|
|
// Reset the position, the swipe was aborted.
|
|
this.setPosition(this.getMaxTranslate(), {
|
|
mode: 'enter'
|
|
});
|
|
} else {
|
|
this.props.onClose();
|
|
}
|
|
} else if (this.isSwiping && !this.props.open) {
|
|
this.props.onOpen();
|
|
} else {
|
|
// Reset the position, the swipe was aborted.
|
|
this.setPosition(0, {
|
|
mode: 'exit'
|
|
});
|
|
}
|
|
|
|
this.isSwiping = null;
|
|
}
|
|
}), Object.defineProperty(this, "backdrop", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: null
|
|
}), Object.defineProperty(this, "paper", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: null
|
|
}), Object.defineProperty(this, "isSwiping", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: null
|
|
}), Object.defineProperty(this, "startX", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: null
|
|
}), Object.defineProperty(this, "startY", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: null
|
|
}), Object.defineProperty(this, "handleBackdropRef", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: node => {
|
|
this.backdrop = node ? ReactDOM.findDOMNode(node) : null;
|
|
}
|
|
}), Object.defineProperty(this, "handlePaperRef", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: node => {
|
|
this.paper = node ? ReactDOM.findDOMNode(node) : null;
|
|
}
|
|
}), _temp;
|
|
}
|
|
|
|
static getDerivedStateFromProps() {
|
|
// Reset the maybeSwiping state everytime we receive new properties.
|
|
return {
|
|
maybeSwiping: false
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.variant === 'temporary') {
|
|
this.listenTouchStart();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const variant = this.props.variant;
|
|
const prevVariant = prevProps.variant;
|
|
|
|
if (variant !== prevVariant) {
|
|
if (variant === 'temporary') {
|
|
this.listenTouchStart();
|
|
} else if (prevVariant === 'temporary') {
|
|
this.removeTouchStart();
|
|
}
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.removeTouchStart();
|
|
this.removeBodyTouchListeners(); // We need to release the lock.
|
|
|
|
if (nodeThatClaimedTheSwipe === this) {
|
|
nodeThatClaimedTheSwipe = null;
|
|
}
|
|
}
|
|
|
|
getMaxTranslate() {
|
|
return isHorizontal(this.props) ? this.paper.clientWidth : this.paper.clientHeight;
|
|
}
|
|
|
|
getTranslate(current) {
|
|
const start = isHorizontal(this.props) ? this.startX : this.startY;
|
|
return Math.min(Math.max(this.props.open ? start - current : this.getMaxTranslate() + start - current, 0), this.getMaxTranslate());
|
|
}
|
|
|
|
setPosition(translate, options = {}) {
|
|
const {
|
|
mode = null,
|
|
changeTransition = true
|
|
} = options;
|
|
const anchor = getAnchor(this.props);
|
|
const rtlTranslateMultiplier = ['right', 'bottom'].indexOf(anchor) !== -1 ? 1 : -1;
|
|
const transform = isHorizontal(this.props) ? `translate(${rtlTranslateMultiplier * translate}px, 0)` : `translate(0, ${rtlTranslateMultiplier * translate}px)`;
|
|
const drawerStyle = this.paper.style;
|
|
drawerStyle.webkitTransform = transform;
|
|
drawerStyle.transform = transform;
|
|
let transition = '';
|
|
|
|
if (mode) {
|
|
transition = this.props.theme.transitions.create('all', getTransitionProps({
|
|
timeout: this.props.transitionDuration
|
|
}, {
|
|
mode
|
|
}));
|
|
}
|
|
|
|
if (changeTransition) {
|
|
drawerStyle.webkitTransition = transition;
|
|
drawerStyle.transition = transition;
|
|
}
|
|
|
|
if (!this.props.disableBackdropTransition) {
|
|
const backdropStyle = this.backdrop.style;
|
|
backdropStyle.opacity = 1 - translate / this.getMaxTranslate();
|
|
|
|
if (changeTransition) {
|
|
backdropStyle.webkitTransition = transition;
|
|
backdropStyle.transition = transition;
|
|
}
|
|
}
|
|
}
|
|
|
|
listenTouchStart() {
|
|
document.body.addEventListener('touchstart', this.handleBodyTouchStart);
|
|
}
|
|
|
|
removeTouchStart() {
|
|
document.body.removeEventListener('touchstart', this.handleBodyTouchStart);
|
|
}
|
|
|
|
removeBodyTouchListeners() {
|
|
document.body.removeEventListener('touchmove', this.handleBodyTouchMove, {
|
|
passive: false
|
|
});
|
|
document.body.removeEventListener('touchend', this.handleBodyTouchEnd);
|
|
document.body.removeEventListener('touchcancel', this.handleBodyTouchEnd);
|
|
}
|
|
|
|
render() {
|
|
const _props = this.props,
|
|
{
|
|
disableBackdropTransition,
|
|
disableDiscovery,
|
|
disableSwipeToOpen,
|
|
ModalProps: {
|
|
BackdropProps
|
|
} = {},
|
|
onOpen,
|
|
open,
|
|
PaperProps,
|
|
swipeAreaWidth,
|
|
variant
|
|
} = _props,
|
|
ModalPropsProp = _objectWithoutProperties(_props.ModalProps, ["BackdropProps"]),
|
|
other = _objectWithoutProperties(_props, ["disableBackdropTransition", "disableDiscovery", "disableSwipeToOpen", "ModalProps", "onOpen", "open", "PaperProps", "swipeAreaWidth", "variant"]);
|
|
|
|
const {
|
|
maybeSwiping
|
|
} = this.state;
|
|
return React.createElement(Fragment, null, React.createElement(Drawer, _extends({
|
|
open: variant === 'temporary' && maybeSwiping ? true : open,
|
|
variant: variant,
|
|
ModalProps: _objectSpread({
|
|
BackdropProps: _objectSpread({}, BackdropProps, {
|
|
ref: this.handleBackdropRef
|
|
})
|
|
}, ModalPropsProp),
|
|
PaperProps: _objectSpread({}, PaperProps, {
|
|
style: {
|
|
pointerEvents: variant === 'temporary' && !open ? 'none' : ''
|
|
},
|
|
ref: this.handlePaperRef
|
|
})
|
|
}, other)), !disableDiscovery && !disableSwipeToOpen && variant === 'temporary' && React.createElement(SwipeArea, {
|
|
anchor: other.anchor,
|
|
swipeAreaWidth: swipeAreaWidth
|
|
}));
|
|
}
|
|
|
|
}
|
|
|
|
SwipeableDrawer.propTypes = process.env.NODE_ENV !== "production" ? {
|
|
/**
|
|
* @ignore
|
|
*/
|
|
anchor: PropTypes.oneOf(['left', 'top', 'right', 'bottom']),
|
|
|
|
/**
|
|
* Disable the backdrop transition.
|
|
* This can improve the FPS on low-end devices.
|
|
*/
|
|
disableBackdropTransition: PropTypes.bool,
|
|
|
|
/**
|
|
* If `true`, touching the screen near the edge of the drawer will not slide in the drawer a bit
|
|
* to promote accidental discovery of the swipe gesture.
|
|
*/
|
|
disableDiscovery: PropTypes.bool,
|
|
|
|
/**
|
|
* If `true`, swipe to open is disabled. This is useful in browsers where swiping triggers
|
|
* navigation actions. Swipe to open is disabled on iOS browsers by default.
|
|
*/
|
|
disableSwipeToOpen: PropTypes.bool,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
ModalProps: PropTypes.object,
|
|
|
|
/**
|
|
* Callback fired when the component requests to be closed.
|
|
*
|
|
* @param {object} event The event source of the callback
|
|
*/
|
|
onClose: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Callback fired when the component requests to be opened.
|
|
*
|
|
* @param {object} event The event source of the callback
|
|
*/
|
|
onOpen: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* If `true`, the drawer is open.
|
|
*/
|
|
open: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
PaperProps: PropTypes.object,
|
|
|
|
/**
|
|
* The width of the left most (or right most) area in pixels where the
|
|
* drawer can be swiped open from.
|
|
*/
|
|
swipeAreaWidth: PropTypes.number,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
theme: PropTypes.object.isRequired,
|
|
|
|
/**
|
|
* The duration for the transition, in milliseconds.
|
|
* You may specify a single timeout for all transitions, or individually with an object.
|
|
*/
|
|
transitionDuration: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({
|
|
enter: PropTypes.number,
|
|
exit: PropTypes.number
|
|
})]),
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
variant: PropTypes.oneOf(['permanent', 'persistent', 'temporary'])
|
|
} : {};
|
|
SwipeableDrawer.defaultProps = {
|
|
anchor: 'left',
|
|
disableBackdropTransition: false,
|
|
disableDiscovery: false,
|
|
disableSwipeToOpen: typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
|
swipeAreaWidth: 20,
|
|
transitionDuration: {
|
|
enter: duration.enteringScreen,
|
|
exit: duration.leavingScreen
|
|
},
|
|
variant: 'temporary' // Mobile first.
|
|
|
|
};
|
|
export default withTheme()(polyfill(SwipeableDrawer)); |