@material-ui/core
Version:
React components that implement Google's Material Design.
475 lines (399 loc) • 14.6 kB
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 Drawer, { getAnchor, isHorizontal } from '../Drawer/Drawer';
import { duration } from '../styles/transitions';
import withTheme from '../styles/withTheme';
import { getTransitionProps } from '../transitions/utils';
import NoSsr from '../NoSsr';
import SwipeArea from './SwipeArea'; // 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;
}
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !React.createContext) {
throw new Error('Material-UI: react@16.3.0 or greater is required.');
}
class SwipeableDrawer extends React.Component {
constructor(...args) {
super(...args);
this.state = {};
this.isSwiping = null;
this.handleBodyTouchStart = 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.paperRef) {
// The ref may be null when a parent component updates while swiping.
this.setPosition(this.getMaxTranslate() + (disableDiscovery ? 20 : -swipeAreaWidth), {
changeTransition: false
});
}
this.velocity = 0;
this.lastTime = null;
this.lastTranslate = null;
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);
};
this.handleBodyTouchMove = event => {
// the ref may be null when a parent component updates while swiping
if (!this.paperRef) 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;
}
const translate = this.getTranslate(horizontalSwipe ? currentX : currentY);
if (this.lastTranslate === null) {
this.lastTranslate = translate;
this.lastTime = performance.now() + 1;
}
const velocity = (translate - this.lastTranslate) / (performance.now() - this.lastTime) * 1e3; // Low Pass filter.
this.velocity = this.velocity * 0.4 + velocity * 0.6;
this.lastTranslate = translate;
this.lastTime = performance.now(); // We are swiping, let's prevent the scroll event on iOS.
event.preventDefault();
this.setPosition(translate);
};
this.handleBodyTouchEnd = event => {
nodeThatClaimedTheSwipe = null;
this.removeBodyTouchListeners();
this.setState({
maybeSwiping: false
}); // The swipe wasn't started.
if (!this.isSwiping) {
this.isSwiping = null;
return;
}
this.isSwiping = null;
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();
if (this.props.open) {
if (this.velocity > this.props.minFlingVelocity || translateRatio > this.props.hysteresis) {
this.props.onClose();
} else {
// Reset the position, the swipe was aborted.
this.setPosition(0, {
mode: 'exit'
});
}
return;
}
if (this.velocity < -this.props.minFlingVelocity || 1 - translateRatio > this.props.hysteresis) {
this.props.onOpen();
} else {
// Reset the position, the swipe was aborted.
this.setPosition(this.getMaxTranslate(), {
mode: 'enter'
});
}
};
this.handleBackdropRef = ref => {
this.backdropRef = ref ? ReactDOM.findDOMNode(ref) : null;
};
this.handlePaperRef = ref => {
this.paperRef = ref ? ReactDOM.findDOMNode(ref) : null;
};
}
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;
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if (typeof prevState.maybeSwiping === 'undefined') {
return {
maybeSwiping: false,
open: nextProps.open
};
}
if (!nextProps.open && prevState.open) {
return {
maybeSwiping: false,
open: nextProps.open
};
}
return {
open: nextProps.open
};
}
getMaxTranslate() {
return isHorizontal(this.props) ? this.paperRef.clientWidth : this.paperRef.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.paperRef.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 && !this.props.hideBackdrop) {
const backdropStyle = this.backdropRef.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 _this$props = this.props,
{
anchor,
disableBackdropTransition,
disableDiscovery,
disableSwipeToOpen,
hysteresis,
minFlingVelocity,
ModalProps: {
BackdropProps
} = {},
onOpen,
open,
PaperProps = {},
swipeAreaWidth,
variant
} = _this$props,
ModalPropsProp = _objectWithoutProperties(_this$props.ModalProps, ["BackdropProps"]),
other = _objectWithoutProperties(_this$props, ["anchor", "disableBackdropTransition", "disableDiscovery", "disableSwipeToOpen", "hysteresis", "minFlingVelocity", "ModalProps", "onOpen", "open", "PaperProps", "swipeAreaWidth", "variant"]);
const {
maybeSwiping
} = this.state;
return React.createElement(React.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: _objectSpread({
pointerEvents: variant === 'temporary' && !open ? 'none' : ''
}, PaperProps.style),
ref: this.handlePaperRef
}),
anchor: anchor
}, other)), !disableDiscovery && !disableSwipeToOpen && variant === 'temporary' && React.createElement(NoSsr, null, React.createElement(SwipeArea, {
anchor: anchor,
width: 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,
/**
* Affects how far the drawer must be opened/closed to change his state.
* Specified as percent (0-1) of the width of the drawer
*/
hysteresis: PropTypes.number,
/**
* Defines, from which (average) velocity on, the swipe is
* defined as complete although hysteresis isn't reached.
* Good threshold is between 250 - 1000 px/s
*/
minFlingVelocity: PropTypes.number,
/**
* @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),
hysteresis: 0.55,
minFlingVelocity: 400,
swipeAreaWidth: 20,
transitionDuration: {
enter: duration.enteringScreen,
exit: duration.leavingScreen
},
variant: 'temporary' // Mobile first.
};
export default withTheme()(SwipeableDrawer);