DropDown Menu

A dropdown menu displays a list of options for the element that triggers it.

installyarn add @clayui/drop-down
versionNPM Version
useimport DropDown from '@clayui/drop-down';

Example

import {Provider} from '@clayui/core';
import DropDown from '@clayui/drop-down';
import Button from '@clayui/button';
import React from 'react';

import '@clayui/css/lib/css/atlas.css';

const items = [
	{
		children: [
			{id: 2, name: 'Apple'},
			{id: 3, name: 'Banana'},
			{id: 4, name: 'Mangos'},
		],
		id: 1,
		name: 'Fruit',
	},
	{
		children: [
			{id: 6, name: 'Potatoes'},
			{id: 7, name: 'Tomatoes'},
			{id: 8, name: 'Onions'},
		],
		id: 5,
		name: 'Vegetable',
	},
];

export default function App() {
	return (
		<Provider spritemap="/public/icons.svg">
			<div className="p-4">
				<DropDown filterKey="name" trigger={<Button>Select</Button>}>
					<DropDown.Search placeholder="Type to filter" />
					<DropDown.ItemList items={items}>
						{(item) => (
							<DropDown.Group
								header={item.name}
								items={item.children}
								key={item.name}
							>
								{(item) => (
									<DropDown.Item
										key={item.name}
										onClick={() => {
											// logic stuff...
										}}
									>
										{item.name}
									</DropDown.Item>
								)}
							</DropDown.Group>
						)}
					</DropDown.ItemList>
				</DropDown>
			</div>
		</Provider>
	);
}

Introduction

The Dropdown Menu component is designed to display menus that when clicked triggers some action, there are different forms of Menu in Clay/Lexicon. This document describes the main ways to build a menu and recommendations.

Clay provides two possible possibilities to build a Menu, using composition that we call low-level that allows you to add new use cases and adapt them to your use case and components like high-level which are property-setting oriented components. We recommend that you always try to use compositing when you need flexibility, whether it’s configuring some internal components that aren’t possible in a high-level component or adding your own elements to an item, group, or other use cases that don’t exist in Clay.

Content

Dropdown Menu provides two options for building the menu using static content when data does not change during an application’s lifecycle and dynamic for content that changes during the application lifecycle or the data comes from the server.

Static

The simplest example of a static menu is rendering a simple list of options.

<DropDown trigger={<Button>Click Me</Button>}>
	<DropDown.ItemList>
		<DropDown.Item>one</DropDown.Item>
		<DropDown.Item>two</DropDown.Item>
		<DropDown.Item>three</DropDown.Item>
	</DropDown.ItemList>
</DropDown>

Other cases are also possible, for example rendering a group of options with a search bar to filter the options.

<DropDown trigger={<Button>Click Me</Button>}>
	<DropDown.Search placeholder="Type to filter" />
	<DropDown.ItemList>
		<DropDown.Group header="Numbers">
			<DropDown.Item>one</DropDown.Item>
			<DropDown.Item>two</DropDown.Item>
			<DropDown.Item>three</DropDown.Item>
		</DropDown.Group>
		<DropDown.Group header="Letters">
			<DropDown.Item>a</DropDown.Item>
			<DropDown.Item>b</DropDown.Item>
			<DropDown.Item>c</DropDown.Item>
		</DropDown.Group>
	</DropDown.ItemList>
</DropDown>

The example above shows how Clay even with the explicit composition of the menu structure manages to provide the filter to work OOTB with static content.

Dynamic

<DropDown items={['one', 'two', 'three']} trigger={<Button>Click Me</Button>}>
	{(item) => <DropDown.Item>{item}</DropDown.Item>}
</DropDown>

Compositing for dynamic content becomes simpler and also provides the same features as static, such as OOTB filter and groups. The component is also data agnostic designed from the ground up for this purpose to avoid transformations having to be done at render time.

<DropDown filterKey="name" trigger={<Button>Click Me</Button>}>
	<DropDown.Search placeholder="Type to filter" />
	<DropDown.ItemList
		items={[
			{
				children: [{name: 'one'}, {name: 'two'}, {name: 'three'}],
				name: 'Numbers',
			},
			{
				children: [{name: 'a'}, {name: 'b'}, {name: 'c'}],
				name: 'Letters',
			},
		]}
	>
		{(item) => (
			<DropDown.Group
				header={item.name}
				items={item.children}
				key={item.name}
			>
				{(item) => (
					<DropDown.Item key={item.name}>{item.name}</DropDown.Item>
				)}
			</DropDown.Group>
		)}
	</DropDown.ItemList>
