function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/

/* global document */

/* eslint-disable cup/no-undef */
import * as React from 'react';
import FocusLock from 'react-focus-lock';
import { LocaleContext } from '../locale';
import { getOverrides } from '../helpers/overrides';
import { Layer } from '../layer';
import { SIZE, ROLE, CLOSE_SOURCE } from './constants';
import { Root as StyledRoot, Dialog as StyledDialog, DialogContainer as StyledDialogContainer, Close as StyledClose } from './styled-components';
import { CloseIcon } from './close-icon';
import { isFocusVisible, forkFocus, forkBlur } from '../utils/focusVisible';

class Modal extends React.Component {
  constructor(...args) {
    super(...args);

    _defineProperty(this, "animateOutTimer", void 0);

    _defineProperty(this, "animateStartTimer", void 0);

    _defineProperty(this, "dialogContainerRef", /*#__PURE__*/React.createRef());

    _defineProperty(this, "lastFocus", null);

    _defineProperty(this, "lastMountNodeOverflowStyle", null);

    _defineProperty(this, "rootRef", /*#__PURE__*/React.createRef());

    _defineProperty(this, "state", {
      isVisible: false,
      mounted: false,
      isFocusVisible: false
    });

    _defineProperty(this, "handleFocus", event => {
      if (isFocusVisible(event)) {
        this.setState({
          isFocusVisible: true
        });
      }
    });

    _defineProperty(this, "handleBlur", event => {
      if (this.state.isFocusVisible !== false) {
        this.setState({
          isFocusVisible: false
        });
      }
    });

    _defineProperty(this, "onEscape", () => {
      if (!this.props.closeable) {
        return;
      }

      this.triggerClose(CLOSE_SOURCE.escape);
    });

    _defineProperty(this, "onDocumentClick", e => {
      if (e.target && e.target instanceof HTMLElement && e.target.contains(this.dialogContainerRef.current)) {
        this.onBackdropClick();
      }
    });

    _defineProperty(this, "onBackdropClick", () => {
      if (!this.props.closeable) {
        return;
      }

      this.triggerClose(CLOSE_SOURCE.backdrop);
    });

    _defineProperty(this, "onCloseClick", () => {
      this.triggerClose(CLOSE_SOURCE.closeButton);
    });

    _defineProperty(this, "animateOutComplete", () => {
      this.setState({
        isVisible: false
      });
    });
  }

  componentDidMount() {
    this.setState({
      mounted: true
    });

    if (process.env.NODE_ENV !== "production") {
      // @ts-expect-error checking for property with incorrect name
      if (this.props.closable) {
        console.warn('The property `closable` is not supported on the Modal. Did you mean `closeable`?');
      }
    }
  }

  componentWillUnmount() {
    this.resetMountNodeScroll();
    this.clearTimers();
  }

  componentDidUpdate(prevProps, prevState) {
    const {
      isOpen
    } = this.props;

    if ( // If isOpen is changing *or* we just mounted and modal should be open
    isOpen !== prevProps.isOpen || isOpen && this.state.mounted && !prevState.mounted) {
      if (isOpen) {
        this.didOpen();
      } else {
        this.didClose();
      }
    }
  }

  disableMountNodeScroll() {
    const mountNode = this.getMountNode();
    this.lastMountNodeOverflowStyle = mountNode.style.overflow || '';
    mountNode.style.overflow = 'hidden';
  }

  resetMountNodeScroll() {
    const mountNode = this.getMountNode();
    const lastStyle = this.lastMountNodeOverflowStyle;

    if (mountNode && lastStyle !== null) {
      // If overflow is not 'hidden', something else has changed the
      // overflow style and we shouldn't try to reset it.
      if (mountNode.style.overflow === 'hidden') {
        mountNode.style.overflow = lastStyle || '';
      }

      this.lastMountNodeOverflowStyle = null;
    }
  }

  clearTimers() {
    if (this.animateOutTimer) {
      clearTimeout(this.animateOutTimer);
    }

    if (this.animateStartTimer) {
      // eslint-disable-next-line cup/no-undef
      cancelAnimationFrame(this.animateStartTimer);
    }
  }

