Skip to content

Commit

Permalink
Composable ActionMenu draft (#1619)
Browse files Browse the repository at this point in the history
* wip

* add memex example

* add complex story

* add changeset

* add actionmenu2 to drafts

* add docs!

* polish the docs just a bit

* move MenuContext + add warning

* return null from selection

* polish stories

* fix story

* remove draft from status

* oops left a comment in

* change setOpen to onOpenChange

* Use Box instead of BorderBox in docs

* missed role in bad merge!

* remove component checklist import

Co-authored-by: Cole Bemis <colebemis@github.com>

Co-authored-by: Cole Bemis <colebemis@github.com>
  • Loading branch information
siddharthkp and colebemis committed Dec 1, 2021
1 parent 65b63e4 commit a13efa4
Show file tree
Hide file tree
Showing 12 changed files with 963 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/composable-actionmenu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/components': minor
---

Add composable `ActionMenu` to `@primer/components/drafts`
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ storybook: '/react/storybook?path=/story/composite-components-actionlist2'
description: An ActionList is a list of items that can be activated or selected. ActionList is the base component for many menu-type components, including DropdownMenu and ActionMenu.
---

import {BorderBox, Avatar} from '@primer/components'
import {Box, Avatar} from '@primer/components'
import {ActionList} from '@primer/components/drafts'
import {ComponentChecklist} from '../src/component-checklist'

import {ImageContainer} from '@primer/gatsby-theme-doctocat'
import {LinkIcon, AlertIcon, ArrowRightIcon} from '@primer/octicons-react'

<br />

<BorderBox sx={{padding: 6}}>
<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6}}>
<ActionList sx={{width: 320}}>
<ActionList.Item>
<ActionList.LeadingVisual>
Expand Down Expand Up @@ -43,7 +40,7 @@ import {LinkIcon, AlertIcon, ArrowRightIcon} from '@primer/octicons-react'
</ActionList.TrailingVisual>
</ActionList.Item>
</ActionList>
</BorderBox>
</Box>

<br />

Expand Down Expand Up @@ -351,11 +348,10 @@ render(<SelectFields />)

- [Interface guidelines: Action List](https://primer.style/design/components/action-list)

<br />

## Related components

- [ActionMenu](/ActionMenu)
- [ActionMenu](/drafts/ActionMenu2)
- [DropdownMenu](/DropdownMenu)
- [SelectPanel](/SelectPanel)

## Component status
Expand Down
251 changes: 251 additions & 0 deletions docs/content/drafts/ActionMenu2.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
---
title: ActionMenu
status: Alpha
source: https://github.com/primer/react/tree/main/src/ActionMenu
storybook: '/react/storybook?path=/story/composite-components-actionmenu2'
description: An ActionMenu is an ActionList-based component for creating a menu of actions that expands through a trigger button.
---

import {Box, Avatar} from '@primer/components'
import {ActionMenu, ActionList} from '@primer/components/drafts'
import {Props} from '../../src/props'

<br />

<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6}}>
<ActionMenu>
<ActionMenu.Button>Menu</ActionMenu.Button>
<ActionList>
<ActionList.Item onSelect={() => onSelect('Copy link')}>
Copy link
<ActionList.TrailingVisual>⌘C</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item onSelect={() => onSelect('Quote reply')}>
Quote reply
<ActionList.TrailingVisual>⌘Q</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item onSelect={() => onSelect('Edit comment')}>
Edit comment
<ActionList.TrailingVisual>⌘E</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Divider />
<ActionList.Item variant="danger" onSelect={() => onSelect('Delete file')}>
Delete file
<ActionList.TrailingVisual>⌘D</ActionList.TrailingVisual>
</ActionList.Item>
</ActionList>

</ActionMenu>
</Box>

<br />

```js
import {ActionMenu} from '@primer/components/drafts'
```

<br />

## Examples

### Minimal example

`ActionMenu` ships with `ActionMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with this component.

&nbsp;

```javascript live noinline
// import {ActionMenu, ActionList} from '@primer/components/drafts'
const {ActionMenu, ActionList} = drafts // ignore docs silliness; import like that ↑

render(
<ActionMenu>
<ActionMenu.Button>Menu</ActionMenu.Button>

<ActionList>
<ActionList.Item onSelect={event => console.log('New file')}>New file</ActionList.Item>
<ActionList.Item>Copy link</ActionList.Item>
<ActionList.Item>Edit file</ActionList.Item>
<ActionList.Divider />
<ActionList.Item variant="danger">Delete file</ActionList.Item>
</ActionList>
</ActionMenu>
)
```

### With a custom anchor

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

&nbsp;

```javascript live noinline
// import {ActionMenu, ActionList} from '@primer/components/drafts'
const {ActionMenu, ActionList} = drafts // ignore docs silliness; import like that ↑

render(
<ActionMenu>
<ActionMenu.Anchor>
<ButtonInvisible aria-label="Open column options">
<KebabHorizontalIcon />
</ButtonInvisible>
</ActionMenu.Anchor>

<ActionList>
<ActionList.Item>
<ActionList.LeadingVisual>
<PencilIcon />
</ActionList.LeadingVisual>
Rename
</ActionList.Item>
<ActionList.Item>
<ActionList.LeadingVisual>
<ArchiveIcon />
</ActionList.LeadingVisual>
Archive all cards
</ActionList.Item>
<ActionList.Item variant="danger">
<ActionList.LeadingVisual>
<TrashIcon />
</ActionList.LeadingVisual>
Delete
</ActionList.Item>
</ActionList>
</ActionMenu>
)
```

### With Groups

```javascript live noinline
// import {ActionMenu, ActionList} from '@primer/components/drafts'
const {ActionMenu, ActionList} = drafts // ignore docs silliness; import like that ↑