</DropDown>

Filter

The filter in a Menu works OOTB, just declaring the <DropDown.Search> component in the composition, as in the examples shown for static and dynamic content. The Filter can be done by DropDown itself as well as the developer can control the filter and make his own filter rule.

function Example() {
	const [value, setValue] = useState('');

	const options = [{name: 'one'}, {name: 'two'}, {name: 'three'}];

	const filteredOptions = useMemo(() => {
		if (!value) {
			return options;
		}

		return options.filter(
			(option) => option.match(new RegExp(value, 'i')) !== null
		);
	}, [options, value]);

	return (
		<DropDown trigger={<Button>Click Me</Button>}>
			<DropDown.Search
				value={value}
				onChange={setValue}
				placeholder="Type to filter"
			/>
			<DropDown.ItemList items={filteredOptions}>
				{(item) => (
					<DropDown.Item key={item.name}>{item.name}</DropDown.Item>
				)}
			</DropDown.ItemList>
		</DropDown>
	);
}

If you just need to set an initial value, just use the defaultValue property.

<DropDown filterKey="name" trigger={<Button>Click Me</Button>}>
	<DropDown.Search defaultValue="o" placeholder="Type to filter" />
	<DropDown.ItemList items={[{name: 'one'}, {name: 'two'}, {name: 'three'}]}>
		{(item) => <DropDown.Item key={item.name}>{item.name}</DropDown.Item>}
	</DropDown.ItemList>
</DropDown>

Value

DropDown filters items according to the value rendered as children of <DropDown.Item>, when there are more detailed compositions in the item, the filter will not work because it cannot determine which value to use to filter, in this scenario configure the item’s value using the textValue property.

<DropDown filterKey="name" trigger={<Button>Click Me</Button>}>
	<DropDown.Search placeholder="Type to filter" />
	<DropDown.ItemList items={[{name: 'one'}, {name: 'two'}, {name: 'three'}]}>
		{(item) => (
			<DropDown.Item key={item.name} textValue={item.name}>
				<Checkbox />
				{item.name}
			</DropDown.Item>
		)}
	</DropDown.ItemList>
</DropDown>

Without Trigger

You may want to create a trigger that is not necessarily in the same tree as DropDown, due to HTML markup issues, for these cases you can use <ClayDropDown.Menu>.

Using <ClayDropDown.Menu> allows you to better control the state of DropDown but you will have to deal with visibility, focus management, and other details.

As a recommendation only use this component as a last resort, it doesn’t provide some OOTB features like focus control or accessibility at all.

import ClayDropDown from '@clayui&#x2215;drop-down';
import React, {useState, useRef} from 'react';

const Menu = ({children, hasLeftSymbols, hasRightSymbols}) => {
	const triggerElementRef = useRef(null);
	const [expand, setExpand] = useState(false);
	const menuElementRef = useRef(null);

	const handleExpand = (event) => {
		// This is not ideal for allowing you to have more than
		// one trigger for the same content but it simulates the
		// advantages of controlling `DropDown.Menu`.
		triggerElementRef.current = event.target;

		setExpand(!expand);
	};

	return (
		<div>
			<div>
				<button type="button" onClick={handleExpand}>
					Home
				</button>
			</div>
			<div>
				<button type="button" onClick={handleExpand}>
					Product
				</button>
			</div>

			<ClayDropDown.Menu
				active={expand}
				alignElementRef={triggerElementRef}
				hasLeftSymbols={hasLeftSymbols}
				hasRightSymbols={hasRightSymbols}
				onActiveChange={() => setExpand(!expand)}
				ref={menuElementRef}
			>
				{children}
			</ClayDropDown.Menu>
		</div>
	);
};

Variants

Clay provides other ways to use the Clay component, which we call high-level components, which are designed to do a specific high-level behavior different from compositing that allow different possibilities, are less flexible.

With Items

Allows you to create a simple DropDown, through its API you are able to create a Menu with groups of checkboxes and radios, links, buttons, search, caption, etc.

import {Provider} from '@clayui/core';
import DropDown from '@clayui/drop-down';
import Button from '@clayui/button';
import React, {useState} from 'react';

import '@clayui/css/lib/css/atlas.css';

