Working tooltips!
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user