Files
tm_panel/src/components/ui/dropdown.tsx
T
2025-09-09 11:20:26 +03:30

140 lines
3.1 KiB
TypeScript

"use client";
import { useClickOutside } from "@/hooks/use-click-outside";
import { cn } from "@/lib/utils";
import { SetStateActionType } from "@/types/set-state-action-type";
import {
createContext,
type PropsWithChildren,
useContext,
useEffect,
useRef,
} from "react";
type DropdownContextType = {
isOpen: boolean;
handleOpen: () => void;
handleClose: () => void;
};
const DropdownContext = createContext<DropdownContextType | null>(null);
function useDropdownContext() {
const context = useContext(DropdownContext);
if (!context) {
throw new Error("useDropdownContext must be used within a Dropdown");
}
return context;
}
type DropdownProps = {
children: React.ReactNode;
isOpen: boolean;
setIsOpen: SetStateActionType<boolean>;
};
export function Dropdown({ children, isOpen, setIsOpen }: DropdownProps) {
const triggerRef = useRef<HTMLElement>(null);
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Escape") {
handleClose();
}
};
useEffect(() => {
if (isOpen) {
triggerRef.current = document.activeElement as HTMLElement;
document.body.style.pointerEvents = "none";
} else {
document.body.style.removeProperty("pointer-events");
setTimeout(() => {
triggerRef.current?.focus();
}, 0);
}
}, [isOpen]);
function handleClose() {
setIsOpen(false);
}
function handleOpen() {
setIsOpen(true);
}
return (
<DropdownContext.Provider value={{ isOpen, handleOpen, handleClose }}>
<div className="relative" onKeyDown={handleKeyDown}>
{children}
</div>
</DropdownContext.Provider>
);
}
type DropdownContentProps = {
align?: "start" | "end" | "center";
className?: string;
children: React.ReactNode;
};
export function DropdownContent({
children,
align = "center",
className,
}: DropdownContentProps) {
const { isOpen, handleClose } = useDropdownContext();
const contentRef = useClickOutside<HTMLDivElement>(() => {
if (isOpen) handleClose();
});
if (!isOpen) return null;
return (
<div
ref={contentRef}
role="menu"
aria-orientation="vertical"
className={cn(
"fade-in-0 zoom-in-95 pointer-events-auto absolute z-99 mt-2 min-w-[8rem] origin-top-right rounded-lg",
{
"animate-in right-0": align === "end",
"left-0": align === "start",
"left-1/2 -translate-x-1/2": align === "center",
},
className,
)}
>
{children}
</div>
);
}
type DropdownTriggerProps = React.HTMLAttributes<HTMLButtonElement> & {
children: React.ReactNode;
};
export function DropdownTrigger({ children, className }: DropdownTriggerProps) {
const { handleOpen, isOpen } = useDropdownContext();
return (
<button
className={className}
onClick={handleOpen}
aria-expanded={isOpen}
aria-haspopup="menu"
data-state={isOpen ? "open" : "closed"}
>
{children}
</button>
);
}
export function DropdownClose({ children }: PropsWithChildren) {
const { handleClose } = useDropdownContext();
return <div onClick={handleClose}>{children}</div>;
}