const items = [
	{
		label: 'clickable',
		onClick: (event) => event.preventDefault(),
	},
	{
		type: 'divider',
	},
	{
		items: [
			{
				label: 'one',
				type: 'radio',
				value: 'one',
			},
			{
				label: 'two',
				type: 'radio',
				value: 'two',
			},
		],
		label: 'radio',
		name: 'radio',
		onChange: (value) => alert('New Radio checked'),
		type: 'radiogroup',
	},
	{
		items: [
			{
				checked: true,
				label: 'checkbox',
				onChange: () => alert('checkbox changed'),
				type: 'checkbox',
			},
			{
				checked: true,
				label: 'checkbox 1',
				onChange: () => alert('checkbox changed'),
				type: 'checkbox',
			},
		],
		label: 'checkbox',
		type: 'group',
	},
	{
		href: '#',
		label: 'linkable',
	},
];

export default function App() {
	const [value, setValue] = useState();

	return (
		<Provider spritemap="/public/icons.svg">
			<div className="p-4">
				<DropDown filterKey="name" trigger={<Button>Select</Button>}>
					<DropDown.Search placeholder="Type to filter" />
					<DropDown.ItemList items={items}>
						{(item) => (
							<DropDown.Group
								header={item.name}
								items={item.children}
								key={item.name}
							>
								{(item) => (
									<DropDown.Item
										key={item.name}
										onClick={() => {
											// logic stuff...
										}}
									>
										{item.name}
									</DropDown.Item>
								)}
							</DropDown.Group>
						)}
					</DropDown.ItemList>
				</DropDown>
			</div>
		</Provider>
	);
}

Cascading Menu

DropDownWithItems allows the possibility to create a contextual menu, the nature of the API allows the creation of more cascade menus but the Lexicon specification recommends using only one level and using DropDownWithDrilldown component.

To render a cascading menu it is necessary to set the type of the item to contextual and add to the items object that follows the same API as the other items.

const items = [
	{label: 'Folder'},
	{type: 'divider'},
	{
		items: [
			{label: 'Basic Document'},
			{label: 'Contract'},
			{label: 'Marketing Banner'},
			{label: 'Spreadsheet'},
			{label: 'Presentation'},
		],
		label: 'Document',
		type: 'contextual',
	},
	{label: 'Shortcut'},
	{label: 'Repository'},
];

Drilldown

A Drilldown menu allows the user to navigate to and/or select an element from a contextual list. It can be triggered by a dropdown button.

import {Provider} from '@clayui/core';
import {ClayDropDownWithDrilldown} from '@clayui/drop-down';
import Button from '@clayui/button';
import React from 'react';

import '@clayui/css/lib/css/atlas.css';

export default function App() {
	return (
		<Provider spritemap="/public/icons.svg">
			<div className="p-4">
				<ClayDropDownWithDrilldown
					defaultActiveMenu="x0a3"
					menus={{
						x0a3: [
							{href: '#', title: 'Hash Link'},
							{type: 'divider'},
							{onClick: () => alert('test'), title: 'Alert!'},
							{type: 'divider'},
							{child: 'x0a4', title: 'Subnav'},
						],
						x0a4: [
							{href: '#', title: '2nd hash link'},
							{child: 'x0a5', title: 'Subnav'},
						],
						x0a5: [
							{title: 'The'},
							{type: 'divider'},
							{title: 'End'},
						],
					}}
					trigger={<Button>{'Click Me'}</Button>}
				/>
			</div>
		</Provider>
	);
}

The way the Drilldown component links the menus is done via reference, the menu needs a unique id.

const menus = {
	of23: [{title: 'First'}],
};

From the id you are able to link to another menu using the child property.

const menus = {
	of23: [{title: 'First', child: 'of09'}],
	of09: [{title: 'Three'}],
};

If you want to add separators between your menu items, it’s possible to do so, by using an item that has this shape {type: 'divider'}.

const menus = {
	of23: [
		{title: 'First', child: 'of09'},
		{type: 'divider'},
		{title: 'Second'},
	],
	of09: [{title: 'Three'}],
};

An important thing to have in mind, is that the DropDownWithDrilldown component, will render its menus in the order that they are specified. This means that if you specify the menus in the wrong order, the menu animation will not behave correctly.

Caveats

One caveat with the drop down menu is that it is rendered inside of a React Portal and is rendered directly to the body element. This means if you are using the menuWidth prop set to auto, it will not respect the size of the node parent of the drop down, since the menu is rendered directly to the body.