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.

FullScreen on Mobile

On mobile, enable isFullScreen to render the sheet in full-screen with a default close button.

<DropdownMenu mobilePopover={{ isFullScreen: true }}>
  <DropdownMenuTrigger>Toggle dropdown menu</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>

API Reference

DropdownMenu

PropTypeDescriptionDefault
isDisabled?booleanWhether the dropdown menu is disabled.false
children((menuState: { isMenuOpen: boolean; triggerProps: TriggerProps<React.ElementType>; }) => React.ReactNode) | React.ReactNodeAccepts either a React node or a render function. The render function provides the menu's state (isMenuOpen) and accessibility/interaction props (triggerProps) for the trigger._
popoverMatchReferenceWidth?booleanMatch the width of the popover with the reference element.false
popoverMaxHeight?numberThe max height of the dropdown panel popover.356
popoverMaxWidth?numberThe max width of the dropdown panel popover.400
popoverOffset?numberThe offset of the dropdown panel popover.4
popoverPlacement?'bottom' | 'bottom-start' | 'bottom-end'The placement of the dropdown panel popover relative to the dropdown menu trigger.'bottom-start'
strategy?'absolute' | 'fixed'The strategy used to position the floating element.'absolute'
mobilePopover?objectConfig options for responsive behavior of the dropdown menu._
mobilePopover?.mobileFriendlybooleanWhether the dropdown menu should be mobile friendlytrue
mobilePopover?.titleForMobilestringTitle for the mobile popover. If titleForMobile is omitted, the sheet header is not rendered._
mobilePopover?.closeButtonPropsForMobile{ label: string, onClick: () => void, size?: 'large' | 'standard' | 'small' }Config options for the close button that appears on mobile._
mobilePopover?.togglePointnumberThe toggle point for the dropdown menu to switch to mobile view. Deprecated with backwards compatibility ⚠️ (The logic will be handled internally in future)768
mobilePopover?.isFullScreenbooleanWhen true, the sheet in mobile view will take up 100% of the available heightfalse

DropdownMenuTrigger

PropTypeDescriptionDefault
variant?'accentPrimary' | 'accentSecondary'| 'criticalPrimary'| 'criticalTertiary'| 'neutralPrimary'| 'neutralSecondary'| 'neutralSecondaryIntense'| 'neutralTertiary'Variant of the button.'neutralSecondary'
size?'large' | 'standard' | 'small'Size of the button.'standard'
iconStart?IconThe icon to display before the buttons children._
iconEnd?IconThe icon to display after the buttons children._
loadingLabel?stringText to read out to assistive technologies when button is loading._
isDisabled?booleanIf true, the button is disabled.false
isLoading?booleanIf true ,the button will display a loading indicator.false
isPressed?booleanIf true, the button will be visually styled as pressed and the aria-pressed attribute will be set accordingly._
children?React.ReactNodeThe content to be displayed inside the button._
form?stringSpecifies the form element the button is associated with._
onBlur?functionFunction to call when the button loses focus._
onFocus?functionFunction to call when the button receives focus._
onKeyDown?functionFunction to call when a key is pressed while the button is focused._

DropdownMenuPopover

PropTypeDescriptionDefault
shouldUsePortal?booleanDetermines whether the DropdownMenuPopover should be rendered in a React Portal. If true, the Popover will be rendered outside the DOM hierarchy of the parent component.true
childrenReact.ReactNodeContent of the DropdownMenu with the options._

DropdownMenuList

PropTypeDescriptionDefault
children?React.ReactNode | ((option: object) => React.ReactNode)Accepts either a ReactNode or a function that returns a ReactNode for each item in the collection._
options?ReadonlyArray<object>An array of items that the collection should render.Note: This is mandatory if children is a function._

DropdownMenuItem

PropTypeDescriptionDefault
childrenReact.ReactNodeThe content of the dropdown menu item._
id?stringAn optional ID for the menu item; auto-generated if not provided._
onClick?onClick?: () => boolean | void;Function to be called when the item is clicked; if it returns false, the dropdown will remain open._
size?'standard'|'large'Size of the menu item.standard

Note: The DropdownMenuItem component can render as different HTML elements (e.g., label, a, div) based on the as prop, providing flexibility for various use cases in dropdown menus.

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.