@primer/components 33.0.0-rc.b495ba4a → 33.0.0-rc.cface7dc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.github/workflows/statuses.yml +32 -0
  2. package/.gitignore +1 -0
  3. package/CHANGELOG.md +6 -0
  4. package/contributor-docs/CONTRIBUTING.md +14 -58
  5. package/docs/content/{ActionList2.mdx → drafts/ActionList2.mdx} +5 -9
  6. package/docs/content/drafts/ActionMenu2.mdx +251 -0
  7. package/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js +9 -1
  8. package/docs/src/component-checklist.js +10 -2
  9. package/lib/ActionList2/Divider.d.ts +3 -2
  10. package/lib/ActionList2/Divider.js +10 -5
  11. package/lib/ActionList2/Item.js +21 -5
  12. package/lib/ActionList2/List.js +11 -1
  13. package/lib/ActionList2/MenuContext.d.ts +10 -0
  14. package/lib/ActionList2/MenuContext.js +15 -0
  15. package/lib/ActionList2/Selection.js +11 -0
  16. package/lib/ActionList2/index.d.ts +1 -2
  17. package/lib/ActionMenu2.d.ts +313 -0
  18. package/lib/ActionMenu2.js +91 -0
  19. package/lib/Autocomplete/Autocomplete.d.ts +2 -1
  20. package/lib/Autocomplete/AutocompleteInput.d.ts +2 -1
  21. package/lib/Button/Button.d.ts +2 -2
  22. package/lib/Button/ButtonClose.d.ts +2 -2
  23. package/lib/Button/ButtonDanger.d.ts +2 -2
  24. package/lib/Button/ButtonInvisible.d.ts +2 -2
  25. package/lib/Button/ButtonOutline.d.ts +2 -2
  26. package/lib/Button/ButtonPrimary.d.ts +2 -2
  27. package/lib/CircleOcticon.d.ts +35 -35
  28. package/lib/Dialog.d.ts +37 -37
  29. package/lib/Dropdown.d.ts +6 -6
  30. package/lib/DropdownMenu/DropdownButton.d.ts +6 -3
  31. package/lib/FilterList.d.ts +1 -1
  32. package/lib/Position.d.ts +4 -4
  33. package/lib/SelectMenu/SelectMenu.d.ts +11 -10
  34. package/lib/SelectMenu/SelectMenuItem.d.ts +1 -1
  35. package/lib/SelectMenu/SelectMenuModal.d.ts +1 -1
  36. package/lib/TextInputWithTokens.d.ts +2 -1
  37. package/lib/Token/AvatarToken.d.ts +1 -1
  38. package/lib/Token/IssueLabelToken.d.ts +1 -1
  39. package/lib/Token/Token.d.ts +1 -1
  40. package/lib/drafts.d.ts +1 -0
  41. package/lib/drafts.js +13 -0
  42. package/lib/stories/ActionMenu2.stories.js +433 -0
  43. package/lib-esm/ActionList2/Divider.d.ts +3 -2
  44. package/lib-esm/ActionList2/Divider.js +8 -5
  45. package/lib-esm/ActionList2/Item.js +19 -5
  46. package/lib-esm/ActionList2/List.js +9 -1
  47. package/lib-esm/ActionList2/MenuContext.d.ts +10 -0
  48. package/lib-esm/ActionList2/MenuContext.js +3 -0
  49. package/lib-esm/ActionList2/Selection.js +9 -0
  50. package/lib-esm/ActionList2/index.d.ts +1 -2
  51. package/lib-esm/ActionMenu2.d.ts +313 -0
  52. package/lib-esm/ActionMenu2.js +67 -0
  53. package/lib-esm/Autocomplete/Autocomplete.d.ts +2 -1
  54. package/lib-esm/Autocomplete/AutocompleteInput.d.ts +2 -1
  55. package/lib-esm/Button/Button.d.ts +2 -2
  56. package/lib-esm/Button/ButtonClose.d.ts +2 -2
  57. package/lib-esm/Button/ButtonDanger.d.ts +2 -2
  58. package/lib-esm/Button/ButtonInvisible.d.ts +2 -2
  59. package/lib-esm/Button/ButtonOutline.d.ts +2 -2
  60. package/lib-esm/Button/ButtonPrimary.d.ts +2 -2
  61. package/lib-esm/CircleOcticon.d.ts +35 -35
  62. package/lib-esm/Dialog.d.ts +37 -37
  63. package/lib-esm/Dropdown.d.ts +6 -6
  64. package/lib-esm/DropdownMenu/DropdownButton.d.ts +6 -3
  65. package/lib-esm/FilterList.d.ts +1 -1
  66. package/lib-esm/Position.d.ts +4 -4
  67. package/lib-esm/SelectMenu/SelectMenu.d.ts +11 -10
  68. package/lib-esm/SelectMenu/SelectMenuItem.d.ts +1 -1
  69. package/lib-esm/SelectMenu/SelectMenuModal.d.ts +1 -1
  70. package/lib-esm/TextInputWithTokens.d.ts +2 -1
  71. package/lib-esm/Token/AvatarToken.d.ts +1 -1
  72. package/lib-esm/Token/IssueLabelToken.d.ts +1 -1
  73. package/lib-esm/Token/Token.d.ts +1 -1
  74. package/lib-esm/drafts.d.ts +1 -0
  75. package/lib-esm/drafts.js +2 -1
  76. package/lib-esm/stories/ActionMenu2.stories.js +376 -0
  77. package/package-lock.json +303 -255
  78. package/package.json +4 -2
  79. package/script/component-status-project/build.ts +100 -0
  80. package/script/component-status-project/deploy.rb +142 -0
  81. package/src/ActionList2/Divider.tsx +13 -8
  82. package/src/ActionList2/Item.tsx +13 -3
  83. package/src/ActionList2/List.tsx +6 -2
  84. package/src/ActionList2/MenuContext.tsx +6 -0
  85. package/src/ActionList2/Selection.tsx +9 -0
  86. package/src/ActionMenu2.tsx +94 -0
  87. package/src/drafts.ts +1 -0
  88. package/src/stories/ActionMenu2.stories.tsx +551 -0
  89. package/stats.html +1 -1
  90. package/tsconfig.build.json +1 -1
  91. package/tsconfig.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/components",
