@primer/components 30.3.0-rc.2010c7d4 → 31.0.0-rc.15aa0a10

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 (180) hide show
  1. package/.eslintrc.json +2 -1
  2. package/.storybook/preview.js +4 -4
  3. package/CHANGELOG.md +12 -2
  4. package/codemods/deprecateUtilityComponents.js +1 -1
  5. package/contributor-docs/adrs/adr-003-prop-norms.md +72 -0
  6. package/dist/browser.esm.js +798 -794
  7. package/dist/browser.esm.js.map +1 -1
  8. package/dist/browser.umd.js +801 -797
  9. package/dist/browser.umd.js.map +1 -1
  10. package/docs/content/Autocomplete.mdx +627 -0
  11. package/docs/content/TextInputTokens.mdx +89 -0
  12. package/docs/content/getting-started.md +1 -1
  13. package/docs/content/overriding-styles.mdx +7 -6
  14. package/docs/content/theming.md +5 -5
  15. package/docs/package-lock.json +288 -511
  16. package/docs/package.json +1 -1
  17. package/docs/src/@primer/gatsby-theme-doctocat/components/hero.js +14 -12
  18. package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
  19. package/docs/src/@primer/gatsby-theme-doctocat/primer-components-hero.svg +7 -7
  20. package/lib/ActionList/Item.js +1 -1
  21. package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  22. package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
  23. package/lib/Autocomplete/Autocomplete.d.ts +304 -0
  24. package/lib/Autocomplete/Autocomplete.js +145 -0
  25. package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
  26. package/lib/Autocomplete/AutocompleteContext.js +11 -0
  27. package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
  28. package/lib/Autocomplete/AutocompleteInput.js +157 -0
  29. package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
  30. package/lib/Autocomplete/AutocompleteMenu.js +224 -0
  31. package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  32. package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
  33. package/lib/Autocomplete/index.d.ts +2 -0
  34. package/lib/Autocomplete/index.js +15 -0
  35. package/lib/BaseStyles.js +1 -1
  36. package/lib/BorderBox.js +1 -1
  37. package/lib/Button/ButtonInvisible.js +1 -1
  38. package/lib/Caret.js +2 -2
  39. package/lib/Dialog.js +1 -1
  40. package/lib/FilteredActionList/FilteredActionList.js +5 -31
  41. package/lib/Flash.js +16 -16
  42. package/lib/Label.js +1 -1
  43. package/lib/Overlay.d.ts +1 -0
  44. package/lib/Overlay.js +3 -1
  45. package/lib/ProgressBar.js +1 -1
  46. package/lib/StateLabel.js +13 -19
  47. package/lib/Token/_RemoveTokenButton.js +1 -1
  48. package/lib/__tests__/Autocomplete.test.d.ts +1 -0
  49. package/lib/__tests__/Autocomplete.test.js +528 -0
  50. package/lib/__tests__/BorderBox.test.js +1 -1
  51. package/lib/__tests__/CircleOcticon.test.js +1 -1
  52. package/lib/__tests__/CounterLabel.test.js +4 -4
  53. package/lib/__tests__/Flash.test.js +4 -4
  54. package/lib/__tests__/Link.test.js +1 -1
  55. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  56. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
  57. package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
  58. package/lib/behaviors/scrollIntoViewingArea.js +39 -0
  59. package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
  60. package/lib/hooks/useOpenAndCloseFocus.js +7 -2
  61. package/lib/hooks/useOverlay.d.ts +2 -1
  62. package/lib/hooks/useOverlay.js +4 -2
  63. package/lib/index.d.ts +2 -0
  64. package/lib/index.js +8 -0
  65. package/lib/stories/Autocomplete.stories.js +608 -0
  66. package/lib/stories/Dialog.stories.js +3 -3
  67. package/lib/stories/IssueLabelToken.stories.js +1 -1
  68. package/lib/stories/ProfileToken.stories.js +1 -1
  69. package/lib/theme-preval.js +370 -3100
  70. package/lib/utils/testing.d.ts +50 -493
  71. package/lib/utils/types/MandateProps.d.ts +3 -0
  72. package/lib/utils/types/MandateProps.js +1 -0
  73. package/lib/utils/types/index.d.ts +1 -0
  74. package/lib/utils/types/index.js +13 -0
  75. package/lib-esm/ActionList/Item.js +1 -1
  76. package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  77. package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
  78. package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
  79. package/lib-esm/Autocomplete/Autocomplete.js +123 -0
  80. package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
  81. package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
  82. package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
  83. package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
  84. package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
  85. package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
  86. package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  87. package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
  88. package/lib-esm/Autocomplete/index.d.ts +2 -0
  89. package/lib-esm/Autocomplete/index.js +1 -0
  90. package/lib-esm/BaseStyles.js +1 -1
  91. package/lib-esm/BorderBox.js +1 -1
  92. package/lib-esm/Button/ButtonInvisible.js +1 -1
  93. package/lib-esm/Caret.js +2 -2
  94. package/lib-esm/Dialog.js +1 -1
  95. package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
  96. package/lib-esm/Flash.js +16 -16
  97. package/lib-esm/Label.js +1 -1
  98. package/lib-esm/Overlay.d.ts +1 -0
  99. package/lib-esm/Overlay.js +3 -1
  100. package/lib-esm/ProgressBar.js +1 -1
  101. package/lib-esm/StateLabel.js +13 -19
  102. package/lib-esm/Token/_RemoveTokenButton.js +1 -1
  103. package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
  104. package/lib-esm/__tests__/Autocomplete.test.js +494 -0
  105. package/lib-esm/__tests__/BorderBox.test.js +1 -1
  106. package/lib-esm/__tests__/CircleOcticon.test.js +1 -1
  107. package/lib-esm/__tests__/CounterLabel.test.js +4 -4
  108. package/lib-esm/__tests__/Flash.test.js +4 -4
  109. package/lib-esm/__tests__/Link.test.js +1 -1
  110. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  111. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
  112. package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
  113. package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
  114. package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
  115. package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
  116. package/lib-esm/hooks/useOverlay.d.ts +2 -1
  117. package/lib-esm/hooks/useOverlay.js +4 -2
  118. package/lib-esm/index.d.ts +2 -0
  119. package/lib-esm/index.js +1 -0
  120. package/lib-esm/stories/Autocomplete.stories.js +549 -0
  121. package/lib-esm/stories/Dialog.stories.js +3 -3
  122. package/lib-esm/stories/IssueLabelToken.stories.js +1 -1
  123. package/lib-esm/stories/ProfileToken.stories.js +1 -1
  124. package/lib-esm/theme-preval.js +370 -3100
  125. package/lib-esm/utils/testing.d.ts +50 -493
  126. package/lib-esm/utils/types/MandateProps.d.ts +3 -0
  127. package/lib-esm/utils/types/MandateProps.js +1 -0
  128. package/lib-esm/utils/types/index.d.ts +1 -0
  129. package/lib-esm/utils/types/index.js +2 -1
  130. package/package-lock.json +11 -8
  131. package/package.json +3 -3
  132. package/src/ActionList/Item.tsx +1 -1
  133. package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
  134. package/src/Autocomplete/Autocomplete.tsx +103 -0
  135. package/src/Autocomplete/AutocompleteContext.tsx +19 -0
  136. package/src/Autocomplete/AutocompleteInput.tsx +179 -0
  137. package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
  138. package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
  139. package/src/Autocomplete/index.ts +2 -0
  140. package/src/BaseStyles.tsx +1 -1
  141. package/src/BorderBox.tsx +1 -1
  142. package/src/Button/ButtonInvisible.tsx +7 -2
  143. package/src/Caret.tsx +2 -2
  144. package/src/Dialog.tsx +1 -1
  145. package/src/FilteredActionList/FilteredActionList.tsx +10 -25
  146. package/src/Flash.tsx +16 -16
  147. package/src/Label.tsx +1 -1
  148. package/src/Overlay.tsx +4 -1
  149. package/src/ProgressBar.tsx +1 -1
  150. package/src/StateLabel.tsx +12 -20
  151. package/src/Token/_RemoveTokenButton.tsx +4 -2
  152. package/src/__tests__/Autocomplete.test.tsx +444 -0
  153. package/src/__tests__/BorderBox.test.tsx +1 -1
  154. package/src/__tests__/CircleOcticon.test.tsx +1 -1
  155. package/src/__tests__/CounterLabel.test.tsx +4 -4
  156. package/src/__tests__/Flash.test.tsx +4 -4
  157. package/src/__tests__/Link.test.tsx +1 -1
  158. package/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap +3 -3
  159. package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
  160. package/src/__tests__/__snapshots__/Button.test.tsx.snap +9 -1
  161. package/src/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap +1 -1
  162. package/src/__tests__/__snapshots__/SelectPanel.test.tsx.snap +1 -1
  163. package/src/__tests__/__snapshots__/StateLabel.test.tsx.snap +0 -21
  164. package/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap +16 -16
  165. package/src/__tests__/__snapshots__/Token.test.tsx.snap +34 -34
  166. package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
  167. package/src/behaviors/scrollIntoViewingArea.ts +27 -0
  168. package/src/hooks/useOpenAndCloseFocus.ts +7 -2
  169. package/src/hooks/useOverlay.tsx +4 -2
  170. package/src/index.ts +2 -0
  171. package/src/stories/Autocomplete.stories.tsx +572 -0
  172. package/src/stories/Dialog.stories.tsx +3 -3
  173. package/src/stories/IssueLabelToken.stories.tsx +1 -1
  174. package/src/stories/ProfileToken.stories.tsx +1 -1
  175. package/src/utils/types/MandateProps.ts +19 -0
  176. package/src/utils/types/index.ts +1 -0
  177. package/stats.html +1 -1
  178. package/docs/src/@primer/gatsby-theme-doctocat/components/live-code.js +0 -84
  179. package/docs/src/@primer/gatsby-theme-doctocat/components/nav-dropdown.js +0 -48
  180. package/docs/src/@primer/gatsby-theme-doctocat/components/wrap-page-element.js +0 -25
