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";

Loading...