render(
<ActionMenu>
<ActionMenu.Button>Open column menu</ActionMenu.Button>

<ActionList showDividers>
<ActionList.Group title="Live query">
<ActionList.Item>
<ActionList.LeadingVisual>
<SearchIcon />
</ActionList.LeadingVisual>
repo:github/memex,github/github
</ActionList.Item>
</ActionList.Group>
<ActionList.Divider />
<ActionList.Group title="Layout" variant="subtle">
<ActionList.Item>
<ActionList.LeadingVisual>
<NoteIcon />
</ActionList.LeadingVisual>
Table
<ActionList.Description variant="block">
Information-dense table optimized for operations across teams
</ActionList.Description>
</ActionList.Item>
<ActionList.Item role="listitem">
<ActionList.LeadingVisual>
<ProjectIcon />
</ActionList.LeadingVisual>
Board
<ActionList.Description variant="block">Kanban-style board focused on visual states</ActionList.Description>
</ActionList.Item>
</ActionList.Group>
<ActionList.Divider />
<ActionList.Group>
<ActionList.Item>
<ActionList.LeadingVisual>
<FilterIcon />
</ActionList.LeadingVisual>
Save sort and filters to current view
</ActionList.Item>
<ActionList.Item>
<ActionList.LeadingVisual>
<FilterIcon />
</ActionList.LeadingVisual>
Save sort and filters to new view
</ActionList.Item>
</ActionList.Group>
<ActionList.Divider />
<ActionList.Group>
<ActionList.Item>
<ActionList.LeadingVisual>
<GearIcon />
</ActionList.LeadingVisual>
View settings
</ActionList.Item>
</ActionList.Group>
</ActionList>
</ActionMenu>
)
```

### With External Anchor

To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass it as `anchorRef` to `ActionMenu`:

```javascript live noinline
// import {ActionMenu, ActionList} from '@primer/components/drafts'
const {ActionMenu, 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>

<ActionMenu open={open} onOpenChange={setOpen} anchorRef={anchorRef}>
<ActionList>
<ActionList.Item>Copy link</ActionList.Item>
<ActionList.Item>Quote reply</ActionList.Item>
<ActionList.Item>Edit comment</ActionList.Item>
<ActionList.Divider />
<ActionList.Item variant="danger">Delete file</ActionList.Item>
</ActionList>
</ActionMenu>
</>
)
}

render(<Example />)
```

## Props / API reference

### ActionMenu

| Name | Type | Default | Description |
| :----------- | :-------------------------------------------------- | :-----: | :----------------------------------------------------------------------------------------------------------------------- |
| children\* | `React.ReactElement[]` | - | Required. Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with [`ActionList`](/drafts/ActionList2) |
| open | `boolean` | - | Optional. If defined, will control the open/closed state of the overlay. Must be used in conjuction with `onOpenChange`. |
| onOpenChange | `(open: boolean) => void` | - | Optional. If defined, will control the open/closed state of the overlay. Must be used in conjuction with `open`. |
| anchorRef | `React.RefObject<HTMLElement>` | - | Optional. Useful for defining an external anchor |
| overlayProps | [`Partial<OverlayProps>`](/Overlay#component-props) | - | Optional. Props to be spread on the internal [`AnchoredOverlay`](/AnchoredOverlay) component. |

### ActionMenu.Button

| Type | Default | Description |
| :-------------------------------------- | :-----: | :------------------------------------------------------------------------------------------------------------------- |
| [ButtonProps](/Buttons#component-props) | - | Optional. You can pass all of the props that you would pass to a [`Button`](/Buttons) component like `variant`, `sx` |

### ActionMenu.Anchor

| Name | Type | Default | Description |
| :--------- | :------------------- | :-----: | :-------------------------------- |
| children\* | `React.ReactElement` | - | Required. Accepts a single child. |

## Further reading

[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list)

## Related components

- [ActionList](/drafts/ActionList2)
- [DropdownMenu](/DropdownMenu)
- [SelectPanel](/SelectPanel)
10 changes: 9 additions & 1 deletion docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import {
LawIcon,
StarIcon,
AlertIcon,
ArrowRightIcon
ArrowRightIcon,
KebabHorizontalIcon,
PencilIcon,
ArchiveIcon,
TrashIcon
} from '@primer/octicons-react'
import State from '../../../components/State'
import {Dialog as Dialog2} from '../../../../src/Dialog/Dialog'
Expand Down Expand Up @@ -65,6 +69,10 @@ export default {
StarIcon,
AlertIcon,
ArrowRightIcon,
KebabHorizontalIcon,
PencilIcon,
ArchiveIcon,
TrashIcon,
Dialog2,
ConfirmationDialog,
useConfirm,
Expand Down
21 changes: 13 additions & 8 deletions src/ActionList2/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ import React from 'react'
import Box from '../Box'
import {get} from '../constants'
import {Theme} from '../ThemeProvider'
import {SxProp, merge} from '../sx'

/**
* Visually separates `Item`s or `Group`s in an `ActionList`.
*/
export function Divider(): JSX.Element {

export const Divider: React.FC<SxProp> = ({sx = {}}) => {
return (
<Box
as="li"
role="separator"
sx={{
height: 1,
backgroundColor: 'actionListItem.inlineDivider',
marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`,
marginBottom: 2,
listStyle: 'none' // hide the ::marker inserted by browser's stylesheet
}}
sx={merge(
{
height: 1,
backgroundColor: 'actionListItem.inlineDivider',
marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`,
marginBottom: 2,
listStyle: 'none' // hide the ::marker inserted by browser's stylesheet
},
sx as SxProp
)}
data-component="ActionList.Divider"
/>
)
Expand Down
Loading

0 comments on commit a13efa4

Please sign in to comment.