Dropdown Menu
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
import { useRef, useState } from "react";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import {
DropdownGroup,
DropdownItem,
DropdownMenu,
} from "@/components/react/styled/dropdownMenu";
import { buttonClassName } from "@/components/react/styled/button";
import { usePopover } from "@/hooks/react/usePopover";
import { DemoWrapper } from "./demoWrapper";
export const DropdownMenuDemo = ({ code }: { code: string }) => {
// States for the main
const fileDorpDownRef = useRef<null | HTMLDialogElement>(null);
const fileButtonRef = useRef<null | HTMLButtonElement>(null);
const [fileDropdownState, setFileDropdownState] = useState(false);
const { open: openFileDDM, close: closeFileDDM } = usePopover({
setState: setFileDropdownState,
invokerRef: fileButtonRef,
dialogRef: fileDorpDownRef,
});
// States for sub
const openRecentDropdownRef = useRef<null | HTMLDialogElement>(null);
const openRecentButtonRef = useRef<null | HTMLButtonElement>(null);
const [openRecentState, setOpenRecentState] = useState(false);
const { open: openRecentOpenDDM, close: closeOpenRecentDDM } = usePopover({
setState: setOpenRecentState,
dialogRef: openRecentDropdownRef,
invokerRef: openRecentButtonRef,
appendTo: "side",
});
// Closes the dialogs
const close = () => {
setFileDropdownState(false);
setOpenRecentState(false);
};
return (
<DemoWrapper code={code}>
<button
ref={fileButtonRef}
onClick={openFileDDM}
className={buttonClassName({ compiledVariant: "primary-subtle" })}
>
File
</button>
<DropdownMenu
close={closeFileDDM}
state={fileDropdownState}
setState={setFileDropdownState}
ref={fileDorpDownRef}
>
<DropdownGroup>
<DropdownItem event={close} label="New Text File" info="⌘ N" />
<DropdownItem event={close} label="New File..." info="⌃ ⌥ ⌘ N" />
<DropdownItem event={close} label="New Window" />
</DropdownGroup>
<DropdownGroup>
<DropdownItem event={close} label="Open..." />
<DropdownItem event={close} label="Open Folder..." info="⌘ O" />
<DropdownItem event={close} label="Open Workspace from File..." />
<DropdownItem
event={openRecentOpenDDM}
ref={openRecentButtonRef}
label="Open Recent"
icon={<ChevronRightIcon className="w-4 h-4" />}
/>
</DropdownGroup>
<DropdownGroup>
<DropdownItem event={close} label="Open..." />
<DropdownItem
event={close}
label="Add Folder to Workspace"
info="⌘ s"
/>
<DropdownItem
event={close}
label="Save Workspace As..."
info="⇧ ⌘ s"
/>
<DropdownItem
event={close}
label="Duplicate Workspace"
info="⌃ ⌘ s"
/>
</DropdownGroup>
<DropdownGroup>
<DropdownItem event={close} label="Save" />
<DropdownItem event={close} label="Save As..." />
<DropdownItem event={close} label="Save All" />
</DropdownGroup>
</DropdownMenu>
<DropdownMenu
isNested={true}
close={closeOpenRecentDDM}
state={openRecentState}
setState={setOpenRecentState}
ref={openRecentDropdownRef}
>
<DropdownItem event={close} label="page.tsx" />
<DropdownItem event={close} label="layout.tsx" />
<DropdownItem event={close} label="global.css" />
</DropdownMenu>
</DemoWrapper>
);
};
Install Instructions
Verifying dependencies
Make sure the following dependencies are satisfied:
- dialog
Copy and paste the code into that file
import {
type ForwardedRef,
type ReactNode,
Suspense,
forwardRef,
lazy,
} from "react";
// Dialog
import type { DialogProps } from "@/components/react/primitive/dialog";
const Dialog = lazy(() =>
import("@/components/react/primitive/dialog").then((module) => ({
default: module.Dialog,
}))
);
// CVA
import { cva } from "class-variance-authority";
const popoverClasses = cva(
[
"backdrop-blur-lg",
"not-prose",
"left-[var(--left,_auto)]",
"right-[var(--right,_auto)]",
"top-[var(--top,_auto)]",
"bottom-[var(--bottom,_auto)]",
"w-auto",
"backdrop:opacity-0!",
"border",
"rounded-[calc(theme(borderRadius.xl)_+_theme(space.1))]",
"overflow-y-auto",
"overflow-x-hidden",
"border-neutral-300",
"bg-neutral-100/75",
"dark:border-neutral-700",
"dark:bg-neutral-900/75",
"w-fit",
"justify-start",
"flex-col",
"data-[display=false]:pointer-events-none",
"opacity-0",
"data-[display=false]:blur-sm",
"data-[display=true]:blur-0",
"data-[display=true]:backdrop:opacity-0",
"data-[display=true]:opacity-100",
"fixed",
"p-1",
"m-0",
"my-1",
"z-50",
"duration-100",
],
{
variants: {
isNested: {
false: [
"motion-safe:data-[display=false]:data-[append-to=top]:translate-y-2",
"motion-safe:data-[display=false]:data-[append-to=bottom]:-translate-y-2",
"motion-safe:data-[display=true]:data-[append-to=bottom]:translate-y-0",
"motion-safe:data-[display=true]:data-[append-to=top]:translate-y-0",
],
true: [
"motion-safe:data-[display=false]:data-[append-to=top]:translate-y-2",
"motion-safe:data-[display=false]:data-[append-to=bottom]:-translate-y-2",
"motion-safe:data-[display=true]:data-[append-to=bottom]:-translate-y-1",
"motion-safe:data-[display=true]:data-[append-to=top]:translate-y-1",
],
},
},
defaultVariants: {
isNested: false,
},
}
);
export interface DropdownMenuProps extends Omit<DialogProps, "ref"> {
close: () => void;
isNested?: boolean;
}
export const DropdownItem = forwardRef(
(
{
label,
event,
onPointerEnter,
info,
icon,
}: {
label: string;
event?: () => void;
onPointerEnter?: () => void;
info?: string;
icon?: ReactNode;
},
ref?: ForwardedRef<HTMLButtonElement | null>
) => (
<button
className="select-none bg-transparent border-none rounded-xl py-1 px-4 hover:bg-primary-700/25 dark:hover:bg-primary-400/75 focus-visible:bg-primary-700/25 dark:focus-visible:bg-primary-400/75 transition-colors flex justify-between align-middle items-center gap-8 w-[100%] outline-none duration-75 text-xs md:text-sm text-left hover:opacity-100 [&:hover_*]:opacity-100"
onClick={event}
onPointerEnter={onPointerEnter}
ref={ref}
>
<span>{label}</span>
<span className="opacity-50 uppercase transition-all duration-150">
{info}
</span>
{icon}
</button>
)
);
DropdownItem.displayName = "DropdownItem";
export const DropdownGroup = ({ children }: { children: ReactNode }) => (
<div className="last:border-b-0 border-b py-1 first:pt-0 last:pb-0 border-neutral-300 dark:border-neutral-700 w-[100%]">
{children}
</div>
);
export const DropdownMenu = forwardRef(
(
{
state,
setState,
close,
children,
isNested = false,
...props
}: DropdownMenuProps,
ref?: ForwardedRef<HTMLDialogElement | null>
) => {
return (
<Suspense fallback={null}>
<Dialog
state={state}
setState={setState}
className={popoverClasses({ isNested })}
onExit={close}
ref={ref}
{...props}
>
{children}
</Dialog>
</Suspense>
);
}
);
DropdownMenu.displayName = "DropdownMenu";