169 lines
4.2 KiB
TypeScript
Executable File
169 lines
4.2 KiB
TypeScript
Executable File
import {
|
|
Dispatch,
|
|
FC,
|
|
PropsWithChildren,
|
|
SetStateAction,
|
|
useCallback,
|
|
useState,
|
|
} from "react";
|
|
import { bulkRound } from "../../utils/bulkRound";
|
|
import { clamp } from "../../utils/clamp";
|
|
import { Portal } from "../../portal/components";
|
|
|
|
type edge = "top" | "bottom" | "left" | "right";
|
|
type alignment = edge | "centered";
|
|
|
|
interface IProps {
|
|
preferredEdge: edge;
|
|
preferredAlign: alignment;
|
|
relativeElement: HTMLElement;
|
|
spacing?: number;
|
|
isClosing: boolean;
|
|
isClosed: boolean;
|
|
setHover: Dispatch<SetStateAction<boolean>>;
|
|
}
|
|
|
|
type position = { top: number; left: number; width?: number; };
|
|
|
|
export const PoppableContent: FC<PropsWithChildren<IProps>> = (
|
|
{
|
|
preferredAlign,
|
|
preferredEdge,
|
|
children,
|
|
relativeElement,
|
|
spacing = 10,
|
|
setHover,
|
|
isClosing,
|
|
},
|
|
) => {
|
|
const [popRef, setPopRef] = useState<HTMLDivElement>();
|
|
const updateRef = useCallback((node: HTMLDivElement) => {
|
|
if (!node) return;
|
|
setPopRef(node);
|
|
}, []);
|
|
|
|
const getAlignment = useCallback(
|
|
(
|
|
relX: number,
|
|
relY: number,
|
|
relWidth: number,
|
|
relHeight: number,
|
|
popWidth: number,
|
|
popHeight: number,
|
|
_edge: edge,
|
|
align: alignment,
|
|
): position => {
|
|
const pos = {
|
|
top: relY,
|
|
left: relX,
|
|
};
|
|
|
|
switch (align) {
|
|
case "centered":
|
|
pos.top = relY + (relHeight / 2) - (popHeight / 2);
|
|
pos.left = relX + (relWidth / 2) - (popWidth / 2);
|
|
break;
|
|
case "top":
|
|
pos.top = relY;
|
|
break;
|
|
case "bottom":
|
|
pos.top = relY + relHeight - popHeight;
|
|
break;
|
|
case "left":
|
|
pos.left = relX;
|
|
break;
|
|
case "right":
|
|
pos.left = relX + relWidth - popWidth;
|
|
break;
|
|
}
|
|
|
|
return pos;
|
|
},
|
|
[],
|
|
);
|
|
|
|
const getPosition = useCallback(
|
|
(popWidth: number, popHeight: number, edge: edge, align: alignment) => {
|
|
const rel = relativeElement.getBoundingClientRect();
|
|
const [relX, relY, relWidth, relHeight] = bulkRound(
|
|
rel.x,
|
|
rel.y,
|
|
rel.width,
|
|
rel.height,
|
|
);
|
|
|
|
const pos: position = { top: 100, left: 100 };
|
|
const alignment = getAlignment(
|
|
relX,
|
|
relY,
|
|
relWidth,
|
|
relHeight,
|
|
popWidth,
|
|
popHeight,
|
|
edge,
|
|
align,
|
|
);
|
|
|
|
switch (edge) {
|
|
case "top":
|
|
pos.top = relY - popHeight - spacing +
|
|
document.documentElement.scrollTop;
|
|
pos.left = alignment.left;
|
|
break;
|
|
case "bottom":
|
|
pos.top = relY + relHeight + spacing +
|
|
document.documentElement.scrollTop;
|
|
pos.left = alignment.left;
|
|
break;
|
|
case "left":
|
|
pos.left = relX - popWidth - spacing;
|
|
pos.top = alignment.top + document.documentElement.scrollTop;
|
|
break;
|
|
case "right":
|
|
pos.left = relX + relWidth + spacing;
|
|
pos.top = alignment.top + document.documentElement.scrollTop;
|
|
break;
|
|
}
|
|
|
|
return pos;
|
|
},
|
|
[getAlignment, relativeElement, spacing],
|
|
);
|
|
|
|
const getClampedPosition = useCallback(() => {
|
|
if (!popRef) return { opacity: 0 };
|
|
|
|
const pop = popRef.getBoundingClientRect();
|
|
const [popWidth, popHeight] = bulkRound(pop.width, pop.height);
|
|
|
|
const pos = getPosition(popWidth, popHeight, preferredEdge, preferredAlign);
|
|
const { innerHeight, innerWidth } = window;
|
|
|
|
pos.top = ["left", "right"].includes(preferredEdge)
|
|
? clamp(pos.top, spacing, innerHeight - popHeight - spacing)
|
|
: pos.top;
|
|
pos.left = ["top", "bottom"].includes(preferredEdge)
|
|
? clamp(pos.left, spacing, innerWidth - popWidth - spacing)
|
|
: pos.left;
|
|
|
|
return pos;
|
|
}, [popRef, getPosition, preferredEdge, preferredAlign, spacing]);
|
|
|
|
return (
|
|
<Portal>
|
|
<div
|
|
ref={updateRef}
|
|
style={getClampedPosition()}
|
|
data-fading={isClosing}
|
|
data-visible={!isClosing}
|
|
onMouseEnter={() => setHover(true)}
|
|
onMouseLeave={() => setHover(false)}
|
|
// className="absolute w-[400px] border"
|
|
className="poppable"
|
|
>
|
|
{children}
|
|
</div>
|
|
</Portal>
|
|
);
|
|
};
|