@theguild/components
Version:
146 lines (145 loc) • 4.15 kB
JavaScript
"use client";
import { jsx } from "react/jsx-runtime";
import { createContext, useContext, useEffect, useId, useRef, useState } from "react";
import NextLink from "next/link";
import { cn } from "../cn";
const DropdownContext = createContext(null);
function useDropdownContext() {
const context = useContext(DropdownContext);
if (!context) {
throw new Error("Dropdown components must be used within a Dropdown");
}
return context;
}
function Dropdown({ children, className, type, ...props }) {
const [isOpen, setIsOpen] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const buttonId = useId();
const menuId = useId();
const menuRef = useRef(null);
const buttonRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (!menuRef.current?.contains(event.target) && !buttonRef.current?.contains(event.target)) {
setIsOpen(false);
}
};
const handleEscape = (event) => {
if (event.key === "Escape") {
setIsOpen(false);
buttonRef.current?.focus();
}
};
const handleFocusElsewhere = (event) => {
if (!menuRef.current?.contains(event.target) && !buttonRef.current?.contains(event.target)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
document.addEventListener("focus", handleFocusElsewhere);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
document.removeEventListener("focus", handleFocusElsewhere);
};
}, [isOpen]);
const dismissDelayMs = 200;
const isHoveringRef = useRef(isHovering);
isHoveringRef.current = isHovering;
return /* @__PURE__ */ jsx(
DropdownContext.Provider,
{
value: { isOpen, setIsOpen, isHovering, setIsHovering, buttonId, menuId, buttonRef, menuRef },
children: /* @__PURE__ */ jsx(
"div",
{
className: cn("relative", className),
...type === "hover" && {
onPointerEnter: () => {
setIsOpen(true);
setIsHovering(true);
},
onPointerLeave: () => {
if (isHovering) {
setIsHovering(false);
setTimeout(() => {
if (!isHoveringRef.current) {
setIsOpen(false);
}
}, dismissDelayMs);
}
}
},
...props,
children
}
)
}
);
}
function DropdownTrigger({ children, className, ...props }) {
const { isOpen, setIsOpen, buttonId, menuId, buttonRef, setIsHovering } = useDropdownContext();
return /* @__PURE__ */ jsx(
"button",
{
ref: buttonRef,
id: buttonId,
"aria-expanded": isOpen,
"aria-controls": menuId,
"aria-haspopup": "true",
onClick: () => {
setIsOpen(true);
setIsHovering(false);
},
className: cn("cursor-pointer", className),
...props,
children
}
);
}
function DropdownContent({ children, className, ...props }) {
const { isOpen, buttonId, menuId, menuRef } = useDropdownContext();
return /* @__PURE__ */ jsx(
"div",
{
ref: menuRef,
id: menuId,
role: "menu",
"aria-labelledby": buttonId,
tabIndex: -1,
className: cn(className),
"data-state": isOpen ? "open" : "closed",
...props,
children
}
);
}
function DropdownItem({ children, onClick, className, href, ...props }) {
if (href) {
return /* @__PURE__ */ jsx(NextLink, { role: "menuitem", href, className, onClick, ...props, children });
}
return /* @__PURE__ */ jsx(
"button",
{
role: "menuitem",
onClick,
className,
onKeyDown: (e) => {
if (e.key === "Enter" || e.key === "Space") {
onClick?.();
}
},
...props,
children
}
);
}
export {
Dropdown,
DropdownContent,
DropdownItem,
DropdownTrigger
};