  didOpen() {
    // Sometimes scroll starts past zero, possibly due to animation
    // Reset scroll to 0 (other libraries do this as well)
    const rootRef = this.rootRef.current;

    if (rootRef) {
      rootRef.scrollTop = 0;
    } // Clear any existing timers (like previous animateOutTimer)


    this.clearTimers();
    this.disableMountNodeScroll(); // eslint-disable-next-line cup/no-undef

    this.animateStartTimer = requestAnimationFrame(() => {
      this.setState({
        isVisible: true
      });
    });
  }

  didClose() {
    this.resetMountNodeScroll();
    this.animateOutTimer = setTimeout(this.animateOutComplete, 500);
  }

  triggerClose(source) {
    // If there's no source, it just means the isOpen prop changed. No need to call onClose.
    if (this.props.onClose && source) {
      this.props.onClose({
        closeSource: source
      });
    }
  }

  getSharedProps() {
    const {
      animate,
      isOpen,
      size,
      role,
      closeable
    } = this.props;
    return {
      $animate: animate,
      $isVisible: this.state.isVisible,
      $isOpen: !!isOpen,
      $size: size,
      $role: role,
      $closeable: !!closeable,
      $isFocusVisible: this.state.isFocusVisible
    };
  }

  getMountNode() {
    const {
      mountNode
    } = this.props;

    if (mountNode) {
      return mountNode;
    } // Flow thinks body could be null (cast through any)


    return document.body;
  }

  getChildren() {
    const {
      children
    } = this.props;
    return typeof children === 'function' ? children() : children;
  }

  renderModal() {
    const {
      overrides = {},
      closeable,
      role,
      autoFocus,
      focusLock,
      returnFocus
    } = this.props;
    const {
      Root: RootOverride,
      Dialog: DialogOverride,
      DialogContainer: DialogContainerOverride,
      Close: CloseOverride
    } = overrides;
    const [Root, rootProps] = getOverrides(RootOverride, StyledRoot);
    const [DialogContainer, dialogContainerProps] = getOverrides(DialogContainerOverride, StyledDialogContainer);
    const [Dialog, dialogProps] = getOverrides(DialogOverride, StyledDialog);
    const [Close, closeProps] = getOverrides(CloseOverride, StyledClose);
    const sharedProps = this.getSharedProps();
    const children = this.getChildren();
    return /*#__PURE__*/React.createElement(LocaleContext.Consumer, null, locale => /*#__PURE__*/React.createElement(FocusLock, {
      disabled: !focusLock // Allow focus to escape when UI is within an iframe
      ,
      crossFrame: false,
      returnFocus: returnFocus,
      autoFocus: autoFocus
    }, /*#__PURE__*/React.createElement(Root, _extends({
      "data-baseweb": "modal",
      ref: this.rootRef
    }, sharedProps, rootProps), /*#__PURE__*/React.createElement(DialogContainer, _extends({
      ref: this.dialogContainerRef
    }, sharedProps, dialogContainerProps), /*#__PURE__*/React.createElement(Dialog, _extends({
      tabIndex: -1,
      "aria-modal": true,
      "aria-label": "dialog",
      role: role
    }, sharedProps, dialogProps), children, closeable ? /*#__PURE__*/React.createElement(Close, _extends({
      "aria-label": locale.modal.close,
      onClick: this.onCloseClick
    }, sharedProps, closeProps, {
      onFocus: forkFocus(closeProps, this.handleFocus),
      onBlur: forkBlur(closeProps, this.handleBlur)
    }), /*#__PURE__*/React.createElement(CloseIcon, null)) : null)))));
  }

  render() {
    // Only render modal on the browser (portals aren't supported server-side)
    if (!this.state.mounted) {
      return null;
    } // Only render the modal if its isOpen is passed, or isVisible is true (still animating)


    if (!this.props.isOpen && !this.state.isVisible) {
      return null;
    }

    return /*#__PURE__*/React.createElement(Layer, {
      onEscape: this.onEscape,
      onDocumentClick: this.onDocumentClick,
      mountNode: this.props.mountNode
    }, this.renderModal());
  }

}

_defineProperty(Modal, "defaultProps", {
  animate: true,
  autoFocus: true,
  focusLock: true,
  returnFocus: true,
  closeable: true,
  name: 'dialog',
  isOpen: false,
  overrides: {},
  role: ROLE.dialog,
  size: SIZE.default
});

export default Modal;