springload/react-portal-popover

View on GitHub
src/components/OverlayTrigger.js

Summary

Maintainability
A
1 hr
Test Coverage
import React from 'react';
import { uniqueId } from '../utils';

class OverlayTrigger extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false,
      id: uniqueId(),
    };
    this.toggleOverlay = this.toggleOverlay.bind(this);
    this.onClose = this.onClose.bind(this);
    this.addCloseHandler = this.addCloseHandler.bind(this);
    this.removeCloseHandler = this.removeCloseHandler.bind(this);
    this.onClickOutside = this.onClickOutside.bind(this);
    this.accessibleLabel = this.accessibleLabel.bind(this);
    this.onScroll = this.onScroll.bind(this);
  }

  addCloseHandler() {
    document.addEventListener('click', this.onClickOutside);
    window.addEventListener('resize', this.onClickOutside);
    if (this.props.closeOnScroll) {
      // Use capture for scroll events
      window.addEventListener('scroll', this.onScroll, true);
    }
  }

  removeCloseHandler() {
    document.removeEventListener('click', this.onClickOutside);
    window.removeEventListener('resize', this.onClickOutside);
    window.removeEventListener('scroll', this.onScroll, true);
  }
  componentDidMount() {
    this.isNodeMounted = true;
  }

  componentDidUpdate() {

  }

  componentWillUnmount() {
    this.isNodeMounted = false;
    this.removeCloseHandler();
  }

  toggleOverlay() {
    this.setState({
      open: !this.state.open,
    }, () => {
      if (this.state.open) {
        this.addCloseHandler();
      } else {
        this.removeCloseHandler();
      }
    });
  }

  onClose() {
    setTimeout(() => {
      if (this.isNodeMounted) {
        this.setState({
          open: false,
        });
        this.removeCloseHandler();
      }
    }, 0);
  }

  onScroll() {
    this.setState({
      open: false,
    });
  }

  onClickOutside() {
    this.removeCloseHandler();
    this.setState({
      open: false,
    });
  }

  accessibleLabel(children) {
    if (this.props.showLabel && this.props.hideLabel) {
      const label = (
        <span className="u-accessible">
          {this.state.open ? this.props.hideLabel : this.props.showLabel}
        </span>
      );
      return [label, children];
    }
    return children;
  }

  render() {
    const { children } = this.props;

    if (!children || !this.props.overlay) {
      return <span />;
    }

    const triggerId = children.id || `trigger_${this.state.id}`;

    const trigger = React.cloneElement(children, {
      onClick: this.toggleOverlay,
      'aria-controls': this.state.id,
      'aria-owns': this.state.id,
      'aria-expanded': this.state.open,
      'aria-haspopup': true,
      id: triggerId,
      children: this.accessibleLabel(children.props.children),
      ref: (ref) => { this.trigger = ref; },
    });

    const overlay = React.cloneElement(this.props.overlay, {
      trigger: this.trigger || null,
      isOpened: this.state.open,
      onClose: this.onClose,
      id: this.state.id,
      label: this.props.label || '',
    });

    return (
      <span>
        {trigger}
        {overlay}
      </span>
    );
  }
}

OverlayTrigger.propTypes = {
  closeOnScroll: React.PropTypes.bool,
  children: React.PropTypes.element.isRequired,
  overlay: React.PropTypes.object.isRequired,
  hideLabel: React.PropTypes.string,
  showLabel: React.PropTypes.string,
  label: React.PropTypes.string,
};

export default OverlayTrigger;