-
Notifications
You must be signed in to change notification settings - Fork 616
Drafts: Composable DropdownMenu v2 #1735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
04c6e73
fc1fcaa
bb8e918
2ffe5bb
f8ceeff
6a11a80
616b36e
f6b6c09
270456a
3c7b2f6
4a920de
619d6fb
a1d820b
7560ebe
f95dce7
4e4decd
2798f95
55a758e
f5e9daa
8de18a5
26867e6
565905c
0682801
7f357cd
4d9e9b9
5b90d71
aeb4b29
772daa0
a29ce21
0a8657d
c89316b
5e7a5e7
40a5ed9
091b9ce
656eb4f
97f7bae
b69d92b
538b216
f61d50a
7b31494
1f5148a
ed06f71
268dc3c
d0da4db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@primer/react': patch | ||
--- | ||
|
||
Add composable `DropdownMenu` to `@primer/components/drafts` |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,379 @@ | ||||||
--- | ||||||
component_id: dropdown_menu | ||||||
title: DropdownMenu v2 | ||||||
status: Alpha | ||||||
source: https://github.com/primer/react/tree/main/src/DropdownMenu2.tsx | ||||||
storybook: '/react/storybook?path=/story/composite-components-dropdownmenu2' | ||||||
description: Use DropdownMenu to select a single option from a list of menu options. | ||||||
--- | ||||||
|
||||||
import {Box, Avatar} from '@primer/react' | ||||||
import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
import {Props} from '../../src/props' | ||||||
import State from '../../components/State' | ||||||
import {CalendarIcon, IterationsIcon, NumberIcon, SingleSelectIcon, TypographyIcon} from '@primer/octicons-react' | ||||||
|
||||||
<br /> | ||||||
|
||||||
<State default={1}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example appears at the top of docs, but with more recent updates we've been moving them under examples heading and keeping import statements at the top. Can we do the same here for consistency? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to vote for keeping it 🤔 I've done this for ActionList and ActionMenu as well to quickly see what this component is. Meanwhile, the examples slowly build up starting from an easy example to more complex ones. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks quite nice actually, almost like a preview / hero. 👍 .. we'll need to comm this to others so we can consider doing same elsewhere. |
||||||
{([selectedIndex, setSelectedIndex]) => { | ||||||
const fieldTypes = [ | ||||||
{icon: TypographyIcon, name: 'Text'}, | ||||||
{icon: NumberIcon, name: 'Number'}, | ||||||
{icon: CalendarIcon, name: 'Date'}, | ||||||
{icon: SingleSelectIcon, name: 'Single select'}, | ||||||
{icon: IterationsIcon, name: 'Iteration'} | ||||||
] | ||||||
const selectedType = fieldTypes[selectedIndex] | ||||||
return ( | ||||||
<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6}}> | ||||||
<DropdownMenu> | ||||||
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}> | ||||||
{selectedType.name} | ||||||
</DropdownMenu.Button> | ||||||
<DropdownMenu.Overlay width="medium"> | ||||||
<ActionList> | ||||||
{fieldTypes.map(({icon: Icon, name}, index) => ( | ||||||
<ActionList.Item | ||||||
key={index} | ||||||
selected={index === selectedIndex} | ||||||
onSelect={() => setSelectedIndex(index)} | ||||||
> | ||||||
<Icon /> {name} | ||||||
</ActionList.Item> | ||||||
))} | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
</Box> | ||||||
) | ||||||
}} | ||||||
</State> | ||||||
|
||||||
<br /> | ||||||
|
||||||
```js | ||||||
import {DropdownMenu} from '@primer/react/drafts' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that you're giving readers an easy thing to copy and paste to import the component. @colebemis - maybe we should consider adding this as we do #1701. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also hoist the import to the top of the page? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aw this is short lived, would need to remove this after #1785 is merged |
||||||
``` | ||||||
|
||||||
<br /> | ||||||
|
||||||
## Examples | ||||||
|
||||||
### Minimal example | ||||||
|
||||||
`DropdownMenu` ships with `DropdownMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `DropdownMenu.Overlay` | ||||||
|
||||||
| ||||||
|
||||||
```javascript live noinline | ||||||
// import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑ | ||||||
|
||||||
const fieldTypes = [ | ||||||
{icon: TypographyIcon, name: 'Text'}, | ||||||
{icon: NumberIcon, name: 'Number'}, | ||||||
{icon: CalendarIcon, name: 'Date'}, | ||||||
{icon: SingleSelectIcon, name: 'Single select'}, | ||||||
{icon: IterationsIcon, name: 'Iteration'} | ||||||
] | ||||||
|
||||||
const Example = () => { | ||||||
const [selectedIndex, setSelectedIndex] = React.useState(1) | ||||||
const selectedType = fieldTypes[selectedIndex] | ||||||
|
||||||
return ( | ||||||
<DropdownMenu> | ||||||
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}> | ||||||
{selectedType.name} | ||||||
</DropdownMenu.Button> | ||||||
<DropdownMenu.Overlay width="medium"> | ||||||
<ActionList> | ||||||
{fieldTypes.map((type, index) => ( | ||||||
siddharthkp marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}> | ||||||
<type.icon /> {type.name} | ||||||
</ActionList.Item> | ||||||
))} | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
) | ||||||
} | ||||||
|
||||||
render(<Example />) | ||||||
``` | ||||||
|
||||||
### Customise Button | ||||||
|
||||||
`Dropdown.Button` uses `Button v2` so you can pass props like `variant` and `leadingIcon` that `Button v2` accepts. | ||||||
|
||||||
```javascript live noinline | ||||||
// import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑ | ||||||
|
||||||
const Example = () => { | ||||||
const [duration, setDuration] = React.useState(1) | ||||||
|
||||||
return ( | ||||||
<DropdownMenu> | ||||||
<DropdownMenu.Button variant="invisible" aria-label="Select iteration duration"> | ||||||
{duration} {duration > 1 ? 'weeks' : 'week'} | ||||||
</DropdownMenu.Button> | ||||||
<DropdownMenu.Overlay width="medium"> | ||||||
<ActionList> | ||||||
{[1, 2, 3, 4, 5, 6].map(weeks => ( | ||||||
<ActionList.Item key={weeks} selected={duration === weeks} onSelect={() => setDuration(weeks)}> | ||||||
{weeks} {weeks > 1 ? 'weeks' : 'week'} | ||||||
</ActionList.Item> | ||||||
))} | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
) | ||||||
} | ||||||
|
||||||
render(<Example />) | ||||||
``` | ||||||
|
||||||
### With External Anchor | ||||||
|
||||||
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`: | ||||||
|
||||||
```javascript live noinline | ||||||
// import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑ | ||||||
|
||||||
const Example = () => { | ||||||
const [open, setOpen] = React.useState(false) | ||||||
const anchorRef = React.createRef() | ||||||
|
||||||
return ( | ||||||
<> | ||||||
<Button ref={anchorRef} onClick={() => setOpen(!open)}> | ||||||
{open ? 'Close Menu' : 'Open Menu'} | ||||||
</Button> | ||||||
|
||||||
<DropdownMenu open={open} onOpenChange={setOpen} anchorRef={anchorRef}> | ||||||
<DropdownMenu.Overlay> | ||||||
<ActionList> | ||||||
<ActionList.Item selected={true}>Text</ActionList.Item> | ||||||
<ActionList.Item>Number</ActionList.Item> | ||||||
<ActionList.Item>Date</ActionList.Item> | ||||||
<ActionList.Item>Iteration</ActionList.Item> | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
</> | ||||||
) | ||||||
} | ||||||
|
||||||
render(<Example />) | ||||||
``` | ||||||
|
||||||
### With Overlay Props | ||||||
|
||||||
```javascript live noinline | ||||||
// import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑ | ||||||
|
||||||
const fieldTypes = [ | ||||||
{icon: TypographyIcon, name: 'Text'}, | ||||||
{icon: NumberIcon, name: 'Number'}, | ||||||
{icon: CalendarIcon, name: 'Date'}, | ||||||
{icon: SingleSelectIcon, name: 'Single select'}, | ||||||
{icon: IterationsIcon, name: 'Iteration'} | ||||||
] | ||||||
|
||||||
const Example = () => { | ||||||
const handleEscape = () => alert('you hit escape!') | ||||||
|
||||||
const [selectedIndex, setSelectedIndex] = React.useState(1) | ||||||
const selectedType = fieldTypes[selectedIndex] | ||||||
|
||||||
return ( | ||||||
<DropdownMenu> | ||||||
<DropdownMenu.Button aria-label="Select field type" leadingIcon={selectedType.icon}> | ||||||
{selectedType.name} | ||||||
</DropdownMenu.Button> | ||||||
<DropdownMenu.Overlay width="medium" onEscape={handleEscape}> | ||||||
<ActionList> | ||||||
{fieldTypes.map((type, index) => ( | ||||||
<ActionList.Item key={index} selected={index === selectedIndex} onSelect={() => setSelectedIndex(index)}> | ||||||
<type.icon /> {type.name} | ||||||
</ActionList.Item> | ||||||
))} | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
) | ||||||
} | ||||||
|
||||||
render(<Example />) | ||||||
``` | ||||||
|
||||||
### With a custom anchor | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could anchor in this title be confusing for readers as it's used in a different context further up?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We call this |
||||||
|
||||||
You can choose to have a different _anchor_ for the Menu dependending on the application's context. | ||||||
|
||||||
| ||||||
|
||||||
```javascript live noinline | ||||||
// import {DropdownMenu, ActionList} from '@primer/react/drafts' | ||||||
const {DropdownMenu, ActionList} = drafts // ignore docs silliness; import like that ↑ | ||||||
|
||||||
render( | ||||||
<DropdownMenu> | ||||||
<DropdownMenu.Anchor> | ||||||
<button>Select a field type</button> | ||||||
</DropdownMenu.Anchor> | ||||||
|
||||||
<DropdownMenu.Overlay> | ||||||
<ActionList> | ||||||
<ActionList.Item selected={true}>Text</ActionList.Item> | ||||||
<ActionList.Item>Number</ActionList.Item> | ||||||
<ActionList.Item>Date</ActionList.Item> | ||||||
<ActionList.Item>Iteration</ActionList.Item> | ||||||
</ActionList> | ||||||
</DropdownMenu.Overlay> | ||||||
</DropdownMenu> | ||||||
) | ||||||
``` | ||||||
|
||||||
<Note variant="warning"> | ||||||
|
||||||
Use `DropdownMenu` to select an option from a small list. If you’re looking for filters or multiple selection, use [SelectPanel](/SelectPanel) instead. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels quite important, shall we hoist it near the top of page so it has better visibility? We've done this for other components before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This came up before with ActionMenu. What we decided was that interface guidelines is the first place where you learn which component to use, and this is the second note near |
||||||
|
||||||
</Note> | ||||||
|
||||||
## Props | ||||||
|
||||||
### DropdownMenu | ||||||
|
||||||
<PropsTable> | ||||||
<PropsTableRow | ||||||
required | ||||||
name="children" | ||||||
type="React.ReactElement[]" | ||||||
description={ | ||||||
<> | ||||||
Recommended: <InlineCode>DropdownMenu.Button</InlineCode> or <InlineCode>DropdownMenu.Anchor</InlineCode> with{' '} | ||||||
<InlineCode>DropdownMenu.Overlay</InlineCode> | ||||||
</> | ||||||
} | ||||||
/> | ||||||
<PropsTableRow | ||||||
name="open" | ||||||
type="boolean" | ||||||
description={ | ||||||
<> | ||||||
If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '} | ||||||
<InlineCode>onOpenChange</InlineCode> | ||||||
</> | ||||||
} | ||||||
/> | ||||||
<PropsTableRow | ||||||
name="onOpenChange" | ||||||
type="(open: boolean) => void" | ||||||
description={ | ||||||
<> | ||||||
If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '} | ||||||
<InlineCode>open</InlineCode> | ||||||
</> | ||||||
} | ||||||
/> | ||||||
<PropsTableRow | ||||||
name="anchorRef" | ||||||
type="React.RefObject<HTMLElement>" | ||||||
description="Useful for defining an external anchor" | ||||||
/> | ||||||
</PropsTable> | ||||||
|
||||||
### DropdownMenu.Button | ||||||
|
||||||
<PropsTable> | ||||||
<PropsTableRow | ||||||
name="ButtonProps" | ||||||
type={ | ||||||
<> | ||||||
<Link href="/drafts/Button2#api-reference">ButtonProps</Link> | ||||||
</> | ||||||
} | ||||||
description={ | ||||||
<> | ||||||
You can pass all of the props that you would pass to a{' '} | ||||||
<Link href="/drafts/Button2#api-reference"> | ||||||
<InlineCode>Button</InlineCode> | ||||||
</Link>{' '} | ||||||
component like <InlineCode>variant</InlineCode>, <InlineCode>leadingIcon</InlineCode>,{' '} | ||||||
<InlineCode>sx</InlineCode>, etc. | ||||||
</> | ||||||
} | ||||||
/> | ||||||
</PropsTable> | ||||||
|
||||||
### DropdownMenu.Anchor | ||||||
|
||||||
<PropsTable> | ||||||
<PropsTableRow required name="children" type="React.ReactElement" description="Accepts a single child" /> | ||||||
</PropsTable> | ||||||
|
||||||
### DropdownMenu.Overlay | ||||||
|
||||||
<PropsTable> | ||||||
<PropsTableRow | ||||||
required | ||||||
name="children" | ||||||
type="React.ReactElement[]" | ||||||
description={ | ||||||
<> | ||||||
Recommended:{' '} | ||||||
<Link href="/drafts/ActionList2"> | ||||||
<InlineCode>ActionList</InlineCode> | ||||||
</Link> | ||||||
</> | ||||||
} | ||||||
/> | ||||||
<PropsTableRow | ||||||
name="OverlayProps" | ||||||
type="OverlayProps" | ||||||
description={ | ||||||
<> | ||||||
Props to be spread on the internal{' '} | ||||||
<Link href="/AnchoredOverlay"> | ||||||
<InlineCode>AnchoredOverlay</InlineCode> | ||||||
</Link> | ||||||
</> | ||||||
} | ||||||
/> | ||||||
</PropsTable> | ||||||
|
||||||
## Status | ||||||
|
||||||
<ComponentChecklist | ||||||
items={{ | ||||||
propsDocumented: true, | ||||||
noUnnecessaryDeps: true, | ||||||
adaptsToThemes: true, | ||||||
adaptsToScreenSizes: true, | ||||||
fullTestCoverage: true, | ||||||
usedInProduction: false, | ||||||
usageExamplesDocumented: true, | ||||||
hasStorybookStories: true, | ||||||
designReviewed: false, | ||||||
a11yReviewed: false, | ||||||
stableApi: false, | ||||||
addressedApiFeedback: false, | ||||||
hasDesignGuidelines: true, | ||||||
hasFigmaComponent: true | ||||||
}} | ||||||
/> | ||||||
|
||||||
## Further reading | ||||||
|
||||||
[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list) | ||||||
|
||||||
## Related components | ||||||
|
||||||
- [ActionList](/drafts/ActionList2) | ||||||
- [ActionMenu](/ActionMenu2) | ||||||
- [SelectPanel](/SelectPanel) | ||||||
siddharthkp marked this conversation as resolved.
Show resolved
Hide resolved
|
Uh oh!
There was an error while loading. Please reload this page.