@@ -5,14 +5,19 @@ export type UseOpenAndCloseFocusSettings = {
5
5
  initialFocusRef?: React.RefObject<HTMLElement>
6
6
  containerRef: React.RefObject<HTMLElement>
7
7
  returnFocusRef: React.RefObject<HTMLElement>
8
+ preventFocusOnOpen?: boolean
8
9
  }
9
10
 
10
11
  export function useOpenAndCloseFocus({
11
12
  initialFocusRef,
12
13
  returnFocusRef,
13
- containerRef
14
+ containerRef,
15
+ preventFocusOnOpen
14
16
  }: UseOpenAndCloseFocusSettings): void {
15
17
  useEffect(() => {
18
+ if (preventFocusOnOpen) {
19
+ return
20
+ }
16
21
  const returnRef = returnFocusRef.current
17
22
  if (initialFocusRef && initialFocusRef.current) {
18
23
  initialFocusRef.current.focus()
@@ -23,5 +28,5 @@ export function useOpenAndCloseFocus({
23
28
  return function () {
24
29
  returnRef?.focus()
25
30
  }
26
- }, [initialFocusRef, returnFocusRef, containerRef])
31
+ }, [initialFocusRef, returnFocusRef, containerRef, preventFocusOnOpen])
27
32
  }
@@ -10,6 +10,7 @@ export type UseOverlaySettings = {
10
10
  onEscape: (e: KeyboardEvent) => void
11
11
  onClickOutside: (e: TouchOrMouseEvent) => void
12
12
  overlayRef?: React.RefObject<HTMLDivElement>
13
+ preventFocusOnOpen?: boolean
13
14
  }
14
15
 
15
16
  export type OverlayReturnProps = {
@@ -22,10 +23,11 @@ export const useOverlay = ({
22
23
  initialFocusRef,
23
24
  onEscape,
24
25
  ignoreClickRefs,
25
- onClickOutside
26
+ onClickOutside,
27
+ preventFocusOnOpen
26
28
  }: UseOverlaySettings): OverlayReturnProps => {
27
29
  const overlayRef = useProvidedRefOrCreate<HTMLDivElement>(_overlayRef)
28
- useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef})
30
+ useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef, preventFocusOnOpen})
29
31
  useOnOutsideClick({containerRef: overlayRef, ignoreClickRefs, onClickOutside})
