UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

475 lines (399 loc) 14.6 kB
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);