Created
July 21, 2018 17:29
-
-
Save jcdang/2ddd3c5288435532ddc03a431a65c644 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import classNames from 'classnames'; | |
import domAlign from 'dom-align'; | |
import React from "react"; | |
import ReactDOM from "react-dom"; | |
export interface PopoverProps { | |
target: HTMLElement | React.ReactInstance; | |
show: boolean; | |
placement: string; | |
children: React.ReactNode; | |
className?: string; | |
} | |
const size = 8; | |
const placements = { | |
'top': { | |
points: ['bc', 'tc'], | |
offset: [0, -size] | |
}, | |
'top-left': { | |
points: ['br', 'tc'], | |
offset: [25, -size] | |
}, | |
'top-right': { | |
points: ['bl', 'tc'], | |
offset: [-25, -size] | |
}, | |
'right': { | |
points: ['cl', 'cr'], | |
offset: [size, 0] | |
}, | |
'right-top': { | |
points: ['bl', 'cr'], | |
offset: [size, 25] | |
}, | |
'right-bottom': { | |
points: ['tl', 'cr'], | |
offset: [size, -25] | |
}, | |
'bottom': { | |
points: ['tc', 'bc'], | |
offset: [0, size] | |
}, | |
'bottom-left': { | |
points: ['tr', 'bc'], | |
offset: [25, size] | |
}, | |
'bottom-right': { | |
points: ['tl', 'bc'], | |
offset: [-25, size] | |
}, | |
'left': { | |
points: ['cr', 'cl'], | |
offset: [-size, 0] | |
}, | |
'left-top': { | |
points: ['br', 'cl'], | |
offset: [-size, 25] | |
}, | |
'left-bottom': { | |
points: ['tr', 'cl'], | |
offset: [-size, -25] | |
} | |
}; | |
export interface PopoverState { | |
placement: string; | |
} | |
export class Popover extends React.PureComponent<PopoverProps, PopoverState> { | |
private popoverEl: React.ReactInstance; | |
public constructor(props, state) { | |
super(props, state); | |
this.maybeUpdateAlignment = this.maybeUpdateAlignment.bind(this); | |
this.getTarget = this.getTarget.bind(this); | |
this.state = { | |
placement: props.placement | |
}; | |
} | |
public getTarget(): HTMLElement { | |
return ReactDOM.findDOMNode(this.props.target) as HTMLElement; | |
} | |
public getAlign() { | |
const placement = placements[this.state.placement]; | |
return { | |
...placement, | |
overflow: { | |
adjustX: true, | |
adjustY: true | |
}, | |
useCssTransform: true | |
} | |
} | |
public componentDidUpdate() { | |
const target = this.getTarget(); | |
if (target) { | |
const popup = ReactDOM.findDOMNode(this.popoverEl); | |
const cfg = domAlign(popup, target, this.getAlign()); | |
this.maybeUpdateAlignment(popup, cfg); | |
} | |
} | |
public maybeUpdateAlignment(popup, align) { | |
function angleRange(num, angleCenter, offset) { | |
return num >= angleCenter - offset && num <= angleCenter + offset; | |
} | |
const target = this.getTarget(); | |
if (!align || !align.points || !this.props.target) { | |
return; | |
} | |
const popRect = popup.getBoundingClientRect(); | |
const targetRect = target.getBoundingClientRect(); | |
const popupCoord = { | |
x: popRect.left + (popRect.width / 2), | |
y: popRect.top + (popRect.height / 2) | |
}; | |
const targetCoord = { | |
x: targetRect.left + (targetRect.width / 2), | |
y: targetRect.top + (targetRect.height / 2) | |
}; | |
let angleDeg = Math.atan2( targetCoord.y - popupCoord.y, targetCoord.x - popupCoord.x) * 180 / Math.PI; | |
if (angleDeg < 0) { | |
angleDeg = 360 - (Math.abs(angleDeg) % 360); | |
} else { | |
angleDeg = angleDeg % 360; | |
} | |
const range = 22.5; | |
let placement; | |
if (angleRange(angleDeg, 0, range)) { | |
placement = "left"; | |
} else if (angleRange(angleDeg, 45, range)) { | |
placement = "top-left"; | |
} else if (angleRange(angleDeg, 90, range)) { | |
placement = "top" | |
} else if (angleRange(angleDeg, 135, range)) { | |
placement = "top-right" | |
} else if (angleRange(angleDeg, 180, range)) { | |
placement = "right" | |
} else if (angleRange(angleDeg, 225, range)) { | |
placement = "bottom-right" | |
} else if (angleRange(angleDeg, 270, range)) { | |
placement = "bottom" | |
} else if (angleRange(angleDeg, 315, range)) { | |
placement = "bottom-left"; | |
} else { | |
placement = this.state.placement; | |
} | |
if (placement !== this.state.placement) { | |
this.setState({ | |
placement | |
}); | |
} | |
} | |
public render() { | |
const { | |
className, | |
children, | |
show | |
} = this.props; | |
let popoverState = "popover-show"; | |
if (!show) { | |
popoverState = "popover-hide"; | |
} | |
return ( | |
<div | |
ref={e => this.popoverEl = e} | |
className={classNames(className, "popover", this.state.placement, popoverState)} | |
> | |
<div className={classNames("arrow")} /> | |
{children} | |
</div> | |
); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment