appirio-tech-react-components-test10
Version:
Topcoder UI Kit library of components for our React apps.
251 lines (223 loc) • 7.34 kB
JSX
/**
* Tooltip
*/
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { Manager, Target, Popper, Arrow } from 'react-popper'
import { Portal } from 'react-portal'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import cn from 'classnames'
import _ from 'lodash'
import './Tooltip.scss'
class Tooltip extends Component {
constructor(props) {
super(props)
this.state = {
isPopoverOpen: false
}
this.handleOnMouseEnter = this.handleOnMouseEnter.bind(this)
this.handleOnMouseLeave = this.handleOnMouseLeave.bind(this)
this.handleOnClick = this.handleOnClick.bind(this)
this.handleOnClickOutside = this.handleOnClickOutside.bind(this)
this.popperMounted = this.popperMounted.bind(this)
this.tooltipShowTimeout = null
this.tooltipHideTimeout = null
this.elementRefs = {}
}
popperMounted(ref) {
this.elementRefs.popper = ref
}
handleOnMouseEnter() {
const { tooltipDelay } = this.props
if (this.tooltipShowTimeout) {
clearTimeout(this.tooltipShowTimeout)
}
if (this.tooltipHideTimeout) {
clearTimeout(this.tooltipHideTimeout)
}
if (!tooltipDelay) {
this.setState({ isPopoverOpen: true })
document.addEventListener('touchstart', this.handleOnClickOutside)
} else {
this.tooltipShowTimeout = setTimeout(() => {
this.setState({ isPopoverOpen: true })
document.addEventListener('touchstart', this.handleOnClickOutside)
}, tooltipDelay)
}
}
handleOnMouseLeave() {
const { tooltipHideDelay } = this.props
if (this.tooltipShowTimeout) {
clearTimeout(this.tooltipShowTimeout)
}
if (this.tooltipHideTimeout) {
clearTimeout(this.tooltipHideTimeout)
}
if (!tooltipHideDelay) {
this.setState({ isPopoverOpen: false })
document.removeEventListener('touchstart', this.handleOnClickOutside)
} else {
this.tooltipHideTimeout = setTimeout(() => {
this.setState({ isPopoverOpen: false })
document.removeEventListener('touchstart', this.handleOnClickOutside)
}, tooltipHideDelay)
}
}
handleOnClickOutside(evt) {
const targetElement = evt.target
const popperElement = ReactDOM.findDOMNode(this.elementRefs.popper)
if (!popperElement || !popperElement.contains(targetElement)) {
document.removeEventListener('click', this.handleOnClickOutside)
document.removeEventListener('touchstart', this.handleOnClickOutside)
this.setState({ isPopoverOpen: false })
}
}
handleOnClick() {
if (this.state.isPopoverOpen) {
document.removeEventListener('click', this.handleOnClickOutside)
document.removeEventListener('touchstart', this.handleOnClickOutside)
} else {
document.addEventListener('click', this.handleOnClickOutside)
document.addEventListener('touchstart', this.handleOnClickOutside)
}
this.setState({ isPopoverOpen: !this.state.isPopoverOpen })
}
render() {
const { isPopoverOpen } = this.state
const { popMethod, pointerWidth, tooltipShowDuration, tooltipHideDuration, usePortal } = this.props
// pointer height is adjacent of the triangle with angle 45 degrees
// cosinus(45)=sqrt(2)/2
const pointerHeight = Math.ceil(pointerWidth * Math.sqrt(2) / 2)
const ttClassName = cn('Tooltip', this.props.theme, this.props.className)
// handlers to handle pop method 'hover' | 'click'
const popMethodHandler = (
popMethod === 'hover' ?
{
onMouseEnter: this.handleOnMouseEnter,
onMouseLeave: this.handleOnMouseLeave,
// add on click handle to show tooltip on mobile devices on tap
onClick: this.handleOnMouseEnter
} : {
onClick: this.handleOnClick
}
)
// tooltip body
const body = (
<div className="tooltip-container">
<div className="tooltip-content-container">
<Arrow
className="tooltip-pointer"
style={{
height: pointerWidth,
width: pointerWidth
}}
/>
<div className="tooltip-body">
{React.Children.map(this.props.children, (child) => {
if (child.props.className === 'tooltip-body')
return child.props.children
})}
</div>
</div>
</div>
)
// tooltip target
const target = (
React.Children.map(this.props.children, (child) => {
if (child.props.className === 'tooltip-target')
return (
<Target
{...child.props}
{...popMethodHandler}
className={cn(child.props.className, { active: isPopoverOpen })}
>
{child.props.children}
</Target>
)
})
)
return (
<Manager className={ttClassName}>
{target}
<TransitionGroup>
{isPopoverOpen && (
<CSSTransition
timeout={{
enter: tooltipShowDuration,
exit: tooltipHideDuration
}}
classNames="fade"
>
{usePortal ? (
<Portal>
<Popper
key="popper"
placement="top"
ref={this.popperMounted}
className="tooltip-popper"
modifiers={{
offset: {
offset: `0, ${pointerHeight}`
},
preventOverflow: {
boundariesElement: 'viewport'
}
}}
{..._.pick(popMethodHandler, [
'onMouseEnter',
'onMouseLeave']
)}
>
{body}
</Popper>
</Portal>
) : (
<Popper
key="popper"
placement="top"
ref={this.popperMounted}
className="tooltip-popper"
modifiers={{
offset: {
offset: `0, ${pointerHeight}`
}
}}
{..._.pick(popMethodHandler, [
'onMouseEnter',
'onMouseLeave']
)}
>
{body}
</Popper>
)}
</CSSTransition>
)}
</TransitionGroup>
</Manager>
)
}
}
Tooltip.propTypes = {
children: PropTypes.array.isRequired,
pointerWidth: PropTypes.number,
// TODO rename to tooltipShowDelay
tooltipDelay: PropTypes.number,
tooltipHideDelay: PropTypes.number,
tooltipShowDuration: PropTypes.number,
tooltipHideDuration: PropTypes.number,
theme: PropTypes.string,
popMethod: PropTypes.oneOf(['hover', 'click']),
usePortal: PropTypes.bool // set to true to append tooltup in body of DOM
}
Tooltip.defaultProps = {
pointerWidth: 10,
tooltipDelay: 150,
tooltipHideDelay: 250,
tooltipShowDuration: 200,
tooltipHideDuration: 0,
theme: 'default',
popMethod: 'hover',
usePortal: false
}
export default Tooltip