adwaita-web
Version:
A GTK inspired toolkit designed to build awesome web apps
329 lines (328 loc) • 9.76 kB
JavaScript
import { createPopper } from "@popperjs/core";
import cx from "clsx";
import React from "react";
import { createPortal, findDOMNode } from "react-dom";
const noop = () => {
};
function getInversePlacement(p) {
if (p.startsWith("top"))
return "bottom";
if (p.startsWith("left"))
return "right";
if (p.startsWith("right"))
return "left";
return "top";
}
class Popover extends React.PureComponent {
static defaultProps = {
arrow: true,
placement: "bottom",
align: "right",
method: "click",
delay: 200,
shouldUpdatePlacement: true,
onOpen: noop,
onClose: noop,
onDidOpen: noop,
onDidClose: noop
};
domNode;
isDomNodeAttached;
isEventListening;
openTimeout;
closeTimeout;
triggerRef;
popoverRef;
arrowRef;
observer;
popper;
state;
constructor(props) {
super(props);
this.domNode = document.createElement("div");
this.domNode.className = "Popover__domNode";
this.isDomNodeAttached = false;
this.isEventListening = false;
this.openTimeout = void 0;
this.closeTimeout = void 0;
this.state = {
open: false,
closing: false,
actualPlacement: props.placement,
styles: {}
};
this.triggerRef = React.createRef();
this.popoverRef = React.createRef();
this.arrowRef = React.createRef();
this.observer = new ResizeObserver(this.onContentResize);
}
componentWillUnmount() {
this.detachDomNode();
this.detachPopper();
}
componentDidMount() {
this.attachPopper();
if (this.props.shouldAttachEarly)
this.attachDomNode();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.open !== this.props.open || prevState.open !== this.state.open) {
if (this.props.shouldUpdatePlacement && this.popper)
this.popper.update();
}
}
attachDomNode() {
if (!this.isDomNodeAttached) {
document.body.append(this.domNode);
document.addEventListener("click", this.onDocumentClick);
this.isDomNodeAttached = true;
this.forceUpdate();
}
}
detachDomNode() {
if (this.isDomNodeAttached) {
document.body.removeChild(this.domNode);
document.removeEventListener("click", this.onDocumentClick);
this.isDomNodeAttached = false;
}
}
attachPopper() {
if (!this.popoverRef.current || !this.triggerRef.current)
return;
if (this.popper)
return;
this.popper = createPopper(this.triggerRef.current, this.popoverRef.current, this.getPopperOptions());
}
detachPopper() {
if (this.popper) {
this.popper.destroy();
this.popper = void 0;
}
}
updatePopperOptions() {
if (!this.popper)
return;
this.popper.setOptions(this.getPopperOptions());
}
getPopperOptions() {
const hasArrow = this.props.arrow;
const isOpen = this.isOpen();
this.isEventListening = isOpen;
return {
placement: this.props.placement,
modifiers: [
{
name: "arrow",
enabled: hasArrow,
options: {
element: this.arrowRef.current,
padding: 15
}
},
{
name: "offset",
options: {
offset: [0, hasArrow ? 10 : 0]
}
},
{
name: "preventOverflow",
options: {
altAxis: true,
padding: 10
}
},
{
name: "eventListeners",
enabled: isOpen
},
{
name: "updateComponentState",
enabled: true,
phase: "write",
fn: this.onUpdatePopper
}
]
};
}
onContentResize = () => {
if (this.popper)
this.popper.update();
};
onRefPopover = (ref) => {
if (!ref) {
if (this.popoverRef.current) {
this.observer.unobserve(this.popoverRef.current);
this.popoverRef.current = null;
}
return;
}
this.popoverRef.current = ref;
this.observer.observe(this.popoverRef.current);
};
onDocumentClick = (ev) => {
if (this.props.method !== "click" && this.props.method !== "click-controlled")
return;
if (!this.isOpen())
return;
if (!(ev.target && this.triggerRef.current?.contains(ev.target) || ev.target && this.popoverRef.current?.contains(ev.target)))
this.close();
};
onTransitionEnd = () => {
this.setState({ closing: false });
if (this.state.open && this.props.onDidOpen)
this.props.onDidOpen();
else if (this.props.onDidClose)
this.props.onDidClose();
};
onUpdatePopper = ({ state }) => {
if (this.state.actualPlacement !== state.placement) {
this.setState({ actualPlacement: state.placement });
}
if (this.props.width) {
const trigger = state.elements.reference;
const rect = trigger.getBoundingClientRect();
if (this.props.width === "trigger") {
const currentWidth = this.state.styles.width;
const newWidth = rect.width - 1;
if (currentWidth !== newWidth)
this.setState({ styles: { width: newWidth } });
} else if (this.props.width === "trigger-min") {
const currentWidth = this.state.styles.minWidth;
const newWidth = rect.width - 1;
if (currentWidth !== newWidth)
this.setState({ styles: { minWidth: newWidth } });
}
}
};
onClick = (ev) => {
if (!this.triggerRef.current?.contains(ev.target))
return;
if (this.isOpen())
this.close();
else
this.open();
};
onMouseOver = () => {
if (this.closeTimeout) {
window.clearTimeout(this.closeTimeout);
this.closeTimeout = void 0;
}
if (this.state.open === false) {
if (!this.props.delay) {
this.open();
} else {
this.openTimeout = window.setTimeout(() => {
this.open();
}, this.props.delay);
}
}
};
onMouseOut = () => {
if (this.openTimeout) {
window.clearTimeout(this.openTimeout);
this.openTimeout = void 0;
}
if (this.state.open) {
if (!this.props.delay) {
this.close();
} else {
this.closeTimeout = window.setTimeout(() => {
this.close();
}, this.props.delay);
}
}
};
isControlled = () => {
return "open" in this.props;
};
open = () => {
if (this.props.open && this.state.open)
return;
if (this.isControlled()) {
if (this.props.open === false) {
if (this.props.onOpen)
return this.props.onOpen();
return;
}
}
this.attachDomNode();
this.updatePopperOptions();
this.openTimeout = void 0;
this.setState({ open: true });
if (!this.isControlled()) {
if (this.props.onOpen)
return this.props.onOpen();
}
};
close = () => {
if (this.isControlled()) {
if (this.props.open === true) {
if (this.props.onClose)
return this.props.onClose();
return;
}
}
this.updatePopperOptions();
this.setState({ open: false, closing: true });
if (!this.isControlled()) {
if (this.props.onClose)
this.props.onClose();
}
};
isOpen() {
return this.props.open ?? this.state.open;
}
getContent() {
return !this.isDomNodeAttached ? null : typeof this.props.content === "function" ? this.props.content() : this.props.content;
}
getEventListeners() {
const { method } = this.props;
return method === "mouseover" ? { onMouseOver: this.onMouseOver, onMouseOut: this.onMouseOut } : method === "click" ? { onClick: this.onClick } : {};
}
render() {
const { method, arrow, children, className } = this.props;
const { actualPlacement, styles, closing } = this.state;
const open = this.isOpen();
const trigger = children;
if (this.props.open && !this.state.open)
setTimeout(this.open, 0);
if (open !== this.isEventListening)
this.updatePopperOptions();
const triggerProps = trigger ? typeof trigger === "object" && trigger !== null ? "props" in trigger ? trigger.props : {} : {} : {};
const eventListeners = this.getEventListeners();
const props = {
...triggerProps,
...eventListeners,
className: cx(triggerProps.className, open ? "with-popover" : void 0, open ? `popover-${actualPlacement}` : void 0),
ref: (node) => {
if (node)
this.triggerRef.current = findDOMNode(node);
if (typeof trigger === "object" && trigger && "ref" in trigger && typeof trigger["ref"] === "function")
trigger.ref(node);
}
};
const arrowPlacement = getInversePlacement(actualPlacement ?? "top");
const popoverEventListeners = method === "mouseover" ? eventListeners : void 0;
const popoverClassName = cx("Popover popover", className, actualPlacement, arrow ? `arrow-${arrowPlacement}` : void 0, { open, arrow, closing });
return /* @__PURE__ */ React.createElement(React.Fragment, null, typeof trigger === "object" && trigger !== null && !Array.isArray(trigger) && React.cloneElement(trigger, props), createPortal(/* @__PURE__ */ React.createElement("div", {
ref: this.onRefPopover,
className: popoverClassName,
onTransitionEnd: this.onTransitionEnd,
...popoverEventListeners
}, /* @__PURE__ */ React.createElement("div", {
className: "Popover__wrapper"
}, arrow && /* @__PURE__ */ React.createElement("div", {
className: cx("Popover__arrow", arrowPlacement),
ref: this.arrowRef
}), /* @__PURE__ */ React.createElement("div", {
className: "Popover__container",
style: styles
}, /* @__PURE__ */ React.createElement("div", {
className: "Popover__content"
}, this.getContent())))), this.domNode));
}
}
export {
Popover
};