From c5eb570a62020c4cb49679105e1dda85ebb06efe Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 15:04:07 +0100
Subject: [PATCH 01/14] merge docs
---
docs/content/drafts/ActionMenu2.mdx | 45 +++-
docs/content/drafts/DropdownMenu2.mdx | 364 --------------------------
src/ActionList2/Selection.tsx | 14 +-
3 files changed, 39 insertions(+), 384 deletions(-)
delete mode 100644 docs/content/drafts/DropdownMenu2.mdx
diff --git a/docs/content/drafts/ActionMenu2.mdx b/docs/content/drafts/ActionMenu2.mdx
index 4a8a8250ebf..037a4b82e20 100644
--- a/docs/content/drafts/ActionMenu2.mdx
+++ b/docs/content/drafts/ActionMenu2.mdx
@@ -175,6 +175,44 @@ You can choose to have a different _anchor_ for the Menu dependending on the app
```
+### 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:
@@ -241,12 +279,6 @@ render(
)
```
-
-
-Use `ActionMenu` to choose an action from a list. If you’re looking for single or multiple selection, use [DropdownMenu](/DropdownMenu) or [SelectPanel](/SelectPanel) instead.
-
-
-
## Props / API reference
### ActionMenu
@@ -305,6 +337,5 @@ Use `ActionMenu` to choose an action from a list. If you’re looking for single
## Related components
- [ActionList](/drafts/ActionList2)
-- [DropdownMenu](/DropdownMenu)
- [SelectPanel](/SelectPanel)
- [Button](/drafts/Button2)
diff --git a/docs/content/drafts/DropdownMenu2.mdx b/docs/content/drafts/DropdownMenu2.mdx
deleted file mode 100644
index 68cd522aa1e..00000000000
--- a/docs/content/drafts/DropdownMenu2.mdx
+++ /dev/null
@@ -1,364 +0,0 @@
----
-component_id: dropdown_menu
-title: DropdownMenu v2
-status: Alpha
-source: https://github.com/primer/react/tree/main/src/DropdownMenu2.tsx
-storybook: '/react/storybook?path=/story/composite-components-dropdownmenu2'
-description: Use DropdownMenu to select a single option from a list of menu options.
----
-
-import {Box, Avatar} from '@primer/react'
-import {DropdownMenu, ActionList} from '@primer/react/drafts'
-import {Props} from '../../src/props'
-import State from '../../components/State'
-import {CalendarIcon, IterationsIcon, NumberIcon, SingleSelectIcon, TypographyIcon} from '@primer/octicons-react'
-
-
-
-
- {([selectedIndex, setSelectedIndex]) => {
- const fieldTypes = [
- {icon: TypographyIcon, name: 'Text'},
- {icon: NumberIcon, name: 'Number'},
- {icon: CalendarIcon, name: 'Date'},
- {icon: SingleSelectIcon, name: 'Single select'},
- {icon: IterationsIcon, name: 'Iteration'}
- ]
- const selectedType = fieldTypes[selectedIndex]
- return (
-
-
-
- {selectedType.name}
-
-
-
- {fieldTypes.map(({icon: Icon, name}, index) => (
- setSelectedIndex(index)}
- >
- {name}
-
- ))}
-
-
-
-
- )
- }}
-
-
-
-
-```js
-import {DropdownMenu} from '@primer/react/drafts'
-```
-
-
-
-## Examples
-
-### Minimal example
-
-`DropdownMenu` ships with `DropdownMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `DropdownMenu.Overlay`
-
-
-
-```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( )
-```
-
-### Customise Button
-
-`Dropdown.Button` uses `Button v2` so you can pass props like `variant` and `leadingIcon` that `Button v2` accepts.
-
-```javascript live noinline drafts
-const Example = () => {
- const [duration, setDuration] = React.useState(1)
-
- return (
-
-
- {duration} {duration > 1 ? 'weeks' : 'week'}
-
-
-
- {[1, 2, 3, 4, 5, 6].map(weeks => (
- setDuration(weeks)}>
- {weeks} {weeks > 1 ? 'weeks' : 'week'}
-
- ))}
-
-
-
- )
-}
-
-render( )
-```
-
-### With External Anchor
-
-To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass `open` and `onOpenChange` along with an `anchorRef` to `DropdownMenu`:
-
-```javascript live noinline drafts
-const Example = () => {
- const [open, setOpen] = React.useState(false)
- const anchorRef = React.createRef()
-
- return (
- <>
- setOpen(!open)}>
- {open ? 'Close Menu' : 'Open Menu'}
-
-
-
-
-
- Text
- Number
- Date
- Iteration
-
-
-
- >
- )
-}
-
-render( )
-```
-
-### With Overlay Props
-
-```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 handleEscape = () => alert('you hit escape!')
-
- const [selectedIndex, setSelectedIndex] = React.useState(1)
- const selectedType = fieldTypes[selectedIndex]
-
- return (
-
-
- {selectedType.name}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- )
-}
-
-render( )
-```
-
-### With a custom anchor
-
-You can choose to have a different _anchor_ for the Menu dependending on the application's context.
-
-
-
-```javascript live noinline drafts
-render(
-
-
- Select a field type
-
-
-
-
- Text
- Number
- Date
- Iteration
-
-
-
-)
-```
-
-
-
-Use `DropdownMenu` to select an option from a small list. If you’re looking for filters or multiple selection, use [SelectPanel](/SelectPanel) instead.
-
-
-
-## Props
-
-### DropdownMenu
-
-
-
- Recommended: DropdownMenu.Button or DropdownMenu.Anchor with{' '}
- DropdownMenu.Overlay
- >
- }
- />
-
- If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
- onOpenChange
- >
- }
- />
-
- If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
- open
- >
- }
- />
-
-
-
-### DropdownMenu.Button
-
-
-
- ButtonProps
- >
- }
- description={
- <>
- You can pass all of the props that you would pass to a{' '}
-
- Button
- {' '}
- component like variant , leadingIcon ,{' '}
- sx , etc.
- >
- }
- />
-
-
-### DropdownMenu.Anchor
-
-
-
-
-
-### DropdownMenu.Overlay
-
-
-
- Recommended:{' '}
-
- ActionList
-
- >
- }
- />
-
- Props to be spread on the internal{' '}
-
- AnchoredOverlay
-
- >
- }
- />
-
-
-## Status
-
-
-
-## Further reading
-
-[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list)
-
-## Related components
-
-- [ActionList](/drafts/ActionList2)
-- [ActionMenu](/ActionMenu2)
-- [SelectPanel](/SelectPanel)
diff --git a/src/ActionList2/Selection.tsx b/src/ActionList2/Selection.tsx
index bb938ffb83c..159b4d38e40 100644
--- a/src/ActionList2/Selection.tsx
+++ b/src/ActionList2/Selection.tsx
@@ -10,7 +10,7 @@ type SelectionProps = Pick
export const Selection: React.FC = ({selected}) => {
const {selectionVariant: listSelectionVariant} = React.useContext(ListContext)
const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
- const {container, selectionVariant: menuSelectionVariant} = React.useContext(ActionListContainerContext)
+ const {selectionVariant: menuSelectionVariant} = React.useContext(ActionListContainerContext)
/** selectionVariant in Group can override the selectionVariant in List root */
/** fallback to selectionVariant from container menu if any (ActionMenu, DropdownMenu, SelectPanel ) */
@@ -28,18 +28,6 @@ export const Selection: React.FC = ({selected}) => {
return null
}
- if (container === 'ActionMenu') {
- throw new Error(
- 'ActionList cannot have a selectionVariant inside ActionMenu, please use DropdownMenu or SelectPanel instead. More information: https://primer.style/design/components/action-list#application'
- )
- }
-
- if (container === 'DropdownMenu' && selectionVariant === 'multiple') {
- throw new Error(
- 'selectionVariant multiple cannot be used in DropdownMenu, please use SelectPanel instead. More information: https://primer.style/design/components/action-list#application'
- )
- }
-
if (selectionVariant === 'single') {
return {selected && }
}
From 74fa5adedebb910125d8f70c9bb0c0548af4344e Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 15:13:14 +0100
Subject: [PATCH 02/14] merge stories/examples
---
src/stories/ActionMenu2/examples.stories.tsx | 229 +++++++++++++++-
.../DropdownMenu2/examples.stories.tsx | 246 ------------------
2 files changed, 223 insertions(+), 252 deletions(-)
delete mode 100644 src/stories/DropdownMenu2/examples.stories.tsx
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu2/examples.stories.tsx
index 0130751049a..04b27b58b22 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu2/examples.stories.tsx
@@ -1,9 +1,20 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider} from '../..'
-import BaseStyles from '../../BaseStyles'
-import {ActionMenu} from '../../ActionMenu2'
-import {ActionList} from '../../ActionList2'
+import {ThemeProvider, BaseStyles, Box, Text} from '../..'
+import {ActionMenu, ActionList} from '../../drafts'
+import {
+ GearIcon,
+ MilestoneIcon,
+ CalendarIcon,
+ IterationsIcon,
+ NumberIcon,
+ SingleSelectIcon,
+ TypographyIcon,
+ IssueOpenedIcon,
+ TableIcon,
+ PeopleIcon,
+ XIcon
+} from '@primer/octicons-react'
const meta: Meta = {
title: 'Composite components/ActionMenu2/examples',
@@ -25,7 +36,7 @@ const meta: Meta = {
}
export default meta
-export function SimpleListStory(): JSX.Element {
+export function MenuWithActions(): JSX.Element {
const [actionFired, fireAction] = React.useState('')
const onSelect = (name: string) => fireAction(name)
@@ -60,4 +71,210 @@ export function SimpleListStory(): JSX.Element {
>
)
}
-SimpleListStory.storyName = 'Simple Menu'
+
+const fieldTypes = [
+ {icon: TypographyIcon, name: 'Text'},
+ {icon: NumberIcon, name: 'Number'},
+ {icon: CalendarIcon, name: 'Date'},
+ {icon: SingleSelectIcon, name: 'Single select'},
+ {icon: IterationsIcon, name: 'Iteration'}
+]
+
+export function SingleSelection(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+ return (
+ <>
+ Single Selection
+
+ This pattern has a single section with the selected value shown in the button
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+
+export function SingleSelectionWithPlaceholder(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(-1)
+ const selectedType = fieldTypes[selectedIndex] || {}
+
+ return (
+ <>
+ With placeholder
+
+ This pattern has a placeholder in menu button when no value is selected yet
+
+
+
+ {selectedType.name || 'Pick a field type'}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+
+const milestones = [
+ {name: 'FY21 - Q2', due: 'December 31, 2021', progress: 90},
+ {name: 'FY22 - Q3', due: 'March 31, 2022', progress: 10},
+ {name: 'FY23 - Q1', due: 'June 30, 2022', progress: 0},
+ {name: 'FY23 - Q2', due: 'December 30, 2022', progress: 0}
+]
+
+export function GroupsAndDescription(): JSX.Element {
+ const [selectedMilestone, setSelectedMilestone] = React.useState()
+
+ return (
+ <>
+ Milestone selector
+
+
+
+ Milestone
+
+
+
+
+ {milestones
+ .filter(milestone => !milestone.name.includes('21'))
+ .map((milestone, index) => (
+ setSelectedMilestone(milestone)}
+ >
+
+
+
+ {milestone.name}
+ Due by {milestone.due}
+
+ ))}
+
+
+ {milestones
+ .filter(milestone => milestone.name.includes('21'))
+ .map((milestone, index) => (
+ setSelectedMilestone(milestone)}
+ >
+
+
+
+ {milestone.name}
+ Due by {milestone.due}
+
+ ))}
+
+
+
+
+ {selectedMilestone ? (
+
+ {selectedMilestone.name}
+
+ ) : (
+
+ No milestone
+
+ )}
+
+ >
+ )
+}
+
+export function MixedSelection(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(1)
+
+ const options = [
+ {text: 'Status', icon: IssueOpenedIcon},
+ {text: 'Stage', icon: TableIcon},
+ {text: 'Assignee', icon: PeopleIcon},
+ {text: 'Team', icon: TypographyIcon},
+ {text: 'Estimate', icon: NumberIcon},
+ {text: 'Due Date', icon: CalendarIcon}
+ ]
+
+ const selectedOption = selectedIndex !== null && options[selectedIndex]
+
+ return (
+ <>
+ List with mixed selection
+
+
+ In this list, there is a ActionList.Group with single selection for picking one option, followed by a Item that
+ is an action. This pattern appears inside a ActionMenu for selection view options in Memex
+
+
+
+
+ {selectedOption ? `Group by ${selectedOption.text}` : 'Group items by'}
+
+
+
+
+ {options.map((option, index) => (
+ setSelectedIndex(index)}
+ >
+
+
+
+ {option.text}
+
+ ))}
+
+ {typeof selectedIndex === 'number' && (
+
+
+ setSelectedIndex(null)} role="menuitem">
+
+
+
+ Clear Group by
+
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/src/stories/DropdownMenu2/examples.stories.tsx b/src/stories/DropdownMenu2/examples.stories.tsx
deleted file mode 100644
index a461dbbf47f..00000000000
--- a/src/stories/DropdownMenu2/examples.stories.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-import React from 'react'
-import {Meta} from '@storybook/react'
-import {ThemeProvider} from '../..'
-import BaseStyles from '../../BaseStyles'
-import {DropdownMenu} from '../../DropdownMenu2'
-import {ActionList} from '../../ActionList2'
-import Box from '../../Box'
-import Text from '../../Text'
-import {
- GearIcon,
- MilestoneIcon,
- CalendarIcon,
- IterationsIcon,
- NumberIcon,
- SingleSelectIcon,
- TypographyIcon,
- IssueOpenedIcon,
- TableIcon,
- PeopleIcon,
- XIcon
-} from '@primer/octicons-react'
-
-const meta: Meta = {
- title: 'Composite components/DropdownMenu2/examples',
- component: DropdownMenu,
- decorators: [
- (Story: React.ComponentType): JSX.Element => (
-
-
-
-
-
- )
- ],
- parameters: {
- controls: {
- disabled: true
- }
- }
-}
-export default meta
-
-const fieldTypes = [
- {icon: TypographyIcon, name: 'Text'},
- {icon: NumberIcon, name: 'Number'},
- {icon: CalendarIcon, name: 'Date'},
- {icon: SingleSelectIcon, name: 'Single select'},
- {icon: IterationsIcon, name: 'Iteration'}
-]
-
-export function SingleSelection(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(0)
- const selectedType = fieldTypes[selectedIndex]
- return (
- <>
- Simple Dropdown Menu
-
- This pattern is the classic dropdown menu - single section with the selected value shown in the button
-
-
-
- {selectedType.name}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-
-export function WithPlaceholder(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(-1)
- const selectedType = fieldTypes[selectedIndex] || {}
-
- return (
- <>
- With placeholder
-
- This pattern is the starting state of the dropdown menu with a placeholder when there is default value
-
-
-
- {selectedType.name || 'Pick a field type'}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-
-const milestones = [
- {name: 'FY21 - Q2', due: 'December 31, 2021', progress: 90},
- {name: 'FY22 - Q3', due: 'March 31, 2022', progress: 10},
- {name: 'FY23 - Q1', due: 'June 30, 2022', progress: 0},
- {name: 'FY23 - Q2', due: 'December 30, 2022', progress: 0}
-]
-
-export function GroupsAndDescription(): JSX.Element {
- const [selectedMilestone, setSelectedMilestone] = React.useState()
-
- return (
- <>
- Milestone selector
-
-
-
- Milestone
-
-
-
-
- {milestones
- .filter(milestone => !milestone.name.includes('21'))
- .map((milestone, index) => (
- setSelectedMilestone(milestone)}
- >
-
-
-
- {milestone.name}
- Due by {milestone.due}
-
- ))}
-
-
- {milestones
- .filter(milestone => milestone.name.includes('21'))
- .map((milestone, index) => (
- setSelectedMilestone(milestone)}
- >
-
-
-
- {milestone.name}
- Due by {milestone.due}
-
- ))}
-
-
-
-
- {selectedMilestone ? (
-
- {selectedMilestone.name}
-
- ) : (
-
- No milestone
-
- )}
-
- >
- )
-}
-
-export function MixedSelection(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(1)
-
- const options = [
- {text: 'Status', icon: IssueOpenedIcon},
- {text: 'Stage', icon: TableIcon},
- {text: 'Assignee', icon: PeopleIcon},
- {text: 'Team', icon: TypographyIcon},
- {text: 'Estimate', icon: NumberIcon},
- {text: 'Due Date', icon: CalendarIcon}
- ]
-
- const selectedOption = selectedIndex && options[selectedIndex]
-
- return (
- <>
- List with mixed selection
-
-
- In this list, there is a ActionList.Group with single selection for picking one option, followed by a Item that
- is an action. This pattern appears inside a DropdownMenu for selection view options in Memex
-
-
-
-
- {selectedOption ? `Group by ${selectedOption.text}` : 'Group items by'}
-
-
-
-
- {options.map((option, index) => (
- setSelectedIndex(index)}
- >
- {option.icon}
- {option.text}
-
- ))}
-
- {typeof selectedIndex === 'number' && (
-
-
- setSelectedIndex(null)} role="menuitem">
-
-
-
- Clear Group by
-
-
- )}
-
-
-
- >
- )
-}
From e2513ea7872ae7037fe442b4d87769d962c6ab94 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 15:26:34 +0100
Subject: [PATCH 03/14] merge stories/fixtures
---
src/stories/ActionMenu2/fixtures.stories.tsx | 170 ++++++---
.../DropdownMenu2/fixtures.stories.tsx | 335 ------------------
2 files changed, 116 insertions(+), 389 deletions(-)
delete mode 100644 src/stories/DropdownMenu2/fixtures.stories.tsx
diff --git a/src/stories/ActionMenu2/fixtures.stories.tsx b/src/stories/ActionMenu2/fixtures.stories.tsx
index 3fbf73013dc..0d1b8f3d303 100644
--- a/src/stories/ActionMenu2/fixtures.stories.tsx
+++ b/src/stories/ActionMenu2/fixtures.stories.tsx
@@ -1,16 +1,7 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider} from '../..'
-import BaseStyles from '../../BaseStyles'
-import {ActionMenu} from '../../ActionMenu2'
-import {ActionList} from '../../ActionList2'
-import {Button} from '../../Button2'
-import {IconButton} from '../../Button2/IconButton'
-import Box from '../../Box'
-import Text from '../../Text'
-import TextInput from '../../TextInput'
-import StyledOcticon from '../../StyledOcticon'
-import FormGroup from '../../FormGroup'
+import {ThemeProvider, BaseStyles, Box, Text, TextInput, StyledOcticon, FormGroup, ProgressBar} from '../..'
+import {ActionMenu, ActionList, Button, IconButton} from '../../drafts'
import {
ServerIcon,
PlusCircleIcon,
@@ -25,6 +16,11 @@ import {
SearchIcon,
VersionsIcon,
TableIcon,
+ CalendarIcon,
+ IterationsIcon,
+ NumberIcon,
+ SingleSelectIcon,
+ TypographyIcon,
IconProps
} from '@primer/octicons-react'
@@ -86,7 +82,6 @@ export function ActionsStory(): JSX.Element {
>
)
}
-ActionsStory.storyName = 'Actions'
export function ExternalAnchor(): JSX.Element {
const [actionFired, fireAction] = React.useState('')
@@ -133,7 +128,6 @@ export function ExternalAnchor(): JSX.Element {
>
)
}
-ExternalAnchor.storyName = 'External Anchor'
export function ControlledMenu(): JSX.Element {
const [actionFired, fireAction] = React.useState('')
@@ -191,7 +185,6 @@ export function ControlledMenu(): JSX.Element {
>
)
}
-ControlledMenu.storyName = 'Controlled Menu'
export function CustomAnchor(): JSX.Element {
const [actionFired, fireAction] = React.useState('')
@@ -233,7 +226,6 @@ export function CustomAnchor(): JSX.Element {
>
)
}
-CustomAnchor.storyName = 'Custom Anchor'
export function MemexTableMenu(): JSX.Element {
const [name, setName] = React.useState('Estimate')
@@ -297,7 +289,6 @@ export function MemexTableMenu(): JSX.Element {
>
)
}
-MemexTableMenu.storyName = 'Memex Table Menu'
/* copied from github/memex */
const LayoutToggleItem = ({
@@ -408,19 +399,15 @@ export function MemexViewOptionsMenu(): JSX.Element {
React
-
-
-
+
+
+
@@ -495,7 +482,105 @@ export function MemexViewOptionsMenu(): JSX.Element {
>
)
}
-MemexViewOptionsMenu.storyName = 'Memex View Options Menu'
+
+export function MemexIteration(): JSX.Element {
+ const [duration, setDuration] = React.useState(1)
+
+ return (
+ <>
+ Memex Iteration Menu
+
+
+
+ {duration} {duration > 1 ? 'weeks' : 'week'}
+
+
+
+ {[1, 2, 3, 4, 5, 6].map(weeks => (
+ setDuration(weeks)}>
+ {weeks} {weeks > 1 ? 'weeks' : 'week'}
+
+ ))}
+
+
+
+ >
+ )
+}
+
+const fieldTypes = [
+ {icon: TypographyIcon, name: 'Text'},
+ {icon: NumberIcon, name: 'Number'},
+ {icon: CalendarIcon, name: 'Date'},
+ {icon: SingleSelectIcon, name: 'Single select'},
+ {icon: IterationsIcon, name: 'Iteration'}
+]
+
+export function MemexAddColumn(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+
+ const [duration, setDuration] = React.useState(1)
+
+ return (
+ <>
+ Memex Add column
+
+
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}
+ >
+ {type.icon} {type.name}
+
+ ))}
+
+
+
+ Options
+
+
+ Duration:
+
+
+ {duration} {duration > 1 ? 'weeks' : 'week'}
+
+
+
+ {[1, 2, 3, 4, 5, 6].map(weeks => (
+ setDuration(weeks)}>
+ {weeks} {weeks > 1 ? 'weeks' : 'week'}
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
export function OverlayProps(): JSX.Element {
const [open, setOpen] = React.useState(false)
@@ -534,30 +619,7 @@ export function OverlayProps(): JSX.Element {
-
- >
- )
-}
-OverlayProps.storyName = 'Overlay Props'
-
-export function UnexpectedSelectionVariant(): JSX.Element {
- return (
- <>
- Expect error if selectionVariant is passed
-
-
- Menu
-
-
- Copy link
- Quote reply
- Edit comment
-
- Delete file
-
-
-
+
>
)
}
-UnexpectedSelectionVariant.storyName = 'Unexpected selectionVariant'
diff --git a/src/stories/DropdownMenu2/fixtures.stories.tsx b/src/stories/DropdownMenu2/fixtures.stories.tsx
deleted file mode 100644
index d39782e46c1..00000000000
--- a/src/stories/DropdownMenu2/fixtures.stories.tsx
+++ /dev/null
@@ -1,335 +0,0 @@
-import React from 'react'
-import {Meta} from '@storybook/react'
-import {ThemeProvider} from '../..'
-import BaseStyles from '../../BaseStyles'
-import {DropdownMenu} from '../../DropdownMenu2'
-import {ActionList} from '../../ActionList2'
-import {Button} from '../../Button2'
-import Box from '../../Box'
-import Text from '../../Text'
-import TextInput from '../../TextInput'
-import ProgressBar from '../../ProgressBar'
-import {
- GearIcon,
- MilestoneIcon,
- CalendarIcon,
- IterationsIcon,
- NumberIcon,
- SingleSelectIcon,
- TypographyIcon
-} from '@primer/octicons-react'
-
-const meta: Meta = {
- title: 'Composite components/DropdownMenu2/fixtures',
- component: DropdownMenu,
- decorators: [
- (Story: React.ComponentType): JSX.Element => (
-
-
-
-
-
- )
- ],
- parameters: {
- controls: {
- disabled: true
- }
- }
-}
-export default meta
-
-const fieldTypes = [
- {icon: TypographyIcon, name: 'Text'},
- {icon: NumberIcon, name: 'Number'},
- {icon: CalendarIcon, name: 'Date'},
- {icon: SingleSelectIcon, name: 'Single select'},
- {icon: IterationsIcon, name: 'Iteration'}
-]
-
-export function SimpleDropdownMenu(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(0)
- const selectedType = fieldTypes[selectedIndex]
- return (
- <>
- Simple Dropdown Menu
-
-
-
- {selectedType.name}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-SimpleDropdownMenu.storyName = 'Simple DropdownMenu'
-
-export function Placeholder(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(-1)
- const selectedType = fieldTypes[selectedIndex] || {}
-
- return (
- <>
- With placeholder
-
-
-
- {selectedType.name || 'Pick a field type'}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-Placeholder.storyName = 'Placeholder'
-
-export function MemexIteration(): JSX.Element {
- const [duration, setDuration] = React.useState(1)
-
- return (
- <>
- Memex Iteration Menu
-
-
-
- {duration} {duration > 1 ? 'weeks' : 'week'}
-
-
-
- {[1, 2, 3, 4, 5, 6].map(weeks => (
- setDuration(weeks)}>
- {weeks} {weeks > 1 ? 'weeks' : 'week'}
-
- ))}
-
-
-
- >
- )
-}
-MemexIteration.storyName = 'Memex Iteration Menu'
-
-const milestones = [
- {name: 'v29.2.0', due: 'September 30, 2021', progress: 95},
- {name: 'v30.0.0', due: 'December 1, 2021', progress: 40},
- {name: 'FY22 - Q3', due: 'December 31, 2021', progress: 10}
-]
-
-export function MemexAddColumn(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(0)
- const selectedType = fieldTypes[selectedIndex]
-
- const [duration, setDuration] = React.useState(1)
-
- return (
- <>
- Memex Add column
-
-
-
-
-
- {selectedType.name}
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}
- >
- {type.icon} {type.name}
-
- ))}
-
-
-
- Options
-
-
- Duration:
-
-
- {duration} {duration > 1 ? 'weeks' : 'week'}
-
-
-
- {[1, 2, 3, 4, 5, 6].map(weeks => (
- setDuration(weeks)}>
- {weeks} {weeks > 1 ? 'weeks' : 'week'}
-
- ))}
-
-
-
-
-
- >
- )
-}
-MemexAddColumn.storyName = 'Memex Add Column'
-
-export function MilestoneStory(): JSX.Element {
- const [selectedIndex, setSelectedIndex] = React.useState(-1)
-
- const selectedMilestone = milestones[selectedIndex] as typeof milestones[0] | undefined
-
- return (
- <>
- Milestone selector
-
-
-
-
-
- Milestone
-
-
-
-
-
- {milestones.map((milestone, index) => (
- setSelectedIndex(index)}
- >
-
-
-
- {milestone.name}
- Due by {milestone.due}
-
- ))}
-
-
-
- {selectedMilestone ? (
-
-
- {selectedMilestone.name}
-
- ) : (
-
- No milestone
-
- )}
-
- >
- )
-}
-MilestoneStory.storyName = 'Milestone selector'
-
-export function ControlledMenu(): JSX.Element {
- const [open, setOpen] = React.useState(false)
- const [selectedIndex, setSelectedIndex] = React.useState(0)
-
- return (
- <>
- Controlled Menu
-
- External Open State: {open ? 'Open' : 'Closed'}
- selected Value: {fieldTypes[selectedIndex].name}
-
-
- setOpen(!open)}>{open ? 'Close Menu' : 'Open Menu'}
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-ControlledMenu.storyName = 'Controlled Menu'
-
-export function ExternalAnchor(): JSX.Element {
- const [open, setOpen] = React.useState(false)
- const triggerRef = React.createRef()
- const anchorRef = React.createRef()
-
- const [selectedIndex, setSelectedIndex] = React.useState(0)
-
- return (
- <>
- External Anchor
-
- External Open State: {open ? 'Open' : 'Closed'}
- selected Value: {fieldTypes[selectedIndex].name}
-
- setOpen(!open)}>
- {open ? 'Close Menu' : 'Open Menu'}
-
-
-
- Anchored on me!
-
-
-
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
- {type.name}
-
- ))}
-
-
-
- >
- )
-}
-ExternalAnchor.storyName = 'External Anchor'
From b53f87a20850ae5280d77bca7ddd44cf13cf0a52 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 15:41:31 +0100
Subject: [PATCH 04/14] move itemRole to item and delete dropdonwmenu
---
.../ActionListContainerContext.tsx | 2 -
src/ActionList2/Item.tsx | 20 ++-
src/ActionList2/Selection.tsx | 20 +--
src/ActionMenu2.tsx | 2 +-
src/DropdownMenu2.tsx | 135 ------------------
src/drafts/index.ts | 1 -
src/utils/types/AriaRole.ts | 2 +-
7 files changed, 29 insertions(+), 153 deletions(-)
delete mode 100644 src/DropdownMenu2.tsx
diff --git a/src/ActionList2/ActionListContainerContext.tsx b/src/ActionList2/ActionListContainerContext.tsx
index 23cc31e9720..53e703d09d8 100644
--- a/src/ActionList2/ActionListContainerContext.tsx
+++ b/src/ActionList2/ActionListContainerContext.tsx
@@ -5,8 +5,6 @@ import React from 'react'
type ContextProps = {
container?: string
listRole?: string
- itemRole?: string
- selectionVariant?: 'single' | 'multiple'
selectionAttribute?: 'aria-selected' | 'aria-checked'
listLabelledBy?: string
// This can be any function, we don't know anything about the arguments
diff --git a/src/ActionList2/Item.tsx b/src/ActionList2/Item.tsx
index ed93839c053..d90c1b74331 100644
--- a/src/ActionList2/Item.tsx
+++ b/src/ActionList2/Item.tsx
@@ -7,7 +7,8 @@ import Box, {BoxProps} from '../Box'
import sx, {SxProp, merge} from '../sx'
import createSlots from '../utils/create-slots'
import {AriaRole} from '../utils/types'
-import {ListContext} from './List'
+import {ListContext, ListProps} from './List'
+import {GroupContext, GroupProps} from './Group'
import {ActionListContainerContext} from './ActionListContainerContext'
import {Selection} from './Selection'
@@ -101,8 +102,21 @@ export const Item = React.forwardRef(
},
forwardedRef
): JSX.Element => {
- const {variant: listVariant, showDividers} = React.useContext(ListContext)
- const {itemRole, afterSelect, selectionAttribute = 'aria-selected'} = React.useContext(ActionListContainerContext)
+ const {variant: listVariant, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext)
+ const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
+ const {container, afterSelect, selectionAttribute = 'aria-selected'} = 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') {
+ if (selectionVariant === 'single') itemRole = 'menuitemradio'
+ else if (selectionVariant === 'multiple') itemRole = 'menuitemcheckbox'
+ else itemRole = 'menuitem'
+ }
const {theme} = useTheme()
diff --git a/src/ActionList2/Selection.tsx b/src/ActionList2/Selection.tsx
index 159b4d38e40..d7f3855b51f 100644
--- a/src/ActionList2/Selection.tsx
+++ b/src/ActionList2/Selection.tsx
@@ -1,8 +1,7 @@
import React from 'react'
import {CheckIcon} from '@primer/octicons-react'
-import {ListContext} from './List'
-import {GroupContext} from './Group'
-import {ActionListContainerContext} from './ActionListContainerContext'
+import {ListContext, ListProps} from './List'
+import {GroupContext, GroupProps} from './Group'
import {ItemProps} from './Item'
import {LeadingVisualContainer} from './Visuals'
@@ -10,22 +9,23 @@ type SelectionProps = Pick
export const Selection: React.FC = ({selected}) => {
const {selectionVariant: listSelectionVariant} = React.useContext(ListContext)
const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
- const {selectionVariant: menuSelectionVariant} = React.useContext(ActionListContainerContext)
/** selectionVariant in Group can override the selectionVariant in List root */
/** fallback to selectionVariant from container menu if any (ActionMenu, DropdownMenu, SelectPanel ) */
- let selectionVariant
+ let selectionVariant: ListProps['selectionVariant'] | GroupProps['selectionVariant']
if (typeof groupSelectionVariant !== 'undefined') selectionVariant = groupSelectionVariant
- else selectionVariant = listSelectionVariant || menuSelectionVariant
+ else selectionVariant = listSelectionVariant
- // if selectionVariant is not set on List, don't show selection
if (!selectionVariant) {
- // to avoid confusion, fail loudly instead of silently ignoring
- if (selected)
+ // if selectionVariant is not set on List, but Item is selected
+ // fail loudly instead of silently ignoring
+ if (selected) {
throw new Error(
'For Item to be selected, ActionList or ActionList.Group needs to have a selectionVariant defined'
)
- return null
+ } else {
+ return null
+ }
}
if (selectionVariant === 'single') {
diff --git a/src/ActionMenu2.tsx b/src/ActionMenu2.tsx
index d01a04f7a13..5819eeca3d6 100644
--- a/src/ActionMenu2.tsx
+++ b/src/ActionMenu2.tsx
@@ -113,8 +113,8 @@ const Overlay: React.FC = ({children, ...overlayProps}) => {
value={{
container: 'ActionMenu',
listRole: 'menu',
- itemRole: 'menuitem',
listLabelledBy: anchorId,
+ selectionAttribute: 'aria-checked', // Should this be here?
afterSelect: onClose
}}
>
diff --git a/src/DropdownMenu2.tsx b/src/DropdownMenu2.tsx
deleted file mode 100644
index 20f48e6e032..00000000000
--- a/src/DropdownMenu2.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React from 'react'
-import {useSSRSafeId} from '@react-aria/ssr'
-import {TriangleDownIcon} from '@primer/octicons-react'
-import {Button, ButtonProps} from './Button2'
-import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay'
-import {OverlayProps} from './Overlay'
-import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus} from './hooks'
-import {Divider} from './ActionList2/Divider'
-import {ActionListContainerContext} from './ActionList2/ActionListContainerContext'
-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 DropdownMenuProps = {
- /**
- * 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
-}: DropdownMenuProps) => {
- 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 DropdownMenuAnchorProps = {children: React.ReactElement}
-const Anchor = React.forwardRef(
- ({children, ...anchorProps}, anchorRef) => {
- return React.cloneElement(children, {...anchorProps, ref: anchorRef})
- }
-)
-
-/** this component is syntactical sugar 🍭 */
-export type DropdownMenuButtonProps = ButtonProps
-const MenuButton = React.forwardRef(
- ({children, ...props}, anchorRef) => {
- return (
-
-
- {children}
-
-
- )
- }
-)
-
-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, openWithFocus} = useMenuInitialFocus(open, onOpen)
-
- return (
-
-
-
- )
-}
-
-Menu.displayName = 'ActionMenu'
-export const DropdownMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider})
diff --git a/src/drafts/index.ts b/src/drafts/index.ts
index 291e56dafbb..cbef05e7eaa 100644
--- a/src/drafts/index.ts
+++ b/src/drafts/index.ts
@@ -9,6 +9,5 @@
export * from '../ActionList2'
export * from '../Button2'
export * from '../ActionMenu2'
-export * from '../DropdownMenu2'
export * from '../Label2'
export * from '../PageLayout'
diff --git a/src/utils/types/AriaRole.ts b/src/utils/types/AriaRole.ts
index 87471c6c67f..b1d1d1f4914 100644
--- a/src/utils/types/AriaRole.ts
+++ b/src/utils/types/AriaRole.ts
@@ -35,7 +35,7 @@ export type AriaRole =
| 'menu'
| 'menubar'
| 'menuitem'
- | 'menuitemcheckbox '
+ | 'menuitemcheckbox'
| 'menuitemradio'
| 'navigation'
| 'none'
From 24c1cbf29b1e0cbaf8436fbb11fc64f7f56c66c3 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 16:31:38 +0100
Subject: [PATCH 05/14] add test for single selection
---
src/ActionList2/Selection.tsx | 2 +-
src/__tests__/ActionMenu2.test.tsx | 86 +++++++-----
src/__tests__/DropdownMenu2.test.tsx | 131 -------------------
src/stories/ActionList2/examples.stories.tsx | 4 +-
src/stories/ActionMenu2/examples.stories.tsx | 69 +++++++++-
src/stories/ActionMenu2/fixtures.stories.tsx | 2 +-
6 files changed, 123 insertions(+), 171 deletions(-)
delete mode 100644 src/__tests__/DropdownMenu2.test.tsx
diff --git a/src/ActionList2/Selection.tsx b/src/ActionList2/Selection.tsx
index d7f3855b51f..acc86509893 100644
--- a/src/ActionList2/Selection.tsx
+++ b/src/ActionList2/Selection.tsx
@@ -11,7 +11,7 @@ export const Selection: React.FC = ({selected}) => {
const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
/** selectionVariant in Group can override the selectionVariant in List root */
- /** fallback to selectionVariant from container menu if any (ActionMenu, DropdownMenu, SelectPanel ) */
+ /** fallback to selectionVariant from container menu if any (ActionMenu, SelectPanel ) */
let selectionVariant: ListProps['selectionVariant'] | GroupProps['selectionVariant']
if (typeof groupSelectionVariant !== 'undefined') selectionVariant = groupSelectionVariant
else selectionVariant = listSelectionVariant
diff --git a/src/__tests__/ActionMenu2.test.tsx b/src/__tests__/ActionMenu2.test.tsx
index 88f9d35b6d9..d4a51478f91 100644
--- a/src/__tests__/ActionMenu2.test.tsx
+++ b/src/__tests__/ActionMenu2.test.tsx
@@ -4,13 +4,13 @@ import {axe, toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
import {ActionMenu} from '../ActionMenu2'
-import {ActionList} from '../ActionList2'
+import {ActionList, ActionListProps} from '../ActionList2'
import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
import {BaseStyles, ThemeProvider, SSRProvider} from '..'
import '@testing-library/jest-dom'
expect.extend(toHaveNoViolations)
-function SimpleActionMenu(): JSX.Element {
+function Example(): JSX.Element {
return (
@@ -35,11 +35,42 @@ function SimpleActionMenu(): JSX.Element {
)
}
+function WithSelection({selectionVariant}: {selectionVariant?: ActionListProps['selectionVariant']}): JSX.Element {
+ const fieldTypes = ['Text', 'Number', 'Date', 'Single select', 'Iteration']
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+
+ return (
+
+
+
+
+ {selectedType}
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}
+ >
+ {selectedType}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
describe('ActionMenu', () => {
behavesAsComponent({
Component: ActionList,
options: {skipAs: true, skipSx: true},
- toRender: () =>
+ toRender: () =>
})
checkExports('ActionMenu2', {
@@ -48,7 +79,7 @@ describe('ActionMenu', () => {
})
it('should open Menu on MenuButton click', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender( )
const button = component.getByText('Toggle Menu')
fireEvent.click(button)
expect(component.getByRole('menu')).toBeInTheDocument()
@@ -56,7 +87,7 @@ describe('ActionMenu', () => {
})
it('should open Menu on MenuButton keypress', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender( )
const button = component.getByText('Toggle Menu')
// We pass keycode here to navigate a implementation detail in react-testing-library
@@ -67,7 +98,7 @@ describe('ActionMenu', () => {
})
it('should close Menu on selecting an action with click', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender( )
const button = component.getByText('Toggle Menu')
fireEvent.click(button)
@@ -79,7 +110,7 @@ describe('ActionMenu', () => {
})
it('should close Menu on selecting an action with Enter', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender( )
const button = component.getByText('Toggle Menu')
fireEvent.click(button)
@@ -91,7 +122,7 @@ describe('ActionMenu', () => {
})
it('should not close Menu if event is prevented', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender( )
const button = component.getByText('Toggle Menu')
fireEvent.click(button)
@@ -103,38 +134,23 @@ describe('ActionMenu', () => {
cleanup()
})
- it('should throw when selectionVariant is provided to ActionList within ActionMenu', 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(() => {
- const component = HTMLRender(
-
-
-
-
- Toggle Menu
-
-
- Primer React
-
-
-
-
-
-
- )
-
- const button = component.getByText('Toggle Menu')
- fireEvent.click(button)
- }).toThrow('ActionList cannot have a selectionVariant inside ActionMenu')
+ 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()
- mockError.mockRestore()
})
it('should have no axe violations', async () => {
- const {container} = HTMLRender( )
+ const {container} = HTMLRender( )
const results = await axe(container)
expect(results).toHaveNoViolations()
cleanup()
diff --git a/src/__tests__/DropdownMenu2.test.tsx b/src/__tests__/DropdownMenu2.test.tsx
deleted file mode 100644
index 1379cecd6ed..00000000000
--- a/src/__tests__/DropdownMenu2.test.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import {cleanup, render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
-import 'babel-polyfill'
-import {toHaveNoViolations} from 'jest-axe'
-import React from 'react'
-import theme from '../theme'
-import {DropdownMenu} from '../DropdownMenu2'
-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 SimpleDropdownMenu(): JSX.Element {
- const fieldTypes = [{name: 'Text'}, {name: 'Number'}, {name: 'Date'}, {name: 'Single select'}, {name: 'Iteration'}]
- const [selectedIndex, setSelectedIndex] = React.useState(0)
- const selectedType = fieldTypes[selectedIndex]
-
- return (
-
-
-
-
- {selectedType.name}
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}
- >
- {type.name}
-
- ))}
-
-
-
-
-
-
- )
-}
-
-describe('DropdownMenu', () => {
- behavesAsComponent({
- Component: ActionList,
- options: {skipAs: true, skipSx: true},
- toRender: () =>
- })
-
- checkExports('DropdownMenu2', {
- default: undefined,
- DropdownMenu
- })
-
- it('should open Menu on Button click', async () => {
- const component = HTMLRender( )
- const button = component.getByLabelText('Select field type')
- fireEvent.click(button)
- expect(component.getByRole('menu')).toBeInTheDocument()
- cleanup()
- })
-
- it('should open Menu on Button keypress', async () => {
- const component = HTMLRender( )
- const button = component.getByLabelText('Select field type')
-
- // 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.getByLabelText('Select field type')
-
- fireEvent.click(button)
- const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
- 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.getByLabelText('Select field type')
-
- fireEvent.click(button)
- const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
- fireEvent.keyPress(menuItems[0], {key: 'Enter', charCode: 13})
- expect(component.queryByRole('menu')).toBeNull()
-
- cleanup()
- })
-
- it('should throw when selectionVariant=multiple is provided to ActionList within DropdownMenu', 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(() => {
- const component = HTMLRender(
-
-
-
-
- Select a field
-
-
- Primer React
-
-
-
-
-
-
- )
-
- const button = component.getByText('Select a field')
- fireEvent.click(button)
- }).toThrow('selectionVariant multiple cannot be used in DropdownMenu')
-
- cleanup()
- mockError.mockRestore()
- })
-
- checkStoriesForAxeViolations('DropdownMenu2/fixtures')
- checkStoriesForAxeViolations('DropdownMenu2/examples')
-})
diff --git a/src/stories/ActionList2/examples.stories.tsx b/src/stories/ActionList2/examples.stories.tsx
index 5f27efe6da5..3f8ed97b884 100644
--- a/src/stories/ActionList2/examples.stories.tsx
+++ b/src/stories/ActionList2/examples.stories.tsx
@@ -108,7 +108,7 @@ export function SingleSelection(): JSX.Element {
<>
Single Selection
- This pattern appears inside a nested DropdownMenu in Memex view options.
+ This pattern appears inside a nested menu in Memex view options.
{options.map((option, index) => (
@@ -266,7 +266,7 @@ export function MixedSelection(): JSX.Element {
In this list, there is a ActionList.Group with single selection for picking one option, followed by a Item that
- is an action. This pattern appears inside a DropdownMenu for selection view options in Memex
+ is an action. This pattern appears inside a menu for selection view options in Memex
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu2/examples.stories.tsx
index 04b27b58b22..de5139c5463 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu2/examples.stories.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider, BaseStyles, Box, Text} from '../..'
+import {ThemeProvider, BaseStyles, Box, Text, Avatar} from '../..'
import {ActionMenu, ActionList} from '../../drafts'
import {
GearIcon,
@@ -218,6 +218,73 @@ export function GroupsAndDescription(): JSX.Element {
)
}
+const users = [
+ {login: 'pksjce', name: 'Pavithra Kodmad'},
+ {login: 'jfuchs', name: 'Jonathan Fuchs'},
+ {login: 'colebemis', name: 'Cole Bemis'},
+ {login: 'mperrotti', name: 'Mike Perrotti'},
+ {login: 'dgreif', name: 'Dusty Greif'},
+ {login: 'smockle', name: 'Clay Miller'},
+ {login: 'siddharthkp', name: 'Siddharth Kshetrapal'}
+]
+
+export function MultipleSelection(): JSX.Element {
+ const [assignees, setAssignees] = React.useState(users.slice(0, 2))
+
+ const toggleAssignee = (assignee: typeof users[number]) => {
+ const assigneeIndex = assignees.findIndex(a => a.login === assignee.login)
+
+ if (assigneeIndex === -1) setAssignees([...assignees, assignee])
+ else setAssignees(assignees.filter((_, index) => index !== assigneeIndex))
+ }
+
+ return (
+ <>
+ Multi Select List
+
+ ActionMenu with multiple selection is not seen in production. You see SelectPanel used instead.
+
+
+
+
+ Assignees
+
+
+
+ {users.map(user => (
+ assignee.login === user.login))}
+ onSelect={() => toggleAssignee(user)}
+ >
+
+
+
+ {user.login}
+ {user.name}
+
+ ))}
+
+
+
+
+ >
+ )
+}
+
export function MixedSelection(): JSX.Element {
const [selectedIndex, setSelectedIndex] = React.useState(1)
diff --git a/src/stories/ActionMenu2/fixtures.stories.tsx b/src/stories/ActionMenu2/fixtures.stories.tsx
index 0d1b8f3d303..1a279b1d46b 100644
--- a/src/stories/ActionMenu2/fixtures.stories.tsx
+++ b/src/stories/ActionMenu2/fixtures.stories.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import {Meta} from '@storybook/react'
-import {ThemeProvider, BaseStyles, Box, Text, TextInput, StyledOcticon, FormGroup, ProgressBar} from '../..'
+import {ThemeProvider, BaseStyles, Box, Text, TextInput, StyledOcticon, FormGroup} from '../..'
import {ActionMenu, ActionList, Button, IconButton} from '../../drafts'
import {
ServerIcon,
From 25aede5877f5b02c059e0ccef2cf8e407c2f95b6 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 17:13:56 +0100
Subject: [PATCH 06/14] Add tests for additional use cases
---
src/__tests__/ActionMenu2.test.tsx | 55 ++++++++------------
src/stories/ActionMenu2/examples.stories.tsx | 12 ++++-
2 files changed, 32 insertions(+), 35 deletions(-)
diff --git a/src/__tests__/ActionMenu2.test.tsx b/src/__tests__/ActionMenu2.test.tsx
index d4a51478f91..20cfb9fcda2 100644
--- a/src/__tests__/ActionMenu2.test.tsx
+++ b/src/__tests__/ActionMenu2.test.tsx
@@ -4,9 +4,10 @@ import {axe, toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
import {ActionMenu} from '../ActionMenu2'
-import {ActionList, ActionListProps} from '../ActionList2'
+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)
@@ -35,37 +36,6 @@ function Example(): JSX.Element {
)
}
-function WithSelection({selectionVariant}: {selectionVariant?: ActionListProps['selectionVariant']}): JSX.Element {
- const fieldTypes = ['Text', 'Number', 'Date', 'Single select', 'Iteration']
- const [selectedIndex, setSelectedIndex] = React.useState(0)
- const selectedType = fieldTypes[selectedIndex]
-
- return (
-
-
-
-
- {selectedType}
-
-
- {fieldTypes.map((type, index) => (
- setSelectedIndex(index)}
- >
- {selectedType}
-
- ))}
-
-
-
-
-
-
- )
-}
-
describe('ActionMenu', () => {
behavesAsComponent({
Component: ActionList,
@@ -135,7 +105,11 @@ describe('ActionMenu', () => {
})
it('should be able to select an Item with selectionVariant', async () => {
- const component = HTMLRender( )
+ const component = HTMLRender(
+
+
+
+ )
const button = component.getByLabelText('Select field type')
fireEvent.click(button)
@@ -149,6 +123,21 @@ describe('ActionMenu', () => {
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)
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu2/examples.stories.tsx
index de5139c5463..3412c22e6e7 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu2/examples.stories.tsx
@@ -96,7 +96,12 @@ export function SingleSelection(): JSX.Element {
{fieldTypes.map((type, index) => (
- setSelectedIndex(index)}>
+ setSelectedIndex(index)}
+ disabled={index === 3}
+ >
{type.name}
))}
@@ -309,7 +314,10 @@ export function MixedSelection(): JSX.Element {
-
+
{selectedOption ? `Group by ${selectedOption.text}` : 'Group items by'}
From ae67737f121205d9eccb3339f9cf789fc65125f8 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 17:14:21 +0100
Subject: [PATCH 07/14] delete outdated snapshot
---
.../__snapshots__/DropdownMenu2.test.tsx.snap | 145 ------------------
1 file changed, 145 deletions(-)
delete mode 100644 src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
diff --git a/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap b/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
deleted file mode 100644
index d559b06cbf5..00000000000
--- a/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
+++ /dev/null
@@ -1,145 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DropdownMenu 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);
-}
-
-
-
-
- Text
-
-
- ",
- }
- }
- 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}
- />
-
-
-
-`;
From 766c2b1234f4b7e21dd21e112ca2511cc9568ff5 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 17:37:59 +0100
Subject: [PATCH 08/14] add changeset
---
.changeset/drafts-dropdownmenu2-merged-into-actionmenu2.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/drafts-dropdownmenu2-merged-into-actionmenu2.md
diff --git a/.changeset/drafts-dropdownmenu2-merged-into-actionmenu2.md b/.changeset/drafts-dropdownmenu2-merged-into-actionmenu2.md
new file mode 100644
index 00000000000..4bb977a8f0e
--- /dev/null
+++ b/.changeset/drafts-dropdownmenu2-merged-into-actionmenu2.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+Merges drafts/DropdownMenu2 into drafts/ActionMenu2
From 1e90bdaec2f9b5d969fa4821a49827a8af759345 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 17:56:18 +0100
Subject: [PATCH 09/14] fix group roles based on latest discussion
---
src/ActionList2/Group.tsx | 6 ++----
src/stories/ActionMenu2/examples.stories.tsx | 17 ++++++++---------
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/src/ActionList2/Group.tsx b/src/ActionList2/Group.tsx
index ff957aa7043..0f5ecbafb56 100644
--- a/src/ActionList2/Group.tsx
+++ b/src/ActionList2/Group.tsx
@@ -3,7 +3,6 @@ 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 = {
/**
@@ -24,7 +23,6 @@ export type GroupProps = {
/**
* 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.
@@ -40,7 +38,6 @@ export const Group: React.FC = ({
variant = 'subtle',
auxiliaryText,
selectionVariant,
- role,
sx = {},
...props
}) => {
@@ -49,6 +46,7 @@ export const Group: React.FC = ({
return (
= ({
>
{title && }
-
+
{props.children}
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu2/examples.stories.tsx
index 3412c22e6e7..c41634fc25d 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu2/examples.stories.tsx
@@ -171,8 +171,8 @@ export function GroupsAndDescription(): JSX.Element {
Milestone
-
-
+
+
{milestones
.filter(milestone => !milestone.name.includes('21'))
.map((milestone, index) => (
@@ -189,7 +189,7 @@ export function GroupsAndDescription(): JSX.Element {
))}
-
+
{milestones
.filter(milestone => milestone.name.includes('21'))
.map((milestone, index) => (
@@ -270,7 +270,6 @@ export function MultipleSelection(): JSX.Element {
{users.map(user => (
assignee.login === user.login))}
onSelect={() => toggleAssignee(user)}
@@ -313,7 +312,7 @@ export function MixedSelection(): JSX.Element {
is an action. This pattern appears inside a ActionMenu for selection view options in Memex
-
+
-
-
+
+
{options.map((option, index) => (
{typeof selectedIndex === 'number' && (
-
+
- setSelectedIndex(null)} role="menuitem">
+ setSelectedIndex(null)}>
From 9a4706dbc8040f359edad09d0d3f5a5c56a29ea1 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Wed, 9 Feb 2022 18:03:24 +0100
Subject: [PATCH 10/14] Revert "fix group roles based on latest discussion"
This reverts commit 1e90bdaec2f9b5d969fa4821a49827a8af759345.
---
src/ActionList2/Group.tsx | 6 ++++--
src/stories/ActionMenu2/examples.stories.tsx | 17 +++++++++--------
2 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/src/ActionList2/Group.tsx b/src/ActionList2/Group.tsx
index 0f5ecbafb56..ff957aa7043 100644
--- a/src/ActionList2/Group.tsx
+++ b/src/ActionList2/Group.tsx
@@ -3,6 +3,7 @@ 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 = {
/**
@@ -23,6 +24,7 @@ export type GroupProps = {
/**
* 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.
@@ -38,6 +40,7 @@ export const Group: React.FC = ({
variant = 'subtle',
auxiliaryText,
selectionVariant,
+ role,
sx = {},
...props
}) => {
@@ -46,7 +49,6 @@ export const Group: React.FC = ({
return (
= ({
>
{title && }
-
+
{props.children}
diff --git a/src/stories/ActionMenu2/examples.stories.tsx b/src/stories/ActionMenu2/examples.stories.tsx
index c41634fc25d..3412c22e6e7 100644
--- a/src/stories/ActionMenu2/examples.stories.tsx
+++ b/src/stories/ActionMenu2/examples.stories.tsx
@@ -171,8 +171,8 @@ export function GroupsAndDescription(): JSX.Element {
Milestone
-
-
+
+
{milestones
.filter(milestone => !milestone.name.includes('21'))
.map((milestone, index) => (
@@ -189,7 +189,7 @@ export function GroupsAndDescription(): JSX.Element {
))}
-
+
{milestones
.filter(milestone => milestone.name.includes('21'))
.map((milestone, index) => (
@@ -270,6 +270,7 @@ export function MultipleSelection(): JSX.Element {
{users.map(user => (
assignee.login === user.login))}
onSelect={() => toggleAssignee(user)}
@@ -312,7 +313,7 @@ export function MixedSelection(): JSX.Element {
is an action. This pattern appears inside a ActionMenu for selection view options in Memex
-
+
-
-
+
+
{options.map((option, index) => (
{typeof selectedIndex === 'number' && (
-
+
- setSelectedIndex(null)}>
+ setSelectedIndex(null)} role="menuitem">
From 0c15a1181eeea391b76fbf04eac7e64f2019915d Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Thu, 17 Feb 2022 13:49:35 +0100
Subject: [PATCH 11/14] Update docs/content/drafts/ActionMenu2.mdx
Co-authored-by: Cole Bemis
---
docs/content/drafts/ActionMenu2.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/content/drafts/ActionMenu2.mdx b/docs/content/drafts/ActionMenu2.mdx
index 037a4b82e20..4ccc9b3c5a3 100644
--- a/docs/content/drafts/ActionMenu2.mdx
+++ b/docs/content/drafts/ActionMenu2.mdx
@@ -175,7 +175,7 @@ You can choose to have a different _anchor_ for the Menu dependending on the app
```
-### With Selection
+### With selection
Use `selectionVariant` on `ActionList` to create a menu with single or multiple selection.
From 21b06c749e776a301ff3ea61e578ea3b1aad9651 Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Thu, 17 Feb 2022 14:01:21 +0100
Subject: [PATCH 12/14] use merged ActionMenu instead of Dropdown in Overlay
---
src/stories/Overlay.stories.tsx | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/stories/Overlay.stories.tsx b/src/stories/Overlay.stories.tsx
index 5392d4c9a15..dd635c6338a 100644
--- a/src/stories/Overlay.stories.tsx
+++ b/src/stories/Overlay.stories.tsx
@@ -23,7 +23,7 @@ import {
} from '..'
import type {AnchorSide} from '@primer/behaviors'
import {DropdownMenu, DropdownButton} from '../DropdownMenu'
-import {DropdownMenu as DropdownMenu2, ActionList as ActionList2} from '../drafts'
+import {ActionMenu, ActionList as ActionList2} from '../drafts'
import {ItemInput} from '../ActionList/List'
export default {
@@ -362,20 +362,20 @@ export const MemexNestedOverlays = () => {
Duration:
-
-
+
+
{duration}
-
-
-
+
+
+
{durations.map(item => (
setDuration(item)}>
{item}
))}
-
-
+
+
From d5d340138f2f0e6dbc22b21461d39f57dfd5c94d Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Thu, 17 Feb 2022 14:18:24 +0100
Subject: [PATCH 13/14] bring back DropdownMenu2 part 1/2
---
docs/content/drafts/DropdownMenu2.mdx | 364 ++++++++++++++++++
.../ActionListContainerContext.tsx | 1 +
src/ActionList2/Item.tsx | 2 +-
src/ActionList2/List.tsx | 12 +-
src/DropdownMenu2.tsx | 134 +++++++
src/drafts/index.ts | 1 +
6 files changed, 511 insertions(+), 3 deletions(-)
create mode 100644 docs/content/drafts/DropdownMenu2.mdx
create mode 100644 src/DropdownMenu2.tsx
diff --git a/docs/content/drafts/DropdownMenu2.mdx b/docs/content/drafts/DropdownMenu2.mdx
new file mode 100644
index 00000000000..68cd522aa1e
--- /dev/null
+++ b/docs/content/drafts/DropdownMenu2.mdx
@@ -0,0 +1,364 @@
+---
+component_id: dropdown_menu
+title: DropdownMenu v2
+status: Alpha
+source: https://github.com/primer/react/tree/main/src/DropdownMenu2.tsx
+storybook: '/react/storybook?path=/story/composite-components-dropdownmenu2'
+description: Use DropdownMenu to select a single option from a list of menu options.
+---
+
+import {Box, Avatar} from '@primer/react'
+import {DropdownMenu, ActionList} from '@primer/react/drafts'
+import {Props} from '../../src/props'
+import State from '../../components/State'
+import {CalendarIcon, IterationsIcon, NumberIcon, SingleSelectIcon, TypographyIcon} from '@primer/octicons-react'
+
+
+
+
+ {([selectedIndex, setSelectedIndex]) => {
+ const fieldTypes = [
+ {icon: TypographyIcon, name: 'Text'},
+ {icon: NumberIcon, name: 'Number'},
+ {icon: CalendarIcon, name: 'Date'},
+ {icon: SingleSelectIcon, name: 'Single select'},
+ {icon: IterationsIcon, name: 'Iteration'}
+ ]
+ const selectedType = fieldTypes[selectedIndex]
+ return (
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map(({icon: Icon, name}, index) => (
+ setSelectedIndex(index)}
+ >
+ {name}
+
+ ))}
+
+
+
+
+ )
+ }}
+
+
+
+
+```js
+import {DropdownMenu} from '@primer/react/drafts'
+```
+
+
+
+## Examples
+
+### Minimal example
+
+`DropdownMenu` ships with `DropdownMenu.Button` which is an accessible trigger for the overlay. It's recommended to compose `ActionList` with `DropdownMenu.Overlay`
+
+
+
+```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( )
+```
+
+### Customise Button
+
+`Dropdown.Button` uses `Button v2` so you can pass props like `variant` and `leadingIcon` that `Button v2` accepts.
+
+```javascript live noinline drafts
+const Example = () => {
+ const [duration, setDuration] = React.useState(1)
+
+ return (
+
+
+ {duration} {duration > 1 ? 'weeks' : 'week'}
+
+
+
+ {[1, 2, 3, 4, 5, 6].map(weeks => (
+ setDuration(weeks)}>
+ {weeks} {weeks > 1 ? 'weeks' : 'week'}
+
+ ))}
+
+
+
+ )
+}
+
+render( )
+```
+
+### With External Anchor
+
+To create an anchor outside of the menu, you need to switch to controlled mode for the menu and pass `open` and `onOpenChange` along with an `anchorRef` to `DropdownMenu`:
+
+```javascript live noinline drafts
+const Example = () => {
+ const [open, setOpen] = React.useState(false)
+ const anchorRef = React.createRef()
+
+ return (
+ <>
+ setOpen(!open)}>
+ {open ? 'Close Menu' : 'Open Menu'}
+
+
+
+
+
+ Text
+ Number
+ Date
+ Iteration
+
+
+
+ >
+ )
+}
+
+render( )
+```
+
+### With Overlay Props
+
+```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 handleEscape = () => alert('you hit escape!')
+
+ const [selectedIndex, setSelectedIndex] = React.useState(1)
+ const selectedType = fieldTypes[selectedIndex]
+
+ return (
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ )
+}
+
+render( )
+```
+
+### With a custom anchor
+
+You can choose to have a different _anchor_ for the Menu dependending on the application's context.
+
+
+
+```javascript live noinline drafts
+render(
+
+
+ Select a field type
+
+
+
+
+ Text
+ Number
+ Date
+ Iteration
+
+
+
+)
+```
+
+
+
+Use `DropdownMenu` to select an option from a small list. If you’re looking for filters or multiple selection, use [SelectPanel](/SelectPanel) instead.
+
+
+
+## Props
+
+### DropdownMenu
+
+
+
+ Recommended: DropdownMenu.Button or DropdownMenu.Anchor with{' '}
+ DropdownMenu.Overlay
+ >
+ }
+ />
+
+ If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
+ onOpenChange
+ >
+ }
+ />
+
+ If defined, will control the open/closed state of the overlay. Must be used in conjuction with{' '}
+ open
+ >
+ }
+ />
+
+
+
+### DropdownMenu.Button
+
+
+
+ ButtonProps
+ >
+ }
+ description={
+ <>
+ You can pass all of the props that you would pass to a{' '}
+
+ Button
+ {' '}
+ component like variant , leadingIcon ,{' '}
+ sx , etc.
+ >
+ }
+ />
+
+
+### DropdownMenu.Anchor
+
+
+
+
+
+### DropdownMenu.Overlay
+
+
+
+ Recommended:{' '}
+
+ ActionList
+
+ >
+ }
+ />
+
+ Props to be spread on the internal{' '}
+
+ AnchoredOverlay
+
+ >
+ }
+ />
+
+
+## Status
+
+
+
+## Further reading
+
+[Interface guidelines: Action List + Menu](https://primer.style/design/components/action-list)
+
+## Related components
+
+- [ActionList](/drafts/ActionList2)
+- [ActionMenu](/ActionMenu2)
+- [SelectPanel](/SelectPanel)
diff --git a/src/ActionList2/ActionListContainerContext.tsx b/src/ActionList2/ActionListContainerContext.tsx
index 53e703d09d8..ddba7448833 100644
--- a/src/ActionList2/ActionListContainerContext.tsx
+++ b/src/ActionList2/ActionListContainerContext.tsx
@@ -5,6 +5,7 @@ import React from 'react'
type ContextProps = {
container?: string
listRole?: string
+ selectionVariant?: 'single' | 'multiple' // TODO: Remove after DropdownMenu2 deprecation
selectionAttribute?: 'aria-selected' | 'aria-checked'
listLabelledBy?: string
// This can be any function, we don't know anything about the arguments
diff --git a/src/ActionList2/Item.tsx b/src/ActionList2/Item.tsx
index d90c1b74331..be233836fa6 100644
--- a/src/ActionList2/Item.tsx
+++ b/src/ActionList2/Item.tsx
@@ -112,7 +112,7 @@ export const Item = React.forwardRef(
/** Infer item role based on the container */
let itemRole: ItemProps['role']
- if (container === 'ActionMenu') {
+ if (container === 'ActionMenu' || container === 'DropdownMenu') {
if (selectionVariant === 'single') itemRole = 'menuitemradio'
else if (selectionVariant === 'multiple') itemRole = 'menuitemcheckbox'
else itemRole = 'menuitem'
diff --git a/src/ActionList2/List.tsx b/src/ActionList2/List.tsx
index 1182b5107e9..de1363ed419 100644
--- a/src/ActionList2/List.tsx
+++ b/src/ActionList2/List.tsx
@@ -41,7 +41,11 @@ export const List = React.forwardRef(
}
/** if list is inside a Menu, it will get a role from the Menu */
- const {listRole, listLabelledBy} = React.useContext(ActionListContainerContext)
+ const {
+ listRole,
+ listLabelledBy,
+ selectionVariant: containerSelectionVariant // TODO: Remove after DropdownMenu2 deprecation
+ } = React.useContext(ActionListContainerContext)
return (
(
{...props}
ref={forwardedRef}
>
- {props.children}
+
+ {props.children}
+
)
}
diff --git a/src/DropdownMenu2.tsx b/src/DropdownMenu2.tsx
new file mode 100644
index 00000000000..0e552bc5cef
--- /dev/null
+++ b/src/DropdownMenu2.tsx
@@ -0,0 +1,134 @@
+import React from 'react'
+import {useSSRSafeId} from '@react-aria/ssr'
+import {TriangleDownIcon} from '@primer/octicons-react'
+import {Button, ButtonProps} from './Button2'
+import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay'
+import {OverlayProps} from './Overlay'
+import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus} from './hooks'
+import {Divider} from './ActionList2/Divider'
+import {ActionListContainerContext} from './ActionList2/ActionListContainerContext'
+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 DropdownMenuProps = {
+ /**
+ * 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
+}: DropdownMenuProps) => {
+ 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 DropdownMenuAnchorProps = {children: React.ReactElement}
+const Anchor = React.forwardRef(
+ ({children, ...anchorProps}, anchorRef) => {
+ return React.cloneElement(children, {...anchorProps, ref: anchorRef})
+ }
+)
+
+/** this component is syntactical sugar 🍭 */
+export type DropdownMenuButtonProps = ButtonProps
+const MenuButton = React.forwardRef(
+ ({children, ...props}, anchorRef) => {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+
+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, openWithFocus} = useMenuInitialFocus(open, onOpen)
+
+ return (
+
+
+
+ )
+}
+
+Menu.displayName = 'ActionMenu'
+export const DropdownMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider})
diff --git a/src/drafts/index.ts b/src/drafts/index.ts
index cbef05e7eaa..291e56dafbb 100644
--- a/src/drafts/index.ts
+++ b/src/drafts/index.ts
@@ -9,5 +9,6 @@
export * from '../ActionList2'
export * from '../Button2'
export * from '../ActionMenu2'
+export * from '../DropdownMenu2'
export * from '../Label2'
export * from '../PageLayout'
From 82535fb834f1c09a063bac7a76a6d5575686f3ed Mon Sep 17 00:00:00 2001
From: Siddharth Kshetrapal
Date: Thu, 17 Feb 2022 14:20:43 +0100
Subject: [PATCH 14/14] bring back DropdownMenu2 part 2/2
---
src/__tests__/DropdownMenu2.test.tsx | 101 ++++++
.../__snapshots__/DropdownMenu2.test.tsx.snap | 145 ++++++++
.../DropdownMenu2/examples.stories.tsx | 246 +++++++++++++
.../DropdownMenu2/fixtures.stories.tsx | 335 ++++++++++++++++++
4 files changed, 827 insertions(+)
create mode 100644 src/__tests__/DropdownMenu2.test.tsx
create mode 100644 src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
create mode 100644 src/stories/DropdownMenu2/examples.stories.tsx
create mode 100644 src/stories/DropdownMenu2/fixtures.stories.tsx
diff --git a/src/__tests__/DropdownMenu2.test.tsx b/src/__tests__/DropdownMenu2.test.tsx
new file mode 100644
index 00000000000..04917c6e3a4
--- /dev/null
+++ b/src/__tests__/DropdownMenu2.test.tsx
@@ -0,0 +1,101 @@
+import {cleanup, render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
+import 'babel-polyfill'
+import {toHaveNoViolations} from 'jest-axe'
+import React from 'react'
+import theme from '../theme'
+import {DropdownMenu} from '../DropdownMenu2'
+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 SimpleDropdownMenu(): JSX.Element {
+ const fieldTypes = [{name: 'Text'}, {name: 'Number'}, {name: 'Date'}, {name: 'Single select'}, {name: 'Iteration'}]
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+
+ return (
+
+
+
+
+ {selectedType.name}
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}
+ >
+ {type.name}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+describe('DropdownMenu', () => {
+ behavesAsComponent({
+ Component: ActionList,
+ options: {skipAs: true, skipSx: true},
+ toRender: () =>
+ })
+
+ checkExports('DropdownMenu2', {
+ default: undefined,
+ DropdownMenu
+ })
+
+ it('should open Menu on Button click', async () => {
+ const component = HTMLRender( )
+ const button = component.getByLabelText('Select field type')
+ fireEvent.click(button)
+ expect(component.getByRole('menu')).toBeInTheDocument()
+ cleanup()
+ })
+
+ it('should open Menu on Button keypress', async () => {
+ const component = HTMLRender( )
+ const button = component.getByLabelText('Select field type')
+
+ // 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.getByLabelText('Select field type')
+
+ fireEvent.click(button)
+ const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
+ 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.getByLabelText('Select field type')
+
+ fireEvent.click(button)
+ const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
+ fireEvent.keyPress(menuItems[0], {key: 'Enter', charCode: 13})
+ expect(component.queryByRole('menu')).toBeNull()
+
+ cleanup()
+ })
+
+ checkStoriesForAxeViolations('DropdownMenu2/fixtures')
+ checkStoriesForAxeViolations('DropdownMenu2/examples')
+})
diff --git a/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap b/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
new file mode 100644
index 00000000000..d559b06cbf5
--- /dev/null
+++ b/src/__tests__/__snapshots__/DropdownMenu2.test.tsx.snap
@@ -0,0 +1,145 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DropdownMenu 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);
+}
+
+
+
+
+ Text
+
+
+ ",
+ }
+ }
+ 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/stories/DropdownMenu2/examples.stories.tsx b/src/stories/DropdownMenu2/examples.stories.tsx
new file mode 100644
index 00000000000..a461dbbf47f
--- /dev/null
+++ b/src/stories/DropdownMenu2/examples.stories.tsx
@@ -0,0 +1,246 @@
+import React from 'react'
+import {Meta} from '@storybook/react'
+import {ThemeProvider} from '../..'
+import BaseStyles from '../../BaseStyles'
+import {DropdownMenu} from '../../DropdownMenu2'
+import {ActionList} from '../../ActionList2'
+import Box from '../../Box'
+import Text from '../../Text'
+import {
+ GearIcon,
+ MilestoneIcon,
+ CalendarIcon,
+ IterationsIcon,
+ NumberIcon,
+ SingleSelectIcon,
+ TypographyIcon,
+ IssueOpenedIcon,
+ TableIcon,
+ PeopleIcon,
+ XIcon
+} from '@primer/octicons-react'
+
+const meta: Meta = {
+ title: 'Composite components/DropdownMenu2/examples',
+ component: DropdownMenu,
+ decorators: [
+ (Story: React.ComponentType): JSX.Element => (
+
+
+
+
+
+ )
+ ],
+ parameters: {
+ controls: {
+ disabled: true
+ }
+ }
+}
+export default meta
+
+const fieldTypes = [
+ {icon: TypographyIcon, name: 'Text'},
+ {icon: NumberIcon, name: 'Number'},
+ {icon: CalendarIcon, name: 'Date'},
+ {icon: SingleSelectIcon, name: 'Single select'},
+ {icon: IterationsIcon, name: 'Iteration'}
+]
+
+export function SingleSelection(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+ return (
+ <>
+ Simple Dropdown Menu
+
+ This pattern is the classic dropdown menu - single section with the selected value shown in the button
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+
+export function WithPlaceholder(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(-1)
+ const selectedType = fieldTypes[selectedIndex] || {}
+
+ return (
+ <>
+ With placeholder
+
+ This pattern is the starting state of the dropdown menu with a placeholder when there is default value
+
+
+
+ {selectedType.name || 'Pick a field type'}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+
+const milestones = [
+ {name: 'FY21 - Q2', due: 'December 31, 2021', progress: 90},
+ {name: 'FY22 - Q3', due: 'March 31, 2022', progress: 10},
+ {name: 'FY23 - Q1', due: 'June 30, 2022', progress: 0},
+ {name: 'FY23 - Q2', due: 'December 30, 2022', progress: 0}
+]
+
+export function GroupsAndDescription(): JSX.Element {
+ const [selectedMilestone, setSelectedMilestone] = React.useState()
+
+ return (
+ <>
+ Milestone selector
+
+
+
+ Milestone
+
+
+
+
+ {milestones
+ .filter(milestone => !milestone.name.includes('21'))
+ .map((milestone, index) => (
+ setSelectedMilestone(milestone)}
+ >
+
+
+
+ {milestone.name}
+ Due by {milestone.due}
+
+ ))}
+
+
+ {milestones
+ .filter(milestone => milestone.name.includes('21'))
+ .map((milestone, index) => (
+ setSelectedMilestone(milestone)}
+ >
+
+
+
+ {milestone.name}
+ Due by {milestone.due}
+
+ ))}
+
+
+
+
+ {selectedMilestone ? (
+
+ {selectedMilestone.name}
+
+ ) : (
+
+ No milestone
+
+ )}
+
+ >
+ )
+}
+
+export function MixedSelection(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(1)
+
+ const options = [
+ {text: 'Status', icon: IssueOpenedIcon},
+ {text: 'Stage', icon: TableIcon},
+ {text: 'Assignee', icon: PeopleIcon},
+ {text: 'Team', icon: TypographyIcon},
+ {text: 'Estimate', icon: NumberIcon},
+ {text: 'Due Date', icon: CalendarIcon}
+ ]
+
+ const selectedOption = selectedIndex && options[selectedIndex]
+
+ return (
+ <>
+ List with mixed selection
+
+
+ In this list, there is a ActionList.Group with single selection for picking one option, followed by a Item that
+ is an action. This pattern appears inside a DropdownMenu for selection view options in Memex
+
+
+
+
+ {selectedOption ? `Group by ${selectedOption.text}` : 'Group items by'}
+
+
+
+
+ {options.map((option, index) => (
+ setSelectedIndex(index)}
+ >
+ {option.icon}
+ {option.text}
+
+ ))}
+
+ {typeof selectedIndex === 'number' && (
+
+
+ setSelectedIndex(null)} role="menuitem">
+
+
+
+ Clear Group by
+
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/src/stories/DropdownMenu2/fixtures.stories.tsx b/src/stories/DropdownMenu2/fixtures.stories.tsx
new file mode 100644
index 00000000000..d39782e46c1
--- /dev/null
+++ b/src/stories/DropdownMenu2/fixtures.stories.tsx
@@ -0,0 +1,335 @@
+import React from 'react'
+import {Meta} from '@storybook/react'
+import {ThemeProvider} from '../..'
+import BaseStyles from '../../BaseStyles'
+import {DropdownMenu} from '../../DropdownMenu2'
+import {ActionList} from '../../ActionList2'
+import {Button} from '../../Button2'
+import Box from '../../Box'
+import Text from '../../Text'
+import TextInput from '../../TextInput'
+import ProgressBar from '../../ProgressBar'
+import {
+ GearIcon,
+ MilestoneIcon,
+ CalendarIcon,
+ IterationsIcon,
+ NumberIcon,
+ SingleSelectIcon,
+ TypographyIcon
+} from '@primer/octicons-react'
+
+const meta: Meta = {
+ title: 'Composite components/DropdownMenu2/fixtures',
+ component: DropdownMenu,
+ decorators: [
+ (Story: React.ComponentType): JSX.Element => (
+
+
+
+
+
+ )
+ ],
+ parameters: {
+ controls: {
+ disabled: true
+ }
+ }
+}
+export default meta
+
+const fieldTypes = [
+ {icon: TypographyIcon, name: 'Text'},
+ {icon: NumberIcon, name: 'Number'},
+ {icon: CalendarIcon, name: 'Date'},
+ {icon: SingleSelectIcon, name: 'Single select'},
+ {icon: IterationsIcon, name: 'Iteration'}
+]
+
+export function SimpleDropdownMenu(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+ return (
+ <>
+ Simple Dropdown Menu
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+SimpleDropdownMenu.storyName = 'Simple DropdownMenu'
+
+export function Placeholder(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(-1)
+ const selectedType = fieldTypes[selectedIndex] || {}
+
+ return (
+ <>
+ With placeholder
+
+
+
+ {selectedType.name || 'Pick a field type'}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+Placeholder.storyName = 'Placeholder'
+
+export function MemexIteration(): JSX.Element {
+ const [duration, setDuration] = React.useState(1)
+
+ return (
+ <>
+ Memex Iteration Menu
+
+
+
+ {duration} {duration > 1 ? 'weeks' : 'week'}
+
+
+
+ {[1, 2, 3, 4, 5, 6].map(weeks => (
+ setDuration(weeks)}>
+ {weeks} {weeks > 1 ? 'weeks' : 'week'}
+
+ ))}
+
+
+
+ >
+ )
+}
+MemexIteration.storyName = 'Memex Iteration Menu'
+
+const milestones = [
+ {name: 'v29.2.0', due: 'September 30, 2021', progress: 95},
+ {name: 'v30.0.0', due: 'December 1, 2021', progress: 40},
+ {name: 'FY22 - Q3', due: 'December 31, 2021', progress: 10}
+]
+
+export function MemexAddColumn(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ const selectedType = fieldTypes[selectedIndex]
+
+ const [duration, setDuration] = React.useState(1)
+
+ return (
+ <>
+ Memex Add column
+
+
+
+
+
+ {selectedType.name}
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}
+ >
+ {type.icon} {type.name}
+
+ ))}
+
+
+
+ Options
+
+
+ Duration:
+
+
+ {duration} {duration > 1 ? 'weeks' : 'week'}
+
+
+
+ {[1, 2, 3, 4, 5, 6].map(weeks => (
+ setDuration(weeks)}>
+ {weeks} {weeks > 1 ? 'weeks' : 'week'}
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
+MemexAddColumn.storyName = 'Memex Add Column'
+
+export function MilestoneStory(): JSX.Element {
+ const [selectedIndex, setSelectedIndex] = React.useState(-1)
+
+ const selectedMilestone = milestones[selectedIndex] as typeof milestones[0] | undefined
+
+ return (
+ <>
+ Milestone selector
+
+
+
+
+
+ Milestone
+
+
+
+
+
+ {milestones.map((milestone, index) => (
+ setSelectedIndex(index)}
+ >
+
+
+
+ {milestone.name}
+ Due by {milestone.due}
+
+ ))}
+
+
+
+ {selectedMilestone ? (
+
+
+ {selectedMilestone.name}
+
+ ) : (
+
+ No milestone
+
+ )}
+
+ >
+ )
+}
+MilestoneStory.storyName = 'Milestone selector'
+
+export function ControlledMenu(): JSX.Element {
+ const [open, setOpen] = React.useState(false)
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+
+ return (
+ <>
+ Controlled Menu
+
+ External Open State: {open ? 'Open' : 'Closed'}
+ selected Value: {fieldTypes[selectedIndex].name}
+
+
+ setOpen(!open)}>{open ? 'Close Menu' : 'Open Menu'}
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+ControlledMenu.storyName = 'Controlled Menu'
+
+export function ExternalAnchor(): JSX.Element {
+ const [open, setOpen] = React.useState(false)
+ const triggerRef = React.createRef()
+ const anchorRef = React.createRef()
+
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+
+ return (
+ <>
+ External Anchor
+
+ External Open State: {open ? 'Open' : 'Closed'}
+ selected Value: {fieldTypes[selectedIndex].name}
+
+ setOpen(!open)}>
+ {open ? 'Close Menu' : 'Open Menu'}
+
+
+
+ Anchored on me!
+
+
+
+
+
+ {fieldTypes.map((type, index) => (
+ setSelectedIndex(index)}>
+ {type.name}
+
+ ))}
+
+
+
+ >
+ )
+}
+ExternalAnchor.storyName = 'External Anchor'