tcmd: Accordion and popover md elements
This commit is contained in:
170
lib/poppables/components/poppable-content.tsx
Normal file
170
lib/poppables/components/poppable-content.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
Dispatch,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
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,
|
||||
isClosed,
|
||||
},
|
||||
) => {
|
||||
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>
|
||||
);
|
||||
};
|
59
lib/poppables/components/poppable.tsx
Normal file
59
lib/poppables/components/poppable.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { PoppableContent } from "./poppable-content";
|
||||
import { useDebounce } from "../../../hooks/useDebounce";
|
||||
|
||||
interface IProps {
|
||||
content: ReactNode;
|
||||
className?: string;
|
||||
preferredEdge: "top" | "bottom" | "left" | "right";
|
||||
preferredAlign: "centered" | "top" | "bottom" | "left" | "right";
|
||||
spacing?: number;
|
||||
}
|
||||
|
||||
export const Poppable: FC<PropsWithChildren<IProps>> = (
|
||||
{ className, content, children, preferredEdge, preferredAlign, spacing },
|
||||
) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const closing = useDebounce(!isHovered, 1000);
|
||||
const closed = useDebounce(closing, 300);
|
||||
|
||||
const [ref, setRef] = useState<HTMLElement>();
|
||||
|
||||
const updateRef = useCallback((node: HTMLElement) => {
|
||||
if (!node) return;
|
||||
setRef(node);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
ref={updateRef}
|
||||
className={className}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{!!ref && (
|
||||
<PoppableContent
|
||||
preferredAlign={preferredAlign}
|
||||
preferredEdge={preferredEdge}
|
||||
spacing={spacing}
|
||||
isClosing={closing}
|
||||
isClosed={closed}
|
||||
relativeElement={ref}
|
||||
setHover={setIsHovered}
|
||||
>
|
||||
{content}
|
||||
</PoppableContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user