instacart/Snacks

View on GitHub
src/components/Tooltip/Tooltip.js

Summary

Maintainability
A
2 hrs
Test Coverage
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import InnerToolTip from './InnerToolTip'
import TooltipOverlay from './TooltipOverlay'

const noop = () => {} // eslint-disable-line no-empty-function

class Tooltip extends PureComponent {
  static propTypes = {
    children: PropTypes.node,
    size: PropTypes.oneOf(['small', 'medium', 'large']),
    placement: PropTypes.oneOf(['top', 'left', 'right', 'bottom']),
    style: PropTypes.shape({
      border: PropTypes.string,
      padding: PropTypes.string,
      boxShadow: PropTypes.string,
    }),
    // phasing in a new style override prop to avoid using native react prop
    customStyle: PropTypes.shape({
      border: PropTypes.string,
      padding: PropTypes.string,
      boxShadow: PropTypes.string,
      backgroundColor: PropTypes.string,
    }),
    overlayStyle: PropTypes.shape({}),
    arrowStyle: PropTypes.shape({
      border: PropTypes.string,
      boxShadowRight: PropTypes.string,
      boxShadowBottom: PropTypes.string,
      boxShadowLeft: PropTypes.string,
      boxShadowTop: PropTypes.string,
    }),
    target: PropTypes.node.isRequired,
    snacksStyle: PropTypes.oneOf(['primary', 'secondary', 'dark']),
    onDismiss: PropTypes.func,
    onShow: PropTypes.func,
    isVisible: PropTypes.bool,
    delayCalculatePosition: PropTypes.bool,
  }

  static defaultProps = {
    snacksStyle: 'dark',
    placement: 'bottom',
    size: 'small',
    onShow: noop,
    onDismiss: noop,
  }

  state = {
    show: false,
  }

  handleToggle = () => {
    const { onDismiss, onShow } = this.props
    this.setState(
      prevState => ({ show: !prevState.show }),
      () => {
        if (this.state.show) {
          onShow()
        } else {
          onDismiss()
        }
      }
    )
  }

  handleHideToolTip = () => {
    const { onDismiss } = this.props
    this.setState({ show: false })
    onDismiss()
  }

  renderTriggerElement() {
    const { target, isVisible } = this.props
    const { show } = this.state

    if (!target) {
      return
    }
    const extraProps = isVisible == null ? { onClick: this.handleToggle.bind(this) } : {}

    return React.cloneElement(target, {
      ref: node => {
        this.trigger = node
      },
      'aria-haspopup': true,
      'aria-expanded': isVisible || show,
      ...extraProps,
    })
  }

  render() {
    const {
      children,
      placement,
      size,
      style,
      customStyle,
      arrowStyle,
      snacksStyle,
      isVisible,
      overlayStyle,
      delayCalculatePosition,
    } = this.props

    return (
      <div>
        {this.renderTriggerElement()}
        <TooltipOverlay
          placement={placement}
          target={() => this.trigger}
          show={isVisible || this.state.show}
          onRootClose={this.handleHideToolTip}
          style={overlayStyle}
          delayCalculatePosition={delayCalculatePosition}
        >
          <InnerToolTip
            size={size}
            // @todo(JP): kill references to style
            customStyle={customStyle || style}
            arrowStyle={arrowStyle}
            snacksStyle={snacksStyle}
          >
            {children}
          </InnerToolTip>
        </TooltipOverlay>
      </div>
    )
  }
}

export default Tooltip