3
- "version": "33.0.0-rc.b495ba4a",
3
+ "version": "33.0.0-rc.cface7dc",
4
4
  "description": "Primer react components",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib-esm/index.js",
@@ -91,6 +91,7 @@
91
91
  "@types/jest-axe": "3.5.3",
92
92
  "@types/lodash.isempty": "4.4.6",
93
93
  "@types/lodash.isobject": "3.0.6",
94
+ "@types/node": "16.11.11",
94
95
  "@typescript-eslint/eslint-plugin": "4.31.2",
95
96
  "@typescript-eslint/parser": "4.26.1",
96
97
  "@wojtekmaj/enzyme-adapter-react-17": "0.6.3",
@@ -116,6 +117,7 @@
116
117
  "eslint-plugin-primer-react": "0.7.0",
117
118
  "eslint-plugin-react": "7.24.0",
118
119
  "eslint-plugin-react-hooks": "4.2.0",
120
+ "front-matter": "4.0.2",
119
121
  "jest": "27.0.4",
120
122
  "jest-axe": "5.0.1",
121
123
  "jest-styled-components": "6.3.4",
@@ -137,7 +139,7 @@
137
139
  "storybook-addon-performance": "0.16.1",
138
140
  "styled-components": "4.4.1",
139
141
  "ts-toolbelt": "9.6.0",
140
- "typescript": "4.2.2"
142
+ "typescript": "4.4.4"
141
143
  },
