DropdownMenu

A component that displays a menu to the user with a list of actions or functions when the menu trigger is pressed.

Quick Start

Installation
npm install @adaptavant/eds-core
Import
import { DropdownMenu } from '@adaptavant/eds-core';

Default

The DropdownMenu and it's subcomponents can be composed to display a list of menu items that can trigger an action via the onClick prop:

<DropdownMenu>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Popover height & width control

You can set the maximum size of the DropdownMenu’s popovers using the popoverMaxHeight and popoverMaxWidth props:

<DropdownMenu popoverMaxHeight={100} popoverMaxWidth={150}>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Popover placement

You can control the position of the popover relative to the trigger by using the popoverPlacement prop. The default value is bottom-start.

The distance between the trigger and the popover can be controlled using the popoverOffset prop. The default value is 8.

<DropdownMenu
  popoverOffset={16}
  popoverPlacement="bottom" // bottom-start | bottom-end
>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Custom trigger

You can provide the DropdownMenu with a custom trigger by passing a callback function to its children. This function provides access to both the button props required for the trigger to function correctly and be labeled accessibly, as well as the open state of the menu.

<DropdownMenu>
  {({ triggerProps, isMenuOpen }) => (
    <>
      <IconButton
        className="aria-expanded:bg-neutral-pressed"
        icon={isMenuOpen ? LockOpenIcon : LockClosedIcon}
        label="Toggle dropdown menu"
        variant="neutralSecondary"
        {...triggerProps}
      />
      <DropdownMenuPopover>
        <DropdownMenuList>
          <DropdownMenuItem onClick={() => alert("Profile")}>
            Profile
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => alert("Messages")}>
            Messages
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => alert("Account settings")}>
            Account settings
          </DropdownMenuItem>
        </DropdownMenuList>
      </DropdownMenuPopover>
    </>
  )}
</DropdownMenu>

Customizing a Custom Trigger:

Here's an example which uses an Avatar and a wrapper Box as custom trigger for the DropdownMenu. Play around with it.

<DropdownMenu>
	{({ triggerProps, isMenuOpen }) => (
		<>
			<Box
				{...triggerProps}
				className="p-1.5 rounded-full cursor-pointer leading-none hover:bg-neutral-secondary-hover focus-visible:focus-ring active:bg-neutral-secondary-pressed aria-expanded:bg-neutral-secondary-pressed"
				tabIndex={0}
			>
				<Avatar name="Freddy" size="32">
					<AvatarIcon icon={ProfileIcon} />
				</Avatar>
			</Box>
			<DropdownMenuPopover>
				<DropdownMenuList>
					<DropdownMenuItem onClick={() => alert('Profile')}>
						Profile
					</DropdownMenuItem>
					<DropdownMenuItem onClick={() => alert('Messages')}>
						Messages
					</DropdownMenuItem>
					<DropdownMenuItem onClick={() => alert('Account settings')}>
						Account settings
					</DropdownMenuItem>
				</DropdownMenuList>
			</DropdownMenuPopover>
		</>
	)}
</DropdownMenu>

You can see that although the custom trigger provided is a non-interactive element, it works as expected with all the accesibility features and UI states included. Here's a breakdown of the API and tokens involved in making a non-interactive trigger into an accessibie trigger.

  • triggerProps - a prop that includes the neccessary a11y attributes and callbacks.
  • isMenuOpen - a prop that includes the open state of the DropdownMenu.
  • tabindex - an HTML attribute that enables focus for an element. See MDN reference for further details.
  • hover:* - a CSS class utilty from Tailwindcss for styling the element on hover.
  • active:* - a CSS class utilty from Tailwindcss for styling the element when active.
  • focus-visible:focus-ring - a CSS class that adds a focus indicator to the element on keyboard focus.
  • aria-expanded:* - a CSS class utilty from Tailwindcss to style the element when aria-expanded=true

Attention 🚧 : Though we are able to customize the custom trigger for any element. It is recommended to use only focusable element as trigger for the DropdownMenu

Strategy

Use the strategy prop to change the way how popover element will be positioned. By default, the strategy is set to absolute, changes it to fixed when the DropdownMenuTrigger is inside a sticky or fixed element.

This option leverages the floating-ui library, which powers the DropdownMenuPopover functionality.

const [showFixedElement, setShowFixedElement] = React.useState(false);

const onButtonClick = () => {
	setShowFixedElement((prevState) => !prevState)
}

return (
	<Stack className="w-full gap-4">
		{showFixedElement ? (
			<div
        className="
          animate-[snackbar-transition_0.3s_cubic-bezier(0.16,_1,_0.3,_1)]
          bg-neutral-secondary
          fixed
          flex
          items-center
          justify-between
          mx-2
          p-4
          right-0
          rounded-8px
          shadow-40
          sm:right-8
          sm:w-[360px]
          top-8
          w-[calc(100%-16px)]
          z-10
      "
      >
        <DropdownMenu strategy="fixed">
          {({ triggerProps, isMenuOpen }) => (
            <>
              <DropdownMenuTrigger>Toggle dropdown</DropdownMenuTrigger>
              <DropdownMenuPopover>
                <DropdownMenuList>
                  <DropdownMenuItem onClick={() => alert("Profile")}>
                    Profile
                  </DropdownMenuItem>
                  <DropdownMenuItem onClick={() => alert("Messages")}>
                    Messages
                  </DropdownMenuItem>
                  <DropdownMenuItem onClick={() => alert("Account settings")}>
                    Account settings
                  </DropdownMenuItem>
                </DropdownMenuList>
              </DropdownMenuPopover>
            </>
          )}
        </DropdownMenu>
        <button
          className="
            focus-visible:focus-ring
            font-stronger
            px-1
            py-0.5
            rounded-4px
            text-body-12
            text-primary
            underline
            underline-offset-2
          "
          onClick={onButtonClick}
          >
          Close
        </button>
			</div>
		) : null}
		<Button onClick={onButtonClick}>
			Show fixed element
		</Button>
	</Stack>
);

Disabled

Utilize the isDisabled prop in the <Field/> to show that a entire DropdownMenu isn't usable.

<DropdownMenu isDisabled>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Disabled MenuItem

Utilize the isDisabled prop in the <DropdownMenuItem /> component to indicate that a specific item is not selectable.

This enhances user experience by clearly displaying the disabled state. Additionally, for improved accessibility, keyboard navigation will bypass disabled MenuItems in the Listbox.

<DropdownMenu>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem isDisabled onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked personal settings")}>
        Personal settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Note: Disabled item can already be selected option but having any interactions on it won't be possible.

Style API

Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.

Please refer to the Style API documentation for more insights.