"use client"; /* eslint-disable react/no-children-prop, react-x/no-children-for-each, react-x/no-children-map, react-x/no-children-to-array, react-x/no-clone-element, sonarjs/function-return-type, sonarjs/no-identical-functions, sonarjs/no-unused-vars, max-lines, unicorn/no-array-callback-reference, unicorn/no-useless-undefined, unicorn/prefer-at, unicorn/prefer-dom-node-dataset, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ import {Drawer as BaseDrawer} from "@base-ui/react/drawer"; import {Menu as BaseMenu} from "@base-ui/react/menu"; import {mergeProps} from "@base-ui/react/merge-props"; import {useRender} from "@base-ui/react/use-render"; import {ChevronLeft, ChevronRight} from "lucide-react"; import {AnimatePresence, motion, type Transition} from "motion/react"; import * as React from "react"; import {useIsMobile} from "@/hooks/useIsMobile"; import {cn} from "@/lib/utilities"; import styles from "./dropdrawer.module.css"; const MOBILE_MENU_TITLE = "Menu"; const MOBILE_SUBMENU_TITLE = "Submenu"; const MOBILE_BACK_LABEL = "Go back"; interface DropDrawerContextValue { isMobile: boolean; } const DropDrawerContext = React.createContext(null); const useDropDrawerContext = (): DropDrawerContextValue => { const context = React.useContext(DropDrawerContext); if (context === null) { throw new Error("DropDrawer components cannot be rendered outside the Context"); } return context; }; const Drawer = BaseDrawer.Root; const DrawerPortal = BaseDrawer.Portal; const DrawerTrigger = React.forwardRef & {asChild?: boolean}>( (props, forwardedRef) => { const {asChild = false, children, className, render, ...otherProps} = props; const renderProp = asChild && React.isValidElement(children) ? children : render; return ( {renderProp ? undefined : children} ); }, ); DrawerTrigger.displayName = "DrawerTrigger"; function DrawerOverlay(props: Readonly>): React.ReactElement { const {className, render, ...otherProps} = props; return ( ); } const DrawerContent = React.forwardRef & {children?: React.ReactNode}>( (props, forwardedRef) => { const {className, children, render, ...otherProps} = props; return (
{children} ); }, ); DrawerContent.displayName = "DrawerContent"; function DrawerHeader( props: Readonly & {render?: useRender.RenderProp>}>, ): React.ReactElement { const {className, children, render, ...otherProps} = props; return useRender({ defaultTagName: "div", render: render as never, props: mergeProps({className: cn(styles.drawerHeader, className)}, otherProps, {children}), }); } function DrawerFooter( props: Readonly & {render?: useRender.RenderProp>}>, ): React.ReactElement { const {className, children, render, ...otherProps} = props; return useRender({ defaultTagName: "div", render: render as never, props: mergeProps({className: cn(styles.drawerFooter, className)}, otherProps, {children}), }); } function DrawerTitle(props: Readonly>): React.ReactElement { const {className, children, render, ...otherProps} = props; return ( {children} ); } const DropdownMenu = BaseMenu.Root; const DropdownMenuSub = BaseMenu.SubmenuRoot; const DropdownMenuTrigger = React.forwardRef & {asChild?: boolean}>( (props, forwardedRef) => { const {asChild = false, children, className, render, ...otherProps} = props; const renderProp = asChild && React.isValidElement(children) ? children : render; return ( {renderProp ? undefined : children} ); }, ); DropdownMenuTrigger.displayName = "DropdownMenuTrigger"; const DropdownMenuContent = React.forwardRef< HTMLDivElement, React.ComponentPropsWithRef & {children?: React.ReactNode} >((props, forwardedRef) => { const {className, children, render, ...otherProps} = props; return ( {children} ); }); DropdownMenuContent.displayName = "DropdownMenuContent"; interface DropdownMenuItemProps extends React.ComponentPropsWithRef { /** @deprecated Prefer Base UI's `render` prop. */ asChild?: boolean; /** * Whether to apply inset spacing to align with grouped menu content. * @default false */ inset?: boolean; } function DropdownMenuItem(props: Readonly): React.ReactElement { // eslint-disable-next-line sonarjs/deprecation -- backward-compatible asChild API const {asChild = false, children, className, inset = false, render, ...otherProps} = props; const renderProp = asChild && React.isValidElement(children) ? children : render; return ( {renderProp ? undefined : children} ); } function DropdownMenuLabel(props: Readonly>): React.ReactElement { const {className, children, render, ...otherProps} = props; return ( {children} ); } function DropdownMenuSeparator(props: Readonly>): React.ReactElement { const {className, render, ...otherProps} = props; return ( ); } function DropdownMenuSubTrigger( props: Readonly & {inset?: boolean}>, ): React.ReactElement { const {className, children, inset = false, render, ...otherProps} = props; return ( {children} ); } function DropdownMenuSubContent( props: Readonly & {children?: React.ReactNode}>, ): React.ReactElement { const {className, children, render, ...otherProps} = props; return ( {children} ); } type DropDrawerRootProps = React.ComponentProps | React.ComponentProps; type DropDrawerTriggerProps = | React.ComponentPropsWithoutRef | React.ComponentPropsWithoutRef; type DropDrawerContentProps = | React.ComponentPropsWithoutRef | React.ComponentPropsWithoutRef; interface MobileSubmenuDataAttributes { "data-parent-submenu-id"?: string; "data-parent-submenu"?: string; "data-submenu-id"?: string; } interface SharedDropDrawerItemProps extends Omit, "children" | "onClick" | "onSelect">, MobileSubmenuDataAttributes { /** * Item contents. * @default undefined */ children?: React.ReactNode; /** * Additional CSS classes merged with the item styles. * @default undefined */ className?: string; /** * Whether the desktop dropdown should close after the item is activated. * @default undefined */ closeOnClick?: boolean; /** * Optional trailing icon or affordance rendered alongside the item content. * @default undefined */ icon?: React.ReactNode; /** * Whether to apply inset spacing to align the item with grouped content. * @default undefined */ inset?: boolean; /** * Mouse click handler invoked when the item is activated. * @default undefined */ onClick?: React.MouseEventHandler; /** * Selection callback invoked with the native event when the item is activated. * @default undefined */ onSelect?: (event: Event) => void; } interface SubmenuContextType { activeSubmenu: string | null; navigateToSubmenu?: (id: string, title: string) => void; registerSubmenuContent?: (id: string, content: ReadonlyArray) => void; setActiveSubmenu: (id: string | null) => void; setSubmenuTitle: (title: string | null) => void; submenuTitle: string | null; } const SubmenuContext = React.createContext({ activeSubmenu: null, navigateToSubmenu: undefined, registerSubmenuContent: undefined, setActiveSubmenu: () => undefined, setSubmenuTitle: () => undefined, submenuTitle: null, }); /** * Provides a responsive dropdown-on-desktop, drawer-on-mobile navigation surface. * * @remarks * - Renders either Base UI `Menu.Root` or `Drawer.Root` depending on viewport size * - Built on Base UI Menu and Drawer primitives * * @example * ```tsx * * Open menu * * Profile * * * ``` * * @see {@link https://base-ui.com/react/components/menu | Base UI Menu Docs} */ function DropDrawer({children, ...props}: DropDrawerRootProps): React.JSX.Element { const isMobile = useIsMobile(); return ( {isMobile ? ( )}> {children} ) : ( )}> {children} )} ); } /** * Renders the control that opens the dropdown or drawer surface. * * @remarks * - Renders either a Base UI menu trigger or drawer trigger * - Built on Base UI Menu and Drawer primitives * * @example * ```tsx * Open menu * ``` * * @see {@link https://base-ui.com/react/components/drawer | Base UI Drawer Docs} */ const DropDrawerTrigger = React.forwardRef(({className, children, ...props}, forwardedRef) => { const {isMobile} = useDropDrawerContext(); return isMobile ? ( )}> {children} ) : ( )}> {children} ); }); /** * Renders the responsive dropdown or drawer content surface. * * @remarks * - Renders a Base UI popup on desktop and drawer content on mobile * - Built on Base UI Menu and Drawer primitives with animated mobile submenu navigation * * @example * ```tsx * * Settings * * ``` * * @see {@link https://base-ui.com/react/components/menu | Base UI Menu Docs} */ const DropDrawerContent = React.forwardRef(({className, children, ...props}, forwardedRef) => { const {isMobile} = useDropDrawerContext(); const [activeSubmenu, setActiveSubmenu] = React.useState(null); const [submenuTitle, setSubmenuTitle] = React.useState(null); const [submenuStack, setSubmenuStack] = React.useState>([]); const [animationDirection, setAnimationDirection] = React.useState<"forward" | "backward">("forward"); const submenuContentRef = React.useRef(new Map>()); React.useEffect(() => { submenuContentRef.current.clear(); }, [children]); const navigateToSubmenu = React.useCallback((id: string, title: string): void => { setAnimationDirection("forward"); setActiveSubmenu(id); setSubmenuTitle(title); setSubmenuStack((previousStack) => [...previousStack, {id, title}]); }, []); const goBack = React.useCallback((): void => { setAnimationDirection("backward"); if (submenuStack.length <= 1) { setActiveSubmenu(null); setSubmenuTitle(null); setSubmenuStack([]); return; } const newStack = [...submenuStack]; newStack.pop(); const previousSubmenu = newStack[newStack.length - 1]; if (!previousSubmenu) { setActiveSubmenu(null); setSubmenuTitle(null); setSubmenuStack([]); return; } setActiveSubmenu(previousSubmenu.id); setSubmenuTitle(previousSubmenu.title); setSubmenuStack(newStack); }, [submenuStack]); const registerSubmenuContent = React.useCallback((id: string, content: ReadonlyArray): void => { submenuContentRef.current.set(id, content); }, []); const extractSubmenuContent = React.useCallback((elements: React.ReactNode, targetId: string): ReadonlyArray => { const result: Array = []; const findSubmenuContent = (node: React.ReactNode): void => { if (!React.isValidElement(node)) { return; } const element = node as React.ReactElement<{ "data-submenu-id"?: string; children?: React.ReactNode; id?: string; }>; if (element.type === DropDrawerSub) { const elementId = element.props.id; const dataSubmenuId = element.props["data-submenu-id"]; if (elementId === targetId || dataSubmenuId === targetId) { if (element.props.children) { React.Children.forEach(element.props.children, (child) => { if (React.isValidElement(child) && child.type === DropDrawerSubContent) { const subContentElement = child as React.ReactElement<{children?: React.ReactNode}>; React.Children.forEach(subContentElement.props.children, (contentChild) => { result.push(contentChild); }); } }); } return; } } if (element.props.children) { React.Children.forEach(element.props.children, findSubmenuContent); } }; React.Children.forEach(elements, findSubmenuContent); return result; }, []); const getSubmenuContent = React.useCallback( (id: string): ReadonlyArray => { const cachedContent = submenuContentRef.current.get(id); if (cachedContent && cachedContent.length > 0) { return cachedContent; } const submenuContent = extractSubmenuContent(children, id); if (submenuContent.length > 0) { submenuContentRef.current.set(id, submenuContent); } return submenuContent; }, [children, extractSubmenuContent], ); const variants = { center: { opacity: 1, x: 0, }, enter: (direction: "forward" | "backward") => ({ opacity: 0, x: direction === "forward" ? "100%" : "-100%", }), exit: (direction: "forward" | "backward") => ({ opacity: 0, x: direction === "forward" ? "-100%" : "100%", }), }; const transition = { duration: 0.3, ease: [0.25, 0.1, 0.25, 1], } satisfies Transition; if (isMobile) { return ( { if (id === null) { setActiveSubmenu(null); setSubmenuTitle(null); setSubmenuStack([]); } }, setSubmenuTitle, submenuTitle, }}> )}> {activeSubmenu ? ( <>
{submenuTitle || MOBILE_SUBMENU_TITLE}
{getSubmenuContent(activeSubmenu)}
) : ( <> {MOBILE_MENU_TITLE}
{children}
)}
); } return ( )}> {children} ); }); /** * Renders an actionable item inside the drop drawer surface. * * @remarks * - Renders a Base UI menu item on desktop and a keyboard-accessible `
` on mobile * - Built on Base UI Menu and Drawer close primitives * * @example * ```tsx * }>Account * ``` * * @see {@link https://base-ui.com/react/components/menu | Base UI Menu Docs} */ function DropDrawerItem({ className, children, closeOnClick, disabled, icon, inset, onClick, onSelect, ...props }: SharedDropDrawerItemProps): React.JSX.Element { const {isMobile} = useDropDrawerContext(); const isInGroup = React.useCallback((element: HTMLElement | null): boolean => { if (!element) { return false; } let parent = element.parentElement; while (parent) { if (parent.hasAttribute("data-drop-drawer-group")) { return true; } parent = parent.parentElement; } return false; }, []); const itemRef = React.useRef(null); const [isInsideGroup, setIsInsideGroup] = React.useState(false); React.useEffect(() => { if (!isMobile) { return; } const timer = globalThis.window.setTimeout(() => { if (itemRef.current) { setIsInsideGroup(isInGroup(itemRef.current)); } }, 0); return () => globalThis.window.clearTimeout(timer); }, [isInGroup, isMobile]); const handleSelect = React.useCallback( (event: Event): void => { if (!disabled) { onSelect?.(event); } }, [disabled, onSelect], ); if (isMobile) { const handleClick: React.MouseEventHandler = (event): void => { if (disabled) { return; } onClick?.(event); handleSelect(event.nativeEvent); }; const handleKeyDown: React.KeyboardEventHandler = (event): void => { if (event.key !== "Enter" && event.key !== " ") { return; } event.preventDefault(); event.currentTarget.click(); }; const content = (
)}>
{children}
{icon ?
{icon}
: null}
); const parentSubmenuId = props["data-parent-submenu-id"] ?? props["data-parent-submenu"]; if (parentSubmenuId) { return content; } return ( ); } const handleDesktopClick: React.MouseEventHandler = (event): void => { if (disabled) { return; } onClick?.(event); handleSelect(event.nativeEvent); }; return (
{children}
{icon ?
{icon}
: null}
); } /** * Renders a visual separator between desktop drop drawer sections. * * @remarks * - Renders a separator only in desktop dropdown mode * - Built on Base UI Menu separator primitives * * @example * ```tsx * * ``` * * @see {@link https://base-ui.com/react/components/menu | Base UI Menu Docs} */ function DropDrawerSeparator({ className, ...props }: React.ComponentPropsWithoutRef): React.JSX.Element | null { const {isMobile} = useDropDrawerContext(); if (isMobile) { return null; } return ( ); } /** * Renders a section label for the drop drawer surface. * * @remarks * - Renders a drawer title on mobile and a menu label on desktop * - Built on Base UI Drawer and Menu primitives * * @example * ```tsx * Actions * ``` * * @see {@link https://base-ui.com/react/components/menu | Base UI Menu Docs} */ function DropDrawerLabel({ className, children, ...props }: React.ComponentProps | React.ComponentProps): React.JSX.Element { const {isMobile} = useDropDrawerContext(); return isMobile ? ( )}> {children} ) : ( )}> {children} ); } /** * Renders footer content aligned to the bottom of the responsive surface. * * @remarks * - Renders a drawer footer on mobile and a styled `
` on desktop * - Built on Base UI Drawer primitives for mobile layouts * * @example * ```tsx * Signed in as Alex * ``` * * @see {@link https://base-ui.com/react/components/drawer | Base UI Drawer Docs} */ function DropDrawerFooter({ className, children, ...props }: React.ComponentProps | React.ComponentProps<"div">): React.JSX.Element { const {isMobile} = useDropDrawerContext(); return isMobile ? ( )}> {children} ) : (
{children}
); } /** * Renders a grouped collection of drop drawer items. * * @remarks * - Renders a Base UI menu group on desktop and a `
` on mobile * - Inserts mobile-only separators between adjacent children * * @example * ```tsx * * Profile * Billing * * ``` * * @see {@link https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles/group_role | ARIA Group Role} */ function DropDrawerGroup({className, children, ...props}: React.ComponentProps<"div"> & {children: React.ReactNode}): React.JSX.Element { const {isMobile} = useDropDrawerContext(); const childrenWithSeparators = React.useMemo(() => { if (!isMobile) { return children; } const childArray = React.Children.toArray(children); const filteredChildren = childArray.filter((child) => !(React.isValidElement(child) && child.type === DropDrawerSeparator)); return filteredChildren.flatMap((child, index) => { if (index === filteredChildren.length - 1) { return [child]; } return [ child,