@primer/components 30.3.0-rc.2010c7d4 → 30.3.0-rc.9dbc85a9

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 (97) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/dist/browser.esm.js +717 -718
  3. package/dist/browser.esm.js.map +1 -1
  4. package/dist/browser.umd.js +320 -321
  5. package/dist/browser.umd.js.map +1 -1
  6. package/docs/content/Autocomplete.mdx +627 -0
  7. package/docs/content/TextInputTokens.mdx +89 -0
  8. package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
  9. package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  10. package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
  11. package/lib/Autocomplete/Autocomplete.d.ts +304 -0
  12. package/lib/Autocomplete/Autocomplete.js +145 -0
  13. package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
  14. package/lib/Autocomplete/AutocompleteContext.js +11 -0
  15. package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
  16. package/lib/Autocomplete/AutocompleteInput.js +157 -0
  17. package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
  18. package/lib/Autocomplete/AutocompleteMenu.js +224 -0
  19. package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  20. package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
  21. package/lib/Autocomplete/index.d.ts +2 -0
  22. package/lib/Autocomplete/index.js +15 -0
  23. package/lib/FilteredActionList/FilteredActionList.js +5 -31
  24. package/lib/Overlay.d.ts +1 -0
  25. package/lib/Overlay.js +3 -1
  26. package/lib/__tests__/Autocomplete.test.d.ts +1 -0
  27. package/lib/__tests__/Autocomplete.test.js +528 -0
  28. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  29. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
  30. package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
  31. package/lib/behaviors/scrollIntoViewingArea.js +39 -0
  32. package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
  33. package/lib/hooks/useOpenAndCloseFocus.js +7 -2
  34. package/lib/hooks/useOverlay.d.ts +2 -1
  35. package/lib/hooks/useOverlay.js +4 -2
  36. package/lib/index.d.ts +2 -0
  37. package/lib/index.js +8 -0
  38. package/lib/stories/Autocomplete.stories.js +608 -0
  39. package/lib/utils/types/MandateProps.d.ts +3 -0
  40. package/lib/utils/types/MandateProps.js +1 -0
  41. package/lib/utils/types/index.d.ts +1 -0
  42. package/lib/utils/types/index.js +13 -0
  43. package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  44. package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
  45. package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
  46. package/lib-esm/Autocomplete/Autocomplete.js +123 -0
  47. package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
  48. package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
  49. package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
  50. package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
  51. package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
  52. package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
  53. package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  54. package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
  55. package/lib-esm/Autocomplete/index.d.ts +2 -0
  56. package/lib-esm/Autocomplete/index.js +1 -0
  57. package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
  58. package/lib-esm/Overlay.d.ts +1 -0
  59. package/lib-esm/Overlay.js +3 -1
  60. package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
  61. package/lib-esm/__tests__/Autocomplete.test.js +494 -0
  62. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  63. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
  64. package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
  65. package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
  66. package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
  67. package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
  68. package/lib-esm/hooks/useOverlay.d.ts +2 -1
  69. package/lib-esm/hooks/useOverlay.js +4 -2
  70. package/lib-esm/index.d.ts +2 -0
  71. package/lib-esm/index.js +1 -0
  72. package/lib-esm/stories/Autocomplete.stories.js +549 -0
  73. package/lib-esm/utils/types/MandateProps.d.ts +3 -0
  74. package/lib-esm/utils/types/MandateProps.js +1 -0
  75. package/lib-esm/utils/types/index.d.ts +1 -0
  76. package/lib-esm/utils/types/index.js +2 -1
  77. package/package.json +1 -1
  78. package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
  79. package/src/Autocomplete/Autocomplete.tsx +103 -0
  80. package/src/Autocomplete/AutocompleteContext.tsx +19 -0
  81. package/src/Autocomplete/AutocompleteInput.tsx +179 -0
  82. package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
  83. package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
  84. package/src/Autocomplete/index.ts +2 -0
  85. package/src/FilteredActionList/FilteredActionList.tsx +10 -25
  86. package/src/Overlay.tsx +4 -1
  87. package/src/__tests__/Autocomplete.test.tsx +444 -0
  88. package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
  89. package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
  90. package/src/behaviors/scrollIntoViewingArea.ts +27 -0
  91. package/src/hooks/useOpenAndCloseFocus.ts +7 -2
  92. package/src/hooks/useOverlay.tsx +4 -2
  93. package/src/index.ts +2 -0
  94. package/src/stories/Autocomplete.stories.tsx +572 -0
  95. package/src/utils/types/MandateProps.ts +19 -0
  96. package/src/utils/types/index.ts +1 -0
  97. package/stats.html +1 -1
