Skip to content

Commit 17f10bd

Browse files
authored
feat(components): add DropdownMenu component (DS-5055) (#457)
1 parent 94b5d46 commit 17f10bd

56 files changed

Lines changed: 3099 additions & 76 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/components/Roadmap/data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ export const rows: Rows = [
412412
},
413413
{
414414
component: 'DropdownMenu',
415-
status: '🚧 Planned',
415+
status: '✅ Done',
416+
stage: '🔵 experimental',
416417
planned: 'Q3 2026',
417418
},
418419
{
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import {
2+
Meta,
3+
Story,
4+
Props,
5+
Status,
6+
} from '../../../../../.storybook/components';
7+
8+
import * as Stories from './DropdownMenu.stories';
9+
10+
<Meta of={Stories} />
11+
12+
# DropdownMenu
13+
14+
<Status variant="experimental" />
15+
16+
A dropdown menu displays a list of actions or options that a user can choose.
17+
18+
Compared to the [Menu](?path=/docs/components-menu--docs) component, it adds
19+
submenus and search.
20+
21+
## Import
22+
23+
```tsx
24+
import { DropdownMenu } from '@koobiq/react-components';
25+
```
26+
27+
## Usage
28+
29+
A dropdown menu is composed of a trigger and its content:
30+
31+
<Story of={Stories.Base} />
32+
33+
## Props
34+
35+
<Props of={Stories.Base} />
36+
37+
## Subcomponents
38+
39+
The component has the following helper components:
40+
41+
- `DropdownMenu.Popover` — the overlay, also used for a submenu
42+
- `DropdownMenu.Content` — the list of items inside the popover
43+
- `DropdownMenu.Item` — a single action
44+
- `DropdownMenu.ItemText` — the text of an item, with an optional caption
45+
- `DropdownMenu.ItemAddon` — an addon of an item, such as an icon or a shortcut
46+
- `DropdownMenu.Section` — a group of items
47+
- `DropdownMenu.Header` — a block of custom content, or the heading of a section
48+
- `DropdownMenu.SubmenuTrigger` — wraps an item and the submenu it opens
49+
- `DropdownMenu.Autocomplete` — filters the menu with a field
50+
- `DropdownMenu.Footer` — the text under the items, outside the scroll area
51+
- `DropdownMenu.Pressable` — makes custom markup usable as a trigger
52+
53+
The popover and the content are separate components, so anything can go between
54+
them. That is how the search field is added.
55+
56+
## Trigger
57+
58+
The first child of the `DropdownMenu` is the trigger. A `Button` works as one
59+
without extra props.
60+
61+
For custom markup that is not a button, wrap it in the `DropdownMenu.Pressable`
62+
(see the [Separators](#separators) example).
63+
64+
The `trigger` prop switches between opening on press (default) and on a long press:
65+
66+
<Story of={Stories.LongPress} />
67+
68+
## Content
69+
70+
The items can be written out one by one, or passed as data through the `items`
71+
prop. Give every item a unique `id`. When an item is chosen, the `onAction`
72+
handler is called with that id.
73+
74+
<Story of={Stories.Content} />
75+
76+
## Item content
77+
78+
An item is composed of slots: the `DropdownMenu.ItemAddon` for icons and
79+
shortcuts, and the `DropdownMenu.ItemText` for the label and its caption.
80+
81+
<Story of={Stories.ItemContent} />
82+
83+
An item built from slots needs the `textValue` prop, because the search and the
84+
typeahead match on it. Without it the item cannot be found by typing, and it
85+
disappears on the first keystroke in a searchable menu. The component warns
86+
about this in development.
87+
88+
Add the `aria-label` prop when an item needs a shorter name for screen readers
89+
than all of its slots read together.
90+
91+
The `align` prop lines the slots up by their top edge instead of centring them.
92+
It is useful for a tall item with a caption.
93+
94+
## Selection
95+
96+
The items are not selectable by default. Selection is turned on with the
97+
`selectionMode` prop. Use the `defaultSelectedKeys` prop to set the selected
98+
items (uncontrolled), and the `selectedKeys` prop to control them. Both take the
99+
`id` of the items.
100+
101+
### Single
102+
103+
<Story of={Stories.SelectionSingle} />
104+
105+
### Multiple
106+
107+
<Story of={Stories.SelectionMultiple} />
108+
109+
## Disabled items
110+
111+
Items can be marked as disabled with the `disabledKeys` prop of the content, or
112+
with the `isDisabled` prop of a single item.
113+
114+
<Story of={Stories.DisabledItems} />
115+
116+
## Links
117+
118+
An item with the `href` prop becomes a link. It navigates instead of calling the
119+
`onAction` handler, and it is never selectable.
120+
121+
<Story of={Stories.Links} />
122+
123+
## Client side routing
124+
125+
Wrap the app in the `RouterProvider`, and the link items will navigate through
126+
your router, be it **Next.js**, **React Router** or another one.
127+
128+
```tsx
129+
import { useRouter } from 'next/navigation';
130+
131+
import { RouterProvider } from '@koobiq/react-components';
132+
133+
export default function App() {
134+
const router = useRouter();
135+
136+
return (
137+
<RouterProvider navigate={router.push}>
138+
<DropdownMenu>
139+
<Button>Pages</Button>
140+
<DropdownMenu.Popover>
141+
<DropdownMenu.Content aria-label="pages">
142+
<DropdownMenu.Item href="/page1">Page 1</DropdownMenu.Item>
143+
<DropdownMenu.Item href="/page2">Page 2</DropdownMenu.Item>
144+
</DropdownMenu.Content>
145+
</DropdownMenu.Popover>
146+
</DropdownMenu>
147+
</RouterProvider>
148+
);
149+
}
150+
```
151+
152+
## Sections
153+
154+
Groups of items can be wrapped in a `DropdownMenu.Section` with the `title` prop.
155+
156+
### Static items
157+
158+
<Story of={Stories.Sections} />
159+
160+
### Dynamic items
161+
162+
<Story of={Stories.SectionsDynamic} />
163+
164+
### With Section Level Selection
165+
166+
Each section takes its own `selectionMode`, `selectedKeys` and `onSelectionChange`
167+
props, so one menu can mix plain actions with a single and a multiple choice.
168+
169+
<Story of={Stories.WithSectionLevelSelection} />
170+
171+
## Separators
172+
173+
Put a `Divider` between items or sections to group them without a title. The
174+
`DropdownMenu.Header` renders custom content inside the menu.
175+
176+
<Story of={Stories.Separators} />
177+
178+
## Submenu
179+
180+
Wrap an item and a nested `DropdownMenu.Popover` in a `DropdownMenu.SubmenuTrigger`
181+
to give the item a submenu. The chevron is added automatically. A submenu opens
182+
on hover after the `delay` prop (200 ms by default), on <kbd>ArrowRight</kbd> and
183+
on press, and closes on <kbd>ArrowLeft</kbd>. Submenus can be nested.
184+
185+
<Story of={Stories.Submenu} />
186+
187+
## Search
188+
189+
Wrap the content in a `DropdownMenu.Autocomplete` and put a `SearchInput` above
190+
it. The field has to be next to the menu, not inside it.
191+
192+
Matching ignores the case and the accents. Pass the `filter` prop to change it.
193+
The focus stays in the field while the arrows move through the items. An item
194+
that opens a submenu stays when its own text matches, but the items inside a
195+
submenu are not searched.
196+
197+
<Story of={Stories.Search} />
198+
199+
The `renderEmptyState` prop of the content replaces the default message, with an
200+
`EmptyState` for instance.
201+
202+
<Story of={Stories.SearchEmpty} />
203+
204+
## Footer
205+
206+
The `DropdownMenu.Footer` shows text under the items. It stays in place while the
207+
items scroll.
208+
209+
<Story of={Stories.DropdownFooter} />
210+
211+
## Placement
212+
213+
The placement of the menu with respect to its trigger can be adjusted using the
214+
`placement` prop of the popover. The menu also flips to the opposite side when
215+
there is not enough space.
216+
217+
<Story of={Stories.Placement} />
218+
219+
## Open
220+
221+
### Default open
222+
223+
The menu isn't opened by default. The `defaultOpen` prop can be used to set the
224+
default state.
225+
226+
### Controlled open
227+
228+
The `isOpen` prop can be used to make the opened state controlled. The
229+
`onOpenChange` event is fired when the open state of the menu changes.
230+
231+
<Story of={Stories.Open} />

0 commit comments

Comments
 (0)