UNPKG

@theguild/components

Version:
146 lines (145 loc) 4.15 kB
"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 };