Skip to content

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

Merged
merged 44 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
04c6e73
add template columns and text align for text
siddharthkp Dec 14, 2021
fc1fcaa
only align left if there's a leading Icon
siddharthkp Dec 14, 2021
bb8e918
add changeset
siddharthkp Dec 14, 2021
2ffe5bb
add warning for multiple selection with DropdownMenu
siddharthkp Dec 14, 2021
f8ceeff
wip: stories
siddharthkp Dec 14, 2021
6a11a80
Merge branch 'siddharth/newbutton-gird-width' into siddharth/composab…
siddharthkp Dec 14, 2021
616b36e
use leadingIcon with #1733
siddharthkp Dec 14, 2021
f6b6c09
add form to story example
siddharthkp Dec 15, 2021
270456a
use menuitemradio + aria-checked
siddharthkp Dec 15, 2021
3c7b2f6
fix types for selectionProperty
siddharthkp Dec 15, 2021
4a920de
it's not a property, it's an attribute
siddharthkp Dec 15, 2021
619d6fb
placeholder in story
siddharthkp Dec 15, 2021
a1d820b
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Dec 15, 2021
7560ebe
story with placeholder
siddharthkp Dec 15, 2021
f95dce7
oops, don't want forms to submit!
siddharthkp Dec 15, 2021
4e4decd
selected milestone can be undefined!
siddharthkp Dec 15, 2021
2798f95
update snapshots
siddharthkp Dec 15, 2021
55a758e
Add story for controlled menu and external anchor
siddharthkp Dec 15, 2021
f5e9daa
add tests!
siddharthkp Dec 15, 2021
8de18a5
export DropdownMenu2 as draft
siddharthkp Dec 15, 2021
26867e6
default to selectionVariant single for DropdownMenu
siddharthkp Dec 15, 2021
565905c
dont need axe anymore
siddharthkp Dec 15, 2021
0682801
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Dec 16, 2021
7f357cd
update octicons for docs
siddharthkp Dec 21, 2021
4d9e9b9
add docs for dropdown menu v2
siddharthkp Dec 21, 2021
5b90d71
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Dec 21, 2021
aeb4b29
remove changes from #1733
siddharthkp Dec 21, 2021
772daa0
update snapshots for removed button
siddharthkp Dec 21, 2021
a29ce21
link to button2 docs
siddharthkp Dec 21, 2021
0a8657d
add changeset
siddharthkp Dec 21, 2021
c89316b
lol everybody makes mistakes
siddharthkp Dec 21, 2021
5e7a5e7
oof!
siddharthkp Dec 21, 2021
40a5ed9
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Jan 14, 2022
091b9ce
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Jan 14, 2022
656eb4f
Use active voice in component description
siddharthkp Jan 14, 2022
97f7bae
update snapshosts with main
siddharthkp Jan 14, 2022
b69d92b
just Props
siddharthkp Jan 18, 2022
538b216
Use PropsTable
siddharthkp Jan 19, 2022
f61d50a
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Jan 19, 2022
7b31494
simplify code example a bit
siddharthkp Jan 19, 2022
1f5148a
make controlled state more clear
siddharthkp Jan 19, 2022
ed06f71
Merge branch 'main' into siddharth/composable-dropdownmenu
siddharthkp Jan 21, 2022
268dc3c
keep lockfile version 1
siddharthkp Jan 25, 2022
d0da4db
remove unrelated change
siddharthkp Jan 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/composable-dropdownmenu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Add composable `DropdownMenu` to `@primer/components/drafts`
379 changes: 379 additions & 0 deletions docs/content/drafts/DropdownMenu2.mdx
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}>
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

@siddharthkp siddharthkp Jan 19, 2022

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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'
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also hoist the import to the top of the page?

Copy link
Member Author

Choose a reason for hiding this comment

The 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`

&nbsp;

```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) => (
<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
Copy link
Contributor

@rezrah rezrah Jan 18, 2022

Choose a reason for hiding this comment

The 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
### With a custom anchor
### With a custom trigger

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call this anchor because the menu still anchors on DropdownMenu.Anchor.


You can choose to have a different _anchor_ for the Menu dependending on the application's context.

&nbsp;

```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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 props just in case you missed that.


</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)
Loading