import React, { DetailedHTMLProps, HTMLAttributes, PropsWithChildren, } from "react"; import { twMerge } from "tailwind-merge"; export const TWUIDropdownContentPositions = [ "left", "right", "center", ] as const; export type TWUI_DROPDOWN_PROPS = PropsWithChildren & DetailedHTMLProps, HTMLDivElement> & { target: React.ReactNode; contentWrapperProps?: DetailedHTMLProps< HTMLAttributes, HTMLDivElement >; targetWrapperProps?: DetailedHTMLProps< HTMLAttributes, HTMLDivElement >; debounce?: number; hoverOpen?: boolean; position?: (typeof TWUIDropdownContentPositions)[number]; topOffset?: number; externalSetOpen?: React.Dispatch>; }; let timeout: any; /** * # Toggle Component * @className_wrapper twui-dropdown-wrapper * @className_wrapper twui-dropdown-target * @className_wrapper twui-dropdown-content */ export default function Dropdown({ contentWrapperProps, targetWrapperProps, hoverOpen, debounce = 500, target, position = "center", topOffset, externalSetOpen, ...props }: TWUI_DROPDOWN_PROPS) { const [open, setOpen] = React.useState(false); const dropdownRef = React.useRef(null); const handleClickOutside = React.useCallback((e: MouseEvent) => { const targetEl = e.target as HTMLElement; const closestWrapper = targetEl.closest(".twui-dropdown-wrapper"); if (!closestWrapper) { externalSetOpen?.(false); return setOpen(false); } if (closestWrapper && closestWrapper !== dropdownRef.current) { externalSetOpen?.(false); return setOpen(false); } }, []); React.useEffect(() => { document.addEventListener("click", handleClickOutside); return () => { document.removeEventListener("click", handleClickOutside); }; }, []); return (
{ if (!hoverOpen) return; window.clearTimeout(timeout); externalSetOpen?.(true); setOpen(true); }} onMouseLeave={() => { if (!hoverOpen) return; timeout = setTimeout(() => { externalSetOpen?.(false); setOpen(false); }, debounce); }} onBlur={() => { window.clearTimeout(timeout); }} ref={dropdownRef} >
{ externalSetOpen?.(!open); setOpen(!open); }} className={twMerge( "cursor-pointer", "twui-dropdown-target", targetWrapperProps?.className )} > {target}
{ if (!hoverOpen) return; window.clearTimeout(timeout); }} onBlur={() => { if (!hoverOpen) return; window.clearTimeout(timeout); }} style={{ top: `calc(100% + ${topOffset || 0}px)`, ...contentWrapperProps?.style, }} > {props.children}
); }