@@ -0,0 +1,627 @@
1
+ ---
2
+ title: Autocomplete
3
+ status: Alpha
4
+ ---
5
+
6
+ import {Props} from '../src/props'
7
+ import {Autocomplete} from '@primer/components'
8
+
9
+ The `Autocomplete` components are used to render a text input that allows a user to quickly filter through a list of options to pick one or more values. It is comprised of an `Autocomplete.Input` component that a user types into, and a `Autocomplete.Menu` component that displays the list of selectable values.
10
+
11
+ ## Basic Example
12
+
13
+ ```jsx live
14
+ <>
15
+ <Box as="label" display="block" htmlFor="autocompleteInput-basic" id="autocompleteLabel-basic">
16
+ Pick a branch
17
+ </Box>
18
+ <Autocomplete>
19
+ <Autocomplete.Input id="autocompleteInput-basic" />
20
+ <Autocomplete.Overlay>
21
+ <Autocomplete.Menu
22
+ items={[
23
+ {text: 'main', id: 0},
24
+ {text: 'autocomplete-tests', id: 1},
25
+ {text: 'a11y-improvements', id: 2},
26
+ {text: 'button-bug-fixes', id: 3},
27
+ {text: 'radio-input-component', id: 4},
28
+ {text: 'release-1.0.0', id: 5},
29
+ {text: 'text-input-implementation', id: 6},
30
+ {text: 'visual-design-tweaks', id: 7}
31
+ ]}
32
+ selectedItemIds={[]}
33
+ aria-labelledby="autocompleteLabel-basic"
34
+ />
35
+ </Autocomplete.Overlay>
36
+ </Autocomplete>
37
+ </>
38
+ ```
39
+
40
+ ## Autocomplete.Input
41
+
42
+ The text input is used to filter the options in the dropdown menu. It is also used to show the selected value (or values).
43
+
44
+ The default input rendered is the `TextInput` component. A different text input component may be rendered by passing a different component to the `as` prop.
45
+
46
+ The `Autocomplete.Input` should not be rendered without a `<label>` who's `htmlFor` prop matches the `Autocomplete.Input`'s `id` prop
47
+
48
+ ### Component Props
49
+
50
+ `Autocomplete.Input` accepts the same props as a native `<input />`. The other props of `Autocomplete.Input` depend on what component is passed to the `as` prop. The default value for `as` is [TextInput](/TextInput)
51
+
52
+ ### Example: Passing a custom text input
53
+
54
+ In this example, we're passing a [TextInputWithTokens](/TextInputWithTokens) component
55
+
56
+ ```javascript live noinline
57
+ const CustomTextInputExample = () => {
58
+ const [tokens, setTokens] = React.useState([{text: 'zero', id: 0}])
59
+ const selectedTokenIds = tokens.map(token => token.id)
60
+ const [selectedItemIds, setSelectedItemIds] = React.useState(selectedTokenIds)
61
+ const onTokenRemove = tokenId => {
62
+ setTokens(tokens.filter(token => token.id !== tokenId))
63
+ setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
64
+ }
65
+ const onSelectedChange = newlySelectedItems => {
66
+ if (!Array.isArray(newlySelectedItems)) {
67
+ return
68
+ }
69
+
70
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
71
+
72
+ if (newlySelectedItems.length < selectedItemIds.length) {
73
+ const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
74
+ const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
75
+
76
+ for (const removedItemId of removedItemIds) {
77
+ onTokenRemove(removedItemId)
78
+ }
79
+
80
+ return
81
+ }
82
+
83
+ setTokens(newlySelectedItems.map(({id, text}) => ({id, text})))
84
+ }
85
+
86
+ return (
87
+ <>
88
+ <Box as="label" display="block" htmlFor="autocompleteInput-customInput" id="autocompleteLabel-customInput">
89
+ Pick options
90
+ </Box>
91
+ <Autocomplete>
92
+ <Autocomplete.Input
93
+ as={TextInputWithTokens}
94
+ tokens={tokens}
95
+ onTokenRemove={onTokenRemove}
96
+ id="autocompleteInput-customInput"
97
+ />
98
+ <Autocomplete.Overlay>
99
+ <Autocomplete.Menu
100
+ items={[
101
+ {text: 'zero', id: 0},
102
+ {text: 'one', id: 1},
103
+ {text: 'two', id: 2},
104
+ {text: 'three', id: 3},
105
+ {text: 'four', id: 4},
106
+ {text: 'five', id: 5},
107
+ {text: 'six', id: 6},
108
+ {text: 'seven', id: 7}
109
+ ]}
110
+ selectedItemIds={selectedItemIds}
111
+ onSelectedChange={onSelectedChange}
112
+ selectionVariant="multiple"
113
+ aria-labelledby="autocompleteLabel-customInput"
114
+ />
115
+ </Autocomplete.Overlay>
116
+ </Autocomplete>
117
+ </>
118
+ )
119
+ }
120
+
121
+ render(<CustomTextInputExample />)
122
+ ```
123
+
124
+ ## Autocomplete.Overlay
125
+
126
+ The `Autocomplete.Overlay` wraps the `Autocomplete.Menu` to display it in an [Overlay]() component.
127
+ Most `Autocomplete` implementations will use the `Autocomplete.Overlay` component, but there could be special cases where the `Autocomplete.Menu` should be rendered directly after the `Autocomplete.Input` (for example: an `Autocomplete` that is already being rendered in an `Overlay`).
128
+
129
+ ### Component Props
130
+
131
+ <Props of={Autocomplete.Overlay} />
132
+
133
+ ### Example: Without `Autocomplete.Overlay`
134
+
135
+ ```jsx live
136
+ <>
137
+ <Box as="label" display="block" htmlFor="autocompleteInput-withoutOverlay" id="autocompleteLabel-withoutOverlay">
138
+ Pick a branch
139
+ </Box>
140
+ <Autocomplete>
141
+ <Autocomplete.Input id="autocompleteInput-withoutOverlay" />
142
+ <Autocomplete.Menu
143
+ items={[
144
+ {text: 'main', id: 0},
145
+ {text: 'autocomplete-tests', id: 1},
146
+ {text: 'a11y-improvements', id: 2},
147
+ {text: 'button-bug-fixes', id: 3},
148
+ {text: 'radio-input-component', id: 4},
149
+ {text: 'release-1.0.0', id: 5},
150
+ {text: 'text-input-implementation', id: 6},
151
+ {text: 'visual-design-tweaks', id: 7}
152
+ ]}
153
+ selectedItemIds={[]}
154
+ aria-labelledby="autocompleteLabel-withoutOverlay"
155
+ />
156
+ </Autocomplete>
157
+ </>
158
+ ```
159
+
160
+ ## Autocomplete.Menu
161
+
162
+ The `Autocomplete.Menu` component renders a list of selectable options in a non-modal dialog. The list is filtered and sorted to make it as easy as possible to find the option/s that a user is looking for.
163
+
164
+ The `Autocomplete.Menu` component should be passed an `aria-labelledby` prop that matches the `id` prop of the `<label>` associated with the `Autocomplete.Input`
165
+
166
+ ### Component Props
167
+
168
+ <Props of={Autocomplete.Menu} />
169
+
170
+ ### Customizing how menu items are rendered
171
+
172
+ By default, menu items are just rendered as a single line of text. The list in the menu is rendered using the [Action List](/ActionList) component, so menu items can be rendered with all of the same options as Action List items.
173
+ However, the `renderGroup`, `groupMetadata`, and `renderItem` props have not been implemented yet.
174
+
175
+ #### Example: Render items using `ActionList.Item` props
176
+
177
+ ```javascript live noinline
178
+ function getColorCircle(color) {
179
+ return function () {
180
+ return (
181
+ <Box
182
+ bg={color}
183
+ borderColor={color}
184
+ width={14}
185
+ height={14}
186
+ borderRadius={10}
187
+ margin="auto"
188
+ borderWidth="1px"
189
+ borderStyle="solid"
190
+ />
191
+ )
192
+ }
193
+ }
194
+
195
+ const CustomRenderedItemExample = () => {
196
+ const [tokens, setTokens] = React.useState([
197
+ {text: 'enhancement', id: 1, fillColor: '#a2eeef'},
198
+ {text: 'bug', id: 2, fillColor: '#d73a4a'},
199
+ {text: 'good first issue', id: 3, fillColor: '#0cf478'}
200
+ ])
201
+ const selectedTokenIds = tokens.map(token => token.id)
202
+ const [selectedItemIds, setSelectedItemIds] = React.useState(selectedTokenIds)
203
+ const onTokenRemove = tokenId => {
204
+ setTokens(tokens.filter(token => token.id !== tokenId))
205
+ setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
206
+ }
207
+ const onSelectedChange = newlySelectedItems => {
208
+ if (!Array.isArray(newlySelectedItems)) {
209
+ return
210
+ }
211
+
212
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
213
+
214
+ if (newlySelectedItems.length < selectedItemIds.length) {
215
+ const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
216
+ const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
217
+
218
+ for (const removedItemId of removedItemIds) {
219
+ onTokenRemove(removedItemId)
220
+ }
221
+
222
+ return
223
+ }
224
+
225
+ setTokens(newlySelectedItems.map(({id, text, metadata}) => ({id, text, fillColor: metadata.fillColor})))
226
+ }
227
+
228
+ return (
229
+ <>
230
+ <Box
231
+ as="label"
232
+ display="block"
233
+ htmlFor="autocompleteInput-customRenderedItem"
234
+ id="autocompleteLabel-customRenderedItem"
235
+ >
236
+ Issue labels
237
+ </Box>
238
+ <Autocomplete>
239
+ <Autocomplete.Input
240
+ as={TextInputWithTokens}
241
+ tokens={tokens}
242
+ tokenComponent={IssueLabelToken}
243
+ onTokenRemove={onTokenRemove}
244
+ id="autocompleteInput-customRenderedItem"
245
+ />
246
+ <Autocomplete.Overlay>
247
+ <Autocomplete.Menu
248
+ items={[
249
+ {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1, metadata: {fillColor: '#a2eeef'}},
250
+ {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2, metadata: {fillColor: '#d73a4a'}},
251
+ {
252
+ leadingVisual: getColorCircle('#0cf478'),
253
+ text: 'good first issue',
254
+ id: 3,
255
+ metadata: {fillColor: '#0cf478'}
256
+ },
257
+ {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4, metadata: {fillColor: '#ffd78e'}},
258
+ {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5, metadata: {fillColor: '#ff0000'}},
259
+ {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6, metadata: {fillColor: '#a4f287'}},
260
+ {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7, metadata: {fillColor: '#8dc6fc'}}
261
+ ]}
262
+ selectedItemIds={selectedItemIds}
263
+ onSelectedChange={onSelectedChange}
264
+ selectionVariant="multiple"
265
+ aria-labelledby="autocompleteLabel-customRenderedItem"
266
+ />
267
+ </Autocomplete.Overlay>
268
+ </Autocomplete>
269
+ </>
270
+ )
271
+ }
272
+
273
+ render(<CustomRenderedItemExample />)
274
+ ```
275
+
276
+ ### Sorting menu items
277
+
278
+ Items can be displayed in any order that makes sense, but the `Autocomplete.Menu` component comes with a default sort behavior to make it easy to find selected items. The default behavior is to sort selected items to the top of the list after the menu has been closed.
279
+
280
+ A function may be passed to the `sortOnCloseFn` prop if this default sorting logic is not helpful for your use case. The sort function will be only be called after the menu is closed so that items don't shift while users are trying to make a selection.
281
+
282
+ #### Example: When the menu is re-opened, selected items get sorted to the end
283
+
284
+ ```javascript live noinline
285
+ const CustomSortAfterMenuClose = () => {
286
+ const [selectedItemIds, setSelectedItemIds] = React.useState([])
287
+ const isItemSelected = itemId => selectedItemIds.includes(itemId)
288
+ const onSelectedChange = newlySelectedItems => {
289
+ if (!Array.isArray(newlySelectedItems)) {
290
+ return
291
+ }
292
+
293
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
294
+ }
295
+ const customSortFn = (itemIdA, itemIdB) =>
296
+ isItemSelected(itemIdA) === isItemSelected(itemIdB) ? 0 : isItemSelected(itemIdA) ? 1 : -1
297
+
298
+ return (
299
+ <>
300
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
301
+ Pick branches
302
+ </Box>
303
+ <Autocomplete>
304
+ <Autocomplete.Input id="autocompleteInput" />
305
+ <Autocomplete.Overlay>
306
+ <Autocomplete.Menu
307
+ items={[
308
+ {text: 'main', id: 0},
309
+ {text: 'autocomplete-tests', id: 1},
310
+ {text: 'a11y-improvements', id: 2},
311
+ {text: 'button-bug-fixes', id: 3},
312
+ {text: 'radio-input-component', id: 4},
313
+ {text: 'release-1.0.0', id: 5},
314
+ {text: 'text-input-implementation', id: 6},
315
+ {text: 'visual-design-tweaks', id: 7}
316
+ ]}
317
+ selectedItemIds={selectedItemIds}
318
+ aria-labelledby="autocompleteLabel"
319
+ onSelectedChange={onSelectedChange}
320
+ sortOnCloseFn={customSortFn}
321
+ selectionVariant="multiple"
322
+ />
323
+ </Autocomplete.Overlay>
324
+ </Autocomplete>
325
+ </>
326
+ )
327
+ }
328
+
329
+ render(<CustomSortAfterMenuClose />)
330
+ ```
331
+
332
+ ### Filtering items
333
+
334
+ By default, menu items are filtered based on whether or not they match the value of the text input. The default filter is case-insensitive.
335
+
336
+ A function may be passed to the `filterFn` prop if this default filtering behavior does not make sense for your use case.
337
+
338
+ #### Example: Show any items that contain the input value
339
+
340
+ ```javascript live noinline
341
+ const CustomSearchFilter = () => {
342
+ const [filterVal, setFilterVal] = React.useState('')
343
+ const handleChange = event => {
344
+ setFilterVal(event.currentTarget.value)
345
+ }
346
+ const customFilterFn = item => item.text.includes(filterVal)
347
+
348
+ return (
349
+ <>
350
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
351
+ Pick a branch
352
+ </Box>
353
+ <Autocomplete>
354
+ <Autocomplete.Input id="autocompleteInput" onChange={handleChange} />
355
+ <Autocomplete.Overlay>
356
+ <Autocomplete.Menu
357
+ items={[
358
+ {text: 'main', id: 0},
359
+ {text: 'autocomplete-tests', id: 1},
360
+ {text: 'a11y-improvements', id: 2},
361
+ {text: 'button-bug-fixes', id: 3},
362
+ {text: 'radio-input-component', id: 4},
363
+ {text: 'release-1.0.0', id: 5},
364
+ {text: 'text-input-implementation', id: 6},
365
+ {text: 'visual-design-tweaks', id: 7}
366
+ ]}
367
+ selectedItemIds={[]}
368
+ aria-labelledby="autocompleteLabel"
369
+ filterFn={customFilterFn}
370
+ />
371
+ </Autocomplete.Overlay>
372
+ </Autocomplete>
373
+ </>
374
+ )
375
+ }
376
+
377
+ render(<CustomSearchFilter />)
378
+ ```
379
+
380
+ ### Rendering the menu without an `Autocomplete.Overlay`
381
+
382
+ If a `Autocomplete.Menu` is rendered without an `Autocomplete.Overlay` inside of a scrollable container, the ref of the scrollable container must be passed to the `customScrollContainerRef` to ensure that highlighted items are always scrolled into view.
383
+
384
+ #### Example: Rendered without `Autocomplete.Overlay` with a `customScrollContainerRef`
385
+
386
+ ```javascript live noinline
387
+ const InOverlayWithCustomScrollContainerRef = () => {
388
+ const scrollContainerRef = React.useRef(null)
389
+ const inputRef = React.useRef(null)
390
+
391
+ const [isOpen, setIsOpen] = React.useState(false)
392
+ const handleOpen = () => {
393
+ setIsOpen(true)
394
+ inputRef.current && inputRef.current.focus()
395
+ }
396
+
397
+ return (
398
+ <AnchoredOverlay
399
+ open={isOpen}
400
+ onOpen={handleOpen}
401
+ onClose={() => setIsOpen(false)}
402
+ width="large"
403
+ height="xsmall"
404
+ focusTrapSettings={{initialFocusRef: inputRef}}
405
+ side="inside-top"
406
+ renderAnchor={props => <ButtonInvisible {...props}>Pick branches</ButtonInvisible>}
407
+ >
408
+ <Box
409
+ as="label"
410
+ display="block"
411
+ htmlFor="autocompleteInput"
412
+ id="autocompleteLabel"
413
+ sx={{
414
+ // visually hides this label for sighted users
415
+ position: 'absolute',
416
+ width: '1px',
417
+ height: '1px',
418
+ padding: '0',
419
+ margin: '-1px',
420
+ overflow: 'hidden',
421
+ clip: 'rect(0, 0, 0, 0)',
422
+ whiteSpace: 'nowrap',
423
+ borderWidth: '0'
424
+ }}
425
+ >
426
+ Pick branches
427
+ </Box>
428
+ <Autocomplete>
429
+ <Box display="flex" flexDirection="column" height="100%">
430
+ <Box
431
+ paddingX="3"
432
+ paddingY="1"
433
+ borderWidth={0}
434
+ borderBottomWidth={1}
435
+ borderColor="border.default"
436
+ borderStyle="solid"
437
+ >
438
+ <Autocomplete.Input
439
+ block
440
+ as={TextInput}
441
+ ref={inputRef}
442
+ id="autocompleteInput"
443
+ sx={{
444
+ display: 'flex',
445
+ border: '0',
446
+ padding: '0',
447
+ boxShadow: 'none',
448
+ ':focus-within': {
449
+ border: '0',
450
+ boxShadow: 'none'
451
+ }
452
+ }}
453
+ />
454
+ </Box>
455
+ <Box overflow="auto" flexGrow={1} ref={scrollContainerRef}>
456
+ <Autocomplete.Menu
457
+ items={[
458
+ {text: 'main', id: 0},
459
+ {text: 'autocomplete-tests', id: 1},
460
+ {text: 'a11y-improvements', id: 2},
461
+ {text: 'button-bug-fixes', id: 3},
462
+ {text: 'radio-input-component', id: 4},
463
+ {text: 'release-1.0.0', id: 5},
464
+ {text: 'text-input-implementation', id: 6},
465
+ {text: 'visual-design-tweaks', id: 7}
466
+ ]}
467
+ selectedItemIds={[]}
468
+ customScrollContainerRef={scrollContainerRef}
469
+ aria-labelledby="autocompleteLabel"
470
+ />
471
+ </Box>
472
+ </Box>
473
+ </Autocomplete>
474
+ </AnchoredOverlay>
475
+ )
476
+ }
477
+
478
+ render(<InOverlayWithCustomScrollContainerRef />)
479
+ ```
480
+
481
+ ### More examples
482
+
483
+ #### Select multiple values
484
+
485
+ ```javascript live noinline
486
+ const MultiSelect = () => {
487
+ const items = [
488
+ {text: 'main', id: 0},
489
+ {text: 'autocomplete-tests', id: 1},
490
+ {text: 'a11y-improvements', id: 22},
491
+ {text: 'button-bug-fixes', id: 3},
492
+ {text: 'radio-input-component', id: 4},
493
+ {text: 'release-1.0.0', id: 5},
494
+ {text: 'text-input-implementation', id: 6},
495
+ {text: 'visual-design-tweaks', id: 7}
496
+ ]
497
+ const [selectedItemIds, setSelectedItemIds] = React.useState([])
498
+ const onSelectedChange = newlySelectedItems => {
499
+ if (!Array.isArray(newlySelectedItems)) {
500
+ return
501
+ }
502
+
503
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
504
+ }
505
+
506
+ const getItemById = id => items.find(item => item.id === id)
507
+
508
+ return (
509
+ <Box display="flex" sx={{gap: '1em'}}>
510
+ <div>
511
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
512
+ Pick branches
513
+ </Box>
514
+ <Autocomplete>
515
+ <Autocomplete.Input id="autocompleteInput" />
516
+ <Autocomplete.Overlay>
517
+ <Autocomplete.Menu
518
+ items={items}
519
+ selectedItemIds={selectedItemIds}
520
+ aria-labelledby="autocompleteLabel"
521
+ onSelectedChange={onSelectedChange}
522
+ selectionVariant="multiple"
523
+ />
524
+ </Autocomplete.Overlay>
525
+ </Autocomplete>
526
+ </div>
527
+ <div>
528
+ <div>Selected items:</div>
529
+ <Box as="ul" my={0}>
530
+ {selectedItemIds.map(selectedItemId => (
531
+ <li key={selectedItemId}>{getItemById(selectedItemId).text}</li>
532
+ ))}
533
+ </Box>
534
+ </div>
535
+ </Box>
536
+ )
537
+ }
538
+
539
+ render(<MultiSelect />)
540
+ ```
541
+
542
+ #### Select multiple values - new values can be added
543
+
544
+ ```javascript live noinline
545
+ const MultiSelectAddNewItem = () => {
546
+ const [selectedItemIds, setSelectedItemIds] = React.useState([])
547
+ const onSelectedChange = newlySelectedItems => {
548
+ if (!Array.isArray(newlySelectedItems)) {
549
+ return
550
+ }
551
+
552
+ setSelectedItemIds(newlySelectedItems.map(item => item.id))
553
+ }
554
+
555
+ const [localItemsState, setLocalItemsState] = React.useState([
556
+ {text: 'main', id: 0},
557
+ {text: 'autocomplete-tests', id: 1},
558
+ {text: 'a11y-improvements', id: 22},
559
+ {text: 'button-bug-fixes', id: 3},
560
+ {text: 'radio-input-component', id: 4},
561
+ {text: 'release-1.0.0', id: 5},
562
+ {text: 'text-input-implementation', id: 6},
563
+ {text: 'visual-design-tweaks', id: 7}
564
+ ])
565
+ const getItemById = id => localItemsState.find(item => item.id === id)
566
+ const [filterVal, setFilterVal] = React.useState('')
567
+
568
+ const onItemSelect = item => {
569
+ onSelectedChange([...selectedItemIds.map(id => localItemsState.find(selectedItem => selectedItem.id === id)), item])
570
+
571
+ if (!localItemsState.some(localItem => localItem.id === item.id)) {
572
+ setLocalItemsState([...localItemsState, item])
573
+ }
574
+ }
575
+
576
+ const handleChange = event => {
577
+ setFilterVal(event.currentTarget.value)
578
+ }
579
+
580
+ return (
581
+ <Box display="flex" sx={{gap: '1em'}}>
582
+ <div>
583
+ <Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
584
+ Pick or add branches
585
+ </Box>
586
+ <Autocomplete>
587
+ <Autocomplete.Input onChange={handleChange} id="autocompleteInput" />
588
+ <Autocomplete.Overlay>
589
+ <Autocomplete.Menu
590
+ addNewItem={
591
+ filterVal && !localItemsState.map(localItem => localItem.text).includes(filterVal)
592
+ ? {
593
+ text: `Add '${filterVal}'`,
594
+ handleAddItem: item => {
595
+ onItemSelect({
596
+ ...item,
597
+ text: filterVal,
598
+ selected: true
599
+ })
600
+ setFilterVal('')
601
+ }
602
+ }
603
+ : undefined
604
+ }
605
+ items={localItemsState}
606
+ selectedItemIds={selectedItemIds}
607
+ onSelectedChange={onSelectedChange}
608
+ selectionVariant="multiple"
609
+ aria-labelledby="autocompleteLabel"
610
+ />
611
+ </Autocomplete.Overlay>
612
+ </Autocomplete>
613
+ </div>
614
+ <div>
615
+ <div>Selected items:</div>
616
+ <Box as="ul" my={0}>
617
+ {selectedItemIds.map(selectedItemId => (
618
+ <li key={selectedItemId}>{getItemById(selectedItemId).text}</li>
619
+ ))}
620
+ </Box>
621
+ </div>
622
+ </Box>
623
+ )
624
+ }
625
+
626
+ render(<MultiSelectAddNewItem />)
627
+ ```