new-personal-site/components/lib/elements/Modal.tsx
Benjamin Toby a0a0ab8ee4 Updates
2025-07-20 10:35:54 +01:00

182 lines
5.5 KiB
TypeScript

import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import ModalComponent from "../(partials)/ModalComponent";
import PopoverComponent from "../(partials)/PopoverComponent";
import { twMerge } from "tailwind-merge";
export const TWUIPopoverStyles = [
"top",
"bottom",
"left",
"right",
"transform",
"bottom-left",
"bottom-right",
] as const;
export const TWUIPopoverTriggers = ["hover", "click"] as const;
export type TWUI_MODAL_PROPS = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
target?: React.ReactNode;
targetRef?: React.RefObject<HTMLDivElement>;
popoverReferenceRef?: React.RefObject<HTMLElement | null>;
targetWrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
open?: boolean;
isPopover?: boolean;
position?: (typeof TWUIPopoverStyles)[number];
trigger?: (typeof TWUIPopoverTriggers)[number];
debounce?: number;
onClose?: () => any;
};
/**
* # Modal Component
* @ID twui-modal-root
* @className twui-modal-content
* @className twui-modal
* @ID twui-popover-root
* @className twui-popover-content
* @className twui-popover-target
*/
export default function Modal(props: TWUI_MODAL_PROPS) {
const {
target,
targetRef,
targetWrapperProps,
open: existingOpen,
setOpen: existingSetOpen,
isPopover,
popoverReferenceRef,
trigger = "hover",
debounce = 500,
onClose,
} = props;
const [ready, setReady] = React.useState(false);
const [open, setOpen] = React.useState(existingOpen || false);
React.useEffect(() => {
const IDName = isPopover ? "twui-popover-root" : "twui-modal-root";
const modalRoot = document.getElementById(IDName);
if (modalRoot) {
setReady(true);
} else {
const newModalRootEl = document.createElement("div");
newModalRootEl.id = IDName;
document.body.appendChild(newModalRootEl);
setReady(true);
}
}, []);
React.useEffect(() => {
existingSetOpen?.(open);
if (open == false) onClose?.();
}, [open]);
React.useEffect(() => {
setOpen(existingOpen || false);
}, [existingOpen]);
const finalTargetRef = targetRef || React.useRef<HTMLDivElement>(null);
const finalPopoverReferenceRef = popoverReferenceRef || finalTargetRef;
const popoverTargetActiveRef = React.useRef(false);
const popoverContentActiveRef = React.useRef(false);
let closeTimeout: any;
const popoverEnterFn = React.useCallback((e: any) => {
popoverTargetActiveRef.current = true;
popoverContentActiveRef.current = false;
setOpen(true);
props.onMouseEnter?.(e);
}, []);
const popoverLeaveFn = React.useCallback((e: any) => {
window.clearTimeout(closeTimeout);
closeTimeout = setTimeout(() => {
// if (popoverTargetActiveRef.current) {
// popoverTargetActiveRef.current = false;
// return;
// }
if (popoverContentActiveRef.current) {
popoverContentActiveRef.current = false;
return;
}
setOpen(false);
}, debounce);
props.onMouseLeave?.(e);
}, []);
const handleClickOutside = React.useCallback((e: MouseEvent) => {
const targetEl = e.target as HTMLElement;
const closestWrapper = targetEl.closest(".twui-popover-content");
const closestTarget = targetEl.closest(".twui-popover-target");
if (closestTarget) return;
if (!closestWrapper) {
return setOpen(false);
}
}, []);
React.useEffect(() => {
if (!isPopover) return;
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
}, []);
return (
<React.Fragment>
{target ? (
<div
{...targetWrapperProps}
onClick={(e) => setOpen(!open)}
ref={finalTargetRef}
onMouseEnter={
isPopover && trigger === "hover"
? popoverEnterFn
: targetWrapperProps?.onMouseEnter
}
onMouseLeave={
isPopover && trigger === "hover"
? popoverLeaveFn
: targetWrapperProps?.onMouseLeave
}
className={twMerge(
"twui-popover-target",
targetWrapperProps?.className
)}
>
{target}
</div>
) : null}
{ready ? (
isPopover ? (
<PopoverComponent
{...props}
open={open}
setOpen={setOpen}
targetElRef={finalPopoverReferenceRef}
debounce={debounce}
popoverTargetActiveRef={popoverTargetActiveRef}
popoverContentActiveRef={popoverContentActiveRef}
/>
) : (
<ModalComponent {...props} open={open} setOpen={setOpen} />
)
) : null}
</React.Fragment>
);
}