Working tooltips!

This commit is contained in:
Melvin Valster
2019-07-26 18:20:08 +02:00
parent 90fdc8d0e9
commit 4f72889e68
18 changed files with 476 additions and 127 deletions
+161
View File
@@ -0,0 +1,161 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { createPortal } from 'react-dom'
import { Map } from 'immutable'
const TOOLTIP_ROOT = document.getElementById('tooltip-root')
interface Props {
position?: TooltipPosition
}
export class Controller extends React.PureComponent<Props> {
static defaultProps = {
position: 'top-right'
}
tooltip = React.createRef<HTMLSpanElement>()
trigger = React.createRef<React.ReactInstance>()
state = {
isVisible: false,
tooltipDimensions: Map({
width: 0,
height: 0
}),
style: Map({
position: 'absolute' as 'absolute',
left: 0,
top: 0,
}),
triggerRect: Map({
left: 0,
top: 0,
width: 0,
height: 0
})
}
handleMouseEnter = () => {
this.setState({
isVisible: true,
style: this.state.style.merge(this.getPosition())
})
}
handleMouseLeave = () => {
this.setState({ isVisible: false })
}
componentDidUpdate(prevProps, prevState) {
if (this.state.isVisible) {
const { width, height } = (ReactDOM.findDOMNode(this.tooltip.current) as HTMLElement).getBoundingClientRect()
const { style, tooltipDimensions } = this.state
this.setState({
tooltipDimensions: tooltipDimensions.merge({ width, height }),
style: style.merge(this.getPosition())
})
}
}
getPosition = (): { top: number, left: number } => {
const { tooltipDimensions, triggerRect } = this.state
return calculatePosition(this.props.position, triggerRect, tooltipDimensions)
}
updateTriggerRect = (triggerRect: { left: number, top: number, width: number, height: number }) => {
this.setState({
triggerRect: this.state.triggerRect.merge(triggerRect)
})
}
render() {
const { children } = this.props
const { isVisible, style } = this.state
return React.Children.map(children, (child: React.ReactElement) => {
const name = (child.type as any).name
if (name === 'Trigger') {
return React.cloneElement(child, {
ref: this.trigger,
resize: this.updateTriggerRect,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
})
} else {
// Tooltip
return isVisible && createPortal(
<span style={style.toJS()} ref={this.tooltip}>
{React.cloneElement(child)}
</span>,
TOOLTIP_ROOT
)
}
})
}
}
const TOOLTIP_BOUNDING_PADDING = 10
const calculatePosition = (pos: TooltipPosition, trigger: Map<string, number>, tooltip: Map<string, number>) => {
const { innerWidth: windowWidth } = window
let top = 0
let left = 0
const triggerTop = trigger.get('top') + window.pageYOffset
// Top
switch (pos) {
case 'top-left':
case 'top-right': {
top = triggerTop - (tooltip.get('height') + 5)
break
}
case 'bottom-left':
case 'bottom-right': {
top = triggerTop + trigger.get('height') + 5
break
}
}
// Left
switch (pos) {
case 'left':
case 'bottom-left':
case 'top-left': {
left = (trigger.get('left')) - tooltip.get('width')
break
}
case 'right':
case 'bottom-right':
case 'top-right': {
left = (trigger.get('left')) + trigger.get('width')
break
}
}
const overflowsRight = left + tooltip.get('width') + TOOLTIP_BOUNDING_PADDING > windowWidth
const overflowsTop = top - (window.scrollY + TOOLTIP_BOUNDING_PADDING) < 0
// Validate
switch (pos) {
case 'top-right': {
if (overflowsRight && !overflowsTop) return calculatePosition('top-left', trigger, tooltip)
if (!overflowsRight && overflowsTop) return calculatePosition('bottom-right', trigger, tooltip)
if (overflowsRight && overflowsTop) return calculatePosition('bottom-left', trigger, tooltip)
break
}
case 'top-left': {
break
}
}
return { top, left }
}