Skip to content

Instantly share code, notes, and snippets.

@jcdang
Created July 21, 2018 17:29
Show Gist options
  • Save jcdang/2ddd3c5288435532ddc03a431a65c644 to your computer and use it in GitHub Desktop.
Save jcdang/2ddd3c5288435532ddc03a431a65c644 to your computer and use it in GitHub Desktop.
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