From d4023572804cf3d8ce6cd1e9480715ab855abefc Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Fri, 25 Feb 2022 20:36:58 +0100 Subject: [PATCH] Deprecate ActionMenu v1 & Promote drafts/ActionMenu v2 (#1897) * Deprecate ActionList v1 * Promote drafts/ActionList2 to main/ActionList * Add changelog * Undo package-lock change * update ActionList import for Menu2 docs * Deprecate ActionMenu - part 1 * Deprecate ActionMenu - part 2 * Promote drafts/ActionMenu2 to main/ActionMenu * Add changelog * Add @deprecated on deprecated/ActionMenu * docs fixed! * reorder deprecated components alphabetically * Update deprecation message * Fix missing icon that only broke on this PR for some reason --- ...ecate-actionmenuv1-promote-actionmenuv2.md | 69 ++++ .changeset/empty-pillows-hunt.md | 149 ++++++- docs/content/ActionList.mdx | 2 +- docs/content/ActionMenu.mdx | 391 +++++++++++++++--- docs/content/SideNav.md | 4 +- docs/content/deprecated/ActionMenu.mdx | 127 ++++++ docs/content/drafts/ActionMenu2.mdx | 341 --------------- docs/content/drafts/DropdownMenu2.mdx | 2 +- .../gatsby-theme-doctocat/live-code-scope.js | 4 +- .../src/@primer/gatsby-theme-doctocat/nav.yml | 22 +- src/ActionMenu.tsx | 173 ++++---- src/ActionMenu2.tsx | 131 ------ src/__tests__/ActionMenu.test.tsx | 210 +++++----- src/__tests__/ActionMenu2.test.tsx | 150 ------- src/__tests__/ConfirmationDialog.test.tsx | 2 +- .../__snapshots__/ActionMenu.test.tsx.snap | 134 ++++-- .../__snapshots__/ActionMenu2.test.tsx.snap | 144 ------- src/__tests__/deprecated/ActionMenu.test.tsx | 136 ++++++ .../__snapshots__/ActionMenu.test.tsx.snap | 80 ++++ src/deprecated/ActionMenu.tsx | 109 +++++ src/deprecated/index.ts | 2 + src/drafts/index.ts | 1 - src/index.ts | 2 +- .../examples.stories.tsx | 5 +- .../fixtures.stories.tsx | 6 +- src/stories/ConfirmationDialog.stories.tsx | 2 +- src/stories/Overlay.stories.tsx | 4 +- .../{ => deprecated}/ActionMenu.stories.tsx | 16 +- 28 files changed, 1340 insertions(+), 1078 deletions(-) create mode 100644 .changeset/deprecate-actionmenuv1-promote-actionmenuv2.md create mode 100644 docs/content/deprecated/ActionMenu.mdx delete mode 100644 docs/content/drafts/ActionMenu2.mdx delete mode 100644 src/ActionMenu2.tsx delete mode 100644 src/__tests__/ActionMenu2.test.tsx delete mode 100644 src/__tests__/__snapshots__/ActionMenu2.test.tsx.snap create mode 100644 src/__tests__/deprecated/ActionMenu.test.tsx create mode 100644 src/__tests__/deprecated/__snapshots__/ActionMenu.test.tsx.snap create mode 100644 src/deprecated/ActionMenu.tsx rename src/stories/{ActionMenu2 => ActionMenu}/examples.stories.tsx (98%) rename src/stories/{ActionMenu2 => ActionMenu}/fixtures.stories.tsx (99%) rename src/stories/{ => deprecated}/ActionMenu.stories.tsx (96%) diff --git a/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md b/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md new file mode 100644 index 00000000000..3fbec5cf4eb --- /dev/null +++ b/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md @@ -0,0 +1,69 @@ +--- +'@primer/react': major +--- + +### ActionMenu + +ActionMenu has been rewritten with a composable API, design updates and accessibility fixes. + +See full list of props and examples: https://primer.style/react/ActionMenu + +Main changes: + +1. Instead of using `items` prop, use `ActionList` inside `ActionMenu` +2. Instead of using `anchorContent` on `ActionMenu`, use `ActionMenu.Button` with `children` +3. Instead of using `onAction` prop on `ActionMenu`, use `onSelect` prop on `ActionList.Item` +4. Instead of using `groupMetadata` on `ActionMenu`, use `ActionList.Group` +5. Instead of `overlayProps` on `ActionMenu`, use `ActionMenu.Overlay` + + + + + + + + + +
Before (v34) After (v35)
+ +```jsx + +``` + + + +```jsx + + Menu + + + New file + Copy link + Edit file + + Delete file + + + +``` + +
+ +To continue to use the deprecated API for now, change the import path to `@primer/react/deprecated`: + +```js +import {ActionMenu} from '@primer/react/deprecated' +``` + +You can use the [one-time codemod](https://github.com/primer/react-migrate#readme) to change your import statements automatically. diff --git a/.changeset/empty-pillows-hunt.md b/.changeset/empty-pillows-hunt.md index 922ed22e895..bda4d62c285 100644 --- a/.changeset/empty-pillows-hunt.md +++ b/.changeset/empty-pillows-hunt.md @@ -2,4 +2,151 @@ '@primer/react': major --- -Prepare library for `v35` +### ActionList + +ActionList now ships a composable API. + +See full list of props and examples: https://primer.style/react/ActionList + + + + + + + + + + + + + + + + + +
Before After
+ +```jsx + +``` + + + +```jsx + + New file + Copy link + Edit file + + Delete file + +``` + +
+ +```jsx + , + text: 'mona', + description: 'Monalisa Octocat', + descriptionVariant: 'block' + }, + { + key: '2', + leadingVisual: GearIcon, + text: 'View Settings', + trailingVisual: ArrowRightIcon + } + ]} +/> +``` + + + +```jsx + + + + + + github/primer + + + + + + mona + Monalisa Octocat + + + + + + View settings + + + + + +``` + +
+ +```jsx + +``` + + + +```jsx + + + repo:github/github + + + + + Table + Board Description> + + + + View settings + +``` + +
+ +To continue to use the deprecated API for now, change the import path to `@primer/react/deprecated`: + +```js +import {ActionList} from '@primer/react/deprecated' +``` diff --git a/docs/content/ActionList.mdx b/docs/content/ActionList.mdx index be09bd7efee..f414bdd2f3d 100644 --- a/docs/content/ActionList.mdx +++ b/docs/content/ActionList.mdx @@ -444,6 +444,6 @@ render() ## Related components -- [ActionMenu](/drafts/ActionMenu2) +- [ActionMenu](/ActionMenu) - [DropdownMenu](/DropdownMenu) - [SelectPanel](/SelectPanel) diff --git a/docs/content/ActionMenu.mdx b/docs/content/ActionMenu.mdx index 1c0e13ecd9b..8c97169f556 100644 --- a/docs/content/ActionMenu.mdx +++ b/docs/content/ActionMenu.mdx @@ -2,81 +2,338 @@ componentId: action_menu title: ActionMenu status: Alpha +source: https://github.com/primer/react/tree/main/src/ActionMenu.tsx +storybook: '/react/storybook?path=/story/composite-components-actionmenu' +description: An ActionMenu is an ActionList-based component for creating a menu of actions that expands through a trigger button. --- -An `ActionMenu` is an ActionList-based component for creating a menu of actions that expands through a trigger button. +import {Box, Avatar, ActionList, ActionMenu} from '@primer/react' -## Default example +
+ + + + Menu + + + + Copy link + ⌘C + + + Quote reply + ⌘Q + + + Edit comment + ⌘E + + + + Delete file + ⌘D + + + + + + +
+ +```js +import {ActionMenu} from '@primer/react/drafts' +``` + +
+ +## Examples + +### Minimal example + +`ActionMenu` ships with `ActionMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `ActionMenu.Overlay` + +  ```jsx live - console.log(text)} - items={[ - {text: 'New file', key: 'new-file'}, - ActionMenu.Divider, - {text: 'Copy link', key: 'copy-link'}, - {text: 'Edit file', key: 'edit-file'}, - {text: 'Delete file', variant: 'danger', key: 'delete-file'} - ]} -/> + + Menu + + + + console.log('New file')}>New file + Copy link + Edit file + + Delete file + + + ``` -## Example with grouped items +### With a custom anchor + +You can choose to have a different _anchor_ for the Menu dependending on the application's context. + +  ```jsx live - console.log(text)} - groupMetadata={[ - {groupId: '0'}, - {groupId: '1', header: {title: 'Live query', variant: 'subtle'}}, - {groupId: '2', header: {title: 'Layout', variant: 'subtle'}}, - {groupId: '3'}, - {groupId: '4'} - ]} - items={[ - {key: '1', leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, - {key: '2', leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'}, - {key: '3', leadingVisual: SearchIcon, text: 'repo:github/github', groupId: '1'}, - { - key: '4', - leadingVisual: NoteIcon, - text: 'Table', - description: 'Information-dense table optimized for operations across teams', - descriptionVariant: 'block', - groupId: '2' - }, - { - key: '5', - leadingVisual: ProjectIcon, - text: 'Board', - description: 'Kanban-style board focused on visual states', - descriptionVariant: 'block', - groupId: '2' - }, - { - key: '6', - leadingVisual: FilterIcon, - text: 'Save sort and filters to current view', - disabled: true, - groupId: '3' - }, - {key: '7', leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'}, - {key: '8', leadingVisual: GearIcon, text: 'View settings', groupId: '4'} - ]} -/> + + + + + + + + + + + + + + Rename + + + + + + Archive all cards + + + + + + Delete + + + + +``` + +### With Groups + +```jsx live + + Open column menu + + + + + + + + + repo:github/memex,github/github + + + + + + + + + Table + + Information-dense table optimized for operations across teams + + + + + + + Board + Kanban-style board focused on visual states + + + + + + + + + Save sort and filters to current view + + + + + + Save sort and filters to new view + + + + + + + + + View settings + + + + + +``` + +### With selection + +Use `selectionVariant` on `ActionList` to create a menu with single or multiple selection. + +```javascript live noinline +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 ( + + + {selectedType.name} + + + + {fieldTypes.map((type, index) => ( + setSelectedIndex(index)}> + {type.name} + + ))} + + + + ) +} + +render() ``` -## Component props - -| Name | Type | Default | Description | -| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | -| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | -| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | -| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | -| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | -| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | -| open | boolean | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `setOpen` prop. | -| setOpen | (state: boolean) => void | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `open` prop. | +### 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`. Make sure you add `aria-expanded` and `aria-haspopup` to the external anchor: + +```javascript live noinline +const Example = () => { + const [open, setOpen] = React.useState(false) + const anchorRef = React.createRef() + + return ( + <> + + + + + + Copy link + Quote reply + Edit comment + + Delete file + + + + + ) +} + +render() +``` + +### With Overlay Props + +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 +const handleEscape = () => alert('you hit escape!') + +render( + + Open Actions Menu + + + + Open current Codespace + + Your existing Codespace will be opened to its previous state, and you'll be asked to manually switch to + new-branch. + + ⌘O + + + Create new Codespace + + Create a brand new Codespace with a fresh image and checkout this branch. + + ⌘C + + + + +) +``` + +## Props / API reference + +### ActionMenu + +| Name | Type | Default | Description | +| :----------- | :----------------------------- | :-----: | :----------------------------------------------------------------------------------------------------------------------- | +| children\* | `React.ReactElement[]` | - | Required. Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay` | +| 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` | - | Optional. Useful for defining an external anchor | + +### ActionMenu.Button + +| Type | Default | Description | +| :----------------------------------------------- | :-----: | :---------------------------------------------------------------------------------------------------------------- | +| [Button v2 props](/drafts/Button2#api-reference) | - | You can pass all of the props that you would pass to a [`Button`](/drafts/Button2) component like `variant`, `sx` | + +### ActionMenu.Anchor + +| Name | Type | Default | Description | +| :--------- | :------------------- | :-----: | :-------------------------------- | +| children\* | `React.ReactElement` | - | Required. Accepts a single child. | + +### ActionMenu.Overlay + +| Name | Type | Default | Description | +| :--------------------------------------- | :-------------------- | :-----------------: | :-------------------------------------------------------------------------------------------- | +| children\* | `React.ReactElement[] | React.ReactElement` | Required. Recommended: [`ActionList`](/ActionList) | +| [OverlayProps](/Overlay#component-props) | - | - | Optional. Props to be spread on the internal [`AnchoredOverlay`](/AnchoredOverlay) component. | + +## Status + + + +## Further reading + +[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list) + +## Related components + +- [ActionList](/ActionList) +- [SelectPanel](/SelectPanel) +- [Button](/drafts/Button2) diff --git a/docs/content/SideNav.md b/docs/content/SideNav.md index bb6d6bf3f67..04c2ff0535d 100644 --- a/docs/content/SideNav.md +++ b/docs/content/SideNav.md @@ -114,11 +114,11 @@ It can also appear nested, as a sub navigation. Use margin/padding [System Props ```jsx live - + Account - + Profile diff --git a/docs/content/deprecated/ActionMenu.mdx b/docs/content/deprecated/ActionMenu.mdx new file mode 100644 index 00000000000..0fa65bbc4b9 --- /dev/null +++ b/docs/content/deprecated/ActionMenu.mdx @@ -0,0 +1,127 @@ +--- +componentId: action_menu +title: ActionMenu +status: Deprecated +source: https://github.com/primer/react/tree/main/src/deprecated/ActionMenu.tsx +--- + +An `ActionMenu` is an ActionList-based component for creating a menu of actions that expands through a trigger button. + +## Deprecation + +Use [new version of ActionMenu](/ActionMenu) with composable API, design updates and accessibility fixes. + +**Before** + +```jsx + +``` + +**After** + +```jsx + + Menu + + + New file + Copy link + Edit file + + Delete file + + + +``` + +Or continue using deprecated API: + +```js +import {ActionMenu} from '@primer/react/deprecated' +``` + +## Default example + +```jsx live deprecated + console.log(text)} + items={[ + {text: 'New file', key: 'new-file'}, + ActionMenu.Divider, + {text: 'Copy link', key: 'copy-link'}, + {text: 'Edit file', key: 'edit-file'}, + {text: 'Delete file', variant: 'danger', key: 'delete-file'} + ]} +/> +``` + +## Example with grouped items + +```jsx live deprecated + console.log(text)} + groupMetadata={[ + {groupId: '0'}, + {groupId: '1', header: {title: 'Live query', variant: 'subtle'}}, + {groupId: '2', header: {title: 'Layout', variant: 'subtle'}}, + {groupId: '3'}, + {groupId: '4'} + ]} + items={[ + {key: '1', leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, + {key: '2', leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'}, + {key: '3', leadingVisual: SearchIcon, text: 'repo:github/github', groupId: '1'}, + { + key: '4', + leadingVisual: NoteIcon, + text: 'Table', + description: 'Information-dense table optimized for operations across teams', + descriptionVariant: 'block', + groupId: '2' + }, + { + key: '5', + leadingVisual: ProjectIcon, + text: 'Board', + description: 'Kanban-style board focused on visual states', + descriptionVariant: 'block', + groupId: '2' + }, + { + key: '6', + leadingVisual: FilterIcon, + text: 'Save sort and filters to current view', + disabled: true, + groupId: '3' + }, + {key: '7', leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'}, + {key: '8', leadingVisual: GearIcon, text: 'View settings', groupId: '4'} + ]} +/> +``` + +## Component props + +| Name | Type | Default | Description | +| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | +| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | +| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | +| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | +| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | +| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | +| open | boolean | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `setOpen` prop. | +| setOpen | (state: boolean) => void | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `open` prop. | diff --git a/docs/content/drafts/ActionMenu2.mdx b/docs/content/drafts/ActionMenu2.mdx deleted file mode 100644 index 5a1af58d8da..00000000000 --- a/docs/content/drafts/ActionMenu2.mdx +++ /dev/null @@ -1,341 +0,0 @@ ---- -componentId: action_menu2 -title: ActionMenu v2 -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, ActionList} from '@primer/react' -import {ActionMenu} from '@primer/react/drafts' -import {Props} from '../../src/props' - -
- - - - Menu - - - - Copy link - ⌘C - - - Quote reply - ⌘Q - - - Edit comment - ⌘E - - - - Delete file - ⌘D - - - - - - -
- -```js -import {ActionMenu} from '@primer/react/drafts' -``` - -
- -## Examples - -### Minimal example - -`ActionMenu` ships with `ActionMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `ActionMenu.Overlay` - -  - -```jsx live drafts - - Menu - - - - console.log('New file')}>New file - Copy link - Edit file - - Delete file - - - -``` - -### With a custom anchor - -You can choose to have a different _anchor_ for the Menu dependending on the application's context. - -  - -```jsx live drafts - - - - - - - - - - - - - - Rename - - - - - - Archive all cards - - - - - - Delete - - - - -``` - -### With Groups - -```jsx live drafts - - Open column menu - - - - - - - - - repo:github/memex,github/github - - - - - - - - - Table - - Information-dense table optimized for operations across teams - - - - - - - Board - Kanban-style board focused on visual states - - - - - - - - - Save sort and filters to current view - - - - - - Save sort and filters to new view - - - - - - - - - View settings - - - - - -``` - -### With selection - -Use `selectionVariant` on `ActionList` to create a menu with single or multiple selection. - -```javascript live noinline drafts -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 ( - - - {selectedType.name} - - - - {fieldTypes.map((type, index) => ( - setSelectedIndex(index)}> - {type.name} - - ))} - - - - ) -} - -render() -``` - -### 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`. Make sure you add `aria-expanded` and `aria-haspopup` to the external anchor: - -```javascript live noinline drafts -const Example = () => { - const [open, setOpen] = React.useState(false) - const anchorRef = React.createRef() - - return ( - <> - - - - - - Copy link - Quote reply - Edit comment - - Delete file - - - - - ) -} - -render() -``` - -### With Overlay Props - -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 drafts -const handleEscape = () => alert('you hit escape!') - -render( - - Open Actions Menu - - - - Open current Codespace - - Your existing Codespace will be opened to its previous state, and you'll be asked to manually switch to - new-branch. - - ⌘O - - - Create new Codespace - - Create a brand new Codespace with a fresh image and checkout this branch. - - ⌘C - - - - -) -``` - -## Props / API reference - -### ActionMenu - -| Name | Type | Default | Description | -| :----------- | :----------------------------- | :-----: | :----------------------------------------------------------------------------------------------------------------------- | -| children\* | `React.ReactElement[]` | - | Required. Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay` | -| 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` | - | Optional. Useful for defining an external anchor | - -### ActionMenu.Button - -| Type | Default | Description | -| :----------------------------------------------- | :-----: | :---------------------------------------------------------------------------------------------------------------- | -| [Button v2 props](/drafts/Button2#api-reference) | - | You can pass all of the props that you would pass to a [`Button`](/drafts/Button2) component like `variant`, `sx` | - -### ActionMenu.Anchor - -| Name | Type | Default | Description | -| :--------- | :------------------- | :-----: | :-------------------------------- | -| children\* | `React.ReactElement` | - | Required. Accepts a single child. | - -### ActionMenu.Overlay - -| Name | Type | Default | Description | -| :--------------------------------------- | :-------------------- | :-----------------: | :-------------------------------------------------------------------------------------------- | -| children\* | `React.ReactElement[] | React.ReactElement` | Required. Recommended: [`ActionList`](/ActionList) | -| [OverlayProps](/Overlay#component-props) | - | - | Optional. Props to be spread on the internal [`AnchoredOverlay`](/AnchoredOverlay) component. | - -## Status - - - -## Further reading - -[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list) - -## Related components - -- [ActionList](/ActionList) -- [SelectPanel](/SelectPanel) -- [Button](/drafts/Button2) diff --git a/docs/content/drafts/DropdownMenu2.mdx b/docs/content/drafts/DropdownMenu2.mdx index 8d517f5facb..47eba34b758 100644 --- a/docs/content/drafts/DropdownMenu2.mdx +++ b/docs/content/drafts/DropdownMenu2.mdx @@ -360,5 +360,5 @@ Use `DropdownMenu` to select an option from a small list. If you’re looking fo ## Related components - [ActionList](/ActionList) -- [ActionMenu](/ActionMenu2) +- [ActionMenu](/ActionMenu) - [SelectPanel](/SelectPanel) diff --git a/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js b/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js index 5caa96ccbcd..a9bbaf0e419 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js +++ b/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js @@ -18,7 +18,7 @@ import { MarkGithubIcon, NoteIcon, NumberIcon, - OctofaceIcon, + SmileyIcon, PencilIcon, PersonIcon, ProjectIcon, @@ -60,7 +60,7 @@ export default function resolveScope(metastring) { ZapIcon, XIcon, DotIcon, - OctofaceIcon, + SmileyIcon, PersonIcon, MailIcon, GitCommitIcon, diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 2c977d13db0..48297fc0d96 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -37,6 +37,8 @@ children: - title: ActionList url: /ActionList + - title: ActionMenu + url: /ActionMenu - title: Autocomplete url: /Autocomplete - title: Avatar @@ -145,25 +147,25 @@ url: /drafts/LinkButton - title: IconButton url: /drafts/IconButton - - title: ActionMenu v2 - url: /drafts/ActionMenu2 - title: Deprecated children: + - title: ActionList + url: /deprecated/ActionList + - title: ActionMenu + url: /deprecated/ActionMenu - title: BorderBox url: /deprecated/BorderBox - - title: Flex - url: /deprecated/Flex - - title: Grid - url: /deprecated/Grid - - title: Position - url: /deprecated/Position - title: Dialog url: /deprecated/Dialog - title: Dropdown url: /deprecated/Dropdown + - title: Flex + url: /deprecated/Flex - title: FormGroup url: /FormGroup + - title: Grid + url: /deprecated/Grid + - title: Position + url: /deprecated/Position - title: SelectMenu url: /deprecated/SelectMenu - - title: ActionList - url: /deprecated/ActionList diff --git a/src/ActionMenu.tsx b/src/ActionMenu.tsx index 142f18c7e24..0107dd178ac 100644 --- a/src/ActionMenu.tsx +++ b/src/ActionMenu.tsx @@ -1,106 +1,131 @@ -import {GroupedListProps, List, ListPropsBase} from './deprecated/ActionList/List' -import {Item, ItemProps} from './deprecated/ActionList/Item' -import {Divider} from './deprecated/ActionList/Divider' -import Button, {ButtonProps} from './Button' -import React, {useCallback, useMemo} from 'react' -import {AnchoredOverlay} from './AnchoredOverlay' -import {useProvidedStateOrCreate} from './hooks/useProvidedStateOrCreate' +import React from 'react' +import {useSSRSafeId} from '@react-aria/ssr' +import {TriangleDownIcon} from '@primer/octicons-react' +import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay' import {OverlayProps} from './Overlay' -import {useProvidedRefOrCreate} from './hooks' -import {AnchoredOverlayWrapperAnchorProps} from './AnchoredOverlay/AnchoredOverlay' +import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus, useTypeaheadFocus} from './hooks' +import {Divider} from './ActionList/Divider' +import {ActionListContainerContext} from './ActionList/ActionListContainerContext' +import {Button, ButtonProps} from './Button2' +import {MandateProps} from './utils/types' -interface ActionMenuBaseProps extends Partial>, ListPropsBase { - /** - * Content that is passed into the renderAnchor component, which is a button by default. - */ - anchorContent?: React.ReactNode +type MenuContextProps = Pick< + AnchoredOverlayProps, + 'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'onClose' | 'anchorId' +> +const MenuContext = React.createContext({renderAnchor: null, open: false}) +export type ActionMenuProps = { /** - * A callback that triggers both on clicks and keyboard events. This callback will be overridden by item level `onAction` callbacks. + * Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay` */ - onAction?: (props: ItemProps, event?: React.MouseEvent | React.KeyboardEvent) => void + children: React.ReactElement[] | React.ReactElement /** - * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `setOpen`. + * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `onOpenChange`. */ open?: boolean /** * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `open`. */ - setOpen?: (s: boolean) => void - - /** - * Props to be spread on the internal `Overlay` component. - */ - overlayProps?: Partial -} + onOpenChange?: (s: boolean) => void +} & Pick -export type ActionMenuProps = ActionMenuBaseProps & AnchoredOverlayWrapperAnchorProps - -const ActionMenuItem = (props: ItemProps) => - -ActionMenuItem.displayName = 'ActionMenu.Item' - -const ActionMenuBase = ({ - anchorContent, - renderAnchor = (props: T) => + `; diff --git a/src/__tests__/__snapshots__/ActionMenu2.test.tsx.snap b/src/__tests__/__snapshots__/ActionMenu2.test.tsx.snap deleted file mode 100644 index ccf46de1387..00000000000 --- a/src/__tests__/__snapshots__/ActionMenu2.test.tsx.snap +++ /dev/null @@ -1,144 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ActionMenu renders consistently 1`] = ` -.c0 { - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; - line-height: 1.5; - color: #24292f; -} - -.c2 { - display: inline-block; - margin-left: 8px; -} - -.c1 { - border-radius: 6px; - border: 1px solid; - border-color: rgba(27,31,36,0.15); - font-family: inherit; - font-weight: 600; - line-height: 20px; - white-space: nowrap; - vertical-align: middle; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-text-decoration: none; - text-decoration: none; - text-align: center; - display: grid; - grid-template-areas: "leadingIcon text trailingIcon"; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 16px; - padding-right: 16px; - font-size: 14px; - color: #24292f; - background-color: #f6f8fa; - box-shadow: 0 1px 0 rgba(27,31,36,0.04),inset 0 1px 0 rgba(255,255,255,0.25); -} - -.c1:focus { - outline: none; -} - -.c1:disabled { - cursor: default; - color: #8c959f; - background-color: btn.disabledBg; -} - -.c1:disabled svg { - opacity: 0.6; -} - -.c1 > :not(:last-child) { - margin-right: 8px; -} - -.c1 [data-component="leadingIcon"] { - grid-area: leadingIcon; -} - -.c1 [data-component="text"] { - grid-area: text; -} - -.c1 [data-component="trailingIcon"] { - grid-area: trailingIcon; -} - -.c1 [data-component="ButtonCounter"] { - font-size: 14px; -} - -.c1:hover:not([disabled]) { - background-color: #f3f4f6; -} - -.c1:focus:not([disabled]) { - box-shadow: 0 0 0 3px rgba(9,105,218,0.3); -} - -.c1:active:not([disabled]) { - background-color: hsla(220,14%,94%,1); - box-shadow: inset 0 0.15em 0.3em rgba(27,31,36,0.15); -} - -
- -
-`; diff --git a/src/__tests__/deprecated/ActionMenu.test.tsx b/src/__tests__/deprecated/ActionMenu.test.tsx new file mode 100644 index 00000000000..e535bce9c84 --- /dev/null +++ b/src/__tests__/deprecated/ActionMenu.test.tsx @@ -0,0 +1,136 @@ +import {cleanup, render as HTMLRender, act, fireEvent} from '@testing-library/react' +import 'babel-polyfill' +import {axe, toHaveNoViolations} from 'jest-axe' +import React from 'react' +import theme from '../../theme' +import {ActionMenu} from '../../deprecated' +import {behavesAsComponent, checkExports} from '../../utils/testing' +import {BaseStyles, SSRProvider, ThemeProvider} from '../..' +import {ItemProps} from '../../deprecated/ActionList/Item' +expect.extend(toHaveNoViolations) + +const items = [ + {text: 'New file'}, + {text: 'Copy link'}, + {text: 'Edit file'}, + {text: 'Delete file', variant: 'danger'} +] as ItemProps[] + +const mockOnActivate = jest.fn() + +function SimpleActionMenu(): JSX.Element { + return ( + + + +
X
+ +
+
+
+
+ ) +} + +describe('ActionMenu', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + behavesAsComponent({ + Component: ActionMenu, + options: {skipAs: true, skipSx: true}, + toRender: () => ( + + + + ) + }) + + checkExports('deprecated/ActionMenu', { + default: undefined, + ActionMenu + }) + + it('should have no axe violations', async () => { + const {container} = HTMLRender() + const results = await axe(container) + expect(results).toHaveNoViolations() + cleanup() + }) + + it('should trigger the overlay on trigger click', async () => { + const menu = HTMLRender() + let portalRoot = menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeNull() + const anchor = await menu.findByText('Menu') + act(() => { + fireEvent.click(anchor) + }) + portalRoot = menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeTruthy() + const itemText = items + .map((i: ItemProps) => { + if (i.hasOwnProperty('text')) { + return i.text + } + }) + .join('') + expect(portalRoot?.textContent?.trim()).toEqual(itemText) + }) + + it('should dismiss the overlay on menuitem click', async () => { + const menu = HTMLRender() + let portalRoot = await menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeNull() + const anchor = await menu.findByText('Menu') + act(() => { + fireEvent.click(anchor) + }) + portalRoot = menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeTruthy() + const menuItem = await menu.queryByText(items[0].text!) + act(() => { + fireEvent.click(menuItem as Element) + }) + expect(portalRoot?.textContent).toEqual('') // menu items are hidden + }) + + it('should dismiss the overlay on clicking outside overlay', async () => { + const menu = HTMLRender() + let portalRoot = await menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeNull() + const anchor = await menu.findByText('Menu') + act(() => { + fireEvent.click(anchor) + }) + portalRoot = menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeTruthy() + const somethingElse = (await menu.baseElement.querySelector('#something-else')) as HTMLElement + act(() => { + fireEvent.mouseDown(somethingElse) + }) + expect(portalRoot?.textContent).toEqual('') // menu items are hidden + }) + + it('should pass correct values to onAction on menu click', async () => { + const menu = HTMLRender() + let portalRoot = await menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeNull() + const anchor = await menu.findByText('Menu') + act(() => { + fireEvent.click(anchor) + }) + portalRoot = menu.baseElement.querySelector('#__primerPortalRoot__') + expect(portalRoot).toBeTruthy() + const menuItem = (await portalRoot?.querySelector("[role='menuitem']")) as HTMLElement + act(() => { + fireEvent.click(menuItem) + }) + + // onAction has been called with correct argument + expect(mockOnActivate).toHaveBeenCalledTimes(1) + const arg = mockOnActivate.mock.calls[0][0] + expect(arg.text).toEqual(items[0].text) + }) +}) diff --git a/src/__tests__/deprecated/__snapshots__/ActionMenu.test.tsx.snap b/src/__tests__/deprecated/__snapshots__/ActionMenu.test.tsx.snap new file mode 100644 index 00000000000..7581ef44ec9 --- /dev/null +++ b/src/__tests__/deprecated/__snapshots__/ActionMenu.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionMenu renders consistently 1`] = ` +.c0 { + position: relative; + display: inline-block; + padding: 6px 16px; + font-family: inherit; + font-weight: 600; + line-height: 20px; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border-radius: 6px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-text-decoration: none; + text-decoration: none; + text-align: center; + font-size: 14px; + color: #24292f; + background-color: #f6f8fa; + border: 1px solid rgba(27,31,36,0.15); + box-shadow: 0 1px 0 rgba(27,31,36,0.04),inset 0 1px 0 rgba(255,255,255,0.25); +} + +.c0:hover { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c0:focus { + outline: none; +} + +.c0:disabled { + cursor: default; +} + +.c0:disabled svg { + opacity: 0.6; +} + +.c0:hover { + background-color: #f3f4f6; + border-color: rgba(27,31,36,0.15); +} + +.c0:focus { + border-color: rgba(27,31,36,0.15); + box-shadow: 0 0 0 3px rgba(9,105,218,0.3); +} + +.c0:active { + background-color: hsla(220,14%,94%,1); + box-shadow: inset 0 0.15em 0.3em rgba(27,31,36,0.15); +} + +.c0:disabled { + color: #8c959f; + background-color: #f6f8fa; + border-color: rgba(27,31,36,0.15); +} + +