Skip to content

Commit 08b3e11

Browse files
committed
Merge branch 'mp/label-sync-with-pvc' of github.com:primer/react into mp/label-sync-with-pvc
2 parents 4f612a5 + 659e6c8 commit 08b3e11

File tree

13 files changed

+1147
-16
lines changed

13 files changed

+1147
-16
lines changed

.changeset/composable-dropdownmenu.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Add composable `DropdownMenu` to `@primer/components/drafts`

docs/content/drafts/DropdownMenu2.mdx

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
---
2+
component_id: dropdown_menu
3+
title: DropdownMenu v2
4+
status: Alpha
5+
source: https://github.com/primer/react/tree/main/src/DropdownMenu2.tsx
6+
storybook: '/react/storybook?path=/story/composite-components-dropdownmenu2'
7+
description: Use DropdownMenu to select a single option from a list of menu options.
8+
---
9+
10+
import {Box, Avatar} from '@primer/react'
11+
import {DropdownMenu, ActionList} from '@primer/react/drafts'
12+
import {Props} from '../../src/props'
13+
import State from '../../components/State'
14+
import {CalendarIcon, IterationsIcon, NumberIcon, SingleSelectIcon, TypographyIcon} from '@primer/octicons-react'
15+
16+
<br />
17+
18+
<State default={1}>
19+
{([selectedIndex, setSelectedIndex]) => {
20+
const fieldTypes = [
21+
{icon: TypographyIcon, name: 'Text'},
22+
{icon: NumberIcon, name: 'Number'},
23+
{icon: CalendarIcon, name: 'Date'},
24+
{icon: SingleSelectIcon, name: 'Single select'},
25+
{icon: IterationsIcon, name: 'Iteration'}
26+
]
27+
const selectedType = fieldTypes[selectedIndex]
28+
return (
29+
<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6}}>
30+
<DropdownMenu>
31+
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}>
32+
{selectedType.name}
33+
</DropdownMenu.Button>
34+
<DropdownMenu.Overlay width="medium">
35+
<ActionList>
36+
{fieldTypes.map(({icon: Icon, name}, index) => (
37+
<ActionList.Item
38+
key={index}
39+
selected={index === selectedIndex}
40+
onSelect={() => setSelectedIndex(index)}
41+
>
42+
<Icon /> {name}
43+
</ActionList.Item>
44+
))}
45+
</ActionList>
46+
</DropdownMenu.Overlay>
47+
</DropdownMenu>
48+
</Box>
49+
)
50+
}}
51+
</State>
52+
53+
<br />
54+
55+
```js
56+
import {DropdownMenu} from '@primer/react/drafts'
57+
```
58+
59+
<br />
60+
61+
## Examples
62+
63+
### Minimal example
64+
65+
`DropdownMenu` ships with `DropdownMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `DropdownMenu.Overlay`
66+
67+
&nbsp;
68+
69+
```javascript live noinline
70+
// import {DropdownMenu, ActionList} from '@primer/react/drafts'
71+
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑
72+
73+
const fieldTypes = [
74+
{icon: TypographyIcon, name: 'Text'},
75+
{icon: NumberIcon, name: 'Number'},
76+
{icon: CalendarIcon, name: 'Date'},
77+
{icon: SingleSelectIcon, name: 'Single select'},
78+
{icon: IterationsIcon, name: 'Iteration'}
79+
]
80+
81+
const Example = () => {
82+
const [selectedIndex, setSelectedIndex] = React.useState(1)
83+
const selectedType = fieldTypes[selectedIndex]
84+
85+
return (
86+
<DropdownMenu>
87+
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}>
88+
{selectedType.name}
89+
</DropdownMenu.Button>
90+
<DropdownMenu.Overlay width="medium">
91+
<ActionList>
92+
{fieldTypes.map((type, index) => (
93+
<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}>
94+
<type.icon /> {type.name}
95+
</ActionList.Item>
96+
))}
97+
</ActionList>
98+
</DropdownMenu.Overlay>
99+
</DropdownMenu>
100+
)
101+
}
102+
103+
render(<Example />)
104+
```
105+
106+
### Customise Button
107+
108+
`Dropdown.Button` uses `Button v2` so you can pass props like `variant` and `leadingIcon` that `Button v2` accepts.
109+
110+
```javascript live noinline
111+
// import {DropdownMenu, ActionList} from '@primer/react/drafts'
112+
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑
113+
114+
const Example = () => {
115+
const [duration, setDuration] = React.useState(1)
116+
117+
return (
118+
<DropdownMenu>
119+
<DropdownMenu.Button variant="invisible" aria-label="Select iteration duration">
120+
{duration} {duration > 1 ? 'weeks' : 'week'}
121+
</DropdownMenu.Button>
122+
<DropdownMenu.Overlay width="medium">
123+
<ActionList>
124+
{[1, 2, 3, 4, 5, 6].map(weeks => (
125+
<ActionList.Item key={weeks} selected={duration === weeks} onSelect={() => setDuration(weeks)}>
126+
{weeks} {weeks > 1 ? 'weeks' : 'week'}
127+
</ActionList.Item>
128+
))}
129+
</ActionList>
130+
</DropdownMenu.Overlay>
131+
</DropdownMenu>
132+
)
133+
}
134+
135+
render(<Example />)
136+
```
137+
138+
### With External Anchor
139+
140+
To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass `open` and `onOpenChange` along with an `anchorRef` to `DropdownMenu`:
141+
142+
```javascript live noinline
143+
// import {DropdownMenu, ActionList} from '@primer/react/drafts'
144+
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑
145+
146+
const Example = () => {
147+
const [open, setOpen] = React.useState(false)
148+
const anchorRef = React.createRef()
149+
150+
return (
151+
<>
152+
<Button ref={anchorRef} onClick={() => setOpen(!open)}>
153+
{open ? 'Close Menu' : 'Open Menu'}
154+
</Button>
155+
156+
<DropdownMenu open={open} onOpenChange={setOpen} anchorRef={anchorRef}>
157+
<DropdownMenu.Overlay>
158+
<ActionList>
159+
<ActionList.Item selected={true}>Text</ActionList.Item>
160+
<ActionList.Item>Number</ActionList.Item>
161+
<ActionList.Item>Date</ActionList.Item>
162+
<ActionList.Item>Iteration</ActionList.Item>
163+
</ActionList>
164+
</DropdownMenu.Overlay>
165+
</DropdownMenu>
166+
</>
167+
)
168+
}
169+
170+
render(<Example />)
171+
```
172+
173+
### With Overlay Props
174+
175+
```javascript live noinline
176+
// import {DropdownMenu, ActionList} from '@primer/react/drafts'
177+
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑
178+
179+
const fieldTypes = [
180+
{icon: TypographyIcon, name: 'Text'},
181+
{icon: NumberIcon, name: 'Number'},
182+
{icon: CalendarIcon, name: 'Date'},
183+
{icon: SingleSelectIcon, name: 'Single select'},
184+
{icon: IterationsIcon, name: 'Iteration'}
185+
]
186+
187+
const Example = () => {
188+
const handleEscape = () => alert('you hit escape!')
189+
190+
const [selectedIndex, setSelectedIndex] = React.useState(1)
191+
const selectedType = fieldTypes[selectedIndex]
192+
193+
return (
194+
<DropdownMenu>
195+
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}>
196+
{selectedType.name}
197+
</DropdownMenu.Button>
198+
<DropdownMenu.Overlay width="medium" onEscape={handleEscape}>
199+
<ActionList>
200+
{fieldTypes.map((type, index) => (
201+
<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}>
202+
<type.icon /> {type.name}
203+
</ActionList.Item>
204+
))}
205+
</ActionList>
206+
</DropdownMenu.Overlay>
207+
</DropdownMenu>
208+
)
209+
}
210+
211+
render(<Example />)
212+
```
213+
214+
### With a custom anchor
215+
216+
You can choose to have a different _anchor_ for the Menu dependending on the application's context.
217+
218+
&nbsp;
219+
220+
```javascript live noinline
221+
// import {DropdownMenu, ActionList} from '@primer/react/drafts'
222+
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑
223+
224+
render(
225+
<DropdownMenu>
226+
<DropdownMenu.Anchor>
227+
<button>Select a field type</button>
228+
</DropdownMenu.Anchor>
229+
230+
<DropdownMenu.Overlay>
231+
<ActionList>
232+
<ActionList.Item selected={true}>Text</ActionList.Item>
233+
<ActionList.Item>Number</ActionList.Item>
234+
<ActionList.Item>Date</ActionList.Item>
235+
<ActionList.Item>Iteration</ActionList.Item>
236+
</ActionList>
237+
</DropdownMenu.Overlay>
238+
</DropdownMenu>
239+
)
240+
```
241+
242+
<Note variant="warning">
243+
244+
Use `DropdownMenu` to select an option from a small list. If you’re looking for filters or multiple selection, use [SelectPanel](/SelectPanel) instead.
245+
246+
</Note>
247+
248+
## Props
249+
250+
### DropdownMenu
251+
252+
<PropsTable>
253+
<PropsTableRow
254+
required
255+
name="children"
256+
type="React.ReactElement[]"
257+
description={
258+
<>
259+
Recommended: <InlineCode>DropdownMenu.Button</InlineCode> or <InlineCode>DropdownMenu.Anchor</InlineCode> with{' '}
260+
<InlineCode>DropdownMenu.Overlay</InlineCode>
261+
</>
262+
}
263+
/>
264+
<PropsTableRow
265+
name="open"
266+
type="boolean"
267+
description={
268+
<>
269+
If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
270+
<InlineCode>onOpenChange</InlineCode>
271+
</>
272+
}
273+
/>
274+
<PropsTableRow
275+
name="onOpenChange"
276+
type="(open: boolean) => void"
277+
description={
278+
<>
279+
If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
280+
<InlineCode>open</InlineCode>
281+
</>
282+
}
283+
/>
284+
<PropsTableRow
285+
name="anchorRef"
286+
type="React.RefObject<HTMLElement>"
287+
description="Useful for defining an external anchor"
288+
/>
289+
</PropsTable>
290+
291+
### DropdownMenu.Button
292+
293+
<PropsTable>
294+
<PropsTableRow
295+
name="ButtonProps"
296+
type={
297+
<>
298+
<Link href="/drafts/Button2#api-reference">ButtonProps</Link>
299+
</>
300+
}
301+
description={
302+
<>
303+
You can pass all of the props that you would pass to a{' '}
304+
<Link href="/drafts/Button2#api-reference">
305+
<InlineCode>Button</InlineCode>
306+
</Link>{' '}
307+
component like <InlineCode>variant</InlineCode>, <InlineCode>leadingIcon</InlineCode>,{' '}
308+
<InlineCode>sx</InlineCode>, etc.
309+
</>
310+
}
311+
/>
312+
</PropsTable>
313+
314+
### DropdownMenu.Anchor
315+
316+
<PropsTable>
317+
<PropsTableRow required name="children" type="React.ReactElement" description="Accepts a single child" />
318+
</PropsTable>
319+
320+
### DropdownMenu.Overlay
321+
322+
<PropsTable>
323+
<PropsTableRow
324+
required
325+
name="children"
326+
type="React.ReactElement[]"
327+
description={
328+
<>
329+
Recommended:{' '}
330+
<Link href="/drafts/ActionList2">
331+
<InlineCode>ActionList</InlineCode>
332+
</Link>
333+
</>
334+
}
335+
/>
336+
<PropsTableRow
337+
name="OverlayProps"
338+
type="OverlayProps"
339+
description={
340+
<>
341+
Props to be spread on the internal{' '}
342+
<Link href="/AnchoredOverlay">
343+
<InlineCode>AnchoredOverlay</InlineCode>
344+
</Link>
345+
</>
346+
}
347+
/>
348+
</PropsTable>
349+
350+
## Status
351+
352+
<ComponentChecklist
353+
items={{
354+
propsDocumented: true,
355+
noUnnecessaryDeps: true,
356+
adaptsToThemes: true,
357+
adaptsToScreenSizes: true,
358+
fullTestCoverage: true,
359+
usedInProduction: false,
360+
usageExamplesDocumented: true,
361+
hasStorybookStories: true,
362+
designReviewed: false,
363+
a11yReviewed: false,
364+
stableApi: false,
365+
addressedApiFeedback: false,
366+
hasDesignGuidelines: true,
367+
hasFigmaComponent: true
368+
}}
369+
/>
370+
371+
## Further reading
372+
373+
[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list)
374+
375+
## Related components
376+
377+
- [ActionList](/drafts/ActionList2)
378+
- [ActionMenu](/ActionMenu2)
379+
- [SelectPanel](/SelectPanel)

0 commit comments

Comments
 (0)