30
32
  useOnEscapePress(onEscape)
31
33
  return {ref: overlayRef}
package/src/index.ts CHANGED
@@ -30,6 +30,8 @@ export {useConfirm} from './Dialog/ConfirmationDialog'
30
30
  export {ActionList} from './ActionList'
31
31
  export {ActionMenu} from './ActionMenu'
32
32
  export type {ActionMenuProps} from './ActionMenu'
33
+ export {default as Autocomplete} from './Autocomplete'
34
+ export type {AutocompleteMenuProps, AutocompleteInputProps, AutocompleteOverlayProps} from './Autocomplete'
33
35
  export {default as Avatar} from './Avatar'
34
36
  export type {AvatarProps} from './Avatar'
35
37
  export {default as AvatarPair} from './AvatarPair'
@@ -0,0 +1,572 @@
1
+ import React, {ChangeEventHandler, RefObject, useCallback, useRef, useState} from 'react'
2
+ import {Meta} from '@storybook/react'
3
+
4
+ import {BaseStyles, Box, Text, TextInput, ThemeProvider} from '..'
5
+ import TextInputTokens from '../TextInputWithTokens'
6
+ import Autocomplete from '../Autocomplete/Autocomplete'
7
+ import {AnchoredOverlay} from '../AnchoredOverlay'
8
+ import {ButtonInvisible} from '../Button'
9
+
10
+ type ItemMetadata = {
11
+ fillColor: React.CSSProperties['backgroundColor']
12
+ }
13
+
14
+ type Datum = {
15
+ id: string | number
16
+ text: string
17
+ selected?: boolean
18
+ metadata?: ItemMetadata
19
+ }
20
+
21
+ const items: Datum[] = [
22
+ {text: 'zero', id: 0},
23
+ {text: 'one', id: 1},
24
+ {text: 'two', id: 2},
25
+ {text: 'three', id: 3},
26
+ {text: 'four', id: 4},
27
+ {text: 'five', id: 5},
28
+ {text: 'six', id: 6},
29
+ {text: 'seven', id: 7},
30
+ {text: 'twenty', id: 20},
31
+ {text: 'twentyone', id: 21}
32
+ ]
33
+
34
+ const mockTokens: Datum[] = [
35
+ {text: 'zero', id: 0},
36
+ {text: 'one', id: 1},
37
+ {text: 'three', id: 3},
38
+ {text: 'four', id: 4}
39
+ ]
40
+
41
+ export default {
42
+ title: 'Forms/Autocomplete',
43
+
44
+ decorators: [
45
+ Story => {
46
+ const [lastKey, setLastKey] = useState('none')
47
+ const reportKey = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
48
+ setLastKey(event.key)
49
+ }, [])
50
+
51
+ return (
52
+ <ThemeProvider>
53
+ <BaseStyles>
54
+ <Box onKeyDownCapture={reportKey}>
55
+ <Box position="absolute" right={5} top={2}>
56
+ Last key pressed: {lastKey}
57
+ </Box>
58
+ <Box paddingTop={5}>
59
+ <Story />
60
+ </Box>
61
+ </Box>
62
+ </BaseStyles>
63
+ </ThemeProvider>
64
+ )
65
+ }
66
+ ]
67
+ } as Meta
68
+
69
+ export const SingleSelect = () => {
70
+ return (
71
+ <>
72
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
73
+ Pick an option
74
+ </Box>
75
+ <Autocomplete>
76
+ <Autocomplete.Input id="autocompleteInput" />
77
+ <Autocomplete.Overlay>
78
+ <Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
79
+ </Autocomplete.Overlay>
80
+ </Autocomplete>
81
+ </>
82
+ )
83
+ }
84
+
85
+ export const MultiSelect = () => {
86
+ const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>([])
87
+ const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
88
+ if (!Array.isArray(newlySelectedItems)) {
89
+ return
90
+ }
91
+
92
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
93
+ }
94
+
95
+ const getItemById = (id: string | number) => items.find(item => item.id === id)
96
+
97
+ return (
98
+ <Box display="flex" sx={{gap: '1em'}}>
99
+ <div>
100
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
101
+ Pick an option
102
+ </Box>
103
+ <Autocomplete>
104
+ <Autocomplete.Input id="autocompleteInput" />
105
+ <Autocomplete.Overlay>
106
+ <Autocomplete.Menu
107
+ items={items}
108
+ selectedItemIds={selectedItemIds}
109
+ aria-labelledby="autocompleteLabel"
110
+ onSelectedChange={onSelectedChange}
111
+ selectionVariant="multiple"
112
+ />
113
+ </Autocomplete.Overlay>
114
+ </Autocomplete>
115
+ </div>
116
+ <div>
117
+ <div>Selected items:</div>
118
+ <Box as="ul" my={0}>
119
+ {selectedItemIds.map(selectedItemId => (
120
+ <li key={selectedItemId}>{getItemById(selectedItemId)?.text}</li>
121
+ ))}
122
+ </Box>
123
+ </div>
124
+ </Box>
125
+ )
126
+ }
127
+
128
+ export const MultiSelectWithTokenInput = () => {
129
+ const [tokens, setTokens] = useState<Datum[]>(mockTokens)
130
+ const selectedTokenIds = tokens.map(token => token.id)
131
+ const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>(selectedTokenIds)
132
+ const onTokenRemove: (tokenId: string | number) => void = tokenId => {
133
+ setTokens(tokens.filter(token => token.id !== tokenId))
134
+ setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
135
+ }
136
+ const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
137
+ if (!Array.isArray(newlySelectedItems)) {
138
+ return
139
+ }
140
+
141
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
142
+
143
+ if (newlySelectedItems.length < selectedItemIds.length) {
144
+ const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
145
+ const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
146
+
147
+ for (const removedItemId of removedItemIds) {
148
+ onTokenRemove(removedItemId)
149
+ }
150
+
151
+ return
152
+ }
153
+
154
+ setTokens(newlySelectedItems.map(({id, text}) => ({id, text})))
155
+ }
156
+
157
+ return (
158
+ <>
159
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
160
+ Pick options
161
+ </Box>
162
+ <Autocomplete>
163
+ <Autocomplete.Input as={TextInputTokens} tokens={tokens} onTokenRemove={onTokenRemove} id="autocompleteInput" />
164
+ <Autocomplete.Overlay>
165
+ <Autocomplete.Menu
166
+ items={items}
167
+ selectedItemIds={selectedItemIds}
168
+ onSelectedChange={onSelectedChange}
169
+ selectionVariant="multiple"
170
+ aria-labelledby="autocompleteLabel"
171
+ />
172
+ </Autocomplete.Overlay>
173
+ </Autocomplete>
174
+ </>
175
+ )
176
+ }
177
+
178
+ export const MultiSelectAddNewItem = () => {
179
+ const [localItemsState, setLocalItemsState] = useState<Datum[]>(items)
180
+ const [filterVal, setFilterVal] = useState<string>('')
181
+ const [tokens, setTokens] = useState<Datum[]>(mockTokens)
182
+ const selectedTokenIds = tokens.map(token => token.id)
183
+ const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>(selectedTokenIds)
184
+ const onTokenRemove: (tokenId: string | number) => void = tokenId => {
185
+ setTokens(tokens.filter(token => token.id !== tokenId))
186
+ setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
187
+ }
188
+ const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
189
+ if (!Array.isArray(newlySelectedItems)) {
190
+ return
191
+ }
192
+
193
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
194
+
195
+ if (newlySelectedItems.length < selectedItemIds.length) {
196
+ const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
197
+ const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
198
+
199
+ for (const removedItemId of removedItemIds) {
200
+ onTokenRemove(removedItemId)
201
+ }
202
+
203
+ return
204
+ }
205
+
206
+ setTokens(newlySelectedItems.map(({id, text}) => ({id, text})))
207
+ }
208
+
209
+ const onItemSelect: (item: Datum) => void = item => {
210
+ onSelectedChange([...selectedItemIds.map(id => items.find(selectedItem => selectedItem.id === id) as Datum), item])
211
+
212
+ if (!localItemsState.some(localItem => localItem.id === item.id)) {
213
+ setLocalItemsState([...localItemsState, item])
214
+ }
215
+ }
216
+
217
+ const handleChange: ChangeEventHandler<HTMLInputElement> = e => {
218
+ setFilterVal(e.currentTarget.value)
219
+ }
220
+
221
+ return (
222
+ <>
223
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
224
+ Pick options
225
+ </Box>
226
+ <Autocomplete>
227
+ <Autocomplete.Input
228
+ as={TextInputTokens}
229
+ tokens={tokens}
230
+ onTokenRemove={onTokenRemove}
231
+ onChange={handleChange}
232
+ id="autocompleteInput"
233
+ />
234
+ <Autocomplete.Overlay>
235
+ <Autocomplete.Menu
236
+ addNewItem={
237
+ filterVal && !localItemsState.map(localItem => localItem.text).includes(filterVal)
238
+ ? {
239
+ text: `Add '${filterVal}'`,
240
+ handleAddItem: item => {
241
+ onItemSelect({
242
+ ...item,
243
+ text: filterVal,
244
+ selected: true
245
+ })
246
+ setFilterVal('')
247
+ }
248
+ }
249
+ : undefined
250
+ }
251
+ items={localItemsState}
252
+ selectedItemIds={selectedItemIds}
253
+ onSelectedChange={onSelectedChange}
254
+ selectionVariant="multiple"
255
+ aria-labelledby="autocompleteLabel"
256
+ />
257
+ </Autocomplete.Overlay>
258
+ </Autocomplete>
259
+ </>
260
+ )
261
+ }
262
+
263
+ export const CustomEmptyStateMessage = () => {
264
+ return (
265
+ <>
266
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
267
+ Pick an option
268
+ </Box>
269
+ <Autocomplete>
270
+ <Autocomplete.Input id="autocompleteInput" />
271
+ <Autocomplete.Overlay>
272
+ <Autocomplete.Menu
273
+ items={items}
274
+ selectedItemIds={[]}
275
+ aria-labelledby="autocompleteLabel"
276
+ emptyStateText="Sorry, no matches"
277
+ />
278
+ </Autocomplete.Overlay>
279
+ </Autocomplete>
280
+ </>
281
+ )
282
+ }
283
+
284
+ export const CustomSearchFilter = () => {
285
+ const [filterVal, setFilterVal] = useState<string>('')
286
+ const handleChange: ChangeEventHandler<HTMLInputElement> = e => {
287
+ setFilterVal(e.currentTarget.value)
288
+ }
289
+ const customFilterFn = (item: Datum) => item.text.includes(filterVal)
290
+
291
+ return (
292
+ <>
293
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
294
+ Pick an option
295
+ </Box>
296
+ <Autocomplete>
297
+ <Autocomplete.Input id="autocompleteInput" onChange={handleChange} />
298
+ <Autocomplete.Overlay>
299
+ <Autocomplete.Menu
300
+ items={items}
301
+ selectedItemIds={[]}
302
+ aria-labelledby="autocompleteLabel"
303
+ filterFn={customFilterFn}
304
+ />
305
+ </Autocomplete.Overlay>
306
+ </Autocomplete>
307
+ <Text fontSize={0} display="block" color="fg.subtle" mt={2}>
308
+ Items in dropdown are filtered if their text has no part that matches the input value
309
+ </Text>
310
+ </>
311
+ )
312
+ }
313
+
314
+ export const CustomSortAfterMenuClose = () => {
315
+ const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>([])
316
+ const isItemSelected = (itemId: string | number) => selectedItemIds.includes(itemId)
317
+ const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
318
+ if (!Array.isArray(newlySelectedItems)) {
319
+ return
320
+ }
321
+
322
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
323
+ }
324
+ const customSortFn = (itemIdA: string | number, itemIdB: string | number) =>
325
+ isItemSelected(itemIdA) === isItemSelected(itemIdB) ? 0 : isItemSelected(itemIdA) ? 1 : -1
326
+
327
+ return (
328
+ <>
329
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
330
+ Pick an option
331
+ </Box>
332
+ <Autocomplete>
333
+ <Autocomplete.Input id="autocompleteInput" />
334
+ <Autocomplete.Overlay>
335
+ <Autocomplete.Menu
336
+ items={items}
337
+ selectedItemIds={selectedItemIds}
338
+ aria-labelledby="autocompleteLabel"
339
+ onSelectedChange={onSelectedChange}
340
+ sortOnCloseFn={customSortFn}
341
+ selectionVariant="multiple"
342
+ />
343
+ </Autocomplete.Overlay>
344
+ </Autocomplete>
345
+ <Text fontSize={0} display="block" color="fg.subtle" mt={2}>
346
+ When the dropdown closes, selected items are sorted to the end
347
+ </Text>
348
+ </>
349
+ )
350
+ }
351
+
352
+ export const WithCallbackWhenOverlayOpenStateChanges = () => {
353
+ const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
354
+ const onOpenChange = (isOpen: boolean) => {
355
+ setIsMenuOpen(isOpen)
356
+ }
357
+
358
+ return (
359
+ <Box display="flex" sx={{gap: '1em'}}>
360
+ <div>
361
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
362
+ Pick an option
363
+ </Box>
364
+ <Autocomplete>
365
+ <Autocomplete.Input id="autocompleteInput" />
366
+ <Autocomplete.Overlay>
367
+ <Autocomplete.Menu
368
+ items={items}
369
+ selectedItemIds={[]}
370
+ aria-labelledby="autocompleteLabel"
371
+ onOpenChange={onOpenChange}
372
+ />
373
+ </Autocomplete.Overlay>
374
+ </Autocomplete>
375
+ </div>
376
+ <div>
377
+ The menu is <strong>{isMenuOpen ? 'opened' : 'closed'}</strong>
378
+ </div>
379
+ </Box>
380
+ )
381
+ }
382
+
383
+ export const AsyncLoadingOfItems = () => {
384
+ const [loadedItems, setLoadedItems] = useState<Datum[]>([])
385
+ const onOpenChange = () => {
386
+ setTimeout(() => {
387
+ setLoadedItems(items)
388
+ }, 1500)
389
+ }
390
+
391
+ return (
392
+ <>
393
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
394
+ Pick an option
395
+ </Box>
396
+ <Autocomplete>
397
+ <Autocomplete.Input id="autocompleteInput" />
398
+ <Autocomplete.Overlay>
399
+ <Autocomplete.Menu
400
+ items={loadedItems}
401
+ selectedItemIds={[]}
402
+ aria-labelledby="autocompleteLabel"
403
+ onOpenChange={onOpenChange}
404
+ loading={loadedItems.length === 0}
405
+ />
406
+ </Autocomplete.Overlay>
407
+ </Autocomplete>
408
+ </>
409
+ )
410
+ }
411
+
412
+ export const RenderingTheMenuOutsideAnOverlay = () => {
413
+ return (
414
+ <>
415
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
416
+ Pick an option
417
+ </Box>
418
+ <Autocomplete>
419
+ <Autocomplete.Input id="autocompleteInput" />
420
+ <Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
421
+ </Autocomplete>
422
+ </>
423
+ )
424
+ }
425
+
426
+ export const CustomOverlayMenuAnchor = () => {
427
+ const menuAnchorRef = useRef<HTMLElement>(null)
428
+ const anchorWrapperStyles = {
429
+ display: 'flex',
430
+ alignItems: 'center',
431
+ flexGrow: 1,
432
+ flexShrink: 0,
433
+ flexBasis: '25%',
434
+ border: '1px solid black',
435
+ padding: '1em'
436
+ }
437
+
438
+ return (
439
+ <>
440
+ <Box as="label" htmlFor="autocompleteInput" id="autocompleteLabel">
441
+ Pick labels
442
+ </Box>
443
+ <Box {...anchorWrapperStyles} ref={menuAnchorRef as React.RefObject<HTMLDivElement>}>
444
+ <Autocomplete>
445
+ <Autocomplete.Input
446
+ as={TextInput}
447
+ id="autocompleteInput"
448
+ sx={{
449
+ border: '0',
450
+ padding: '0',
451
+ boxShadow: 'none',
452
+ ':focus-within': {
453
+ border: '0',
454
+ boxShadow: 'none'
455
+ }
456
+ }}
457
+ />
458
+ <Autocomplete.Overlay menuAnchorRef={menuAnchorRef}>
459
+ <Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
460
+ </Autocomplete.Overlay>
461
+ </Autocomplete>
462
+ </Box>
463
+ <Text fontSize={0} display="block" color="fg.subtle" mt={2}>
464
+ The overlay menu&apos;s position is anchored to the div with the black border instead of to the text input
465
+ </Text>
466
+ </>
467
+ )
468
+ }
469
+
470
+ export const WithCustomOverlayProps = () => {
471
+ return (
472
+ <>
473
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
474
+ Pick an option
475
+ </Box>
476
+ <Autocomplete>
477
+ <Autocomplete.Input id="autocompleteInput" />
478
+ <Autocomplete.Overlay
479
+ overlayProps={{
480
+ width: 'large',
481
+ height: 'xsmall'
482
+ }}
483
+ >
484
+ <Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
485
+ </Autocomplete.Overlay>
486
+ </Autocomplete>
487
+ </>
488
+ )
489
+ }
490
+
491
+ export const InOverlayWithCustomScrollContainerRef = () => {
492
+ const scrollContainerRef = useRef<HTMLElement>(null)
493
+ const inputRef = useRef<HTMLInputElement>(null)
494
+
495
+ const [isOpen, setIsOpen] = useState(false)
496
+ const handleOpen = () => {
497
+ setIsOpen(true)
498
+ inputRef.current && inputRef.current.focus()
499
+ }
500
+
501
+ return (
502
+ <AnchoredOverlay
503
+ open={isOpen}
504
+ onOpen={handleOpen}
505
+ onClose={() => setIsOpen(false)}
506
+ width="large"
507
+ height="xsmall"
508
+ focusTrapSettings={{initialFocusRef: inputRef}}
509
+ side="inside-top"
510
+ renderAnchor={props => <ButtonInvisible {...props}>open overlay</ButtonInvisible>}
511
+ >
512
+ <Box
513
+ as="label"
514
+ display="block"
515
+ htmlFor="autocompleteInput"
516
+ id="autocompleteLabel"
517
+ sx={{
518
+ // visually hides this label for sighted users
519
+ position: 'absolute',
520
+ width: '1px',
521
+ height: '1px',
522
+ padding: '0',
523
+ margin: '-1px',
524
+ overflow: 'hidden',
525
+ clip: 'rect(0, 0, 0, 0)',
526
+ whiteSpace: 'nowrap',
527
+ borderWidth: '0'
528
+ }}
529
+ >
530
+ Pick options
531
+ </Box>
532
+ <Autocomplete>
533
+ <Box display="flex" flexDirection="column" height="100%">
534
+ <Box
535
+ paddingX="3"
536
+ paddingY="1"
537
+ borderWidth={0}
538
+ borderBottomWidth={1}
539
+ borderColor="border.default"
540
+ borderStyle="solid"
541
+ >
542
+ <Autocomplete.Input
543
+ block
544
+ as={TextInput}
545
+ ref={inputRef}
546
+ id="autocompleteInput"
547
+ sx={{
548
+ display: 'flex',
549
+ border: '0',
550
+ padding: '0',
551
+ boxShadow: 'none',
552
+ ':focus-within': {
553
+ border: '0',
554
+ boxShadow: 'none'
555
+ }
556
+ }}
557
+ />
558
+ </Box>
559
+ <Box overflow="auto" flexGrow={1} ref={scrollContainerRef as RefObject<HTMLDivElement>}>
560
+ <Autocomplete.Menu
561
+ items={items}
562
+ selectedItemIds={[]}
563
+ // onSelectedChange={onSelectedChange}
564
+ customScrollContainerRef={scrollContainerRef}
565
+ aria-labelledby="autocompleteLabel"
566
+ />
567
+ </Box>
568
+ </Box>
569
+ </Autocomplete>
570
+ </AnchoredOverlay>
571
+ )
572
+ }
@@ -150,7 +150,7 @@ function CustomHeader({
150
150
  }, [onClose])
