Updates
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import React, {
|
||||
DetailedHTMLProps,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const TWUIDropdownContentPositions = [
|
||||
"left",
|
||||
"bottom-left",
|
||||
"top-left",
|
||||
"top",
|
||||
"bottom",
|
||||
"right",
|
||||
"bottom-right",
|
||||
"top-right",
|
||||
"center",
|
||||
] as const;
|
||||
|
||||
export type TWUI_DROPDOWN_PROPS = PropsWithChildren &
|
||||
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
target: React.ReactNode;
|
||||
contentWrapperProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
targetWrapperProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
debounce?: number;
|
||||
openDebounce?: number;
|
||||
hoverOpen?: boolean;
|
||||
above?: boolean;
|
||||
position?: (typeof TWUIDropdownContentPositions)[number];
|
||||
topOffset?: number;
|
||||
externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
externalOpen?: boolean;
|
||||
keepOpen?: boolean;
|
||||
disableClickActions?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Toggle Component
|
||||
* @className_wrapper twui-dropdown-wrapper
|
||||
* @className_wrapper twui-dropdown-target
|
||||
* @className_wrapper twui-dropdown-content
|
||||
*
|
||||
* @note use the class `cancel-link` to prevent popup open on click
|
||||
*/
|
||||
export default function Dropdown({
|
||||
contentWrapperProps,
|
||||
targetWrapperProps,
|
||||
hoverOpen,
|
||||
above,
|
||||
debounce = 200,
|
||||
openDebounce = 200,
|
||||
target,
|
||||
position = "center",
|
||||
topOffset,
|
||||
externalSetOpen,
|
||||
keepOpen,
|
||||
disableClickActions,
|
||||
externalOpen,
|
||||
...props
|
||||
}: TWUI_DROPDOWN_PROPS) {
|
||||
const [open, setOpen] = React.useState(externalOpen);
|
||||
|
||||
let timeout: any;
|
||||
let openTimeout: any;
|
||||
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const dropdownContentRef = React.useRef<HTMLDivElement>(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(() => {
|
||||
if (keepOpen) return;
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOpen(externalOpen);
|
||||
}, [externalOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-col items-center relative",
|
||||
"twui-dropdown-wrapper",
|
||||
props.className
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
window.clearTimeout(openTimeout);
|
||||
|
||||
openTimeout = setTimeout(() => {
|
||||
externalSetOpen?.(true);
|
||||
setOpen(true);
|
||||
}, openDebounce);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!hoverOpen) return;
|
||||
|
||||
window.clearTimeout(openTimeout);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
externalSetOpen?.(false);
|
||||
setOpen(false);
|
||||
}, debounce);
|
||||
}}
|
||||
onBlur={() => {
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
const targetEl = e.target as HTMLElement | null;
|
||||
if (targetEl?.closest?.(".cancel-link")) return;
|
||||
if (disableClickActions) return;
|
||||
externalSetOpen?.(!open);
|
||||
setOpen(!open);
|
||||
}}
|
||||
className={twMerge(
|
||||
"cursor-pointer",
|
||||
"twui-dropdown-target",
|
||||
targetWrapperProps?.className
|
||||
)}
|
||||
>
|
||||
{target}
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...contentWrapperProps}
|
||||
className={twMerge(
|
||||
"absolute z-10 mt-1",
|
||||
position == "left"
|
||||
? "left-[100%] top-[50%] -translate-y-[50%]"
|
||||
: position == "right"
|
||||
? "right-[100%] top-[50%] -translate-y-[50%]"
|
||||
: position == "bottom-left"
|
||||
? "left-0 top-[100%]"
|
||||
: position == "bottom-right"
|
||||
? "right-0 top-[100%]"
|
||||
: position == "center"
|
||||
? "left-[50%] -translate-x-[50%] top-[100%]"
|
||||
: position == "top"
|
||||
? "left-[50%] -translate-x-[50%] bottom-[100%]"
|
||||
: "top-[100%]",
|
||||
above ? "-translate-y-[120%]" : "",
|
||||
open ? "flex" : "hidden",
|
||||
"twui-dropdown-content",
|
||||
contentWrapperProps?.className
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
style={{
|
||||
// top: `calc(100% + ${topOffset || 0}px)`,
|
||||
...contentWrapperProps?.style,
|
||||
}}
|
||||
ref={dropdownContentRef}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user