@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,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
+ }
@@ -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'