151
151
  if (typeof title === 'string' && typeof subtitle === 'string') {
152
152
  return (
153
- <Box bg="auto.blue.3">
153
+ <Box bg="accent.subtle">
154
154
  <h1 id={dialogLabelId}>{title.toUpperCase()}</h1>
155
155
  <h2 id={dialogDescriptionId}>{subtitle.toLowerCase()}</h2>
156
156
  <Dialog.CloseButton onClose={onCloseClick} />
@@ -160,11 +160,11 @@ function CustomHeader({
160
160
  return null
161
161
  }
162
162
  function CustomBody({children}: React.PropsWithChildren<DialogProps>) {
163
- return <Dialog.Body bg="auto.red.3">{children}</Dialog.Body>
163
+ return <Dialog.Body bg="danger.subtle">{children}</Dialog.Body>
164
164
  }
165
165
  function CustomFooter({footerButtons}: React.PropsWithChildren<DialogProps>) {
166
166
  return (
167
- <Dialog.Footer bg="auto.yellow.3">
167
+ <Dialog.Footer bg="attention.subtle">
168
168
  {footerButtons ? <Dialog.Buttons buttons={footerButtons} /> : null}
169
169
  </Dialog.Footer>
170
170
  )
@@ -47,7 +47,7 @@ const SingleExampleContainer: React.FC<{label?: string}> = ({children, label}) =
47
47
  }}
48
48
  >
49
49
  {label ? (
50
- <Text fontSize={0} color="text.tertiary">
50
+ <Text fontSize={0} color="fg.muted">
51
51
  {label}
52
52
  </Text>
53
53
  ) : null}
@@ -44,7 +44,7 @@ const SingleExampleContainer: React.FC<{label?: string}> = ({children, label}) =
44
44
  }}
45
45
  >
46
46
  {label ? (
47
- <Text fontSize={0} color="text.tertiary">
47
+ <Text fontSize={0} color="fg.muted">
48
48
  {label}
49
49
  </Text>
50
50
  ) : null}
@@ -0,0 +1,19 @@
1
+ /*
2
+ Used to convert a list of properties in a type from optional to required
3
+
4
+ For example, we could make a new type from `Datum`
5
+ where 'id' and 'label' required:
6
+ type Datum = {
7
+ description?: string
8
+ id?: string
9
+ label?: string
10
+ value: string
11
+ }
12
+
13
+ type DatumWithRequiredIdAndLabel = MandateProps<Datum, 'id' | 'label'>
14
+ */
15
+
16
+ export type MandateProps<T extends unknown, K extends keyof T> = Omit<T, K> &
17
+ {
18
+ [MK in K]-?: NonNullable<T[MK]>
19
+ }
@@ -2,3 +2,4 @@ export * from './AriaRole'
2
2
  export * from './ComponentProps'
3
3
  export * from './Flatten'
4
4
  export * from './Merge'
5
+ export * from './MandateProps'