text: /* 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 {
    backdropRef = null;
  
    isSwiping = null;
  
    paperRef = null;
  
    startX = null;
  
    startY = null;
  
    lastTime = null;
  
    lastTranslate = null;
  
    velocity = 0;
  
    state = {};
  
    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;
        }
      }
    }
  
    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);
    };
  
    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);
    };
  
    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',
        });
      }
    };
  
    handleBackdropRef = ref => {
      this.backdropRef = ref ? ReactDOM.findDOMNode(ref) : null;
    };
  
    handlePaperRef = ref => {
      this.paperRef = ref ? ReactDOM.findDOMNode(ref) : null;
    };
  
    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 {
        anchor,
        disableBackdropTransition,
        disableDiscovery,
        disableSwipeToOpen,
        hysteresis,
        minFlingVelocity,
        ModalProps: { BackdropProps, ...ModalPropsProp } = {},
        onOpen,
        open,
        PaperProps = {},
        swipeAreaWidth,
        variant,
        ...other
      } = this.props;
      const { maybeSwiping } = this.state;
  
      return (
        <React.Fragment>
          <Drawer
            open={variant === 'temporary' && maybeSwiping ? true : open}
            variant={variant}
            ModalProps={{
              BackdropProps: {
                ...BackdropProps,
                ref: this.handleBackdropRef,
              },
              ...ModalPropsProp,
            }}
            PaperProps={{
              ...PaperProps,
              style: {
                pointerEvents: variant === 'temporary' && !open ? 'none' : '',
                ...PaperProps.style,
              },
              ref: this.handlePaperRef,
            }}
            anchor={anchor}
            {...other}
          />
          {!disableDiscovery &&
            !disableSwipeToOpen &&
            variant === 'temporary' && (
              <NoSsr>
                <SwipeArea anchor={anchor} width={swipeAreaWidth} />
              </NoSsr>
            )}
        </React.Fragment>
      );
    }
  }
  
  SwipeableDrawer.propTypes = {
    /**
     * @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);
  