@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.
- package/CHANGELOG.md +4 -2
- package/dist/browser.esm.js +717 -718
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +320 -321
- package/dist/browser.umd.js.map +1 -1
- package/docs/content/Autocomplete.mdx +627 -0
- package/docs/content/TextInputTokens.mdx +89 -0
- package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
- package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib/Autocomplete/Autocomplete.js +145 -0
- package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib/Autocomplete/AutocompleteContext.js +11 -0
- package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib/Autocomplete/AutocompleteInput.js +157 -0
- package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib/Autocomplete/AutocompleteMenu.js +224 -0
- package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
- package/lib/Autocomplete/index.d.ts +2 -0
- package/lib/Autocomplete/index.js +15 -0
- package/lib/FilteredActionList/FilteredActionList.js +5 -31
- package/lib/Overlay.d.ts +1 -0
- package/lib/Overlay.js +3 -1
- package/lib/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib/__tests__/Autocomplete.test.js +528 -0
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
- package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib/behaviors/scrollIntoViewingArea.js +39 -0
- package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib/hooks/useOverlay.d.ts +2 -1
- package/lib/hooks/useOverlay.js +4 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/lib/stories/Autocomplete.stories.js +608 -0
- package/lib/utils/types/MandateProps.d.ts +3 -0
- package/lib/utils/types/MandateProps.js +1 -0
- package/lib/utils/types/index.d.ts +1 -0
- package/lib/utils/types/index.js +13 -0
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib-esm/Autocomplete/Autocomplete.js +123 -0
- package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
- package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
- package/lib-esm/Autocomplete/index.d.ts +2 -0
- package/lib-esm/Autocomplete/index.js +1 -0
- package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
- package/lib-esm/Overlay.d.ts +1 -0
- package/lib-esm/Overlay.js +3 -1
- package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib-esm/__tests__/Autocomplete.test.js +494 -0
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
- package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib-esm/hooks/useOverlay.d.ts +2 -1
- package/lib-esm/hooks/useOverlay.js +4 -2
- package/lib-esm/index.d.ts +2 -0
- package/lib-esm/index.js +1 -0
- package/lib-esm/stories/Autocomplete.stories.js +549 -0
- package/lib-esm/utils/types/MandateProps.d.ts +3 -0
- package/lib-esm/utils/types/MandateProps.js +1 -0
- package/lib-esm/utils/types/index.d.ts +1 -0
- package/lib-esm/utils/types/index.js +2 -1
- package/package.json +1 -1
- package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
- package/src/Autocomplete/Autocomplete.tsx +103 -0
- package/src/Autocomplete/AutocompleteContext.tsx +19 -0
- package/src/Autocomplete/AutocompleteInput.tsx +179 -0
- package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
- package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
- package/src/Autocomplete/index.ts +2 -0
- package/src/FilteredActionList/FilteredActionList.tsx +10 -25
- package/src/Overlay.tsx +4 -1
- package/src/__tests__/Autocomplete.test.tsx +444 -0
- package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
- package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
- package/src/behaviors/scrollIntoViewingArea.ts +27 -0
- package/src/hooks/useOpenAndCloseFocus.ts +7 -2
- package/src/hooks/useOverlay.tsx +4 -2
- package/src/index.ts +2 -0
- package/src/stories/Autocomplete.stories.tsx +572 -0
- package/src/utils/types/MandateProps.ts +19 -0
- package/src/utils/types/index.ts +1 -0
- 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
|
+
```
|