diff --git a/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md b/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md
new file mode 100644
index 00000000000..4c769755314
--- /dev/null
+++ b/.changeset/deprecate-actionmenuv1-promote-actionmenuv2.md
@@ -0,0 +1,67 @@
+---
+'@primer/react': major
+---
+
+### ActionMenu
+
+ActionMenu now ships a composable API that utilises ActionList
+
+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 After
+
+
+
+
+```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'
+```
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 7eb5066cca2..f414bdd2f3d 100644
--- a/docs/content/ActionList.mdx
+++ b/docs/content/ActionList.mdx
@@ -3,98 +3,447 @@ componentId: action_list
title: ActionList
status: Alpha
source: https://github.com/primer/react/tree/main/src/ActionList
+storybook: '/react/storybook?path=/story/composite-components-actionlist'
+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.
---
-An `ActionList` is a list of items which can be activated or selected. `ActionList` is the base component for many of our menu-type components, including `DropdownMenu` and `ActionMenu`.
+import {Avatar} from '@primer/react'
+import {ActionList} from '@primer/react'
+import {LinkIcon, AlertIcon, ArrowRightIcon} from '@primer/octicons-react'
+import InlineCode from '@primer/gatsby-theme-doctocat/src/components/inline-code'
-## Minimal example
+
+
+
+
+
+
+ github.com/primer
+
+ A React implementation of GitHub's Primer Design System
+
+
+
+
+
+
+ mona
+ Monalisa Octocat
+
+
+
+
+
+ 4 vulnerabilities
+
+
+
+
+
+
+
+```js
+import {ActionList} from '@primer/react'
+```
+
+## Examples
+
+### Minimal example
```jsx live
-
+
+ New file
+ Copy link
+ Edit file
+
+ Delete file
+
```
-## Example with grouped items
+### With leading visual
+
+Leading visuals are optional and appear at the start of an item. They can be octicons, avatars, and other custom visuals that fit a small area.
+
```jsx live
-
+
+
+
+ github.com/primer
+
+
+
+ 4 vulnerabilities
+
+
+
+ mona
+
+
+
```
-## Example with custom item renderer
-
-```jsx
-
- },
- {
- text: 'React Router link',
- renderItem: props =>
- },
- {
- text: 'NextJS style',
- renderItem: props => (
-
-
-
- )
- }
- ]}
-/>
+### With trailing visual
+
+Trailing visual and trailing text can display auxiliary information. They're placed at the right of the item, and can denote status, keyboard shortcuts, or be used to set expectations about what the action does.
+
+```jsx live
+
+
+ New file
+ ⌘ + N
+
+
+ Copy link
+ ⌘ + C
+
+
+ Edit file
+ ⌘ + E
+
+
+ Delete file
+ ⌫
+
+
+```
+
+### With description and dividers
+
+Item dividers allow users to parse heavier amounts of information. They're placed between items and are useful in complex lists, particularly when descriptions or multi-line text is present.
+
+```jsx live
+
+
+
+
+
+ mona
+ Monalisa Octocat
+
+
+
+
+
+ hubot
+ Hubot
+
+
+
+
+
+ primer-css
+ GitHub Design Systems Bot
+
+
+```
+
+### With links
+
+When you want to add links to the List instead of actions, use `ActionList.LinkItem`
+
+
+```jsx live
+
+
+
+
+
+ github/primer
+
+
+
+
+
+ MIT License
+
+
+
+
+
+ 1.4k stars
+
+
+```
+
+### With groups
+
+```javascript live noinline
+const SelectFields = () => {
+ const [options, setOptions] = React.useState([
+ {text: 'Status', selected: true},
+ {text: 'Stage', selected: true},
+ {text: 'Assignee', selected: true},
+ {text: 'Team', selected: true},
+ {text: 'Estimate', selected: false},
+ {text: 'Due Date', selected: false}
+ ])
+
+ const visibleOptions = options.filter(option => option.selected)
+ const hiddenOptions = options.filter(option => !option.selected)
+
+ const toggle = text => {
+ setOptions(
+ options.map(option => {
+ if (option.text === text) option.selected = !option.selected
+ return option
+ })
+ )
+ }
+
+ return (
+
+
+ {visibleOptions.map(option => (
+ toggle(option.text)}>
+ {option.text}
+
+ ))}
+
+
+ {hiddenOptions.map((option, index) => (
+ toggle(option.text)}>
+ {option.text}
+
+ ))}
+ {hiddenOptions.length === 0 && No hidden fields }
+
+
+ )
+}
+
+render( )
```
## Props
-| Name | Type | Default | Description |
-| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| items | `(ItemProps & Omit, keyof ItemProps>) \| ((Partial & {renderItem: RenderItemFn}) & {key?: Key})` | `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. |
-| showItemDividers | `boolean` | `false` | Optional. If `true` dividers will be displayed above each `ActionList.Item` which does not follow an `ActionList.Header` or `ActionList.Divider` |
+### ActionList
+
+
+
+
+ inset children are offset (vertically and horizontally) from list edges.{' '}
+ full children are flush (vertically and horizontally) with list edges
+ >
+ }
+ />
+
+
+ AriaRole
+ }
+ description={
+ <>
+ ARIA role describing the function of the list. listbox and{' '}
+ menu are a common values.
+ >
+ }
+ />
+
+
+
+### ActionList.Item
+
+
+
+
+ danger indicates that the item is destructive.
+ >
+ }
+ />
+
+
+
+ AriaRole
+ }
+ description={
+ <>
+ ARIA role describing the function of the item. option is a common value.
+ >
+ }
+ />
+
+
+
+### ActionList.LinkItem
+
+
+
+ MDN
+ }
+ />
+
+
+### ActionList.LeadingVisual
+
+
+
+
+
+
+### ActionList.TrailingVisual
+
+
+
+
+
+
+### ActionList.Description
+
+
+
+
+ inline descriptions are positioned beside primary text. block {' '}
+ descriptions are positioned below primary text.
+ >
+ }
+ />
+
+
+
+### ActionList.Group
+
+
+
+
+
+
+ inline descriptions are positioned beside primary text. block {' '}
+ descriptions are positioned below primary text.
+ >
+ }
+ />
+
+ Set selectionVariant at the group level.
+ >
+ }
+ />
+ AriaRole
+ }
+ description={
+ <>
+ ARIA role describing the function of the list inside the group. listbox and{' '}
+ menu are a common values.
+ >
+ }
+ />
+
+
+
+## Status
+
+
+
+## Further reading
+
+- [Interface guidelines: Action List](https://primer.style/design/components/action-list)
+
+## Related components
+
+- [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 (
+ <>
+ setOpen(!open)}>
+ {open ? 'Close Menu' : 'Open Menu'}
+
+
+
+
+
+ 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/deprecated/ActionList.mdx b/docs/content/deprecated/ActionList.mdx
new file mode 100644
index 00000000000..1b933448b03
--- /dev/null
+++ b/docs/content/deprecated/ActionList.mdx
@@ -0,0 +1,136 @@
+---
+componentId: action_list
+title: ActionList
+status: Deprecated
+source: https://github.com/primer/react/tree/main/src/deprecated/ActionList
+---
+
+An `ActionList` is a list of items which can be activated or selected. `ActionList` is the base component for many of our menu-type components, including `DropdownMenu` and `ActionMenu`.
+
+## Deprecation
+
+Use [composable API instead](/ActionList) instead.
+
+**Before**
+
+```jsx
+
+```
+
+**After**
+
+```jsx
+
+ New file
+ Copy link
+ Edit file
+
+ Delete file
+
+```
+
+Or continue using deprecated API:
+
+```js
+import {ActionList} from '@primer/react/deprecated'
+```
+
+## Minimal example
+
+```jsx live deprecated
+
+```
+
+## Example with grouped items
+
+```jsx live deprecated
+
+```
+
+## Example with custom item renderer
+
+```jsx deprecated
+
+ },
+ {
+ text: 'React Router link',
+ renderItem: props =>
+ },
+ {
+ text: 'NextJS style',
+ renderItem: props => (
+
+
+
+ )
+ }
+ ]}
+/>
+```
+
+## Props
+
+| Name | Type | Default | Description |
+| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| items | `(ItemProps & Omit, keyof ItemProps>) \| ((Partial & {renderItem: RenderItemFn}) & {key?: Key})` | `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. |
+| showItemDividers | `boolean` | `false` | Optional. If `true` dividers will be displayed above each `ActionList.Item` which does not follow an `ActionList.Header` or `ActionList.Divider` |
diff --git a/docs/content/deprecated/ActionMenu.mdx b/docs/content/deprecated/ActionMenu.mdx
new file mode 100644
index 00000000000..46c542ba2e3
--- /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 [composable API](/ActionMenu) with ActionList instead.
+
+**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/ActionList2.mdx b/docs/content/drafts/ActionList2.mdx
deleted file mode 100644
index bf842be1fe6..00000000000
--- a/docs/content/drafts/ActionList2.mdx
+++ /dev/null
@@ -1,449 +0,0 @@
----
-componentId: action_list2
-title: ActionList v2
-status: Alpha
-source: https://github.com/primer/react/tree/main/src/ActionList2
-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 {Avatar} from '@primer/react'
-import {ActionList} from '@primer/react/drafts'
-import {LinkIcon, AlertIcon, ArrowRightIcon} from '@primer/octicons-react'
-import InlineCode from '@primer/gatsby-theme-doctocat/src/components/inline-code'
-
-
-
-
-
-
-
- github.com/primer
-
- A React implementation of GitHub's Primer Design System
-
-
-
-
-
-
- mona
- Monalisa Octocat
-
-
-
-
-
- 4 vulnerabilities
-
-
-
-
-
-
-
-```js
-import {ActionList} from '@primer/react/drafts'
-```
-
-## Examples
-
-### Minimal example
-
-```jsx live drafts
-
- New file
- Copy link
- Edit file
-
- Delete file
-
-```
-
-### With leading visual
-
-Leading visuals are optional and appear at the start of an item. They can be octicons, avatars, and other custom visuals that fit a small area.
-
-
-```jsx live drafts
-
-
-
- github.com/primer
-
-
-
- 4 vulnerabilities
-
-
-
- mona
-
-
-
-```
-
-### With trailing visual
-
-Trailing visual and trailing text can display auxiliary information. They're placed at the right of the item, and can denote status, keyboard shortcuts, or be used to set expectations about what the action does.
-
-```jsx live drafts
-
-
- New file
- ⌘ + N
-
-
- Copy link
- ⌘ + C
-
-
- Edit file
- ⌘ + E
-
-
- Delete file
- ⌫
-
-
-```
-
-### With description and dividers
-
-Item dividers allow users to parse heavier amounts of information. They're placed between items and are useful in complex lists, particularly when descriptions or multi-line text is present.
-
-```jsx live drafts
-
-
-
-
-
- mona
- Monalisa Octocat
-
-
-
-
-
- hubot
- Hubot
-
-
-
-
-
- primer-css
- GitHub Design Systems Bot
-
-
-```
-
-### With links
-
-When you want to add links to the List instead of actions, use `ActionList.LinkItem`
-
-
-```jsx live drafts
-
-
-
-
-
- github/primer
-
-
-
-
-
- MIT License
-
-
-
-
-
- 1.4k stars
-
-
-```
-
-### With groups
-
-```javascript live noinline drafts
-const SelectFields = () => {
- const [options, setOptions] = React.useState([
- {text: 'Status', selected: true},
- {text: 'Stage', selected: true},
- {text: 'Assignee', selected: true},
- {text: 'Team', selected: true},
- {text: 'Estimate', selected: false},
- {text: 'Due Date', selected: false}
- ])
-
- const visibleOptions = options.filter(option => option.selected)
- const hiddenOptions = options.filter(option => !option.selected)
-
- const toggle = text => {
- setOptions(
- options.map(option => {
- if (option.text === text) option.selected = !option.selected
- return option
- })
- )
- }
-
- return (
-
-
- {visibleOptions.map(option => (
- toggle(option.text)}>
- {option.text}
-
- ))}
-
-
- {hiddenOptions.map((option, index) => (
- toggle(option.text)}>
- {option.text}
-
- ))}
- {hiddenOptions.length === 0 && No hidden fields }
-
-
- )
-}
-
-render( )
-```
-
-## Props
-
-### ActionList
-
-
-
-
- inset children are offset (vertically and horizontally) from list edges.{' '}
- full children are flush (vertically and horizontally) with list edges
- >
- }
- />
-
-
- AriaRole
- }
- description={
- <>
- ARIA role describing the function of the list. listbox and{' '}
- menu are a common values.
- >
- }
- />
-
-
-
-### ActionList.Item
-
-
-
-
- danger indicates that the item is destructive.
- >
- }
- />
-
-
-
- AriaRole
- }
- description={
- <>
- ARIA role describing the function of the item. option is a common value.
- >
- }
- />
-
-
-
-### ActionList.LinkItem
-
-
-
- MDN
- }
- />
-
-
-### ActionList.LeadingVisual
-
-
-
-
-
-
-### ActionList.TrailingVisual
-
-
-
-
-
-
-### ActionList.Description
-
-
-
-
- inline descriptions are positioned beside primary text. block {' '}
- descriptions are positioned below primary text.
- >
- }
- />
-
-
-
-### ActionList.Group
-
-
-
-
-
-
- inline descriptions are positioned beside primary text. block {' '}
- descriptions are positioned below primary text.
- >
- }
- />
-
- Set selectionVariant at the group level.
- >
- }
- />
- AriaRole
- }
- description={
- <>
- ARIA role describing the function of the list inside the group. listbox and{' '}
- menu are a common values.
- >
- }
- />
-
-
-
-## Status
-
-
-
-## Further reading
-
-- [Interface guidelines: Action List](https://primer.style/design/components/action-list)
-
-## Related components
-
-- [ActionMenu](/drafts/ActionMenu2)
-- [DropdownMenu](/DropdownMenu)
-- [SelectPanel](/SelectPanel)
diff --git a/docs/content/drafts/ActionMenu2.mdx b/docs/content/drafts/ActionMenu2.mdx
deleted file mode 100644
index 4ccc9b3c5a3..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} from '@primer/react'
-import {ActionMenu, ActionList} 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 (
- <>
- setOpen(!open)}>
- {open ? 'Close Menu' : 'Open Menu'}
-
-
-
-
-
- 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`](/drafts/ActionList2) |
-| [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](/drafts/ActionList2)
-- [SelectPanel](/SelectPanel)
-- [Button](/drafts/Button2)
diff --git a/docs/content/drafts/DropdownMenu2.mdx b/docs/content/drafts/DropdownMenu2.mdx
index 68cd522aa1e..47eba34b758 100644
--- a/docs/content/drafts/DropdownMenu2.mdx
+++ b/docs/content/drafts/DropdownMenu2.mdx
@@ -7,8 +7,8 @@ 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 {Box, Avatar, ActionList} from '@primer/react'
+import {DropdownMenu} from '@primer/react/drafts'
import {Props} from '../../src/props'
import State from '../../components/State'
import {CalendarIcon, IterationsIcon, NumberIcon, SingleSelectIcon, TypographyIcon} from '@primer/octicons-react'
@@ -312,7 +312,7 @@ Use `DropdownMenu` to select an option from a small list. If you’re looking fo
description={
<>
Recommended:{' '}
-
+
ActionList
>
@@ -359,6 +359,6 @@ Use `DropdownMenu` to select an option from a small list. If you’re looking fo
## Related components
-- [ActionList](/drafts/ActionList2)
-- [ActionMenu](/ActionMenu2)
+- [ActionList](/ActionList)
+- [ActionMenu](/ActionMenu)
- [SelectPanel](/SelectPanel)
diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
index 0a313c91e00..68d87527e5e 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml
+++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
@@ -35,6 +35,10 @@
# url: /useOverlay
- title: Components
children:
+ - title: ActionList
+ url: /ActionList
+ - title: ActionMenu
+ url: /ActionMenu
- title: Autocomplete
url: /Autocomplete
- title: Avatar
@@ -143,10 +147,6 @@
url: /drafts/LinkButton
- title: IconButton
url: /drafts/IconButton
- - title: ActionList v2
- url: /drafts/ActionList2
- - title: ActionMenu v2
- url: /drafts/ActionMenu2
- title: Deprecated
children:
- title: BorderBox
@@ -165,3 +165,7 @@
url: /FormGroup
- title: SelectMenu
url: /deprecated/SelectMenu
+ - title: ActionList
+ url: /deprecated/ActionList
+ - title: ActionMenu
+ url: /deprecated/ActionMenu
diff --git a/src/ActionList2/ActionListContainerContext.tsx b/src/ActionList/ActionListContainerContext.tsx
similarity index 100%
rename from src/ActionList2/ActionListContainerContext.tsx
rename to src/ActionList/ActionListContainerContext.tsx
diff --git a/src/ActionList2/Description.tsx b/src/ActionList/Description.tsx
similarity index 100%
rename from src/ActionList2/Description.tsx
rename to src/ActionList/Description.tsx
diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx
index 0513b60f1ce..e286448eb42 100644
--- a/src/ActionList/Divider.tsx
+++ b/src/ActionList/Divider.tsx
@@ -1,25 +1,29 @@
import React from 'react'
-import styled from 'styled-components'
+import Box from '../Box'
import {get} from '../constants'
-
-export const StyledDivider = styled.div`
- height: 1px;
- background: ${get('colors.border.muted')};
- margin-top: calc(${get('space.2')} - 1px);
- margin-bottom: ${get('space.2')};
-`
+import {Theme} from '../ThemeProvider'
+import {SxProp, merge} from '../sx'
/**
* Visually separates `Item`s or `Group`s in an `ActionList`.
*/
-export function Divider(): JSX.Element {
- return
-}
-/**
- * `Divider` fulfills the `ItemPropsWithCustomRenderer` contract,
- * so it can be used inline in an `ActionList`’s `items` prop.
- * In other words, `items={[ActionList.Divider]}` is supported as a concise
- * alternative to `items={[{renderItem: () => }]}`.
- */
-Divider.renderItem = Divider
+export const Divider: React.FC = ({sx = {}}) => {
+ return (
+ `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"
+ />
+ )
+}
diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx
index 915b54e6643..d67be85f3ce 100644
--- a/src/ActionList/Group.tsx
+++ b/src/ActionList/Group.tsx
@@ -1,45 +1,110 @@
import React from 'react'
-import styled from 'styled-components'
-import sx, {SxProp} from '../sx'
-import {Header, HeaderProps} from './Header'
+import {useSSRSafeId} from '@react-aria/ssr'
+import Box from '../Box'
+import {SxProp} from '../sx'
+import {ListContext, ListProps} from './List'
+import {AriaRole} from '../utils/types'
-/**
- * Contract for props passed to the `Group` component.
- */
-export interface GroupProps extends React.ComponentPropsWithoutRef<'div'>, SxProp {
+export type GroupProps = {
/**
- * Props for a `Header` to render in the `Group`.
+ * Style variations. Usage is discretionary.
+ *
+ * - `"filled"` - Superimposed on a background, offset from nearby content
+ * - `"subtle"` - Relatively less offset from nearby content
*/
- header?: HeaderProps
-
+ variant?: 'subtle' | 'filled'
/**
- * The id of the group.
+ * Primary text which names a `Group`.
*/
- groupId?: string
-
+ title?: string
/**
- * `Items` to render in the `Group`.
+ * Secondary text which provides additional information about a `Group`.
*/
- items?: JSX.Element[]
-
+ auxiliaryText?: string
/**
- * Whether to display a divider above each `Item` in this `Group` when it does not follow a `Header` or `Divider`.
+ * The ARIA role describing the function of the list inside `Group` component. `listbox` or `menu` are a common values.
*/
- showItemDividers?: boolean
+ role?: AriaRole
+} & SxProp & {
+ /**
+ * Whether multiple Items or a single Item can be selected in the Group. Overrides value on ActionList root.
+ */
+ selectionVariant?: ListProps['selectionVariant'] | false
+ }
+
+type ContextProps = Pick
+export const GroupContext = React.createContext({})
+
+export const Group: React.FC = ({
+ title,
+ variant = 'subtle',
+ auxiliaryText,
+ selectionVariant,
+ role,
+ sx = {},
+ ...props
+}) => {
+ const labelId = useSSRSafeId()
+ const {role: listRole} = React.useContext(ListContext)
+
+ return (
+
+ {title && }
+
+
+ {props.children}
+
+
+
+ )
}
-const StyledGroup = styled.div`
- ${sx}
-`
+export type HeaderProps = Pick & {
+ labelId: string
+}
/**
- * Collects related `Items` in an `ActionList`.
+ * Displays the name and description of a `Group`.
+ *
+ * For visual presentation only. It's hidden from screen readers.
*/
-export function Group({header, items, ...props}: GroupProps): JSX.Element {
+const Header: React.FC = ({variant, title, auxiliaryText, labelId, ...props}) => {
+ const {variant: listVariant} = React.useContext(ListContext)
+
+ const styles = {
+ paddingY: '6px',
+ paddingX: listVariant === 'full' ? 2 : 3,
+ fontSize: 0,
+ fontWeight: 'bold',
+ color: 'fg.muted',
+ ...(variant === 'filled' && {
+ backgroundColor: 'canvas.subtle',
+ marginX: 0,
+ marginBottom: 2,
+ borderTop: '1px solid',
+ borderBottom: '1px solid',
+ borderColor: 'neutral.muted'
+ })
+ }
+
return (
-
- {header && }
- {items}
-
+
+ {title}
+ {auxiliaryText && {auxiliaryText} }
+
)
}
diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx
index 0c37a1dc131..db1c4ed04db 100644
--- a/src/ActionList/Item.tsx
+++ b/src/ActionList/Item.tsx
@@ -1,66 +1,57 @@
-import {CheckIcon, IconProps} from '@primer/octicons-react'
-import React, {useCallback} from 'react'
-import {get} from '../constants'
-import sx, {SxProp} from '../sx'
-import Truncate from '../Truncate'
-import {ItemInput} from './List'
+import React from 'react'
+import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
+import {useSSRSafeId} from '@react-aria/ssr'
import styled from 'styled-components'
-import {StyledHeader} from './Header'
-import {StyledDivider} from './Divider'
import {useTheme} from '../ThemeProvider'
-import {
- activeDescendantActivatedDirectly,
- activeDescendantActivatedIndirectly,
- isActiveDescendantAttribute
-} from '@primer/behaviors'
-import {useSSRSafeId} from '@react-aria/ssr'
-import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
+import Box, {BoxProps} from '../Box'
+import sx, {SxProp, merge} from '../sx'
+import createSlots from '../utils/create-slots'
import {AriaRole} from '../utils/types'
+import {ListContext, ListProps} from './List'
+import {GroupContext, GroupProps} from './Group'
+import {ActionListContainerContext} from './ActionListContainerContext'
+import {Selection} from './Selection'
-/**
- * Contract for props passed to the `Item` component.
- */
-export interface ItemProps extends SxProp {
- /**
- * Primary text which names an `Item`.
- */
- text?: string
-
- /**
- * Secondary text which provides additional information about an `Item`.
- */
- description?: string
+export const getVariantStyles = (variant: ItemProps['variant'], disabled: ItemProps['disabled']) => {
+ if (disabled) {
+ return {
+ color: 'primer.fg.disabled',
+ iconColor: 'primer.fg.disabled',
+ annotationColor: 'primer.fg.disabled'
+ }
+ }
- /**
- * Secondary text style variations. Usage is discretionary.
- *
- * - `"inline"` - Secondary text is positioned beside primary text.
- * - `"block"` - Secondary text is positioned below primary text.
- */
- descriptionVariant?: 'inline' | 'block'
+ switch (variant) {
+ case 'danger':
+ return {
+ color: 'danger.fg',
+ iconColor: 'danger.fg',
+ annotationColor: 'fg.muted',
+ hoverColor: 'actionListItem.danger.hoverText'
+ }
+ default:
+ return {
+ color: 'fg.default',
+ iconColor: 'fg.muted',
+ annotationColor: 'fg.muted',
+ hoverColor: 'fg.default'
+ }
+ }
+}
+export type ItemProps = {
/**
- * Icon (or similar) positioned before `Item` text.
+ * Primary content for an Item
*/
- leadingVisual?: React.FunctionComponent
-
+ children?: React.ReactNode
/**
- * @deprecated Use `trailingVisual` instead
- * Icon (or similar) positioned after `Item` text.
+ * Callback that will trigger both on click selection and keyboard selection.
*/
- trailingIcon?: React.FunctionComponent
-
+ onSelect?: (event: React.MouseEvent | React.KeyboardEvent) => void
/**
- * @deprecated Use `trailingVisual` instead
- * Text positioned after `Item` text and optional trailing icon.
+ * Is the `Item` is currently selected?
*/
- trailingText?: string
-
- /**
- * Icon or text positioned after `Item` text.
- */
- trailingVisual?: React.ReactNode
-
+ selected?: boolean
/**
* Style variations associated with various `Item` types.
*
@@ -68,414 +59,211 @@ export interface ItemProps extends SxProp {
* - `"danger"` - A destructive action `Item`.
*/
variant?: 'default' | 'danger'
-
- /**
- * Whether to display a divider above the `Item` when it does not follow a `Header` or `Divider`.
- */
- showDivider?: boolean
-
- /**
- * For `Item`s which can be selected, whether the `Item` is currently selected.
- */
- selected?: boolean
-
- /**
- * For `Item`s which can be selected, whether `multiple` `Item`s or a `single` `Item` can be selected
- */
- selectionVariant?: 'single' | 'multiple'
-
- /**
- * Designates the group that an item belongs to.
- */
- groupId?: string
-
/**
* Items that are disabled can not be clicked, selected, or navigated through.
*/
disabled?: boolean
-
- /**
- * Callback that will trigger both on click selection and keyboard selection.
- */
- onAction?: (item: ItemProps, event: React.MouseEvent | React.KeyboardEvent) => void
-
/**
- * An id associated with this item. Should be unique between items
+ * The ARIA role describing the function of `Item` component. `option` is a common value.
*/
- id?: number | string
-
- /**
- * Node to be included inside the item before the text.
- */
- children?: React.ReactNode
-
+ role?: AriaRole
/**
- * The ARIA role describing the function of `List` component. `option` is a common value.
+ * id to attach to the root element of the Item
*/
- role?: AriaRole
-
+ id?: string
/**
- * An item to pass back in the `onAction` callback, meant as
+ * Private API for use internally only. Used by LinkItem to wrap contents in an anchor
*/
- item?: ItemInput
+ _PrivateItemWrapper?: React.FC
+} & SxProp
+
+const {Slots, Slot} = createSlots(['LeadingVisual', 'InlineDescription', 'BlockDescription', 'TrailingVisual'])
+export {Slot}
+export type ItemContext = Pick & {
+ inlineDescriptionId: string
+ blockDescriptionId: string
}
-const getItemVariant = (variant = 'default', disabled?: boolean) => {
- if (disabled) {
- return {
- color: get('colors.primer.fg.disabled'),
- iconColor: get('colors.primer.fg.disabled'),
- annotationColor: get('colors.primer.fg.disabled'),
- hoverCursor: 'default'
+const LiBox = styled.li(sx)
+export const TEXT_ROW_HEIGHT = '20px' // custom value off the scale
+
+export const Item = React.forwardRef(
+ (
+ {
+ variant = 'default',
+ disabled = false,
+ selected = undefined,
+ onSelect,
+ sx: sxProp = {},
+ id,
+ role,
+ _PrivateItemWrapper,
+ ...props
+ },
+ forwardedRef
+ ): JSX.Element => {
+ const {variant: listVariant, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext)
+ const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
+ const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext)
+
+ let selectionVariant: ListProps['selectionVariant'] | GroupProps['selectionVariant']
+ if (typeof groupSelectionVariant !== 'undefined') selectionVariant = groupSelectionVariant
+ else selectionVariant = listSelectionVariant
+
+ /** Infer item role based on the container */
+ let itemRole: ItemProps['role']
+ if (container === 'ActionMenu' || container === 'DropdownMenu') {
+ if (selectionVariant === 'single') itemRole = 'menuitemradio'
+ else if (selectionVariant === 'multiple') itemRole = 'menuitemcheckbox'
+ else itemRole = 'menuitem'
}
- }
- switch (variant) {
- case 'danger':
- return {
- color: get('colors.danger.fg'),
- iconColor: get('colors.danger.fg'),
- annotationColor: get('colors.fg.muted'),
- hoverCursor: 'pointer',
- hoverBg: get('colors.actionListItem.danger.hoverBg'),
- focusBg: get('colors.actionListItem.danger.activeBg'),
- hoverText: get('colors.actionListItem.danger.hoverText')
- }
- default:
- return {
- color: get('colors.fg.default'),
- iconColor: get('colors.fg.muted'),
- annotationColor: get('colors.fg.muted'),
- hoverCursor: 'pointer',
- hoverBg: get('colors.actionListItem.default.hoverBg'),
- focusBg: get('colors.actionListItem.default.activeBg')
+ const {theme} = useTheme()
+
+ const styles = {
+ display: 'flex',
+ paddingX: 2,
+ fontSize: 1,
+ paddingY: '6px', // custom value off the scale
+ lineHeight: TEXT_ROW_HEIGHT,
+ minHeight: 5,
+ marginX: listVariant === 'inset' ? 2 : 0,
+ borderRadius: listVariant === 'inset' ? 2 : 0,
+ transition: 'background 33.333ms linear',
+ color: getVariantStyles(variant, disabled).color,
+ cursor: 'pointer',
+ '&[aria-disabled]': {cursor: 'not-allowed'},
+
+ '@media (hover: hover) and (pointer: fine)': {
+ ':hover:not([aria-disabled])': {
+ backgroundColor: `actionListItem.${variant}.hoverBg`,
+ color: getVariantStyles(variant, disabled).hoverColor
+ },
+ ':focus:not([data-focus-visible-added])': {
+ backgroundColor: `actionListItem.${variant}.selectedBg`,
+ color: getVariantStyles(variant, disabled).hoverColor,
+ outline: 'none'
+ },
+ '&[data-focus-visible-added]': {
+ // we don't use :focus-visible because not all browsers (safari) have it yet
+ outline: 'none',
+ border: `2 solid`,
+ boxShadow: `0 0 0 2px ${theme?.colors.accent.emphasis}`
+ },
+ ':active:not([aria-disabled])': {
+ backgroundColor: `actionListItem.${variant}.activeBg`,
+ color: getVariantStyles(variant, disabled).hoverColor
+ }
+ },
+
+ /** Divider styles */
+ '[data-component="ActionList.Item--DividerContainer"]': {
+ position: 'relative'
+ },
+ '[data-component="ActionList.Item--DividerContainer"]::before': {
+ content: '" "',
+ display: 'block',
+ position: 'absolute',
+ width: '100%',
+ top: '-7px',
+ border: '0 solid',
+ borderTopWidth: showDividers ? `1px` : '0',
+ borderColor: 'var(--divider-color, transparent)'
+ },
+ // show between 2 items
+ ':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider},
+ // hide divider after dividers & group header, with higher importance!
+ '[data-component="ActionList.Divider"] + &': {'--divider-color': 'transparent !important'},
+ // hide border on current and previous item
+ '&:hover:not([aria-disabled]), &:focus:not([aria-disabled]), &[data-focus-visible-added]:not([aria-disabled])': {
+ '--divider-color': 'transparent'
+ },
+ '&:hover:not([aria-disabled]) + &, &:focus:not([aria-disabled]) + &, &[data-focus-visible-added] + li': {
+ '--divider-color': 'transparent'
}
- }
-}
-
-const DividedContent = styled.div`
- display: flex;
- min-width: 0;
-
- /* Required for dividers */
- position: relative;
- flex-grow: 1;
-`
-
-const MainContent = styled.div`
- align-items: baseline;
- display: flex;
- min-width: 0;
- flex-direction: var(--main-content-flex-direction);
- flex-grow: 1;
-`
-
-const StyledItem = styled.div<
- {
- variant: ItemProps['variant']
- showDivider: ItemProps['showDivider']
- item?: ItemInput
- } & SxProp
->`
- /* 6px vertical padding + 20px line height = 32px total height
- *
- * TODO: When rem-based spacing on a 4px scale lands, replace
- * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'.
- */
- padding: 6px ${get('space.2')};
- display: flex;
- border-radius: ${get('radii.2')};
- color: ${({variant, item}) => getItemVariant(variant, item?.disabled).color};
- // 2 frames on a 60hz monitor
- transition: background 33.333ms linear;
- text-decoration: none;
-
- @media (hover: hover) and (pointer: fine) {
- :hover {
- // allow override in case another item in the list is active/focused
- background: var(
- --item-hover-bg-override,
- ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg}
- );
- color: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverText};
- cursor: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverCursor};
- }
- }
-
- // Item dividers
- :not(:first-of-type):not(${StyledDivider} + &):not(${StyledHeader} + &) {
- margin-top: ${({showDivider}) => (showDivider ? `1px` : '0')};
-
- ${DividedContent}::before {
- content: ' ';
- display: block;
- position: absolute;
- width: 100%;
- top: -7px;
- // NB: This 'get' won’t execute if it’s moved into the arrow function below.
- border: 0 solid ${get('colors.border.muted')};
- border-top-width: ${({showDivider}) => (showDivider ? `1px` : '0')};
}
- }
-
- // Item dividers should not be visible:
- // - above Hovered
- &:hover ${DividedContent}::before,
- // - below Hovered
- // '*' instead of '&' because '&' maps to separate class names depending on 'variant'
- :hover + * ${DividedContent}::before {
- // allow override in case another item in the list is active/focused
- border-color: var(--item-hover-divider-border-color-override, transparent) !important;
- }
-
- // - above Focused
- &:focus ${DividedContent}::before,
- // - below Focused
- // '*' instead of '&' because '&' maps to separate class names depending on 'variant'
- :focus + * ${DividedContent}::before,
- // - above Active Descendent
- &[${isActiveDescendantAttribute}] ${DividedContent}::before,
- // - below Active Descendent
- [${isActiveDescendantAttribute}] + & ${DividedContent}::before {
- // '!important' because all the ':not's above give higher specificity
- border-color: transparent !important;
- }
-
- // Active Descendant
- &[${isActiveDescendantAttribute}='${activeDescendantActivatedDirectly}'] {
- background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
- }
- &[${isActiveDescendantAttribute}='${activeDescendantActivatedIndirectly}'] {
- background: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg};
- }
- &:focus {
- background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
- outline: none;
- }
-
- &:active {
- background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
- }
-
- ${sx}
-`
-
-export const TextContainer = styled.span<{
- dangerouslySetInnerHtml?: React.DOMAttributes['dangerouslySetInnerHTML']
-}>``
-
-const BaseVisualContainer = styled.div<{variant?: ItemProps['variant']; disabled?: boolean}>`
- // Match visual height to adjacent text line height.
- // TODO: When rem-based spacing on a 4px scale lands, replace
- // hardcoded '20px' with '${get('space.s20')}'.
- height: 20px;
- width: ${get('space.3')};
- margin-right: ${get('space.2')};
- display: flex;
- justify-content: center;
- align-items: center;
- flex-shrink: 0;
-`
-
-const ColoredVisualContainer = styled(BaseVisualContainer)`
- svg {
- fill: ${({variant, disabled}) => getItemVariant(variant, disabled).iconColor};
- font-size: ${get('fontSizes.0')};
- }
-`
-
-const LeadingVisualContainer = styled(ColoredVisualContainer)`
- display: flex;
- flex-direction: column;
- justify-content: center;
-`
-
-const TrailingContent = styled(ColoredVisualContainer)`
- color: ${({variant, disabled}) => getItemVariant(variant, disabled).annotationColor}};
- margin-left: ${get('space.2')};
- margin-right: 0;
- width: auto;
- div:nth-child(2) {
- margin-left: ${get('space.2')};
- }
-`
-
-const DescriptionContainer = styled.span`
- color: ${get('colors.fg.muted')};
- font-size: ${get('fontSizes.0')};
- // TODO: When rem-based spacing on a 4px scale lands, replace
- // hardcoded '16px' with '${get('lh-12')}'.
- line-height: 16px;
- margin-left: var(--description-container-margin-left);
- min-width: 0;
- flex-grow: 1;
- flex-basis: var(--description-container-flex-basis);
-`
-
-const MultiSelectIcon = styled.svg<{selected?: boolean}>`
- rect {
- fill: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.canvas.default'))};
- stroke: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.border.default'))};
- shape-rendering: auto; // this is a workaround to override global style in github/github, see primer/react#1666
- }
- path {
- fill: ${get('colors.fg.onEmphasis')};
- boxshadow: ${get('shadow.small')};
- opacity: ${({selected}) => (selected ? 1 : 0)};
+ const clickHandler = React.useCallback(
+ event => {
+ if (disabled) return
+ if (!event.defaultPrevented) {
+ if (typeof onSelect === 'function') onSelect(event)
+ // if this Item is inside a Menu, close the Menu
+ if (typeof afterSelect === 'function') afterSelect()
+ }
+ },
+ [onSelect, disabled, afterSelect]
+ )
+
+ const keyPressHandler = React.useCallback(
+ event => {
+ if (disabled) return
+ if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
+ if (typeof onSelect === 'function') onSelect(event)
+ // if this Item is inside a Menu, close the Menu
+ if (typeof afterSelect === 'function') afterSelect()
+ }
+ },
+ [onSelect, disabled, afterSelect]
+ )
+
+ // use props.id if provided, otherwise generate one.
+ const labelId = useSSRSafeId(id)
+ const inlineDescriptionId = useSSRSafeId(id && `${id}--inline-description`)
+ const blockDescriptionId = useSSRSafeId(id && `${id}--block-description`)
+
+ const ItemWrapper = _PrivateItemWrapper || React.Fragment
+
+ return (
+
+ {slots => (
+
+
+
+ {slots.LeadingVisual}
+
+
+
+
+ {props.children}
+
+ {slots.InlineDescription}
+
+ {slots.TrailingVisual}
+
+ {slots.BlockDescription}
+
+
+
+ )}
+
+ )
}
-`
+) as PolymorphicForwardRefComponent<'li', ItemProps>
-/**
- * An actionable or selectable `Item` with an optional icon and description.
- */
-export const Item = React.forwardRef((itemProps, ref) => {
- const {
- as: Component,
- text,
- description,
- descriptionVariant = 'inline',
- selected,
- selectionVariant,
- leadingVisual: LeadingVisual,
- trailingIcon: TrailingIcon,
- trailingVisual: TrailingVisual,
- trailingText,
- variant = 'default',
- showDivider,
- disabled,
- onAction,
- onKeyPress,
- children,
- onClick,
- id,
- ...props
- } = itemProps
-
- const labelId = useSSRSafeId()
- const descriptionId = useSSRSafeId()
-
- const keyPressHandler = useCallback(
- event => {
- if (disabled) {
- return
- }
- onKeyPress?.(event)
-
- if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
- onAction?.(itemProps, event)
- }
- },
- [onAction, disabled, itemProps, onKeyPress]
- )
-
- const clickHandler = useCallback(
- event => {
- if (disabled) {
- return
- }
- onClick?.(event)
- if (!event.defaultPrevented) {
- onAction?.(itemProps, event)
- }
- },
- [onAction, disabled, itemProps, onClick]
- )
+Item.displayName = 'ActionList.Item'
- const {theme} = useTheme()
+const ConditionalBox: React.FC<{if: boolean} & BoxProps> = props => {
+ const {if: condition, ...rest} = props
- return (
-
- {!!selected === selected && (
-
- {selectionVariant === 'multiple' ? (
- <>
- {/**
- * we use a svg instead of an input because there should not
- * be an interactive element inside an option
- * svg copied from primer/css
- */}
-
-
-
-
- >
- ) : (
- selected &&
- )}
-
- )}
- {LeadingVisual && (
-
-
-
- )}
-
-
- {children}
- {text ? {text} : null}
- {description ? (
-
- {descriptionVariant === 'block' ? (
- description
- ) : (
-
- {description}
-
- )}
-
- ) : null}
-
- {/* backward compatibility: prefer TrailingVisual but fallback to TrailingIcon */}
- {TrailingVisual ? (
-
- {typeof TrailingVisual === 'function' ? : TrailingVisual}
-
- ) : TrailingIcon || trailingText ? (
-
- {trailingText}
- {TrailingIcon && }
-
- ) : null}
-
-
- )
-}) as PolymorphicForwardRefComponent<'div', ItemProps>
-
-Item.displayName = 'ActionList.Item'
+ if (condition) return {props.children}
+ else return <>{props.children}>
+}
diff --git a/src/ActionList2/LinkItem.tsx b/src/ActionList/LinkItem.tsx
similarity index 100%
rename from src/ActionList2/LinkItem.tsx
rename to src/ActionList/LinkItem.tsx
diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx
index f0d4768396f..a3c060a532a 100644
--- a/src/ActionList/List.tsx
+++ b/src/ActionList/List.tsx
@@ -1,258 +1,73 @@
-import React, {Key} from 'react'
-import type {AriaRole} from '../utils/types'
-import {Group, GroupProps} from './Group'
-import {Item, ItemProps} from './Item'
-import {Divider} from './Divider'
+import React from 'react'
+import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
import styled from 'styled-components'
-import {get} from '../constants'
-import {SystemCssProperties} from '@styled-system/css'
-import {hasActiveDescendantAttribute} from '@primer/behaviors'
-import {Merge} from '../utils/types/Merge'
-
-type RenderItemFn = (props: ItemProps) => React.ReactElement
-
-export type ItemInput =
- | Merge, ItemProps>
- | ((Partial & {renderItem: RenderItemFn}) & {key?: Key})
-
-/**
- * Contract for props passed to the `List` component.
- */
-export interface ListPropsBase {
- /**
- * A collection of `Item` props and `Item`-level custom `Item` renderers.
- */
- items: ItemInput[]
-
- /**
- * The ARIA role describing the function of `List` component. `listbox` is a common value.
- */
- role?: AriaRole
-
- /**
- * id to attach to the base DOM node of the list
- */
- id?: string
-
- /**
- * A `List`-level custom `Item` renderer. Every `Item` within this `List`
- * without a `Group`-level or `Item`-level custom `Item` renderer will be
- * rendered using this function component.
- */
- renderItem?: RenderItemFn
+import sx, {SxProp, merge} from '../sx'
+import {AriaRole} from '../utils/types'
+import {ActionListContainerContext} from './ActionListContainerContext'
+export type ListProps = {
/**
- * A `List`-level custom `Group` renderer. Every `Group` within this `List`
- * without a `Group`-level custom `Item` renderer will be rendered using
- * this function component.
- */
- renderGroup?: typeof Group
-
- /**
- * Style variations. Usage is discretionary.
- *
- * - `"inset"` - `List` children are offset (vertically and horizontally) from `List`’s edges
- * - `"full"` - `List` children are flush (vertically and horizontally) with `List` edges
+ * `inset` children are offset (vertically and horizontally) from `List`’s edges, `full` children are flush (vertically and horizontally) with `List` edges
*/
variant?: 'inset' | 'full'
-
/**
- * For `Item`s which can be selected, whether `multiple` `Item`s or a `single` `Item` can be selected
+ * Whether multiple Items or a single Item can be selected.
*/
selectionVariant?: 'single' | 'multiple'
-
/**
- * Whether to display a divider above each `Item` in this `List` when it does not follow a `Header` or `Divider`.
+ * Display a divider above each `Item` in this `List` when it does not follow a `Header` or `Divider`.
*/
- showItemDividers?: boolean
-}
-
-/**
- * Contract for props passed to the `List` component, when its `Item`s are collected in `Group`s.
- */
-export interface GroupedListProps extends ListPropsBase {
- /**
- * A collection of `Group` props (except `items`), plus a unique group identifier
- * and `Group`-level custom `Item` or `Group` renderers.
- */
- groupMetadata: ((
- | Omit
- | Omit & {renderItem?: RenderItemFn; renderGroup?: typeof Group}, 'items'>
- ) & {groupId: string})[]
-
+ showDividers?: boolean
/**
- * A collection of `Item` props, plus associated group identifiers
- * and `Item`-level custom `Item` renderers.
- */
- items: ((ItemProps | (Partial & {renderItem: RenderItemFn})) & {groupId: string})[]
-}
-
-/**
- * Asserts that the given value fulfills the `GroupedListProps` contract.
- * @param props A value which fulfills either the `ListPropsBase` or the `GroupedListProps` contract.
- */
-function isGroupedListProps(props: ListProps): props is GroupedListProps {
- return 'groupMetadata' in props
-}
-
-/**
- * Contract for props passed to the `List` component.
- */
-export type ListProps = ListPropsBase | GroupedListProps
-
-const StyledList = styled.div`
- font-size: ${get('fontSizes.1')};
- /* 14px font-size * 1.428571429 = 20px line height
- *
- * TODO: When rem-based spacing on a 4px scale lands, replace
- * hardcoded '20px'
+ * The ARIA role describing the function of `List` component. `listbox` or `menu` are a common values.
*/
- line-height: 20px;
-
- &[${hasActiveDescendantAttribute}], &:focus-within {
- --item-hover-bg-override: none;
- --item-hover-divider-border-color-override: ${get('colors.border.muted')};
- }
-`
-
-/**
- * Returns `sx` prop values for `List` children matching the given `List` style variation.
- * @param variant `List` style variation.
- */
-function useListVariant(variant: ListProps['variant'] = 'inset'): {
- firstGroupStyle?: SystemCssProperties
- lastGroupStyle?: SystemCssProperties
- headerStyle?: SystemCssProperties
- itemStyle?: SystemCssProperties
-} {
- switch (variant) {
- case 'full':
- return {
- headerStyle: {paddingX: get('space.2')},
- itemStyle: {borderRadius: 0}
- }
- default:
- return {
- firstGroupStyle: {marginTop: get('space.2')},
- lastGroupStyle: {marginBottom: get('space.2')},
- itemStyle: {marginX: get('space.2')}
- }
- }
-}
-
-/**
- * Lists `Item`s, either grouped or ungrouped, with a `Divider` between each `Group`.
- */
-export const List = React.forwardRef((props, forwardedRef): JSX.Element => {
- // Get `sx` prop values for `List` children matching the given `List` style variation.
- const {firstGroupStyle, lastGroupStyle, headerStyle, itemStyle} = useListVariant(props.variant)
+ role?: AriaRole
+} & SxProp
+
+type ContextProps = Pick
+export const ListContext = React.createContext({})
+
+const ListBox = styled.ul(sx)
+
+export const List = React.forwardRef(
+ (
+ {variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = {}, ...props},
+ forwardedRef
+ ): JSX.Element => {
+ const styles = {
+ margin: 0,
+ paddingInlineStart: 0, // reset ul styles
+ paddingY: variant === 'inset' ? 2 : 0
+ }
- /**
- * Render a `Group` using the first of the following renderers that is defined:
- * A `Group`-level or `List`-level custom `Group` renderer, or
- * the default `Group` renderer.
- */
- const renderGroup = (
- groupProps: GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group})
- ) => {
- const GroupComponent = (('renderGroup' in groupProps && groupProps.renderGroup) ?? props.renderGroup) || Group
- return
- }
+ /** if list is inside a Menu, it will get a role from the Menu */
+ const {
+ listRole,
+ listLabelledBy,
+ selectionVariant: containerSelectionVariant // TODO: Remove after DropdownMenu2 deprecation
+ } = React.useContext(ActionListContainerContext)
- /**
- * Render an `Item` using the first of the following renderers that is defined:
- * An `Item`-level, `Group`-level, or `List`-level custom `Item` renderer,
- * or the default `Item` renderer.
- */
- const renderItem = (itemProps: ItemInput, item: ItemInput, itemIndex: number) => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item
- const key = ('key' in itemProps ? itemProps.key : undefined) ?? itemProps.id?.toString() ?? itemIndex.toString()
return (
-
+
+
+ {props.children}
+
+
)
}
-
- /**
- * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`.
- */
- let groups: (GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group}))[] = []
-
- // Collect rendered `Item`s into `Group`s, avoiding excess iteration over the lists of `items` and `groupMetadata`:
- if (!isGroupedListProps(props)) {
- // When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`.
- groups = [{items: props.items.map((item, index) => renderItem(item, item, index)), groupId: '0'}]
- } else {
- // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s.
-
- /**
- * A map of group identifiers to `Group`s, each with an associated array of `Item`s belonging to that `Group`.
- */
- const groupMap = props.groupMetadata.reduce(
- (groupAccumulator, groupMetadata) => groupAccumulator.set(groupMetadata.groupId, groupMetadata),
- new Map & {renderItem?: typeof Item; renderGroup?: typeof Group})>()
- )
-
- for (const itemProps of props.items) {
- // Look up the group associated with the current item.
- const group = groupMap.get(itemProps.groupId)
- const itemIndex = group?.items?.length ?? 0
-
- // Upsert the group to include the current item (rendered).
- groupMap.set(itemProps.groupId, {
- ...group,
- items: [
- ...(group?.items ?? []),
- renderItem(
- {
- showDivider: group?.showItemDividers,
- ...(group && 'renderItem' in group && {renderItem: group.renderItem}),
- ...itemProps
- },
- itemProps,
- itemIndex
- )
- ]
- })
- }
-
- groups = [...groupMap.values()]
- }
-
- return (
-
- {groups.map(({header, ...groupProps}, index) => {
- const hasFilledHeader = header?.variant === 'filled'
- const shouldShowDivider = index > 0 && !hasFilledHeader
- return (
-
- {shouldShowDivider ? : null}
- {renderGroup({
- sx: {
- ...(index === 0 && firstGroupStyle),
- ...(index === groups.length - 1 && lastGroupStyle),
- ...(index > 0 && !shouldShowDivider && {mt: 2})
- },
- ...(header && {
- header: {
- ...header,
- sx: {...headerStyle, ...header.sx}
- }
- }),
- ...groupProps
- })}
-
- )
- })}
-
- )
-})
+) as PolymorphicForwardRefComponent<'ul', ListProps>
List.displayName = 'ActionList'
diff --git a/src/ActionList2/Selection.tsx b/src/ActionList/Selection.tsx
similarity index 100%
rename from src/ActionList2/Selection.tsx
rename to src/ActionList/Selection.tsx
diff --git a/src/ActionList2/Visuals.tsx b/src/ActionList/Visuals.tsx
similarity index 100%
rename from src/ActionList2/Visuals.tsx
rename to src/ActionList/Visuals.tsx
diff --git a/src/ActionList/index.ts b/src/ActionList/index.ts
index 71eb71708f8..f27a3b53d50 100644
--- a/src/ActionList/index.ts
+++ b/src/ActionList/index.ts
@@ -1,10 +1,19 @@
import {List} from './List'
import {Group} from './Group'
import {Item} from './Item'
+import {LinkItem} from './LinkItem'
import {Divider} from './Divider'
+import {Description} from './Description'
+import {LeadingVisual, TrailingVisual} from './Visuals'
+
export type {ListProps as ActionListProps} from './List'
-export type {GroupProps} from './Group'
-export type {ItemProps} from './Item'
+export type {GroupProps as ActionListGroupProps} from './Group'
+export type {ItemProps as ActionListItemProps} from './Item'
+export type {DescriptionProps as ActionListDescriptionProps} from './Description'
+export type {
+ LeadingVisualProps as ActionListLeadingVisualProps,
+ TrailingVisualProps as ActionListTrailingVisualProps
+} from './Visuals'
/**
* Collection of list-related components.
@@ -13,9 +22,21 @@ export const ActionList = Object.assign(List, {
/** Collects related `Items` in an `ActionList`. */
Group,
- /** An actionable or selectable `Item` with an optional icon and description. */
+ /** An actionable or selectable `Item` */
Item,
+ /** A `Item` that renders a full-size anchor inside ListItem */
+ LinkItem,
+
/** Visually separates `Item`s or `Group`s in an `ActionList`. */
- Divider
+ Divider,
+
+ /** Secondary text which provides additional information about an `Item`. */
+ Description,
+
+ /** Icon (or similar) positioned before `Item` text. */
+ LeadingVisual,
+
+ /** Icon (or similar) positioned after `Item` text. */
+ TrailingVisual
})
diff --git a/src/ActionList2/Divider.tsx b/src/ActionList2/Divider.tsx
deleted file mode 100644
index e286448eb42..00000000000
--- a/src/ActionList2/Divider.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-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 const Divider: React.FC = ({sx = {}}) => {
- return (
- `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"
- />
- )
-}
diff --git a/src/ActionList2/Group.tsx b/src/ActionList2/Group.tsx
deleted file mode 100644
index d67be85f3ce..00000000000
--- a/src/ActionList2/Group.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React from 'react'
-import {useSSRSafeId} from '@react-aria/ssr'
-import Box from '../Box'
-import {SxProp} from '../sx'
-import {ListContext, ListProps} from './List'
-import {AriaRole} from '../utils/types'
-
-export type GroupProps = {
- /**
- * Style variations. Usage is discretionary.
- *
- * - `"filled"` - Superimposed on a background, offset from nearby content
- * - `"subtle"` - Relatively less offset from nearby content
- */
- variant?: 'subtle' | 'filled'
- /**
- * Primary text which names a `Group`.
- */
- title?: string
- /**
- * Secondary text which provides additional information about a `Group`.
- */
- auxiliaryText?: string
- /**
- * The ARIA role describing the function of the list inside `Group` component. `listbox` or `menu` are a common values.
- */
- role?: AriaRole
-} & SxProp & {
- /**
- * Whether multiple Items or a single Item can be selected in the Group. Overrides value on ActionList root.
- */
- selectionVariant?: ListProps['selectionVariant'] | false
- }
-
-type ContextProps = Pick
-export const GroupContext = React.createContext({})
-
-export const Group: React.FC = ({
- title,
- variant = 'subtle',
- auxiliaryText,
- selectionVariant,
- role,
- sx = {},
- ...props
-}) => {
- const labelId = useSSRSafeId()
- const {role: listRole} = React.useContext(ListContext)
-
- return (
-
- {title && }
-
-
- {props.children}
-
-
-
- )
-}
-
-export type HeaderProps = Pick & {
- labelId: string
-}
-
-/**
- * Displays the name and description of a `Group`.
- *
- * For visual presentation only. It's hidden from screen readers.
- */
-const Header: React.FC = ({variant, title, auxiliaryText, labelId, ...props}) => {
- const {variant: listVariant} = React.useContext(ListContext)
-
- const styles = {
- paddingY: '6px',
- paddingX: listVariant === 'full' ? 2 : 3,
- fontSize: 0,
- fontWeight: 'bold',
- color: 'fg.muted',
- ...(variant === 'filled' && {
- backgroundColor: 'canvas.subtle',
- marginX: 0,
- marginBottom: 2,
- borderTop: '1px solid',
- borderBottom: '1px solid',
- borderColor: 'neutral.muted'
- })
- }
-
- return (
-
- {title}
- {auxiliaryText && {auxiliaryText} }
-
- )
-}
diff --git a/src/ActionList2/Item.tsx b/src/ActionList2/Item.tsx
deleted file mode 100644
index db1c4ed04db..00000000000
--- a/src/ActionList2/Item.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-import React from 'react'
-import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
-import {useSSRSafeId} from '@react-aria/ssr'
-import styled from 'styled-components'
-import {useTheme} from '../ThemeProvider'
-import Box, {BoxProps} from '../Box'
-import sx, {SxProp, merge} from '../sx'
-import createSlots from '../utils/create-slots'
-import {AriaRole} from '../utils/types'
-import {ListContext, ListProps} from './List'
-import {GroupContext, GroupProps} from './Group'
-import {ActionListContainerContext} from './ActionListContainerContext'
-import {Selection} from './Selection'
-
-export const getVariantStyles = (variant: ItemProps['variant'], disabled: ItemProps['disabled']) => {
- if (disabled) {
- return {
- color: 'primer.fg.disabled',
- iconColor: 'primer.fg.disabled',
- annotationColor: 'primer.fg.disabled'
- }
- }
-
- switch (variant) {
- case 'danger':
- return {
- color: 'danger.fg',
- iconColor: 'danger.fg',
- annotationColor: 'fg.muted',
- hoverColor: 'actionListItem.danger.hoverText'
- }
- default:
- return {
- color: 'fg.default',
- iconColor: 'fg.muted',
- annotationColor: 'fg.muted',
- hoverColor: 'fg.default'
- }
- }
-}
-
-export type ItemProps = {
- /**
- * Primary content for an Item
- */
- children?: React.ReactNode
- /**
- * Callback that will trigger both on click selection and keyboard selection.
- */
- onSelect?: (event: React.MouseEvent | React.KeyboardEvent) => void
- /**
- * Is the `Item` is currently selected?
- */
- selected?: boolean
- /**
- * Style variations associated with various `Item` types.
- *
- * - `"default"` - An action `Item`.
- * - `"danger"` - A destructive action `Item`.
- */
- variant?: 'default' | 'danger'
- /**
- * Items that are disabled can not be clicked, selected, or navigated through.
- */
- disabled?: boolean
- /**
- * The ARIA role describing the function of `Item` component. `option` is a common value.
- */
- role?: AriaRole
- /**
- * id to attach to the root element of the Item
- */
- id?: string
- /**
- * Private API for use internally only. Used by LinkItem to wrap contents in an anchor
- */
- _PrivateItemWrapper?: React.FC
-} & SxProp
-
-const {Slots, Slot} = createSlots(['LeadingVisual', 'InlineDescription', 'BlockDescription', 'TrailingVisual'])
-export {Slot}
-export type ItemContext = Pick & {
- inlineDescriptionId: string
- blockDescriptionId: string
-}
-
-const LiBox = styled.li(sx)
-export const TEXT_ROW_HEIGHT = '20px' // custom value off the scale
-
-export const Item = React.forwardRef(
- (
- {
- variant = 'default',
- disabled = false,
- selected = undefined,
- onSelect,
- sx: sxProp = {},
- id,
- role,
- _PrivateItemWrapper,
- ...props
- },
- forwardedRef
- ): JSX.Element => {
- const {variant: listVariant, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext)
- const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
- const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext)
-
- let selectionVariant: ListProps['selectionVariant'] | GroupProps['selectionVariant']
- if (typeof groupSelectionVariant !== 'undefined') selectionVariant = groupSelectionVariant
- else selectionVariant = listSelectionVariant
-
- /** Infer item role based on the container */
- let itemRole: ItemProps['role']
- if (container === 'ActionMenu' || container === 'DropdownMenu') {
- if (selectionVariant === 'single') itemRole = 'menuitemradio'
- else if (selectionVariant === 'multiple') itemRole = 'menuitemcheckbox'
- else itemRole = 'menuitem'
- }
-
- const {theme} = useTheme()
-
- const styles = {
- display: 'flex',
- paddingX: 2,
- fontSize: 1,
- paddingY: '6px', // custom value off the scale
- lineHeight: TEXT_ROW_HEIGHT,
- minHeight: 5,
- marginX: listVariant === 'inset' ? 2 : 0,
- borderRadius: listVariant === 'inset' ? 2 : 0,
- transition: 'background 33.333ms linear',
- color: getVariantStyles(variant, disabled).color,
- cursor: 'pointer',
- '&[aria-disabled]': {cursor: 'not-allowed'},
-
- '@media (hover: hover) and (pointer: fine)': {
- ':hover:not([aria-disabled])': {
- backgroundColor: `actionListItem.${variant}.hoverBg`,
- color: getVariantStyles(variant, disabled).hoverColor
- },
- ':focus:not([data-focus-visible-added])': {
- backgroundColor: `actionListItem.${variant}.selectedBg`,
- color: getVariantStyles(variant, disabled).hoverColor,
- outline: 'none'
- },
- '&[data-focus-visible-added]': {
- // we don't use :focus-visible because not all browsers (safari) have it yet
- outline: 'none',
- border: `2 solid`,
- boxShadow: `0 0 0 2px ${theme?.colors.accent.emphasis}`
- },
- ':active:not([aria-disabled])': {
- backgroundColor: `actionListItem.${variant}.activeBg`,
- color: getVariantStyles(variant, disabled).hoverColor
- }
- },
-
- /** Divider styles */
- '[data-component="ActionList.Item--DividerContainer"]': {
- position: 'relative'
- },
- '[data-component="ActionList.Item--DividerContainer"]::before': {
- content: '" "',
- display: 'block',
- position: 'absolute',
- width: '100%',
- top: '-7px',
- border: '0 solid',
- borderTopWidth: showDividers ? `1px` : '0',
- borderColor: 'var(--divider-color, transparent)'
- },
- // show between 2 items
- ':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider},
- // hide divider after dividers & group header, with higher importance!
- '[data-component="ActionList.Divider"] + &': {'--divider-color': 'transparent !important'},
- // hide border on current and previous item
- '&:hover:not([aria-disabled]), &:focus:not([aria-disabled]), &[data-focus-visible-added]:not([aria-disabled])': {
- '--divider-color': 'transparent'
- },
- '&:hover:not([aria-disabled]) + &, &:focus:not([aria-disabled]) + &, &[data-focus-visible-added] + li': {
- '--divider-color': 'transparent'
- }
- }
-
- const clickHandler = React.useCallback(
- event => {
- if (disabled) return
- if (!event.defaultPrevented) {
- if (typeof onSelect === 'function') onSelect(event)
- // if this Item is inside a Menu, close the Menu
- if (typeof afterSelect === 'function') afterSelect()
- }
- },
- [onSelect, disabled, afterSelect]
- )
-
- const keyPressHandler = React.useCallback(
- event => {
- if (disabled) return
- if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
- if (typeof onSelect === 'function') onSelect(event)
- // if this Item is inside a Menu, close the Menu
- if (typeof afterSelect === 'function') afterSelect()
- }
- },
- [onSelect, disabled, afterSelect]
- )
-
- // use props.id if provided, otherwise generate one.
- const labelId = useSSRSafeId(id)
- const inlineDescriptionId = useSSRSafeId(id && `${id}--inline-description`)
- const blockDescriptionId = useSSRSafeId(id && `${id}--block-description`)
-
- const ItemWrapper = _PrivateItemWrapper || React.Fragment
-
- return (
-
- {slots => (
-
-
-
- {slots.LeadingVisual}
-
-
-
-
- {props.children}
-
- {slots.InlineDescription}
-
- {slots.TrailingVisual}
-
- {slots.BlockDescription}
-
-
-
- )}
-
- )
- }
-) as PolymorphicForwardRefComponent<'li', ItemProps>
-
-Item.displayName = 'ActionList.Item'
-
-const ConditionalBox: React.FC<{if: boolean} & BoxProps> = props => {
- const {if: condition, ...rest} = props
-
- if (condition) return {props.children}
- else return <>{props.children}>
-}
diff --git a/src/ActionList2/List.tsx b/src/ActionList2/List.tsx
deleted file mode 100644
index a3c060a532a..00000000000
--- a/src/ActionList2/List.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React from 'react'
-import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
-import styled from 'styled-components'
-import sx, {SxProp, merge} from '../sx'
-import {AriaRole} from '../utils/types'
-import {ActionListContainerContext} from './ActionListContainerContext'
-
-export type ListProps = {
- /**
- * `inset` children are offset (vertically and horizontally) from `List`’s edges, `full` children are flush (vertically and horizontally) with `List` edges
- */
- variant?: 'inset' | 'full'
- /**
- * Whether multiple Items or a single Item can be selected.
- */
- selectionVariant?: 'single' | 'multiple'
- /**
- * Display a divider above each `Item` in this `List` when it does not follow a `Header` or `Divider`.
- */
- showDividers?: boolean
- /**
- * The ARIA role describing the function of `List` component. `listbox` or `menu` are a common values.
- */
- role?: AriaRole
-} & SxProp
-
-type ContextProps = Pick
-export const ListContext = React.createContext({})
-
-const ListBox = styled.ul(sx)
-
-export const List = React.forwardRef(
- (
- {variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = {}, ...props},
- forwardedRef
- ): JSX.Element => {
- const styles = {
- margin: 0,
- paddingInlineStart: 0, // reset ul styles
- paddingY: variant === 'inset' ? 2 : 0
- }
-
- /** if list is inside a Menu, it will get a role from the Menu */
- const {
- listRole,
- listLabelledBy,
- selectionVariant: containerSelectionVariant // TODO: Remove after DropdownMenu2 deprecation
- } = React.useContext(ActionListContainerContext)
-
- return (
-
-
- {props.children}
-
-
- )
- }
-) as PolymorphicForwardRefComponent<'ul', ListProps>
-
-List.displayName = 'ActionList'
diff --git a/src/ActionList2/index.ts b/src/ActionList2/index.ts
deleted file mode 100644
index f27a3b53d50..00000000000
--- a/src/ActionList2/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import {List} from './List'
-import {Group} from './Group'
-import {Item} from './Item'
-import {LinkItem} from './LinkItem'
-import {Divider} from './Divider'
-import {Description} from './Description'
-import {LeadingVisual, TrailingVisual} from './Visuals'
-
-export type {ListProps as ActionListProps} from './List'
-export type {GroupProps as ActionListGroupProps} from './Group'
-export type {ItemProps as ActionListItemProps} from './Item'
-export type {DescriptionProps as ActionListDescriptionProps} from './Description'
-export type {
- LeadingVisualProps as ActionListLeadingVisualProps,
- TrailingVisualProps as ActionListTrailingVisualProps
-} from './Visuals'
-
-/**
- * Collection of list-related components.
- */
-export const ActionList = Object.assign(List, {
- /** Collects related `Items` in an `ActionList`. */
- Group,
-
- /** An actionable or selectable `Item` */
- Item,
-
- /** A `Item` that renders a full-size anchor inside ListItem */
- LinkItem,
-
- /** Visually separates `Item`s or `Group`s in an `ActionList`. */
- Divider,
-
- /** Secondary text which provides additional information about an `Item`. */
- Description,
-
- /** Icon (or similar) positioned before `Item` text. */
- LeadingVisual,
-
- /** Icon (or similar) positioned after `Item` text. */
- TrailingVisual
-})
diff --git a/src/ActionMenu.tsx b/src/ActionMenu.tsx
index dc080001083..0107dd178ac 100644
--- a/src/ActionMenu.tsx
+++ b/src/ActionMenu.tsx
@@ -1,106 +1,131 @@
-import {GroupedListProps, List, ListPropsBase} from './ActionList/List'
-import {Item, ItemProps} from './ActionList/Item'
-import {Divider} from './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
+ onOpenChange?: (s: boolean) => void
+} & Pick
- /**
- * Props to be spread on the internal `Overlay` component.
- */
- overlayProps?: Partial
-}
-
-export type ActionMenuProps = ActionMenuBaseProps & AnchoredOverlayWrapperAnchorProps
-
-const ActionMenuItem = (props: ItemProps) =>
-
-ActionMenuItem.displayName = 'ActionMenu.Item'
-
-const ActionMenuBase = ({
- anchorContent,
- renderAnchor = (props: T) => ,
+const Menu: React.FC = ({
anchorRef: externalAnchorRef,
- onAction,
open,
- setOpen,
- overlayProps,
- items,
- ...listProps
-}: ActionMenuProps): JSX.Element => {
- const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, setOpen, false)
+ onOpenChange,
+ children
+}: ActionMenuProps) => {
+ const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
+ const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
+ const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
+
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
- const onOpen = useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
- const onClose = useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
+ const anchorId = useSSRSafeId()
+ let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null
- const renderMenuAnchor = useMemo(() => {
- if (renderAnchor === null) {
+ // 🚨 Hack for good API!
+ // we strip out Anchor from children and pass it to AnchoredOverlay to render
+ // with additional props for accessibility
+ const contents = React.Children.map(children, child => {
+ if (child.type === MenuButton || child.type === Anchor) {
+ renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
return null
}
- return >(props: T) => {
- return renderAnchor({
- 'aria-label': 'menu',
- children: anchorContent,
- ...props
- })
- }
- }, [anchorContent, renderAnchor])
+ return child
+ })
- const itemsToRender = useMemo(() => {
- return items.map(item => {
- return {
- ...item,
- role: 'menuitem',
- onAction: (props, event) => {
- const actionCallback = item.onAction ?? onAction
- actionCallback?.(props as ItemProps, event)
- if (!event.defaultPrevented) {
- onClose()
- }
- }
- } as ItemProps
- })
- }, [items, onAction, onClose])
+ return (
+
+ {contents}
+
+ )
+}
+
+export type ActionMenuAnchorProps = {children: React.ReactElement}
+const Anchor = React.forwardRef(
+ ({children, ...anchorProps}, anchorRef) => {
+ return React.cloneElement(children, {...anchorProps, ref: anchorRef})
+ }
+)
+
+/** this component is syntactical sugar 🍭 */
+export type ActionMenuButtonProps = ButtonProps
+const MenuButton = React.forwardRef((props, anchorRef) => {
+ return (
+
+
+
+ )
+})
+
+type MenuOverlayProps = Partial & {
+ /**
+ * Recommended: `ActionList`
+ */
+ children: React.ReactElement[] | React.ReactElement
+}
+const Overlay: React.FC = ({children, ...overlayProps}) => {
+ // we typecast anchorRef as required instead of optional
+ // because we know that we're setting it in context in Menu
+ const {anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React.useContext(MenuContext) as MandateProps<
+ MenuContextProps,
+ 'anchorRef'
+ >
+
+ const containerRef = React.createRef()
+ const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef)
+ useTypeaheadFocus(open, containerRef)
return (
-
+
)
}
-ActionMenuBase.displayName = 'ActionMenu'
-
-export const ActionMenu = Object.assign(ActionMenuBase, {Divider, Item: ActionMenuItem})
+Menu.displayName = 'ActionMenu'
+export const ActionMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider})
diff --git a/src/ActionMenu2.tsx b/src/ActionMenu2.tsx
deleted file mode 100644
index 97622d2a101..00000000000
--- a/src/ActionMenu2.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-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, useProvidedStateOrCreate, useMenuInitialFocus, useTypeaheadFocus} from './hooks'
-import {Divider} from './ActionList2/Divider'
-import {ActionListContainerContext} from './ActionList2/ActionListContainerContext'
-import {Button, ButtonProps} from './Button2'
-import {MandateProps} from './utils/types'
-
-type MenuContextProps = Pick<
- AnchoredOverlayProps,
- 'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'onClose' | 'anchorId'
->
-const MenuContext = React.createContext({renderAnchor: null, open: false})
-
-export type ActionMenuProps = {
- /**
- * Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay`
- */
- children: React.ReactElement[] | React.ReactElement
-
- /**
- * 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`.
- */
- onOpenChange?: (s: boolean) => void
-} & Pick
-
-const Menu: React.FC = ({
- anchorRef: externalAnchorRef,
- open,
- onOpenChange,
- children
-}: ActionMenuProps) => {
- const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
- const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
- const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
-
- const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
- const anchorId = useSSRSafeId()
- let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null
-
- // 🚨 Hack for good API!
- // we strip out Anchor from children and pass it to AnchoredOverlay to render
- // with additional props for accessibility
- const contents = React.Children.map(children, child => {
- if (child.type === MenuButton || child.type === Anchor) {
- renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
- return null
- }
- return child
- })
-
- return (
-
- {contents}
-
- )
-}
-
-export type ActionMenuAnchorProps = {children: React.ReactElement}
-const Anchor = React.forwardRef(
- ({children, ...anchorProps}, anchorRef) => {
- return React.cloneElement(children, {...anchorProps, ref: anchorRef})
- }
-)
-
-/** this component is syntactical sugar 🍭 */
-export type ActionMenuButtonProps = ButtonProps
-const MenuButton = React.forwardRef((props, anchorRef) => {
- return (
-
-
-
- )
-})
-
-type MenuOverlayProps = Partial & {
- /**
- * Recommended: `ActionList`
- */
- children: React.ReactElement[] | React.ReactElement
-}
-const Overlay: React.FC = ({children, ...overlayProps}) => {
- // we typecast anchorRef as required instead of optional
- // because we know that we're setting it in context in Menu
- const {anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React.useContext(MenuContext) as MandateProps<
- MenuContextProps,
- 'anchorRef'
- >
-
- const containerRef = React.createRef()
- const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef)
- useTypeaheadFocus(open, containerRef)
-
- return (
-
-
-
- )
-}
-
-Menu.displayName = 'ActionMenu'
-export const ActionMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider})
diff --git a/src/Autocomplete/AutocompleteMenu.tsx b/src/Autocomplete/AutocompleteMenu.tsx
index 4151132ef47..22e99c61652 100644
--- a/src/Autocomplete/AutocompleteMenu.tsx
+++ b/src/Autocomplete/AutocompleteMenu.tsx
@@ -1,7 +1,7 @@
import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'
import {scrollIntoView} from '@primer/behaviors'
import type {ScrollIntoViewOptions} from '@primer/behaviors'
-import {ActionList, ItemProps} from '../ActionList'
+import {ActionList, ItemProps} from '../deprecated/ActionList'
import {useFocusZone} from '../hooks/useFocusZone'
import {ComponentProps, MandateProps} from '../utils/types'
import {Box, Spinner, useSSRSafeId} from '../'
diff --git a/src/DropdownMenu/DropdownMenu.tsx b/src/DropdownMenu/DropdownMenu.tsx
index 37b2687449e..3e084002318 100644
--- a/src/DropdownMenu/DropdownMenu.tsx
+++ b/src/DropdownMenu/DropdownMenu.tsx
@@ -1,7 +1,7 @@
import React, {useCallback, useMemo} from 'react'
-import {List, GroupedListProps, ListPropsBase, ItemInput} from '../ActionList/List'
+import {List, GroupedListProps, ListPropsBase, ItemInput} from '../deprecated/ActionList/List'
import {DropdownButton, DropdownButtonProps} from './DropdownButton'
-import {ItemProps} from '../ActionList/Item'
+import {ItemProps} from '../deprecated/ActionList/Item'
import {AnchoredOverlay} from '../AnchoredOverlay'
import {OverlayProps} from '../Overlay'
import {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay'
diff --git a/src/DropdownMenu2.tsx b/src/DropdownMenu2.tsx
index 1773eb8e85e..02697c66c66 100644
--- a/src/DropdownMenu2.tsx
+++ b/src/DropdownMenu2.tsx
@@ -5,8 +5,8 @@ import {Button, ButtonProps} from './Button2'
import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay'
import {OverlayProps} from './Overlay'
import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus, useTypeaheadFocus} from './hooks'
-import {Divider} from './ActionList2/Divider'
-import {ActionListContainerContext} from './ActionList2/ActionListContainerContext'
+import {Divider} from './ActionList/Divider'
+import {ActionListContainerContext} from './ActionList/ActionListContainerContext'
import {MandateProps} from './utils/types'
type MenuContextProps = Pick<
diff --git a/src/FilteredActionList/FilteredActionList.tsx b/src/FilteredActionList/FilteredActionList.tsx
index ebbb16c59d3..665cf6a9c12 100644
--- a/src/FilteredActionList/FilteredActionList.tsx
+++ b/src/FilteredActionList/FilteredActionList.tsx
@@ -1,9 +1,9 @@
import React, {KeyboardEventHandler, useCallback, useEffect, useRef} from 'react'
import {useSSRSafeId} from '@react-aria/ssr'
-import {GroupedListProps, ListPropsBase} from '../ActionList/List'
+import {GroupedListProps, ListPropsBase} from '../deprecated/ActionList/List'
import TextInput, {TextInputProps} from '../TextInput'
import Box from '../Box'
-import {ActionList} from '../ActionList'
+import {ActionList} from '../deprecated/ActionList'
import Spinner from '../Spinner'
import {useFocusZone} from '../hooks/useFocusZone'
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
diff --git a/src/SelectPanel/SelectPanel.tsx b/src/SelectPanel/SelectPanel.tsx
index 5d688a55ae1..be8477f1786 100644
--- a/src/SelectPanel/SelectPanel.tsx
+++ b/src/SelectPanel/SelectPanel.tsx
@@ -1,10 +1,10 @@
import React, {useCallback, useMemo} from 'react'
import {FilteredActionList, FilteredActionListProps} from '../FilteredActionList'
import {OverlayProps} from '../Overlay'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
import {FocusZoneHookSettings} from '../hooks/useFocusZone'
import {DropdownButton} from '../DropdownMenu'
-import {ItemProps} from '../ActionList'
+import {ItemProps} from '../deprecated/ActionList'
import {AnchoredOverlay, AnchoredOverlayProps} from '../AnchoredOverlay'
import {TextInputProps} from '../TextInput'
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
diff --git a/src/__tests__/ActionList.test.tsx b/src/__tests__/ActionList.test.tsx
index 0003873d445..8325fa7d17c 100644
--- a/src/__tests__/ActionList.test.tsx
+++ b/src/__tests__/ActionList.test.tsx
@@ -1,36 +1,63 @@
-import {cleanup, render as HTMLRender} from '@testing-library/react'
+import {cleanup, render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
import 'babel-polyfill'
import {axe, toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
import {ActionList} from '../ActionList'
-import {behavesAsComponent, checkExports} from '../utils/testing'
-import {BaseStyles, ThemeProvider} from '..'
+import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
+import {BaseStyles, ThemeProvider, SSRProvider} from '..'
+import '@testing-library/jest-dom'
expect.extend(toHaveNoViolations)
function SimpleActionList(): JSX.Element {
return (
-
-
-
+
+
+
+ New file
+
+ Copy link
+ Edit file
+ Delete file
+
+
+
)
}
+const projects = [
+ {name: 'Primer Backlog', scope: 'GitHub'},
+ {name: 'Primer React', scope: 'github/primer'},
+ {name: 'Disabled Project', scope: 'github/primer', disabled: true}
+]
+function SingleSelectListStory(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+
+ return (
+
+ {projects.map((project, index) => (
+ setSelectedIndex(index)}
+ disabled={project.disabled}
+ >
+ {project.name}
+
+ ))}
+
+ )
+}
+
describe('ActionList', () => {
behavesAsComponent({
Component: ActionList,
options: {skipAs: true, skipSx: true},
- toRender: () =>
+ toRender: () =>
})
checkExports('ActionList', {
@@ -44,10 +71,88 @@ describe('ActionList', () => {
expect(results).toHaveNoViolations()
cleanup()
})
-})
-describe('ActionList.Item', () => {
- behavesAsComponent({
- Component: ActionList.Item
+ it('should fire onSelect on click and keypress', async () => {
+ const component = HTMLRender( )
+ const options = await waitFor(() => component.getAllByRole('option'))
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'true')
+ expect(options[1]).toHaveAttribute('aria-selected', 'false')
+
+ fireEvent.click(options[1])
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'false')
+ expect(options[1]).toHaveAttribute('aria-selected', 'true')
+
+ // We pass keycode here to navigate a implementation detail in react-testing-library
+ // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
+ fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13})
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'true')
+ expect(options[1]).toHaveAttribute('aria-selected', 'false')
+
+ fireEvent.keyPress(options[1], {key: ' ', charCode: 32})
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'false')
+ expect(options[1]).toHaveAttribute('aria-selected', 'true')
+
+ cleanup()
+ })
+
+ it('should skip onSelect on disabled items', async () => {
+ const component = HTMLRender( )
+ const options = await waitFor(() => component.getAllByRole('option'))
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'true')
+ expect(options[2]).toHaveAttribute('aria-selected', 'false')
+
+ fireEvent.click(options[2])
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'true')
+ expect(options[2]).toHaveAttribute('aria-selected', 'false')
+
+ fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13})
+
+ expect(options[0]).toHaveAttribute('aria-selected', 'true')
+ expect(options[2]).toHaveAttribute('aria-selected', 'false')
+
+ cleanup()
+ })
+
+ it('should throw when selected is provided without a selectionVariant on parent', async () => {
+ // we expect console.error to be called, so we suppress that in the test
+ const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
+
+ expect(() => {
+ HTMLRender(
+
+
+ Primer React
+
+
+ )
+ }).toThrow('For Item to be selected, ActionList or ActionList.Group needs to have a selectionVariant defined')
+
+ cleanup()
+ mockError.mockRestore()
})
+
+ it('should not crash when clicking an item without an onSelect', async () => {
+ const component = HTMLRender(
+
+ Primer React
+
+ )
+ const option = await waitFor(() => component.getByRole('option'))
+ expect(option).toBeInTheDocument()
+
+ fireEvent.click(option)
+ fireEvent.keyPress(option, {key: 'Enter', charCode: 13})
+ expect(option).toBeInTheDocument()
+
+ cleanup()
+ })
+
+ checkStoriesForAxeViolations('ActionList/fixtures')
+ checkStoriesForAxeViolations('ActionList/examples')
})
diff --git a/src/__tests__/ActionList2.test.tsx b/src/__tests__/ActionList2.test.tsx
deleted file mode 100644
index d266852fb20..00000000000
--- a/src/__tests__/ActionList2.test.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import {cleanup, render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
-import 'babel-polyfill'
-import {axe, toHaveNoViolations} from 'jest-axe'
-import React from 'react'
-import theme from '../theme'
-import {ActionList} from '../ActionList2'
-import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
-import {BaseStyles, ThemeProvider, SSRProvider} from '..'
-import '@testing-library/jest-dom'
-expect.extend(toHaveNoViolations)
-
-function SimpleActionList(): JSX.Element {
- return (
-
-
-
-
- New file
-
- Copy link
- Edit file
- Delete file
-
-
-
-
- )
-}
-
-const projects = [
- {name: 'Primer Backlog', scope: 'GitHub'},
- {name: 'Primer React', scope: 'github/primer'},
- {name: 'Disabled Project', scope: 'github/primer', disabled: true}
-]
-function SingleSelectListStory(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(0)
-
- return (
-
- {projects.map((project, index) => (
- setSelectedIndex(index)}
- disabled={project.disabled}
- >
- {project.name}
-
- ))}
-
- )
-}
-
-describe('ActionList', () => {
- behavesAsComponent({
- Component: ActionList,
- options: {skipAs: true, skipSx: true},
- toRender: () =>
- })
-
- checkExports('ActionList2', {
- default: undefined,
- ActionList
- })
-
- it('should have no axe violations', async () => {
- const {container} = HTMLRender( )
- const results = await axe(container)
- expect(results).toHaveNoViolations()
- cleanup()
- })
-
- it('should fire onSelect on click and keypress', async () => {
- const component = HTMLRender( )
- const options = await waitFor(() => component.getAllByRole('option'))
-
- expect(options[0]).toHaveAttribute('aria-selected', 'true')
- expect(options[1]).toHaveAttribute('aria-selected', 'false')
-
- fireEvent.click(options[1])
-
- expect(options[0]).toHaveAttribute('aria-selected', 'false')
- expect(options[1]).toHaveAttribute('aria-selected', 'true')
-
- // We pass keycode here to navigate a implementation detail in react-testing-library
- // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
- fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13})
-
- expect(options[0]).toHaveAttribute('aria-selected', 'true')
- expect(options[1]).toHaveAttribute('aria-selected', 'false')
-
- fireEvent.keyPress(options[1], {key: ' ', charCode: 32})
-
- expect(options[0]).toHaveAttribute('aria-selected', 'false')
- expect(options[1]).toHaveAttribute('aria-selected', 'true')
-
- cleanup()
- })
-
- it('should skip onSelect on disabled items', async () => {
- const component = HTMLRender( )
- const options = await waitFor(() => component.getAllByRole('option'))
-
- expect(options[0]).toHaveAttribute('aria-selected', 'true')
- expect(options[2]).toHaveAttribute('aria-selected', 'false')
-
- fireEvent.click(options[2])
-
- expect(options[0]).toHaveAttribute('aria-selected', 'true')
- expect(options[2]).toHaveAttribute('aria-selected', 'false')
-
- fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13})
-
- expect(options[0]).toHaveAttribute('aria-selected', 'true')
- expect(options[2]).toHaveAttribute('aria-selected', 'false')
-
- cleanup()
- })
-
- it('should throw when selected is provided without a selectionVariant on parent', async () => {
- // we expect console.error to be called, so we suppress that in the test
- const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
-
- expect(() => {
- HTMLRender(
-
-
- Primer React
-
-
- )
- }).toThrow('For Item to be selected, ActionList or ActionList.Group needs to have a selectionVariant defined')
-
- cleanup()
- mockError.mockRestore()
- })
-
- it('should not crash when clicking an item without an onSelect', async () => {
- const component = HTMLRender(
-
- Primer React
-
- )
- const option = await waitFor(() => component.getByRole('option'))
- expect(option).toBeInTheDocument()
-
- fireEvent.click(option)
- fireEvent.keyPress(option, {key: 'Enter', charCode: 13})
- expect(option).toBeInTheDocument()
-
- cleanup()
- })
-
- checkStoriesForAxeViolations('ActionList2/fixtures')
- checkStoriesForAxeViolations('ActionList2/examples')
-})
diff --git a/src/__tests__/ActionMenu.test.tsx b/src/__tests__/ActionMenu.test.tsx
index 504693992eb..7aa30c560ec 100644
--- a/src/__tests__/ActionMenu.test.tsx
+++ b/src/__tests__/ActionMenu.test.tsx
@@ -1,31 +1,33 @@
-import {cleanup, render as HTMLRender, act, fireEvent} from '@testing-library/react'
+import {cleanup, render as HTMLRender, waitFor, 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 '../ActionMenu'
-import {behavesAsComponent, checkExports} from '../utils/testing'
-import {BaseStyles, SSRProvider, ThemeProvider} from '..'
-import {ItemProps} from '../ActionList/Item'
+import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider} from '..'
+import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
+import {SingleSelection, MixedSelection} from '../stories/ActionMenu/examples.stories'
+import '@testing-library/jest-dom'
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 {
+function Example(): JSX.Element {
return (
- X
-
-
+
+ Toggle Menu
+
+
+ New file
+
+ Copy link
+ Edit file
+ event.preventDefault()}>
+ Delete file
+
+
+
+
@@ -33,18 +35,10 @@ function SimpleActionMenu(): JSX.Element {
}
describe('ActionMenu', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
behavesAsComponent({
- Component: ActionMenu,
+ Component: ActionList,
options: {skipAs: true, skipSx: true},
- toRender: () => (
-
-
-
- )
+ toRender: () =>
})
checkExports('ActionMenu', {
@@ -52,85 +46,103 @@ describe('ActionMenu', () => {
ActionMenu
})
- it('should have no axe violations', async () => {
- const {container} = HTMLRender( )
- const results = await axe(container)
- expect(results).toHaveNoViolations()
+ it('should open Menu on MenuButton click', async () => {
+ const component = HTMLRender( )
+ const button = component.getByText('Toggle Menu')
+ fireEvent.click(button)
+ expect(component.getByRole('menu')).toBeInTheDocument()
+ cleanup()
+ })
+
+ it('should open Menu on MenuButton keypress', async () => {
+ const component = HTMLRender( )
+ const button = component.getByText('Toggle Menu')
+
+ // We pass keycode here to navigate a implementation detail in react-testing-library
+ // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
+ fireEvent.keyDown(button, {key: 'Enter', charCode: 13})
+ expect(component.getByRole('menu')).toBeInTheDocument()
+ cleanup()
+ })
+
+ it('should close Menu on selecting an action with click', async () => {
+ const component = HTMLRender( )
+ const button = component.getByText('Toggle Menu')
+
+ fireEvent.click(button)
+ const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
+ fireEvent.click(menuItems[0])
+ expect(component.queryByRole('menu')).toBeNull()
+
+ cleanup()
+ })
+
+ it('should close Menu on selecting an action with Enter', async () => {
+ const component = HTMLRender( )
+ const button = component.getByText('Toggle Menu')
+
+ fireEvent.click(button)
+ const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
+ fireEvent.keyPress(menuItems[0], {key: 'Enter', charCode: 13})
+ expect(component.queryByRole('menu')).toBeNull()
+
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 not close Menu if event is prevented', async () => {
+ const component = HTMLRender( )
+ const button = component.getByText('Toggle Menu')
+
+ fireEvent.click(button)
+ const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
+ fireEvent.click(menuItems[3])
+ // menu should still be open
+ expect(component.getByRole('menu')).toBeInTheDocument()
+
+ cleanup()
})
- 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 be able to select an Item with selectionVariant', async () => {
+ const component = HTMLRender(
+
+
+
+ )
+ const button = component.getByLabelText('Select field type')
+ fireEvent.click(button)
+
+ // select first item by role, that would close the menu
+ fireEvent.click(component.getAllByRole('menuitemradio')[0])
+ expect(component.queryByRole('menu')).not.toBeInTheDocument()
+
+ // open menu again and check if the first option is checked
+ fireEvent.click(button)
+ expect(component.getAllByRole('menuitemradio')[0]).toHaveAttribute('aria-checked', 'true')
+ cleanup()
})
- 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 assign the right roles with groups & mixed selectionVariant', async () => {
+ const component = HTMLRender(
+
+
+
+ )
+ const button = component.getByLabelText('Select field type to group by')
+ fireEvent.click(button)
+
+ expect(component.getByLabelText('Status')).toHaveAttribute('role', 'menuitemradio')
+ expect(component.getByLabelText('Clear Group by')).toHaveAttribute('role', 'menuitem')
+
+ cleanup()
})
- 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)
+ it('should have no axe violations', async () => {
+ const {container} = HTMLRender( )
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
+ cleanup()
})
+
+ checkStoriesForAxeViolations('ActionMenu/fixtures')
+ checkStoriesForAxeViolations('ActionMenu/examples')
})
diff --git a/src/__tests__/ActionMenu2.test.tsx b/src/__tests__/ActionMenu2.test.tsx
deleted file mode 100644
index 20cfb9fcda2..00000000000
--- a/src/__tests__/ActionMenu2.test.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import {cleanup, render as HTMLRender, waitFor, 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 '../ActionMenu2'
-import {ActionList} from '../ActionList2'
-import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
-import {BaseStyles, ThemeProvider, SSRProvider} from '..'
-import {SingleSelection, MixedSelection} from '../../src/stories/ActionMenu2/examples.stories'
-import '@testing-library/jest-dom'
-expect.extend(toHaveNoViolations)
-
-function Example(): JSX.Element {
- return (
-
-
-
-
- Toggle Menu
-
-
- New file
-
- Copy link
- Edit file
- event.preventDefault()}>
- Delete file
-
-
-
-
-
-
-
- )
-}
-
-describe('ActionMenu', () => {
- behavesAsComponent({
- Component: ActionList,
- options: {skipAs: true, skipSx: true},
- toRender: () =>
- })
-
- checkExports('ActionMenu2', {
- default: undefined,
- ActionMenu
- })
-
- it('should open Menu on MenuButton click', async () => {
- const component = HTMLRender( )
- const button = component.getByText('Toggle Menu')
- fireEvent.click(button)
- expect(component.getByRole('menu')).toBeInTheDocument()
- cleanup()
- })
-
- it('should open Menu on MenuButton keypress', async () => {
- const component = HTMLRender( )
- const button = component.getByText('Toggle Menu')
-
- // We pass keycode here to navigate a implementation detail in react-testing-library
- // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
- fireEvent.keyDown(button, {key: 'Enter', charCode: 13})
- expect(component.getByRole('menu')).toBeInTheDocument()
- cleanup()
- })
-
- it('should close Menu on selecting an action with click', async () => {
- const component = HTMLRender( )
- const button = component.getByText('Toggle Menu')
-
- fireEvent.click(button)
- const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
- fireEvent.click(menuItems[0])
- expect(component.queryByRole('menu')).toBeNull()
-
- cleanup()
- })
-
- it('should close Menu on selecting an action with Enter', async () => {
- const component = HTMLRender( )
- const button = component.getByText('Toggle Menu')
-
- fireEvent.click(button)
- const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
- fireEvent.keyPress(menuItems[0], {key: 'Enter', charCode: 13})
- expect(component.queryByRole('menu')).toBeNull()
-
- cleanup()
- })
-
- it('should not close Menu if event is prevented', async () => {
- const component = HTMLRender( )
- const button = component.getByText('Toggle Menu')
-
- fireEvent.click(button)
- const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
- fireEvent.click(menuItems[3])
- // menu should still be open
- expect(component.getByRole('menu')).toBeInTheDocument()
-
- cleanup()
- })
-
- it('should be able to select an Item with selectionVariant', async () => {
- const component = HTMLRender(
-
-
-
- )
- const button = component.getByLabelText('Select field type')
- fireEvent.click(button)
-
- // select first item by role, that would close the menu
- fireEvent.click(component.getAllByRole('menuitemradio')[0])
- expect(component.queryByRole('menu')).not.toBeInTheDocument()
-
- // open menu again and check if the first option is checked
- fireEvent.click(button)
- expect(component.getAllByRole('menuitemradio')[0]).toHaveAttribute('aria-checked', 'true')
- cleanup()
- })
-
- it('should assign the right roles with groups & mixed selectionVariant', async () => {
- const component = HTMLRender(
-
-
-
- )
- const button = component.getByLabelText('Select field type to group by')
- fireEvent.click(button)
-
- expect(component.getByLabelText('Status')).toHaveAttribute('role', 'menuitemradio')
- expect(component.getByLabelText('Clear Group by')).toHaveAttribute('role', 'menuitem')
-
- cleanup()
- })
-
- it('should have no axe violations', async () => {
- const {container} = HTMLRender( )
- const results = await axe(container)
- expect(results).toHaveNoViolations()
- cleanup()
- })
-
- checkStoriesForAxeViolations('ActionMenu2/fixtures')
- checkStoriesForAxeViolations('ActionMenu2/examples')
-})
diff --git a/src/__tests__/Autocomplete.test.tsx b/src/__tests__/Autocomplete.test.tsx
index 8c2b89d046c..3e17f37dcb0 100644
--- a/src/__tests__/Autocomplete.test.tsx
+++ b/src/__tests__/Autocomplete.test.tsx
@@ -10,7 +10,7 @@ import BaseStyles from '../BaseStyles'
import {ThemeProvider} from '../ThemeProvider'
import userEvent from '@testing-library/user-event'
import {AutocompleteMenuInternalProps} from '../Autocomplete/AutocompleteMenu'
-import {ItemProps} from '../ActionList'
+import {ItemProps} from '../deprecated/ActionList'
import {MandateProps} from '../utils/types'
expect.extend(toHaveNoViolations)
diff --git a/src/__tests__/ConfirmationDialog.test.tsx b/src/__tests__/ConfirmationDialog.test.tsx
index ef91e86786b..7e7cff17bb6 100644
--- a/src/__tests__/ConfirmationDialog.test.tsx
+++ b/src/__tests__/ConfirmationDialog.test.tsx
@@ -3,7 +3,7 @@ import {render as HTMLRender, cleanup, act, fireEvent} from '@testing-library/re
import {axe, toHaveNoViolations} from 'jest-axe'
import React, {useCallback, useRef, useState} from 'react'
-import {ActionMenu} from '../ActionMenu'
+import {ActionMenu} from '../deprecated/ActionMenu'
import BaseStyles from '../BaseStyles'
import Box from '../Box'
import Button from '../Button/Button'
diff --git a/src/__tests__/DropdownMenu.test.tsx b/src/__tests__/DropdownMenu.test.tsx
index aa93d7f5fb7..3a8665ccc55 100644
--- a/src/__tests__/DropdownMenu.test.tsx
+++ b/src/__tests__/DropdownMenu.test.tsx
@@ -6,7 +6,7 @@ import theme from '../theme'
import {DropdownMenu, DropdownButton} from '../DropdownMenu'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {BaseStyles, ThemeProvider, SSRProvider} from '..'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
expect.extend(toHaveNoViolations)
diff --git a/src/__tests__/DropdownMenu2.test.tsx b/src/__tests__/DropdownMenu2.test.tsx
index 04917c6e3a4..2e91a21b0c0 100644
--- a/src/__tests__/DropdownMenu2.test.tsx
+++ b/src/__tests__/DropdownMenu2.test.tsx
@@ -4,7 +4,7 @@ import {toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
import {DropdownMenu} from '../DropdownMenu2'
-import {ActionList} from '../ActionList2'
+import {ActionList} from '../ActionList'
import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
import {BaseStyles, ThemeProvider, SSRProvider} from '..'
import '@testing-library/jest-dom'
diff --git a/src/__tests__/SelectPanel.test.tsx b/src/__tests__/SelectPanel.test.tsx
index aefd92e25b8..b88cb8f3149 100644
--- a/src/__tests__/SelectPanel.test.tsx
+++ b/src/__tests__/SelectPanel.test.tsx
@@ -6,7 +6,7 @@ import theme from '../theme'
import {SelectPanel} from '../SelectPanel'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {BaseStyles, SSRProvider, ThemeProvider} from '..'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
expect.extend(toHaveNoViolations)
diff --git a/src/__tests__/__snapshots__/ActionList.test.tsx.snap b/src/__tests__/__snapshots__/ActionList.test.tsx.snap
index 852201546b0..5bbbad35875 100644
--- a/src/__tests__/__snapshots__/ActionList.test.tsx.snap
+++ b/src/__tests__/__snapshots__/ActionList.test.tsx.snap
@@ -1,223 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActionList renders consistently 1`] = `
-.c1 {
- margin-top: 8px;
- margin-bottom: 8px;
-}
-
.c0 {
- font-size: 14px;
- line-height: 20px;
-}
-
-.c0[data-has-active-descendant],
-.c0:focus-within {
- --item-hover-bg-override: none;
- --item-hover-divider-border-color-override: hsla(210,18%,87%,1);
+ margin: 0;
+ padding-inline-start: 0;
+ padding-top: 8px;
+ padding-bottom: 8px;
}
-
-`;
-
-exports[`ActionList.Item renders consistently 1`] = `
-.c3 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- min-width: 0;
- position: relative;
- -webkit-box-flex: 1;
- -webkit-flex-grow: 1;
- -ms-flex-positive: 1;
- flex-grow: 1;
-}
-
-.c4 {
- -webkit-align-items: baseline;
- -webkit-box-align: baseline;
- -ms-flex-align: baseline;
- align-items: baseline;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- min-width: 0;
- -webkit-flex-direction: var(--main-content-flex-direction);
- -ms-flex-direction: var(--main-content-flex-direction);
- flex-direction: var(--main-content-flex-direction);
- -webkit-box-flex: 1;
- -webkit-flex-grow: 1;
- -ms-flex-positive: 1;
- flex-grow: 1;
-}
-
-.c5:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
- margin-top: 0;
-}
-
-.c5:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
- content: ' ';
- display: block;
- position: absolute;
- width: 100%;
- top: -7px;
- border: 0 solid hsla(210,18%,87%,1);
- border-top-width: 0;
-}
-
-.c5:hover .c2::before,
-.c5:hover + * .c2::before {
- border-color: var(--item-hover-divider-border-color-override,transparent) !important;
-}
-
-.c5:focus .c2::before,
-.c5:focus + * .c2::before,
-.c5[data-is-active-descendant] .c2::before,
-[data-is-active-descendant] + .c5 .c2::before {
- border-color: transparent !important;
-}
-
-.c6:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
- margin-top: 0;
-}
-
-.c6:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
- content: ' ';
- display: block;
- position: absolute;
- width: 100%;
- top: -7px;
- border: 0 solid hsla(210,18%,87%,1);
- border-top-width: 0;
-}
-
-.c6:hover .c2::before,
-.c6:hover + * .c2::before {
- border-color: var(--item-hover-divider-border-color-override,transparent) !important;
-}
-
-.c6:focus .c2::before,
-.c6:focus + * .c2::before,
-.c6[data-is-active-descendant] .c2::before,
-[data-is-active-descendant] + .c6 .c2::before {
- border-color: transparent !important;
-}
-
-.c7:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
- margin-top: 0;
-}
-
-.c7:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
- content: ' ';
- display: block;
- position: absolute;
- width: 100%;
- top: -7px;
- border: 0 solid hsla(210,18%,87%,1);
- border-top-width: 0;
-}
-
-.c7:hover .c2::before,
-.c7:hover + * .c2::before {
- border-color: var(--item-hover-divider-border-color-override,transparent) !important;
-}
-
-.c7:focus .c2::before,
-.c7:focus + * .c2::before,
-.c7[data-is-active-descendant] .c2::before,
-[data-is-active-descendant] + .c7 .c2::before {
- border-color: transparent !important;
-}
-
-.c1 {
- padding: 6px 8px;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- border-radius: 6px;
- color: #24292f;
- -webkit-transition: background 33.333ms linear;
- transition: background 33.333ms linear;
- -webkit-text-decoration: none;
- text-decoration: none;
-}
-
-.c1:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
- margin-top: 0;
-}
-
-.c1:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
- content: ' ';
- display: block;
- position: absolute;
- width: 100%;
- top: -7px;
- border: 0 solid hsla(210,18%,87%,1);
- border-top-width: 0;
-}
-
-.c1:hover .c2::before,
-.c1:hover + * .c2::before {
- border-color: var(--item-hover-divider-border-color-override,transparent) !important;
-}
-
-.c1:focus .c2::before,
-.c1:focus + * .c2::before,
-.c1[data-is-active-descendant] .c2::before,
-[data-is-active-descendant] + .c1 .c2::before {
- border-color: transparent !important;
-}
-
-.c1[data-is-active-descendant='activated-directly'] {
- background: rgba(208,215,222,0.48);
-}
-
-.c1[data-is-active-descendant='activated-indirectly'] {
- background: rgba(208,215,222,0.32);
-}
-
-.c1:focus {
- background: rgba(208,215,222,0.48);
- outline: none;
-}
-
-.c1:active {
- background: rgba(208,215,222,0.48);
-}
-
-@media (hover:hover) and (pointer:fine) {
- .c1:hover {
- background: var( --item-hover-bg-override,rgba(208,215,222,0.32) );
- cursor: pointer;
- }
-}
-
-
+/>
`;
diff --git a/src/__tests__/__snapshots__/ActionList2.test.tsx.snap b/src/__tests__/__snapshots__/ActionList2.test.tsx.snap
deleted file mode 100644
index 5bbbad35875..00000000000
--- a/src/__tests__/__snapshots__/ActionList2.test.tsx.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ActionList renders consistently 1`] = `
-.c0 {
- margin: 0;
- padding-inline-start: 0;
- padding-top: 8px;
- padding-bottom: 8px;
-}
-
-
-`;
diff --git a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap
index 7581ef44ec9..ccf46de1387 100644
--- a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap
+++ b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap
@@ -2,79 +2,143 @@
exports[`ActionMenu renders consistently 1`] = `
.c0 {
- position: relative;
+ 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;
- padding: 6px 16px;
+ 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;
- border-radius: 6px;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: 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;
- 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 {
+.c1:focus {
outline: none;
}
-.c0:disabled {
+.c1:disabled {
cursor: default;
+ color: #8c959f;
+ background-color: btn.disabledBg;
}
-.c0:disabled svg {
+.c1:disabled svg {
opacity: 0.6;
}
-.c0:hover {
+.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;
- border-color: rgba(27,31,36,0.15);
}
-.c0:focus {
- border-color: rgba(27,31,36,0.15);
+.c1:focus:not([disabled]) {
box-shadow: 0 0 0 3px rgba(9,105,218,0.3);
}
-.c0:active {
+.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);
}
-.c0:disabled {
- color: #8c959f;
- background-color: #f6f8fa;
- border-color: rgba(27,31,36,0.15);
-}
-
-
+ color="fg.default"
+ data-portal-root={true}
+ fontFamily="normal"
+>
+
+
+ Toggle Menu
+
+
+ ",
+ }
+ }
+ fill="currentColor"
+ height={16}
+ role="img"
+ style={
+ Object {
+ "display": "inline-block",
+ "overflow": "visible",
+ "userSelect": "none",
+ "verticalAlign": "text-bottom",
+ }
+ }
+ viewBox="0 0 16 16"
+ width={16}
+ />
+
+
+
`;
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);
-}
-
-
-
-
- Toggle Menu
-
-
- ",
- }
- }
- fill="currentColor"
- height={16}
- role="img"
- style={
- Object {
- "display": "inline-block",
- "overflow": "visible",
- "userSelect": "none",
- "verticalAlign": "text-bottom",
- }
- }
- viewBox="0 0 16 16"
- width={16}
- />
-
-
-
-`;
diff --git a/src/__tests__/deprecated/ActionList.test.tsx b/src/__tests__/deprecated/ActionList.test.tsx
new file mode 100644
index 00000000000..7432588b70a
--- /dev/null
+++ b/src/__tests__/deprecated/ActionList.test.tsx
@@ -0,0 +1,53 @@
+import {cleanup, render as HTMLRender} from '@testing-library/react'
+import 'babel-polyfill'
+import {axe, toHaveNoViolations} from 'jest-axe'
+import React from 'react'
+import theme from '../../theme'
+import {ActionList} from '../../deprecated/ActionList'
+import {behavesAsComponent, checkExports} from '../../utils/testing'
+import {BaseStyles, ThemeProvider} from '../..'
+expect.extend(toHaveNoViolations)
+
+function SimpleActionList(): JSX.Element {
+ return (
+
+
+
+
+
+ )
+}
+
+describe('ActionList', () => {
+ behavesAsComponent({
+ Component: ActionList,
+ options: {skipAs: true, skipSx: true},
+ toRender: () =>
+ })
+
+ checkExports('deprecated/ActionList', {
+ default: undefined,
+ ActionList
+ })
+
+ it('should have no axe violations', async () => {
+ const {container} = HTMLRender( )
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
+ cleanup()
+ })
+})
+
+describe('ActionList.Item', () => {
+ behavesAsComponent({
+ Component: ActionList.Item
+ })
+})
diff --git a/src/__tests__/ActionList.types.test.tsx b/src/__tests__/deprecated/ActionList.types.test.tsx
similarity index 93%
rename from src/__tests__/ActionList.types.test.tsx
rename to src/__tests__/deprecated/ActionList.types.test.tsx
index 8ff53f3d295..bcb1f2e3cb7 100644
--- a/src/__tests__/ActionList.types.test.tsx
+++ b/src/__tests__/deprecated/ActionList.types.test.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {ActionList} from '..'
+import {ActionList} from '../../deprecated/ActionList'
export function emptyList() {
return
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__/ActionList.test.tsx.snap b/src/__tests__/deprecated/__snapshots__/ActionList.test.tsx.snap
new file mode 100644
index 00000000000..852201546b0
--- /dev/null
+++ b/src/__tests__/deprecated/__snapshots__/ActionList.test.tsx.snap
@@ -0,0 +1,223 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActionList renders consistently 1`] = `
+.c1 {
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+
+.c0 {
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.c0[data-has-active-descendant],
+.c0:focus-within {
+ --item-hover-bg-override: none;
+ --item-hover-divider-border-color-override: hsla(210,18%,87%,1);
+}
+
+
+`;
+
+exports[`ActionList.Item renders consistently 1`] = `
+.c3 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ min-width: 0;
+ position: relative;
+ -webkit-box-flex: 1;
+ -webkit-flex-grow: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+}
+
+.c4 {
+ -webkit-align-items: baseline;
+ -webkit-box-align: baseline;
+ -ms-flex-align: baseline;
+ align-items: baseline;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ min-width: 0;
+ -webkit-flex-direction: var(--main-content-flex-direction);
+ -ms-flex-direction: var(--main-content-flex-direction);
+ flex-direction: var(--main-content-flex-direction);
+ -webkit-box-flex: 1;
+ -webkit-flex-grow: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+}
+
+.c5:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
+ margin-top: 0;
+}
+
+.c5:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ width: 100%;
+ top: -7px;
+ border: 0 solid hsla(210,18%,87%,1);
+ border-top-width: 0;
+}
+
+.c5:hover .c2::before,
+.c5:hover + * .c2::before {
+ border-color: var(--item-hover-divider-border-color-override,transparent) !important;
+}
+
+.c5:focus .c2::before,
+.c5:focus + * .c2::before,
+.c5[data-is-active-descendant] .c2::before,
+[data-is-active-descendant] + .c5 .c2::before {
+ border-color: transparent !important;
+}
+
+.c6:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
+ margin-top: 0;
+}
+
+.c6:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ width: 100%;
+ top: -7px;
+ border: 0 solid hsla(210,18%,87%,1);
+ border-top-width: 0;
+}
+
+.c6:hover .c2::before,
+.c6:hover + * .c2::before {
+ border-color: var(--item-hover-divider-border-color-override,transparent) !important;
+}
+
+.c6:focus .c2::before,
+.c6:focus + * .c2::before,
+.c6[data-is-active-descendant] .c2::before,
+[data-is-active-descendant] + .c6 .c2::before {
+ border-color: transparent !important;
+}
+
+.c7:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
+ margin-top: 0;
+}
+
+.c7:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ width: 100%;
+ top: -7px;
+ border: 0 solid hsla(210,18%,87%,1);
+ border-top-width: 0;
+}
+
+.c7:hover .c2::before,
+.c7:hover + * .c2::before {
+ border-color: var(--item-hover-divider-border-color-override,transparent) !important;
+}
+
+.c7:focus .c2::before,
+.c7:focus + * .c2::before,
+.c7[data-is-active-descendant] .c2::before,
+[data-is-active-descendant] + .c7 .c2::before {
+ border-color: transparent !important;
+}
+
+.c1 {
+ padding: 6px 8px;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ border-radius: 6px;
+ color: #24292f;
+ -webkit-transition: background 33.333ms linear;
+ transition: background 33.333ms linear;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.c1:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) {
+ margin-top: 0;
+}
+
+.c1:not(:first-of-type):not(.c9 + .c0):not(.c8 + .c0) .c2::before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ width: 100%;
+ top: -7px;
+ border: 0 solid hsla(210,18%,87%,1);
+ border-top-width: 0;
+}
+
+.c1:hover .c2::before,
+.c1:hover + * .c2::before {
+ border-color: var(--item-hover-divider-border-color-override,transparent) !important;
+}
+
+.c1:focus .c2::before,
+.c1:focus + * .c2::before,
+.c1[data-is-active-descendant] .c2::before,
+[data-is-active-descendant] + .c1 .c2::before {
+ border-color: transparent !important;
+}
+
+.c1[data-is-active-descendant='activated-directly'] {
+ background: rgba(208,215,222,0.48);
+}
+
+.c1[data-is-active-descendant='activated-indirectly'] {
+ background: rgba(208,215,222,0.32);
+}
+
+.c1:focus {
+ background: rgba(208,215,222,0.48);
+ outline: none;
+}
+
+.c1:active {
+ background: rgba(208,215,222,0.48);
+}
+
+@media (hover:hover) and (pointer:fine) {
+ .c1:hover {
+ background: var( --item-hover-bg-override,rgba(208,215,222,0.32) );
+ cursor: pointer;
+ }
+}
+
+
+`;
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);
+}
+
+
+`;
diff --git a/src/deprecated/ActionList/Divider.tsx b/src/deprecated/ActionList/Divider.tsx
new file mode 100644
index 00000000000..48102214ba8
--- /dev/null
+++ b/src/deprecated/ActionList/Divider.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import styled from 'styled-components'
+import {get} from '../../constants'
+
+export const StyledDivider = styled.div`
+ height: 1px;
+ background: ${get('colors.border.muted')};
+ margin-top: calc(${get('space.2')} - 1px);
+ margin-bottom: ${get('space.2')};
+`
+
+/**
+ * Visually separates `Item`s or `Group`s in an `ActionList`.
+ */
+export function Divider(): JSX.Element {
+ return
+}
+
+/**
+ * `Divider` fulfills the `ItemPropsWithCustomRenderer` contract,
+ * so it can be used inline in an `ActionList`’s `items` prop.
+ * In other words, `items={[ActionList.Divider]}` is supported as a concise
+ * alternative to `items={[{renderItem: () => }]}`.
+ */
+Divider.renderItem = Divider
diff --git a/src/deprecated/ActionList/Group.tsx b/src/deprecated/ActionList/Group.tsx
new file mode 100644
index 00000000000..638ea4abf64
--- /dev/null
+++ b/src/deprecated/ActionList/Group.tsx
@@ -0,0 +1,45 @@
+import React from 'react'
+import styled from 'styled-components'
+import sx, {SxProp} from '../../sx'
+import {Header, HeaderProps} from './Header'
+
+/**
+ * Contract for props passed to the `Group` component.
+ */
+export interface GroupProps extends React.ComponentPropsWithoutRef<'div'>, SxProp {
+ /**
+ * Props for a `Header` to render in the `Group`.
+ */
+ header?: HeaderProps
+
+ /**
+ * The id of the group.
+ */
+ groupId?: string
+
+ /**
+ * `Items` to render in the `Group`.
+ */
+ items?: JSX.Element[]
+
+ /**
+ * Whether to display a divider above each `Item` in this `Group` when it does not follow a `Header` or `Divider`.
+ */
+ showItemDividers?: boolean
+}
+
+const StyledGroup = styled.div`
+ ${sx}
+`
+
+/**
+ * Collects related `Items` in an `ActionList`.
+ */
+export function Group({header, items, ...props}: GroupProps): JSX.Element {
+ return (
+
+ {header && }
+ {items}
+
+ )
+}
diff --git a/src/ActionList/Header.tsx b/src/deprecated/ActionList/Header.tsx
similarity index 96%
rename from src/ActionList/Header.tsx
rename to src/deprecated/ActionList/Header.tsx
index 9ac7cbd318d..c165e324efb 100644
--- a/src/ActionList/Header.tsx
+++ b/src/deprecated/ActionList/Header.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import styled, {css} from 'styled-components'
-import {get} from '../constants'
-import sx, {SxProp} from '../sx'
+import {get} from '../../constants'
+import sx, {SxProp} from '../../sx'
/**
* Contract for props passed to the `Header` component.
diff --git a/src/deprecated/ActionList/Item.tsx b/src/deprecated/ActionList/Item.tsx
new file mode 100644
index 00000000000..70f4a3db399
--- /dev/null
+++ b/src/deprecated/ActionList/Item.tsx
@@ -0,0 +1,481 @@
+import {CheckIcon, IconProps} from '@primer/octicons-react'
+import React, {useCallback} from 'react'
+import {get} from '../../constants'
+import sx, {SxProp} from '../../sx'
+import Truncate from '../../Truncate'
+import {ItemInput} from './List'
+import styled from 'styled-components'
+import {StyledHeader} from './Header'
+import {StyledDivider} from './Divider'
+import {useTheme} from '../../ThemeProvider'
+import {
+ activeDescendantActivatedDirectly,
+ activeDescendantActivatedIndirectly,
+ isActiveDescendantAttribute
+} from '@primer/behaviors'
+import {useSSRSafeId} from '@react-aria/ssr'
+import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
+import {AriaRole} from '../../utils/types'
+
+/**
+ * Contract for props passed to the `Item` component.
+ */
+export interface ItemProps extends SxProp {
+ /**
+ * Primary text which names an `Item`.
+ */
+ text?: string
+
+ /**
+ * Secondary text which provides additional information about an `Item`.
+ */
+ description?: string
+
+ /**
+ * Secondary text style variations. Usage is discretionary.
+ *
+ * - `"inline"` - Secondary text is positioned beside primary text.
+ * - `"block"` - Secondary text is positioned below primary text.
+ */
+ descriptionVariant?: 'inline' | 'block'
+
+ /**
+ * Icon (or similar) positioned before `Item` text.
+ */
+ leadingVisual?: React.FunctionComponent
+
+ /**
+ * @deprecated Use `trailingVisual` instead
+ * Icon (or similar) positioned after `Item` text.
+ */
+ trailingIcon?: React.FunctionComponent
+
+ /**
+ * @deprecated Use `trailingVisual` instead
+ * Text positioned after `Item` text and optional trailing icon.
+ */
+ trailingText?: string
+
+ /**
+ * Icon or text positioned after `Item` text.
+ */
+ trailingVisual?: React.ReactNode
+
+ /**
+ * Style variations associated with various `Item` types.
+ *
+ * - `"default"` - An action `Item`.
+ * - `"danger"` - A destructive action `Item`.
+ */
+ variant?: 'default' | 'danger'
+
+ /**
+ * Whether to display a divider above the `Item` when it does not follow a `Header` or `Divider`.
+ */
+ showDivider?: boolean
+
+ /**
+ * For `Item`s which can be selected, whether the `Item` is currently selected.
+ */
+ selected?: boolean
+
+ /**
+ * For `Item`s which can be selected, whether `multiple` `Item`s or a `single` `Item` can be selected
+ */
+ selectionVariant?: 'single' | 'multiple'
+
+ /**
+ * Designates the group that an item belongs to.
+ */
+ groupId?: string
+
+ /**
+ * Items that are disabled can not be clicked, selected, or navigated through.
+ */
+ disabled?: boolean
+
+ /**
+ * Callback that will trigger both on click selection and keyboard selection.
+ */
+ onAction?: (item: ItemProps, event: React.MouseEvent | React.KeyboardEvent) => void
+
+ /**
+ * An id associated with this item. Should be unique between items
+ */
+ id?: number | string
+
+ /**
+ * Node to be included inside the item before the text.
+ */
+ children?: React.ReactNode
+
+ /**
+ * The ARIA role describing the function of `List` component. `option` is a common value.
+ */
+ role?: AriaRole
+
+ /**
+ * An item to pass back in the `onAction` callback, meant as
+ */
+ item?: ItemInput
+}
+
+const getItemVariant = (variant = 'default', disabled?: boolean) => {
+ if (disabled) {
+ return {
+ color: get('colors.primer.fg.disabled'),
+ iconColor: get('colors.primer.fg.disabled'),
+ annotationColor: get('colors.primer.fg.disabled'),
+ hoverCursor: 'default'
+ }
+ }
+
+ switch (variant) {
+ case 'danger':
+ return {
+ color: get('colors.danger.fg'),
+ iconColor: get('colors.danger.fg'),
+ annotationColor: get('colors.fg.muted'),
+ hoverCursor: 'pointer',
+ hoverBg: get('colors.actionListItem.danger.hoverBg'),
+ focusBg: get('colors.actionListItem.danger.activeBg'),
+ hoverText: get('colors.actionListItem.danger.hoverText')
+ }
+ default:
+ return {
+ color: get('colors.fg.default'),
+ iconColor: get('colors.fg.muted'),
+ annotationColor: get('colors.fg.muted'),
+ hoverCursor: 'pointer',
+ hoverBg: get('colors.actionListItem.default.hoverBg'),
+ focusBg: get('colors.actionListItem.default.activeBg')
+ }
+ }
+}
+
+const DividedContent = styled.div`
+ display: flex;
+ min-width: 0;
+
+ /* Required for dividers */
+ position: relative;
+ flex-grow: 1;
+`
+
+const MainContent = styled.div`
+ align-items: baseline;
+ display: flex;
+ min-width: 0;
+ flex-direction: var(--main-content-flex-direction);
+ flex-grow: 1;
+`
+
+const StyledItem = styled.div<
+ {
+ variant: ItemProps['variant']
+ showDivider: ItemProps['showDivider']
+ item?: ItemInput
+ } & SxProp
+>`
+ /* 6px vertical padding + 20px line height = 32px total height
+ *
+ * TODO: When rem-based spacing on a 4px scale lands, replace
+ * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'.
+ */
+ padding: 6px ${get('space.2')};
+ display: flex;
+ border-radius: ${get('radii.2')};
+ color: ${({variant, item}) => getItemVariant(variant, item?.disabled).color};
+ // 2 frames on a 60hz monitor
+ transition: background 33.333ms linear;
+ text-decoration: none;
+
+ @media (hover: hover) and (pointer: fine) {
+ :hover {
+ // allow override in case another item in the list is active/focused
+ background: var(
+ --item-hover-bg-override,
+ ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg}
+ );
+ color: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverText};
+ cursor: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverCursor};
+ }
+ }
+
+ // Item dividers
+ :not(:first-of-type):not(${StyledDivider} + &):not(${StyledHeader} + &) {
+ margin-top: ${({showDivider}) => (showDivider ? `1px` : '0')};
+
+ ${DividedContent}::before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ width: 100%;
+ top: -7px;
+ // NB: This 'get' won’t execute if it’s moved into the arrow function below.
+ border: 0 solid ${get('colors.border.muted')};
+ border-top-width: ${({showDivider}) => (showDivider ? `1px` : '0')};
+ }
+ }
+
+ // Item dividers should not be visible:
+ // - above Hovered
+ &:hover ${DividedContent}::before,
+ // - below Hovered
+ // '*' instead of '&' because '&' maps to separate class names depending on 'variant'
+ :hover + * ${DividedContent}::before {
+ // allow override in case another item in the list is active/focused
+ border-color: var(--item-hover-divider-border-color-override, transparent) !important;
+ }
+
+ // - above Focused
+ &:focus ${DividedContent}::before,
+ // - below Focused
+ // '*' instead of '&' because '&' maps to separate class names depending on 'variant'
+ :focus + * ${DividedContent}::before,
+ // - above Active Descendent
+ &[${isActiveDescendantAttribute}] ${DividedContent}::before,
+ // - below Active Descendent
+ [${isActiveDescendantAttribute}] + & ${DividedContent}::before {
+ // '!important' because all the ':not's above give higher specificity
+ border-color: transparent !important;
+ }
+
+ // Active Descendant
+ &[${isActiveDescendantAttribute}='${activeDescendantActivatedDirectly}'] {
+ background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
+ }
+ &[${isActiveDescendantAttribute}='${activeDescendantActivatedIndirectly}'] {
+ background: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg};
+ }
+
+ &:focus {
+ background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
+ outline: none;
+ }
+
+ &:active {
+ background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
+ }
+
+ ${sx}
+`
+
+export const TextContainer = styled.span<{
+ dangerouslySetInnerHtml?: React.DOMAttributes['dangerouslySetInnerHTML']
+}>``
+
+const BaseVisualContainer = styled.div<{variant?: ItemProps['variant']; disabled?: boolean}>`
+ // Match visual height to adjacent text line height.
+ // TODO: When rem-based spacing on a 4px scale lands, replace
+ // hardcoded '20px' with '${get('space.s20')}'.
+ height: 20px;
+ width: ${get('space.3')};
+ margin-right: ${get('space.2')};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+`
+
+const ColoredVisualContainer = styled(BaseVisualContainer)`
+ svg {
+ fill: ${({variant, disabled}) => getItemVariant(variant, disabled).iconColor};
+ font-size: ${get('fontSizes.0')};
+ }
+`
+
+const LeadingVisualContainer = styled(ColoredVisualContainer)`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`
+
+const TrailingContent = styled(ColoredVisualContainer)`
+ color: ${({variant, disabled}) => getItemVariant(variant, disabled).annotationColor}};
+ margin-left: ${get('space.2')};
+ margin-right: 0;
+ width: auto;
+ div:nth-child(2) {
+ margin-left: ${get('space.2')};
+ }
+`
+
+const DescriptionContainer = styled.span`
+ color: ${get('colors.fg.muted')};
+ font-size: ${get('fontSizes.0')};
+ // TODO: When rem-based spacing on a 4px scale lands, replace
+ // hardcoded '16px' with '${get('lh-12')}'.
+ line-height: 16px;
+ margin-left: var(--description-container-margin-left);
+ min-width: 0;
+ flex-grow: 1;
+ flex-basis: var(--description-container-flex-basis);
+`
+
+const MultiSelectIcon = styled.svg<{selected?: boolean}>`
+ rect {
+ fill: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.canvas.default'))};
+ stroke: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.border.default'))};
+ shape-rendering: auto; // this is a workaround to override global style in github/github, see primer/react#1666
+ }
+ path {
+ fill: ${get('colors.fg.onEmphasis')};
+ boxshadow: ${get('shadow.small')};
+ opacity: ${({selected}) => (selected ? 1 : 0)};
+ }
+`
+
+/**
+ * An actionable or selectable `Item` with an optional icon and description.
+ */
+export const Item = React.forwardRef((itemProps, ref) => {
+ const {
+ as: Component,
+ text,
+ description,
+ descriptionVariant = 'inline',
+ selected,
+ selectionVariant,
+ leadingVisual: LeadingVisual,
+ trailingIcon: TrailingIcon,
+ trailingVisual: TrailingVisual,
+ trailingText,
+ variant = 'default',
+ showDivider,
+ disabled,
+ onAction,
+ onKeyPress,
+ children,
+ onClick,
+ id,
+ ...props
+ } = itemProps
+
+ const labelId = useSSRSafeId()
+ const descriptionId = useSSRSafeId()
+
+ const keyPressHandler = useCallback(
+ event => {
+ if (disabled) {
+ return
+ }
+ onKeyPress?.(event)
+
+ if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
+ onAction?.(itemProps, event)
+ }
+ },
+ [onAction, disabled, itemProps, onKeyPress]
+ )
+
+ const clickHandler = useCallback(
+ event => {
+ if (disabled) {
+ return
+ }
+ onClick?.(event)
+ if (!event.defaultPrevented) {
+ onAction?.(itemProps, event)
+ }
+ },
+ [onAction, disabled, itemProps, onClick]
+ )
+
+ const {theme} = useTheme()
+
+ return (
+
+ {!!selected === selected && (
+
+ {selectionVariant === 'multiple' ? (
+ <>
+ {/**
+ * we use a svg instead of an input because there should not
+ * be an interactive element inside an option
+ * svg copied from primer/css
+ */}
+
+
+
+
+ >
+ ) : (
+ selected &&
+ )}
+
+ )}
+ {LeadingVisual && (
+
+
+
+ )}
+
+
+ {children}
+ {text ? {text} : null}
+ {description ? (
+
+ {descriptionVariant === 'block' ? (
+ description
+ ) : (
+
+ {description}
+
+ )}
+
+ ) : null}
+
+ {/* backward compatibility: prefer TrailingVisual but fallback to TrailingIcon */}
+ {TrailingVisual ? (
+
+ {typeof TrailingVisual === 'function' ? : TrailingVisual}
+
+ ) : TrailingIcon || trailingText ? (
+
+ {trailingText}
+ {TrailingIcon && }
+
+ ) : null}
+
+
+ )
+}) as PolymorphicForwardRefComponent<'div', ItemProps>
+
+Item.displayName = 'ActionList.Item'
diff --git a/src/deprecated/ActionList/List.tsx b/src/deprecated/ActionList/List.tsx
new file mode 100644
index 00000000000..0af5190d7c1
--- /dev/null
+++ b/src/deprecated/ActionList/List.tsx
@@ -0,0 +1,258 @@
+import React, {Key} from 'react'
+import type {AriaRole} from '../../utils/types'
+import {Group, GroupProps} from './Group'
+import {Item, ItemProps} from './Item'
+import {Divider} from './Divider'
+import styled from 'styled-components'
+import {get} from '../../constants'
+import {SystemCssProperties} from '@styled-system/css'
+import {hasActiveDescendantAttribute} from '@primer/behaviors'
+import {Merge} from '../../utils/types/Merge'
+
+type RenderItemFn = (props: ItemProps) => React.ReactElement
+
+export type ItemInput =
+ | Merge, ItemProps>
+ | ((Partial & {renderItem: RenderItemFn}) & {key?: Key})
+
+/**
+ * Contract for props passed to the `List` component.
+ */
+export interface ListPropsBase {
+ /**
+ * A collection of `Item` props and `Item`-level custom `Item` renderers.
+ */
+ items: ItemInput[]
+
+ /**
+ * The ARIA role describing the function of `List` component. `listbox` is a common value.
+ */
+ role?: AriaRole
+
+ /**
+ * id to attach to the base DOM node of the list
+ */
+ id?: string
+
+ /**
+ * A `List`-level custom `Item` renderer. Every `Item` within this `List`
+ * without a `Group`-level or `Item`-level custom `Item` renderer will be
+ * rendered using this function component.
+ */
+ renderItem?: RenderItemFn
+
+ /**
+ * A `List`-level custom `Group` renderer. Every `Group` within this `List`
+ * without a `Group`-level custom `Item` renderer will be rendered using
+ * this function component.
+ */
+ renderGroup?: typeof Group
+
+ /**
+ * Style variations. Usage is discretionary.
+ *
+ * - `"inset"` - `List` children are offset (vertically and horizontally) from `List`’s edges
+ * - `"full"` - `List` children are flush (vertically and horizontally) with `List` edges
+ */
+ variant?: 'inset' | 'full'
+
+ /**
+ * For `Item`s which can be selected, whether `multiple` `Item`s or a `single` `Item` can be selected
+ */
+ selectionVariant?: 'single' | 'multiple'
+
+ /**
+ * Whether to display a divider above each `Item` in this `List` when it does not follow a `Header` or `Divider`.
+ */
+ showItemDividers?: boolean
+}
+
+/**
+ * Contract for props passed to the `List` component, when its `Item`s are collected in `Group`s.
+ */
+export interface GroupedListProps extends ListPropsBase {
+ /**
+ * A collection of `Group` props (except `items`), plus a unique group identifier
+ * and `Group`-level custom `Item` or `Group` renderers.
+ */
+ groupMetadata: ((
+ | Omit
+ | Omit & {renderItem?: RenderItemFn; renderGroup?: typeof Group}, 'items'>
+ ) & {groupId: string})[]
+
+ /**
+ * A collection of `Item` props, plus associated group identifiers
+ * and `Item`-level custom `Item` renderers.
+ */
+ items: ((ItemProps | (Partial & {renderItem: RenderItemFn})) & {groupId: string})[]
+}
+
+/**
+ * Asserts that the given value fulfills the `GroupedListProps` contract.
+ * @param props A value which fulfills either the `ListPropsBase` or the `GroupedListProps` contract.
+ */
+function isGroupedListProps(props: ListProps): props is GroupedListProps {
+ return 'groupMetadata' in props
+}
+
+/**
+ * Contract for props passed to the `List` component.
+ */
+export type ListProps = ListPropsBase | GroupedListProps
+
+const StyledList = styled.div`
+ font-size: ${get('fontSizes.1')};
+ /* 14px font-size * 1.428571429 = 20px line height
+ *
+ * TODO: When rem-based spacing on a 4px scale lands, replace
+ * hardcoded '20px'
+ */
+ line-height: 20px;
+
+ &[${hasActiveDescendantAttribute}], &:focus-within {
+ --item-hover-bg-override: none;
+ --item-hover-divider-border-color-override: ${get('colors.border.muted')};
+ }
+`
+
+/**
+ * Returns `sx` prop values for `List` children matching the given `List` style variation.
+ * @param variant `List` style variation.
+ */
+function useListVariant(variant: ListProps['variant'] = 'inset'): {
+ firstGroupStyle?: SystemCssProperties
+ lastGroupStyle?: SystemCssProperties
+ headerStyle?: SystemCssProperties
+ itemStyle?: SystemCssProperties
+} {
+ switch (variant) {
+ case 'full':
+ return {
+ headerStyle: {paddingX: get('space.2')},
+ itemStyle: {borderRadius: 0}
+ }
+ default:
+ return {
+ firstGroupStyle: {marginTop: get('space.2')},
+ lastGroupStyle: {marginBottom: get('space.2')},
+ itemStyle: {marginX: get('space.2')}
+ }
+ }
+}
+
+/**
+ * Lists `Item`s, either grouped or ungrouped, with a `Divider` between each `Group`.
+ */
+export const List = React.forwardRef((props, forwardedRef): JSX.Element => {
+ // Get `sx` prop values for `List` children matching the given `List` style variation.
+ const {firstGroupStyle, lastGroupStyle, headerStyle, itemStyle} = useListVariant(props.variant)
+
+ /**
+ * Render a `Group` using the first of the following renderers that is defined:
+ * A `Group`-level or `List`-level custom `Group` renderer, or
+ * the default `Group` renderer.
+ */
+ const renderGroup = (
+ groupProps: GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group})
+ ) => {
+ const GroupComponent = (('renderGroup' in groupProps && groupProps.renderGroup) ?? props.renderGroup) || Group
+ return
+ }
+
+ /**
+ * Render an `Item` using the first of the following renderers that is defined:
+ * An `Item`-level, `Group`-level, or `List`-level custom `Item` renderer,
+ * or the default `Item` renderer.
+ */
+ const renderItem = (itemProps: ItemInput, item: ItemInput, itemIndex: number) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item
+ const key = ('key' in itemProps ? itemProps.key : undefined) ?? itemProps.id?.toString() ?? itemIndex.toString()
+ return (
+
+ )
+ }
+
+ /**
+ * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`.
+ */
+ let groups: (GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group}))[] = []
+
+ // Collect rendered `Item`s into `Group`s, avoiding excess iteration over the lists of `items` and `groupMetadata`:
+ if (!isGroupedListProps(props)) {
+ // When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`.
+ groups = [{items: props.items.map((item, index) => renderItem(item, item, index)), groupId: '0'}]
+ } else {
+ // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s.
+
+ /**
+ * A map of group identifiers to `Group`s, each with an associated array of `Item`s belonging to that `Group`.
+ */
+ const groupMap = props.groupMetadata.reduce(
+ (groupAccumulator, groupMetadata) => groupAccumulator.set(groupMetadata.groupId, groupMetadata),
+ new Map & {renderItem?: typeof Item; renderGroup?: typeof Group})>()
+ )
+
+ for (const itemProps of props.items) {
+ // Look up the group associated with the current item.
+ const group = groupMap.get(itemProps.groupId)
+ const itemIndex = group?.items?.length ?? 0
+
+ // Upsert the group to include the current item (rendered).
+ groupMap.set(itemProps.groupId, {
+ ...group,
+ items: [
+ ...(group?.items ?? []),
+ renderItem(
+ {
+ showDivider: group?.showItemDividers,
+ ...(group && 'renderItem' in group && {renderItem: group.renderItem}),
+ ...itemProps
+ },
+ itemProps,
+ itemIndex
+ )
+ ]
+ })
+ }
+
+ groups = [...groupMap.values()]
+ }
+
+ return (
+
+ {groups.map(({header, ...groupProps}, index) => {
+ const hasFilledHeader = header?.variant === 'filled'
+ const shouldShowDivider = index > 0 && !hasFilledHeader
+ return (
+
+ {shouldShowDivider ? : null}
+ {renderGroup({
+ sx: {
+ ...(index === 0 && firstGroupStyle),
+ ...(index === groups.length - 1 && lastGroupStyle),
+ ...(index > 0 && !shouldShowDivider && {mt: 2})
+ },
+ ...(header && {
+ header: {
+ ...header,
+ sx: {...headerStyle, ...header.sx}
+ }
+ }),
+ ...groupProps
+ })}
+
+ )
+ })}
+
+ )
+})
+
+List.displayName = 'ActionList'
diff --git a/src/deprecated/ActionList/index.ts b/src/deprecated/ActionList/index.ts
new file mode 100644
index 00000000000..71eb71708f8
--- /dev/null
+++ b/src/deprecated/ActionList/index.ts
@@ -0,0 +1,21 @@
+import {List} from './List'
+import {Group} from './Group'
+import {Item} from './Item'
+import {Divider} from './Divider'
+export type {ListProps as ActionListProps} from './List'
+export type {GroupProps} from './Group'
+export type {ItemProps} from './Item'
+
+/**
+ * Collection of list-related components.
+ */
+export const ActionList = Object.assign(List, {
+ /** Collects related `Items` in an `ActionList`. */
+ Group,
+
+ /** An actionable or selectable `Item` with an optional icon and description. */
+ Item,
+
+ /** Visually separates `Item`s or `Group`s in an `ActionList`. */
+ Divider
+})
diff --git a/src/deprecated/ActionMenu.tsx b/src/deprecated/ActionMenu.tsx
new file mode 100644
index 00000000000..c2a34dcff42
--- /dev/null
+++ b/src/deprecated/ActionMenu.tsx
@@ -0,0 +1,109 @@
+import {GroupedListProps, List, ListPropsBase} from './ActionList/List'
+import {Item, ItemProps} from './ActionList/Item'
+import {Divider} from './ActionList/Divider'
+import Button, {ButtonProps} from '../Button'
+import React, {useCallback, useMemo} from 'react'
+import {AnchoredOverlay} from '../AnchoredOverlay'
+import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
+import {OverlayProps} from '../Overlay'
+import {useProvidedRefOrCreate} from '../hooks'
+import {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay'
+
+interface ActionMenuBaseProps extends Partial>, ListPropsBase {
+ /**
+ * Content that is passed into the renderAnchor component, which is a button by default.
+ */
+ anchorContent?: React.ReactNode
+
+ /**
+ * A callback that triggers both on clicks and keyboard events. This callback will be overridden by item level `onAction` callbacks.
+ */
+ onAction?: (props: ItemProps, event?: React.MouseEvent | React.KeyboardEvent) => void
+
+ /**
+ * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `setOpen`.
+ */
+ 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
+}
+
+export type ActionMenuProps = ActionMenuBaseProps & AnchoredOverlayWrapperAnchorProps
+
+const ActionMenuItem = (props: ItemProps) =>
+
+ActionMenuItem.displayName = 'ActionMenu.Item'
+
+const ActionMenuBase = ({
+ anchorContent,
+ renderAnchor = (props: T) => ,
+ anchorRef: externalAnchorRef,
+ onAction,
+ open,
+ setOpen,
+ overlayProps,
+ items,
+ ...listProps
+}: ActionMenuProps): JSX.Element => {
+ const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, setOpen, false)
+ const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
+ const onOpen = useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
+ const onClose = useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
+
+ const renderMenuAnchor = useMemo(() => {
+ if (renderAnchor === null) {
+ return null
+ }
+ return >(props: T) => {
+ return renderAnchor({
+ 'aria-label': 'menu',
+ children: anchorContent,
+ ...props
+ })
+ }
+ }, [anchorContent, renderAnchor])
+
+ const itemsToRender = useMemo(() => {
+ return items.map(item => {
+ return {
+ ...item,
+ role: 'menuitem',
+ onAction: (props, event) => {
+ const actionCallback = item.onAction ?? onAction
+ actionCallback?.(props as ItemProps, event)
+ if (!event.defaultPrevented) {
+ onClose()
+ }
+ }
+ } as ItemProps
+ })
+ }, [items, onAction, onClose])
+
+ return (
+
+
+
+ )
+}
+
+ActionMenuBase.displayName = 'ActionMenu'
+
+/**
+ * @deprecated Use ActionMenu with composable API instead. See https://primer.style/react/ActionMenu for more details.
+ */
+export const ActionMenu = Object.assign(ActionMenuBase, {Divider, Item: ActionMenuItem})
diff --git a/src/deprecated/index.ts b/src/deprecated/index.ts
index df51794a190..b5641b8e5c1 100644
--- a/src/deprecated/index.ts
+++ b/src/deprecated/index.ts
@@ -37,3 +37,7 @@ export type {
SelectMenuTabPanelProps,
SelectMenuLoadingAnimationProps
} from '../SelectMenu'
+export {ActionList} from './ActionList'
+export type {ActionListProps} from './ActionList'
+export {ActionMenu} from './ActionMenu'
+export type {ActionMenuProps} from './ActionMenu'
diff --git a/src/drafts/index.ts b/src/drafts/index.ts
index efa9ff196c5..69143a5a08b 100644
--- a/src/drafts/index.ts
+++ b/src/drafts/index.ts
@@ -6,8 +6,6 @@
*/
// Components
-export * from '../ActionList2'
export * from '../Button2'
-export * from '../ActionMenu2'
export * from '../DropdownMenu2'
export * from '../Label2'
diff --git a/src/index.ts b/src/index.ts
index ff38b050969..c1f9aca96f8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,8 +30,16 @@ export {useConfirm} from './Dialog/ConfirmationDialog'
export {default as Radio} from './Radio'
export type {RadioProps} from './Radio'
export {ActionList} from './ActionList'
+export type {
+ ActionListProps,
+ ActionListGroupProps,
+ ActionListItemProps,
+ ActionListDescriptionProps,
+ ActionListLeadingVisualProps,
+ ActionListTrailingVisualProps
+} from './ActionList'
export {ActionMenu} from './ActionMenu'
-export type {ActionMenuProps} from './ActionMenu'
+export type {ActionMenuProps, ActionMenuAnchorProps, ActionMenuButtonProps} from './ActionMenu'
export {default as Autocomplete} from './Autocomplete'
export type {AutocompleteMenuProps, AutocompleteInputProps, AutocompleteOverlayProps} from './Autocomplete'
export {default as Avatar} from './Avatar'
diff --git a/src/stories/ActionList2/examples.stories.tsx b/src/stories/ActionList/examples.stories.tsx
similarity index 98%
rename from src/stories/ActionList2/examples.stories.tsx
rename to src/stories/ActionList/examples.stories.tsx
index bb923be9c4f..fdf306934bd 100644
--- a/src/stories/ActionList2/examples.stories.tsx
+++ b/src/stories/ActionList/examples.stories.tsx
@@ -16,8 +16,7 @@ import {
} from '@primer/octicons-react'
import {ThemeProvider} from '../..'
-import {ActionList as _ActionList} from '../../ActionList2'
-import {Header} from '../../ActionList/Header'
+import {ActionList} from '../../ActionList'
import BaseStyles from '../../BaseStyles'
import Avatar from '../../Avatar'
import TextInput from '../../TextInput'
@@ -25,12 +24,8 @@ import Spinner from '../../Spinner'
import Box from '../../Box'
import Text from '../../Text'
-const ActionList = Object.assign(_ActionList, {
- Header
-})
-
const meta: Meta = {
- title: 'Composite components/ActionList2/examples',
+ title: 'Composite components/ActionList/examples',
component: ActionList,
decorators: [
(Story: React.ComponentType): JSX.Element => (
diff --git a/src/stories/ActionList2/fixtures.stories.tsx b/src/stories/ActionList/fixtures.stories.tsx
similarity index 98%
rename from src/stories/ActionList2/fixtures.stories.tsx
rename to src/stories/ActionList/fixtures.stories.tsx
index a7fbcddd3c9..301b58684b7 100644
--- a/src/stories/ActionList2/fixtures.stories.tsx
+++ b/src/stories/ActionList/fixtures.stories.tsx
@@ -28,20 +28,15 @@ import styled from 'styled-components'
import {DndProvider, useDrag, useDrop} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
import {Label, ThemeProvider} from '../..'
-import {ActionList as _ActionList, ItemProps} from '../../ActionList2'
-import {Header} from '../../ActionList/Header'
+import {ActionList, ActionListItemProps} from '../../ActionList'
import BaseStyles from '../../BaseStyles'
import Avatar from '../../Avatar'
import {ButtonInvisible} from '../../Button'
import Box from '../../Box'
import {AnchoredOverlay} from '../../AnchoredOverlay'
-const ActionList = Object.assign(_ActionList, {
- Header
-})
-
const meta: Meta = {
- title: 'Composite components/ActionList2/fixtures',
+ title: 'Composite components/ActionList/fixtures',
component: ActionList,
decorators: [
(Story: React.ComponentType): JSX.Element => (
@@ -476,14 +471,14 @@ export function LinkItemStory(): JSX.Element {
as ReactRouterLink
-
+
@@ -491,7 +486,7 @@ export function LinkItemStory(): JSX.Element {
NextJS style Link
-
+
@@ -995,8 +990,8 @@ MemexSortable.storyName = 'Memex Sortable List'
type SortableItemProps = {
option: Option
- role: ItemProps['role']
- onSelect: ItemProps['onSelect']
+ role: ActionListItemProps['role']
+ onSelect: ActionListItemProps['onSelect']
reorder: ({optionToMove, moveAfterOption}: {optionToMove: Option; moveAfterOption: Option}) => void
}
const SortableItem: React.FC = ({option, role, onSelect, reorder}) => {
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu/examples.stories.tsx
similarity index 98%
rename from src/stories/ActionMenu2/examples.stories.tsx
rename to src/stories/ActionMenu/examples.stories.tsx
index 8f3592d316b..9bce65fcc8b 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu/examples.stories.tsx
@@ -1,7 +1,6 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider, BaseStyles, Box, Text, Avatar} from '../..'
-import {ActionMenu, ActionList} from '../../drafts'
+import {ThemeProvider, BaseStyles, Box, Text, Avatar, ActionMenu, ActionList} from '../..'
import {
GearIcon,
MilestoneIcon,
@@ -17,7 +16,7 @@ import {
} from '@primer/octicons-react'
const meta: Meta = {
- title: 'Composite components/ActionMenu2/examples',
+ title: 'Composite components/ActionMenu/examples',
component: ActionMenu,
decorators: [
(Story: React.ComponentType): JSX.Element => (
diff --git a/src/stories/ActionMenu2/fixtures.stories.tsx b/src/stories/ActionMenu/fixtures.stories.tsx
similarity index 99%
rename from src/stories/ActionMenu2/fixtures.stories.tsx
rename to src/stories/ActionMenu/fixtures.stories.tsx
index bc867f300f4..ebf881eea59 100644
--- a/src/stories/ActionMenu2/fixtures.stories.tsx
+++ b/src/stories/ActionMenu/fixtures.stories.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider, BaseStyles, Box, Text, TextInput, StyledOcticon, FormGroup} from '../..'
-import {ActionMenu, ActionList, Button, IconButton} from '../../drafts'
+import {ThemeProvider, BaseStyles, Box, Text, TextInput, StyledOcticon, FormGroup, ActionMenu, ActionList} from '../..'
+import {Button, IconButton} from '../../drafts'
import {
ServerIcon,
PlusCircleIcon,
@@ -25,7 +25,7 @@ import {
} from '@primer/octicons-react'
const meta: Meta = {
- title: 'Composite components/ActionMenu2/fixtures',
+ title: 'Composite components/ActionMenu/fixtures',
component: ActionMenu,
decorators: [
(Story: React.ComponentType): JSX.Element => (
diff --git a/src/stories/ConfirmationDialog.stories.tsx b/src/stories/ConfirmationDialog.stories.tsx
index 69e9619f4e9..8e91c8a9f39 100644
--- a/src/stories/ConfirmationDialog.stories.tsx
+++ b/src/stories/ConfirmationDialog.stories.tsx
@@ -3,7 +3,7 @@ import {Meta} from '@storybook/react'
import {BaseStyles, Button, Box, ThemeProvider, useTheme} from '..'
import {ConfirmationDialog, useConfirm} from '../Dialog/ConfirmationDialog'
-import {ActionMenu} from '../ActionMenu'
+import {ActionMenu} from '../deprecated/ActionMenu'
export default {
title: 'Internal components/ConfirmationDialog',
diff --git a/src/stories/DropdownMenu.stories.tsx b/src/stories/DropdownMenu.stories.tsx
index 1f524c0de8d..13812de3b11 100644
--- a/src/stories/DropdownMenu.stories.tsx
+++ b/src/stories/DropdownMenu.stories.tsx
@@ -1,7 +1,7 @@
import {Meta} from '@storybook/react'
import React from 'react'
import {theme, ThemeProvider} from '..'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
import BaseStyles from '../BaseStyles'
import Box from '../Box'
import {DropdownMenu, DropdownButton} from '../DropdownMenu'
diff --git a/src/stories/DropdownMenu2/examples.stories.tsx b/src/stories/DropdownMenu2/examples.stories.tsx
index a461dbbf47f..c72878f65e1 100644
--- a/src/stories/DropdownMenu2/examples.stories.tsx
+++ b/src/stories/DropdownMenu2/examples.stories.tsx
@@ -3,7 +3,7 @@ import {Meta} from '@storybook/react'
import {ThemeProvider} from '../..'
import BaseStyles from '../../BaseStyles'
import {DropdownMenu} from '../../DropdownMenu2'
-import {ActionList} from '../../ActionList2'
+import {ActionList} from '../../ActionList'
import Box from '../../Box'
import Text from '../../Text'
import {
diff --git a/src/stories/DropdownMenu2/fixtures.stories.tsx b/src/stories/DropdownMenu2/fixtures.stories.tsx
index d39782e46c1..b0c462b9b17 100644
--- a/src/stories/DropdownMenu2/fixtures.stories.tsx
+++ b/src/stories/DropdownMenu2/fixtures.stories.tsx
@@ -3,7 +3,7 @@ import {Meta} from '@storybook/react'
import {ThemeProvider} from '../..'
import BaseStyles from '../../BaseStyles'
import {DropdownMenu} from '../../DropdownMenu2'
-import {ActionList} from '../../ActionList2'
+import {ActionList} from '../../ActionList'
import {Button} from '../../Button2'
import Box from '../../Box'
import Text from '../../Text'
diff --git a/src/stories/Overlay.stories.tsx b/src/stories/Overlay.stories.tsx
index dd635c6338a..d5235581ff6 100644
--- a/src/stories/Overlay.stories.tsx
+++ b/src/stories/Overlay.stories.tsx
@@ -17,14 +17,14 @@ import {
Checkbox,
ChoiceInputField,
TextInput,
- ActionList,
Link,
- Label
+ Label,
+ ActionList,
+ ActionMenu
} from '..'
import type {AnchorSide} from '@primer/behaviors'
import {DropdownMenu, DropdownButton} from '../DropdownMenu'
-import {ActionMenu, ActionList as ActionList2} from '../drafts'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
export default {
title: 'Internal components/Overlay',
@@ -367,13 +367,13 @@ export const MemexNestedOverlays = () => {
{duration}
-
+
{durations.map(item => (
- setDuration(item)}>
+ setDuration(item)}>
{item}
-
+
))}
-
+
diff --git a/src/stories/SelectPanel.stories.tsx b/src/stories/SelectPanel.stories.tsx
index 31bd00556fd..6978e4ac174 100644
--- a/src/stories/SelectPanel.stories.tsx
+++ b/src/stories/SelectPanel.stories.tsx
@@ -2,7 +2,7 @@ import type {OverlayProps} from '../Overlay'
import {Meta} from '@storybook/react'
import React, {useRef, useState} from 'react'
import {theme, ThemeProvider} from '..'
-import {ItemInput} from '../ActionList/List'
+import {ItemInput} from '../deprecated/ActionList/List'
import BaseStyles from '../BaseStyles'
import {DropdownButton} from '../DropdownMenu'
import {SelectPanel} from '../SelectPanel'
diff --git a/src/stories/ActionList.stories.tsx b/src/stories/deprecated/ActionList.stories.tsx
similarity index 97%
rename from src/stories/ActionList.stories.tsx
rename to src/stories/deprecated/ActionList.stories.tsx
index 46e21e2c660..615511ac43d 100644
--- a/src/stories/ActionList.stories.tsx
+++ b/src/stories/deprecated/ActionList.stories.tsx
@@ -14,18 +14,18 @@ import {
import {Meta} from '@storybook/react'
import React, {forwardRef} from 'react'
import styled from 'styled-components'
-import {Label, ThemeProvider} from '..'
-import {ActionList as _ActionList} from '../ActionList'
-import {Header} from '../ActionList/Header'
-import BaseStyles from '../BaseStyles'
-import sx from '../sx'
+import {Label, ThemeProvider} from '../..'
+import {ActionList as _ActionList} from '../../deprecated/ActionList'
+import {Header} from '../../deprecated/ActionList/Header'
+import BaseStyles from '../../BaseStyles'
+import sx from '../../sx'
const ActionList = Object.assign(_ActionList, {
Header
})
const meta: Meta = {
- title: 'Composite components/ActionList',
+ title: 'Deprecated components/ActionList',
component: ActionList,
decorators: [
(Story: React.ComponentType): JSX.Element => (
diff --git a/src/stories/ActionMenu.stories.tsx b/src/stories/deprecated/ActionMenu.stories.tsx
similarity index 96%
rename from src/stories/ActionMenu.stories.tsx
rename to src/stories/deprecated/ActionMenu.stories.tsx
index 5d9165b5969..05ec0a193bb 100644
--- a/src/stories/ActionMenu.stories.tsx
+++ b/src/stories/deprecated/ActionMenu.stories.tsx
@@ -13,16 +13,16 @@ import {
import {Meta} from '@storybook/react'
import React, {useCallback, useState, useRef} from 'react'
import styled from 'styled-components'
-import {ThemeProvider} from '..'
-import {ActionMenu, ActionMenuProps} from '../ActionMenu'
-import Link, {LinkProps} from '../Link'
-import Button from '../Button'
-import {ActionList, ItemProps} from '../ActionList'
-import BaseStyles from '../BaseStyles'
-import {DropdownButton} from '../DropdownMenu'
+import {ThemeProvider} from '../..'
+import Link, {LinkProps} from '../../Link'
+import Button from '../../Button'
+import {ActionMenu, ActionMenuProps, ActionList} from '../../deprecated'
+import {ItemProps} from '../../deprecated/ActionList'
+import BaseStyles from '../../BaseStyles'
+import {DropdownButton} from '../../DropdownMenu'
const meta: Meta = {
- title: 'Composite components/ActionMenu',
+ title: 'Deprecated components/ActionMenu',
component: ActionMenu,
decorators: [
(Story: React.ComponentType): JSX.Element => (