142
144
  "peerDependencies": {
143
145
  "react": "^17.0.0",
@@ -0,0 +1,100 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
5
+ const fm = require('front-matter') // FIXME after this bugfix is merged https://github.com/jxson/front-matter/pull/77
6
+
7
+ const sourceDirectory = path.resolve(__dirname, '../../docs/content/')
8
+ const outputDir = path.resolve(__dirname, '../../dist/')
9
+
10
+ type ComponentStatus = {
11
+ [component: string]: string
12
+ }
13
+
14
+ /**
15
+ * Extracts the component status for each file in the given directory.
16
+ *
17
+ * @param filenames Array of filenames to read front-matter from
18
+ * @param dir Absolute path to directory containing files
19
+ * @returns A promise that resolves to an array containing outcome of file front-matter extraction
20
+ */
21
+ function getComponentStatuses(filenames: string[], dir: string) {
22
+ const promises: Promise<ComponentStatus | null>[] = []
23
+
24
+ const handleCallback = (
25
+ filename: string,
26
+ resolve: (value: ComponentStatus | null) => void,
27
+ reject: (value: unknown) => void
28
+ ) => {
29
+ fs.readFile(path.resolve(dir, filename), 'utf-8', (err, content) => {
30
+ if (err) return reject(err)
31
+
32
+ if (fm.test(content)) {
33
+ const {
34
+ attributes: {title, status}
35
+ } = fm(content)
36
+
37
+ if (status) {
38
+ return resolve({[title]: status})
39
+ }
40
+ }
41
+
42
+ resolve(null)
43
+ })
44
+ }
45
+
46
+ for (const filename of filenames) {
47
+ const promise: Promise<ComponentStatus | null> = new Promise((resolve, reject) => {
48
+ return handleCallback(filename, resolve, reject)
49
+ })
50
+ promises.push(promise)
51
+ }
52
+
53
+ return Promise.all(promises)
54
+ }
55
+
56
+ /**
57
+ * Orchestrates the process of reading component status for each file in the given directory.
58
+ *
59
+ * @param dir Directory to source files where status will be extracted from
60
+ * @returns A promise that resolves to an object containing component statuses
61
+ */
62
+ async function readFiles(dir: string) {
63
+ try {
64
+ const dirContents = fs.readdirSync(dir, {withFileTypes: true})
65
+ const filenames = dirContents.filter(dirent => dirent.isFile()).map(dirent => dirent.name)
66
+ const componentStatuses = await getComponentStatuses(filenames, dir)
67
+
68
+ return componentStatuses
69
+ .filter(Boolean)
70
+ .reverse()
71
+ .reduce(
72
+ (acc, file) => ({
73
+ ...acc,
74
+ ...file
75
+ }),
76
+ {}
77
+ )
78
+ } catch (err) {
79
+ throw new Error(`error reading files: ${err}`)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Writes the component status to the given file.
85
+ */
86
+ async function build() {
87
+ try {
88
+ const componentStatuses = await readFiles(sourceDirectory)
89
+
90
+ if (!fs.existsSync(outputDir)) {
91
+ fs.mkdirSync(outputDir)
92
+ }
93
+
94
+ fs.writeFileSync(`${outputDir}/component-status.json`, JSON.stringify(componentStatuses))
95
+ } catch (error) {
96
+ throw new Error(`error building component status object: ${error}`)
97
+ }
98
+ }
99
+
100
+ build()
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+ # Adapted from https://github.com/primer/view_components/blob/main/script/update-statuses-project.rb
3
+ # Usage: script/update-statuses-project
4
+ # frozen_string_literal: true
5
+
6
+ require "graphql/client"
7
+ require "graphql/client/http"
8
+
9
+ statuses = File.read(File.join(File.dirname(__FILE__), "../../dist/component-status.json"))
10
+ statuses_json = JSON.parse(statuses)
11
+
12
+ class QueryExecutionError < StandardError; end
13
+ NOTE_SEPARATOR = " --- "
14
+
15
+ module Github
16
+ GITHUB_ACCESS_TOKEN = ENV.fetch("GITHUB_TOKEN")
17
+ URL = "https://api.github.com/graphql"
18
+ HttpAdapter = GraphQL::Client::HTTP.new(URL) do
19
+ def headers(_)
20
+ {
21
+ "Authorization" => "Bearer #{GITHUB_ACCESS_TOKEN}",
22
+ "User-Agent" => "Ruby"
23
+ }
24
+ end
25
+ end
26
+ Schema = GraphQL::Client.load_schema(HttpAdapter)
27
+ Client = GraphQL::Client.new(schema: Schema, execute: HttpAdapter)
28
+ end
29
+
30
+ # Project is a GraphQL wrapper for interacting with GitHub projects
31
+ class Project
32
+ ProjectQuery = Github::Client.parse <<-'GRAPHQL'
33
+ query {
34
+ repository(owner: "primer", name: "react") {
35
+ project(number: 5) {
36
+ columns(first: 100) {
37
+ nodes {
38
+ name
39
+ id
40
+ databaseId
41
+ cards {
42
+ nodes {
43
+ id
44
+ databaseId
45
+ note
46
+ column {
47
+ name
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ GRAPHQL
57
+
58
+ CreateCard = Github::Client.parse <<-'GRAPHQL'
59
+ mutation($note: String!, $projectColumnId: ID!) {
60
+ addProjectCard(input:{note: $note, projectColumnId: $projectColumnId, clientMutationId: "prc-actions"}) {
61
+ __typename
62
+ }
63
+ }
64
+ GRAPHQL
65
+
66
+ MoveCard = Github::Client.parse <<-'GRAPHQL'
67
+ mutation($cardId: ID!, $columnId: ID!) {
68
+ moveProjectCard(input:{cardId: $cardId, columnId: $columnId, clientMutationId: "prc-actions"}) {
69
+ __typename
70
+ }
71
+ }
72
+ GRAPHQL
73
+
74
+ def self.create_card(note:, column_id:)
75
+ response = Github::Client.query(CreateCard, variables: { note: note, projectColumnId: column_id })
76
+ return unless response.errors.any?
77
+
78
+ raise QueryExecutionError, response.errors[:data].join(", ")
79
+ end
80
+
81
+ def self.move_card(card_id:, column_id:)
82
+ response = Github::Client.query(MoveCard, variables: { cardId: card_id, columnId: column_id })
83
+ return unless response.errors.any?
84
+
85
+ raise(QueryExecutionError, response.errors[:data].join(", "))
86
+ end
87
+
88
+ def self.fetch_columns
89
+ response = Github::Client.query(ProjectQuery)
90
+ return response.data.repository.project.columns unless response.errors.any?
91
+
92
+ raise(QueryExecutionError, response.errors[:data].join(", "))
93
+ end
94
+ end
95
+
96
+ columns = Project.fetch_columns.nodes
97
+
98
+ @column_mapping = {}
99
+ columns.each do |column|
100
+ @column_mapping[column.name.downcase] = column.id
101
+ end
102
+
103
+ @cards = columns.map(&:cards).map(&:nodes).flatten
104
+
105
+ def get_card(name_prefix:)
106
+ @cards.find { |card| card.note.start_with?(name_prefix) }
107
+ end
108
+
109
+ def on_correct_column?(card_id:, status:)
110
+ card = @cards.find { |c| c.id == card_id }
111
+ card.column.name.casecmp(status).zero?
112
+ end
113
+
114
+ def move_card(card_id:, status:)
115
+ column_id = @column_mapping[status.downcase]
116
+
117
+ puts "move card with #{card_id} to #{status} on column #{column_id}"
118
+
119
+ Project.move_card(card_id: card_id, column_id: column_id)
120
+ end
121
+
122
+ def create_card(component_name:, status:)
123
+ column_id = @column_mapping[status.downcase]
124
+
125
+ puts "create card with #{component_name} on #{status} on column #{column_id}"
126
+
127
+ Project.create_card(note: component_name, column_id: column_id)
128
+ end
129
+
130
+ statuses_json.each do |component_name, component_status|
131
+ card = get_card(name_prefix: component_name)
132
+
133
+ if card
134
+ if on_correct_column?(card_id: card.id, status: component_status)
135
+ puts "#{card.id} is on the right column. noop"
136
+ else
137
+ move_card(card_id: card.id, status: component_status)
138
+ end
139
+ else
140
+ create_card(component_name: component_name, status: component_status)
141
+ end
142
+ end
@@ -2,22 +2,27 @@ import React from 'react'
2
2
  import Box from '../Box'
3
3
  import {get} from '../constants'
4
4
  import {Theme} from '../ThemeProvider'
5
+ import {SxProp, merge} from '../sx'
5
6
 
6
7
  /**
7
8
  * Visually separates `Item`s or `Group`s in an `ActionList`.
8
9
  */
9
- export function Divider(): JSX.Element {
10
+
11
+ export const Divider: React.FC<SxProp> = ({sx = {}}) => {
10
12
  return (
11
13
  <Box
12
14
  as="li"
13
15
  role="separator"
14
- sx={{
15
- height: 1,
16
- backgroundColor: 'actionListItem.inlineDivider',
17
- marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`,
18
- marginBottom: 2,
19
- listStyle: 'none' // hide the ::marker inserted by browser's stylesheet
20
- }}
16
+ sx={merge(
17
+ {
18
+ height: 1,
19
+ backgroundColor: 'actionListItem.inlineDivider',
20
+ marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`,
21
+ marginBottom: 2,
22
+ listStyle: 'none' // hide the ::marker inserted by browser's stylesheet
23
+ },
24
+ sx as SxProp
25
+ )}
21
26
  data-component="ActionList.Divider"
22
27
  />
23
28
  )
@@ -8,6 +8,7 @@ import sx, {SxProp, merge} from '../sx'
8
8
  import createSlots from '../utils/create-slots'
9
9
  import {AriaRole} from '../utils/types'
10
10
  import {ListContext} from './List'
11
+ import {MenuContext} from './MenuContext'
11
12
  import {Selection} from './Selection'
12
13
 
13
14
  export const getVariantStyles = (variant: ItemProps['variant'], disabled: ItemProps['disabled']) => {
@@ -94,12 +95,14 @@ export const Item = React.forwardRef<HTMLLIElement, ItemProps>(
94
95
  onSelect,
95
96
  sx: sxProp = {},
96
97
  id,
98
+ role,
97
99
  _PrivateItemWrapper,
98
100
  ...props
99
101
  },
100
102
  forwardedRef
101
103
  ): JSX.Element => {
102
104
  const {variant: listVariant, showDividers} = React.useContext(ListContext)
105
+ const {itemRole, afterSelect} = React.useContext(MenuContext)
103
106
 
104
107
  const {theme} = useTheme()
105
108
 
@@ -170,9 +173,13 @@ export const Item = React.forwardRef<HTMLLIElement, ItemProps>(
170
173
  event => {
171
174
  if (typeof onSelect !== 'function') return
172
175
  if (disabled) return
173
- if (!event.defaultPrevented) onSelect(event)
176
+ if (!event.defaultPrevented) {
177
+ onSelect(event)
178
+ // if this Item is inside a Menu, close the Menu
179
+ if (typeof afterSelect === 'function') afterSelect()
180
+ }
174
181
  },
175
- [onSelect, disabled]
182
+ [onSelect, disabled, afterSelect]
176
183
  )
177
184
 
178
185
  const keyPressHandler = React.useCallback(
@@ -181,9 +188,11 @@ export const Item = React.forwardRef<HTMLLIElement, ItemProps>(
181
188
  if (disabled) return
182
189
  if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
183
190
  onSelect(event)
191
+ // if this Item is inside a Menu, close the Menu
192
+ if (typeof afterSelect === 'function') afterSelect()
184
193
  }
185
194
  },
186
- [onSelect, disabled]
195
+ [onSelect, disabled, afterSelect]
187
196
  )
188
197
 
189
198
  // use props.id if provided, otherwise generate one.
@@ -206,6 +215,7 @@ export const Item = React.forwardRef<HTMLLIElement, ItemProps>(
206
215
  tabIndex={disabled || _PrivateItemWrapper ? undefined : 0}
207
216
  aria-labelledby={`${labelId} ${slots.InlineDescription ? inlineDescriptionId : ''}`}
208
217
  aria-describedby={slots.BlockDescription ? blockDescriptionId : undefined}
218
+ role={role || itemRole}
209
219
  {...props}
210
220
  >
211
221
  <ItemWrapper>
@@ -3,6 +3,7 @@ import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/r
3
3
  import styled from 'styled-components'
4
4
  import sx, {SxProp, merge} from '../sx'
5
5
  import {AriaRole} from '../utils/types'
6
+ import {MenuContext} from './MenuContext'
6
7
 
7
8
  export type ListProps = {
8
9
  /**
@@ -30,7 +31,7 @@ const ListBox = styled.ul<SxProp>(sx)
30
31
 
31
32
  export const List = React.forwardRef<HTMLUListElement, ListProps>(
32
33
  (
33
- {variant = 'inset', selectionVariant, showDividers = false, sx: sxProp = {}, ...props},
34
+ {variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = {}, ...props},
34
35
  forwardedRef
35
36
  ): JSX.Element => {
36
37
  const styles = {
@@ -39,8 +40,11 @@ export const List = React.forwardRef<HTMLUListElement, ListProps>(
39
40
  paddingY: variant === 'inset' ? 2 : 0
40
41
  }
41
42
 
43
+ /** if list is inside a Menu, it will get a role from the Menu */
44
+ const {listRole} = React.useContext(MenuContext)
45
+
42
46
  return (
43
- <ListBox sx={merge(styles, sxProp as SxProp)} {...props} ref={forwardedRef}>
47
+ <ListBox sx={merge(styles, sxProp as SxProp)} role={role || listRole} {...props} ref={forwardedRef}>
44
48
  <ListContext.Provider value={{variant, selectionVariant, showDividers}}>{props.children}</ListContext.Provider>
45
49
  </ListBox>
46
50
  )
@@ -0,0 +1,6 @@
1
+ /** This context can be used by components that compose ActionList inside a Menu */
2
+
3
+ import React from 'react'
4
+
5
+ type ContextProps = {parent?: string; listRole?: string; itemRole?: string; afterSelect?: () => void}
6
+ export const MenuContext = React.createContext<ContextProps>({})
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import {CheckIcon} from '@primer/octicons-react'
3
3
  import {ListContext} from './List'
4
4
  import {GroupContext} from './Group'
5
+ import {MenuContext} from './MenuContext'
5
6
  import {ItemProps} from './Item'
6
7
  import {LeadingVisualContainer} from './Visuals'
7
8
 
@@ -9,6 +10,7 @@ type SelectionProps = Pick<ItemProps, 'selected'>
9
10
  export const Selection: React.FC<SelectionProps> = ({selected}) => {
10
11
  const {selectionVariant: listSelectionVariant} = React.useContext(ListContext)
11
12
  const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
13
+ const {parent} = React.useContext(MenuContext)
12
14
 
13
15
  /** selectionVariant in Group can override the selectionVariant in List root */
14
16
  const selectionVariant = typeof groupSelectionVariant !== 'undefined' ? groupSelectionVariant : listSelectionVariant
@@ -23,6 +25,13 @@ export const Selection: React.FC<SelectionProps> = ({selected}) => {
23
25
  return null
24
26
  }
25
27
 
28
+ if (parent === 'ActionMenu') {
29
+ throw new Error(
30
+ 'ActionList cannot have a selectionVariant inside ActionMenu, please use DropdownMenu or SelectPanel instead. More information: https://primer.style/design/components/action-list#application'
31
+ )
32
+ return null
33
+ }
34
+
26
35
  if (selectionVariant === 'single') {
27
36
  return <LeadingVisualContainer>{selected && <CheckIcon />}</LeadingVisualContainer>
28
37
  }
@@ -0,0 +1,94 @@
1
+ import Button, {ButtonProps} from './Button'
2
+ import React from 'react'
3
+ import {AnchoredOverlay} from './AnchoredOverlay'
4
+ import {useProvidedStateOrCreate} from './hooks/useProvidedStateOrCreate'
5
+ import {OverlayProps} from './Overlay'
6
+ import {useProvidedRefOrCreate} from './hooks'
7
+ import {AnchoredOverlayWrapperAnchorProps} from './AnchoredOverlay/AnchoredOverlay'
8
+ import {Divider} from './ActionList2/Divider'
9
+ import {MenuContext as ActionListMenuContext} from './ActionList2/MenuContext'
10
+
11
+ type ActionMenuBaseProps = {
12
+ /**
13
+ * Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with ActionList`
14
+ */
15
+ children: React.ReactElement[] | React.ReactElement
16
+
17
+ /**
18
+ * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `onOpenChange`.
19
+ */
20
+ open?: boolean
21
+
22
+ /**
23
+ * If defined, will control the open/closed state of the overlay. Must be used in conjuction with `open`.
24
+ */
25
+ onOpenChange?: (s: boolean) => void
26
+
27
+ /**
28
+ * Props to be spread on the internal `Overlay` component.
29
+ */
30
+ overlayProps?: Partial<OverlayProps>
31
+ }
32
+
33
+ export type ActionMenuProps = ActionMenuBaseProps & AnchoredOverlayWrapperAnchorProps
34
+
35
+ const ActionMenuBase: React.FC<ActionMenuProps> = ({
36
+ anchorRef: externalAnchorRef,
37
+ open,
38
+ onOpenChange,
39
+ overlayProps,
40
+ children
41
+ }: ActionMenuProps) => {
42
+ const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
43
+ const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
44
+ const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
45
+ const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
46
+
47
+ let renderAnchor: AnchoredOverlayWrapperAnchorProps['renderAnchor'] = null
48
+ const contents: React.ReactElement[] = []
49
+
50
+ React.Children.map(children, child => {
51
+ if (child.type === MenuButton || child.type === Anchor) {
52
+ renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
53
+ } else {
54
+ contents.push(child)
55
+ }
56
+ })
57
+
58
+ return (
59
+ <AnchoredOverlay
60
+ renderAnchor={renderAnchor}
61
+ anchorRef={anchorRef}
62
+ open={combinedOpenState}
63
+ onOpen={onOpen}
64
+ onClose={onClose}
65
+ overlayProps={overlayProps}
66
+ >
67
+ <ActionListMenuContext.Provider
68
+ value={{parent: 'ActionMenu', listRole: 'menu', itemRole: 'menuitem', afterSelect: onClose}}
69
+ >
70
+ {contents}
71
+ </ActionListMenuContext.Provider>
72
+ </AnchoredOverlay>
73
+ )
74
+ }
75
+
76
+ type AnchorRef = AnchoredOverlayWrapperAnchorProps['anchorRef']
77
+
78
+ export type MenuAnchorProps = {children: React.ReactElement}
79
+ const Anchor = React.forwardRef<AnchorRef, MenuAnchorProps>(({children, ...anchorProps}, anchorRef) => {
80
+ return React.cloneElement(children, {...anchorProps, ref: anchorRef})
81
+ })
82
+
83
+ /** this component is syntactical sugar 🍭 */
84
+ export type MenuButtonProps = ButtonProps
85
+ const MenuButton = React.forwardRef<AnchorRef, ButtonProps>((props, anchorRef) => {
86
+ return (
87
+ <Anchor ref={anchorRef}>
88
+ <Button {...props} />
89
+ </Anchor>
90
+ )
91
+ })
92
+
93
+ ActionMenuBase.displayName = 'ActionMenu'
94
+ export const ActionMenu = Object.assign(ActionMenuBase, {Button: MenuButton, Anchor, Divider})
package/src/drafts.ts CHANGED
@@ -8,3 +8,4 @@
8
8
  // Components
9
9
  export * from './ActionList2'
10
10
  export * from './NewButton'
11
+ export * from './ActionMenu2'