|
| 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 | + |
| 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 | + |
| 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