Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';

import Icon from '../../visuals/Icon/Icon';
import Button from '../../controls/Button/Button';

import closeIcon from '../../../components/visuals/Icon/svg/ic_close.svg';

const escButtonCode = 27;

class Dialog extends PureComponent {
  constructor(props) {
    super(props);

    this.dialogRef = null;
    this.parentNode = null;

    this.recalculatePosition = this.recalculatePosition.bind(this);
    this.recalculateTopPosition = this.recalculateTopPosition.bind(this);
    this.recalculateLeftPosition = this.recalculateLeftPosition.bind(this);
    this.handleKeyDownDismiss = this.handleKeyDownDismiss.bind(this);
    this.handleDismiss = this.handleDismiss.bind(this);
    this.addListeners = this.addListeners.bind(this);
    this.removeListeners = this.removeListeners.bind(this);
  }

  componentDidMount() {
    const { id, isVisible } = this.props;

    this.parentNode = document.querySelector(`[data-source="${id}"]`);
    this.recalculatePosition();

    if (isVisible) {
      this.addListeners();
    }
  }

  componentDidUpdate(previousProps) {
    const { isVisible, offset, horizontalAlign, verticalAlign } = this.props;
    const {
      isVisible: prevIsVisible,
      offset: prevOffset,
      horizontalAlign: prevHorizontalAlign,
      verticalAlign: prevVerticalAlign,
    } = previousProps;

    if (
      (isVisible && !prevIsVisible) ||
      horizontalAlign !== prevHorizontalAlign ||
      verticalAlign !== prevVerticalAlign ||
      !isEqual(offset, prevOffset)
    ) {
      this.recalculatePosition();
    }

    if (isVisible && !prevIsVisible) {
      this.addListeners();
    }

    if (!isVisible && prevIsVisible) {
      this.removeListeners();
    }
  }

  componentWillUnmount() {
    this.removeListeners();
  }

  addListeners() {
    const { disableCloseOnScroll } = this.props;

    // need for skipping opening click
    setTimeout(() => {
      if (!disableCloseOnScroll) {
        window.addEventListener('scroll', this.handleDismiss, true);
      }
      window.addEventListener('click', this.handleDismiss);
      window.addEventListener('resize', this.handleDismiss);
      window.addEventListener('keydown', this.handleKeyDownDismiss);
    }, 0);
  }

  removeListeners() {
    const { disableCloseOnScroll } = this.props;

    if (!disableCloseOnScroll) {
      window.removeEventListener('scroll', this.handleDismiss, true);
    }
    window.removeEventListener('resize', this.handleDismiss);
    window.removeEventListener('click', this.handleDismiss);
    window.removeEventListener('keydown', this.handleKeyDownDismiss);
  }

  recalculateTopPosition(parentRect, dialogRect, currentOffset) {
    const { verticalAlign } = this.props;

    switch (verticalAlign) {
      case Dialog.alignType.bottom: {
        this.dialogRef.style.top = `${parentRect.bottom - dialogRect.height + currentOffset.bottom}px`;
        break;
      }
      case Dialog.alignType.middle: {
        this.dialogRef.style.top = `${parentRect.top +
          parentRect.height / 2 -
          dialogRect.height / 2 +
          currentOffset.middle}px`;
        break;
      }
      default: {
        this.dialogRef.style.top = `${parentRect.top + currentOffset.top}px`;
      }
    }
  }

  recalculateLeftPosition(parentRect, dialogRect, currentOffset) {
    const { horizontalAlign } = this.props;

    switch (horizontalAlign) {
      case Dialog.alignType.left: {
        this.dialogRef.style.left = `${parentRect.left - dialogRect.width + currentOffset.left}px`;
        break;
      }
      case Dialog.alignType.center: {
        this.dialogRef.style.left = `${parentRect.left +
          parentRect.width / 2 -
          dialogRect.width / 2 +
          currentOffset.center}px`;
        break;
      }
      default: {
        this.dialogRef.style.left = `${parentRect.right + currentOffset.right}px`;
      }
    }
  }
  recalculatePosition() {
    const { isVisible, offset, onRecalculate } = this.props;
    const currentOffset = { ...Dialog.defaultOffset, ...offset };

    if (!isVisible || !this.parentNode || !this.dialogRef) return;

    if (onRecalculate) {
      onRecalculate(this.dialogRef, this.parentNode);
    } else {
      const parentRect = this.parentNode.getBoundingClientRect();
      const dialogRect = this.dialogRef.getBoundingClientRect();

      this.recalculateTopPosition(parentRect, dialogRect, currentOffset);
      this.recalculateLeftPosition(parentRect, dialogRect, currentOffset);
    }
  }

  handleKeyDownDismiss(e) {
    if (e.keyCode === escButtonCode) {
      this.handleDismiss();
    }
  }

  handleDismiss() {
    const { onDismiss } = this.props;
    onDismiss();
  }

  render() {
    const { children, isVisible, isDismissible, onDismiss, className, title } = this.props;
    return isVisible ? (
      // need for implementation outside click
      // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
      <div
        role="dialog"
        className={`Dialog ${className}`}
        aria-label={title}
        ref={ref => {
          this.dialogRef = ref;
        }}
        onClick={e => {
          e.stopPropagation();
        }}
      >
        {isDismissible && (
          <Button variant="linkDark" className="Dialog__close-button" onClick={onDismiss} aria-label="Close button">
            <Icon className="Dialog__close-icon" icon={closeIcon} />
          </Button>
        )}
        {children}
      </div>
    ) : null;
  }
}

Dialog.defaultOffset = {
  left: 0,
  right: 0,
  top: 0,
  bottom: 0,
  center: 0,
  middle: 0,
};

Dialog.alignType = {
  left: 'left',
  right: 'right',
  top: 'top',
  bottom: 'bottom',
  center: 'center',
  middle: 'middle',
};

Dialog.propTypes = {
  /** Dialog content */
  children: PropTypes.node.isRequired,
  /** Control flag for displaying dialog component */
  isVisible: PropTypes.bool.isRequired,
  /** Callback was fired if user clicks on close button or scroll/resize page */
  onDismiss: PropTypes.func,
  /** Control flag for displaying close button */
  isDismissible: PropTypes.bool,
  /** An id that was placed in data-source parent attribute */
  id: PropTypes.string.isRequired,
  /**  A11y-enabled modal title */
  title: PropTypes.string.isRequired,
  /** Align value */
  horizontalAlign: PropTypes.oneOf([Dialog.alignType.left, Dialog.alignType.right, Dialog.alignType.center]),
  /** Align value */
  verticalAlign: PropTypes.oneOf([Dialog.alignType.top, Dialog.alignType.bottom, Dialog.alignType.middle]),
  /** Offset values for aligning */
  offset: PropTypes.shape({
    left: PropTypes.number,
    right: PropTypes.number,
    top: PropTypes.number,
    bottom: PropTypes.number,
    center: PropTypes.number,
    middle: PropTypes.number,
  }),
  /** Wrapper class for customization */
  className: PropTypes.string,
  onRecalculate: PropTypes.func,
  /** Disables dialog closing on scroll so we can scroll on dialog if necessary */
  disableCloseOnScroll: PropTypes.bool,
};

Dialog.defaultProps = {
  isDismissible: false,
  onDismiss: () => {},
  className: '',
  offset: Dialog.defaultOffset,
  horizontalAlign: Dialog.alignType.right,
  verticalAlign: Dialog.alignType.top,
  onRecalculate: null,
  disableCloseOnScroll: false,
};

export default Dialog;