@ceed/ads 1.20.0 → 1.20.1-next.1
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/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/data-display/Markdown.md +832 -0
- package/dist/components/feedback/Dialog.md +605 -3
- package/dist/components/feedback/Modal.md +656 -24
- package/dist/components/feedback/llms.txt +1 -1
- package/dist/components/inputs/Autocomplete.md +734 -2
- package/dist/components/inputs/Calendar.md +655 -1
- package/dist/components/inputs/DatePicker.md +699 -3
- package/dist/components/inputs/DateRangePicker.md +815 -1
- package/dist/components/inputs/MonthPicker.md +626 -4
- package/dist/components/inputs/MonthRangePicker.md +682 -4
- package/dist/components/inputs/Select.md +600 -0
- package/dist/components/layout/Container.md +507 -0
- package/dist/components/navigation/Breadcrumbs.md +582 -0
- package/dist/components/navigation/IconMenuButton.md +693 -0
- package/dist/components/navigation/InsetDrawer.md +1150 -3
- package/dist/components/navigation/Link.md +526 -0
- package/dist/components/navigation/MenuButton.md +632 -0
- package/dist/components/navigation/NavigationGroup.md +401 -1
- package/dist/components/navigation/NavigationItem.md +311 -0
- package/dist/components/navigation/Navigator.md +373 -0
- package/dist/components/navigation/Pagination.md +521 -0
- package/dist/components/navigation/ProfileMenu.md +605 -0
- package/dist/components/navigation/Tabs.md +609 -7
- package/dist/components/surfaces/Accordions.md +947 -3
- package/dist/index.cjs +3 -1
- package/dist/index.js +3 -1
- package/dist/llms.txt +1 -1
- package/framer/index.js +1 -1
- package/package.json +3 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
4
4
|
|
|
5
|
+
Autocomplete is an enhanced input component that provides real-time suggestions as users type, helping them quickly find and select values from a predefined list. It combines the flexibility of a text input with the convenience of a dropdown selection, supporting features like filtering, keyboard navigation, custom option rendering, and grouping. Autocomplete is ideal for scenarios where users need to select from a large set of options, such as searching for countries, products, or users.
|
|
6
|
+
|
|
5
7
|
```tsx
|
|
6
8
|
<Autocomplete options={['Option1', 'Option2']} />
|
|
7
9
|
```
|
|
@@ -11,8 +13,49 @@
|
|
|
11
13
|
| label | — | — |
|
|
12
14
|
| loading | — | — |
|
|
13
15
|
|
|
16
|
+
> ⚠️ **Usage Warning** ⚠️
|
|
17
|
+
>
|
|
18
|
+
> Choose the right input component for your use case:
|
|
19
|
+
>
|
|
20
|
+
> - **Autocomplete**: For searchable selection from large option lists (20+ items)
|
|
21
|
+
> - **Select**: For simple selection from small lists (under 20 items)
|
|
22
|
+
> - **Dropdown**: For action menus, not form value selection
|
|
23
|
+
> - **Input**: For free-text entry without predefined options
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Autocomplete } from '@ceed/ads';
|
|
29
|
+
|
|
30
|
+
function CountrySelector() {
|
|
31
|
+
const [country, setCountry] = useState<string | undefined>();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Autocomplete
|
|
35
|
+
label="Country"
|
|
36
|
+
placeholder="Search countries..."
|
|
37
|
+
options={['United States', 'United Kingdom', 'Canada', 'Australia']}
|
|
38
|
+
value={country}
|
|
39
|
+
onChange={(e) => setCountry(e.target.value)}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
### Playground
|
|
48
|
+
|
|
49
|
+
Interactive example with basic string options.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Autocomplete options={['Option1', 'Option2']} />
|
|
53
|
+
```
|
|
54
|
+
|
|
14
55
|
### Sizes
|
|
15
56
|
|
|
57
|
+
Available size options: `sm`, `md`, `lg`.
|
|
58
|
+
|
|
16
59
|
```tsx
|
|
17
60
|
<div style={{
|
|
18
61
|
display: 'flex',
|
|
@@ -26,6 +69,8 @@
|
|
|
26
69
|
|
|
27
70
|
### Option Groups
|
|
28
71
|
|
|
72
|
+
Group options into categories using the `groupBy` function.
|
|
73
|
+
|
|
29
74
|
```tsx
|
|
30
75
|
<Autocomplete
|
|
31
76
|
options={[{
|
|
@@ -50,7 +95,9 @@
|
|
|
50
95
|
/>
|
|
51
96
|
```
|
|
52
97
|
|
|
53
|
-
###
|
|
98
|
+
### Custom Options
|
|
99
|
+
|
|
100
|
+
Options can include decorators for rich content display.
|
|
54
101
|
|
|
55
102
|
```tsx
|
|
56
103
|
<Autocomplete
|
|
@@ -69,7 +116,9 @@
|
|
|
69
116
|
/>
|
|
70
117
|
```
|
|
71
118
|
|
|
72
|
-
### Loading
|
|
119
|
+
### Loading State
|
|
120
|
+
|
|
121
|
+
Show a loading indicator while fetching options.
|
|
73
122
|
|
|
74
123
|
```tsx
|
|
75
124
|
<Autocomplete
|
|
@@ -80,6 +129,8 @@
|
|
|
80
129
|
|
|
81
130
|
### Controlled
|
|
82
131
|
|
|
132
|
+
Manage value externally with controlled state.
|
|
133
|
+
|
|
83
134
|
```tsx
|
|
84
135
|
<Stack gap={4}>
|
|
85
136
|
<Autocomplete value={value} label="Select a brand" options={[{
|
|
@@ -101,3 +152,684 @@
|
|
|
101
152
|
<Button onClick={() => setValue("Johnson's baby")}>Set Johnson's baby</Button>
|
|
102
153
|
</Stack>
|
|
103
154
|
```
|
|
155
|
+
|
|
156
|
+
## When to Use
|
|
157
|
+
|
|
158
|
+
### ✅ Good Use Cases
|
|
159
|
+
|
|
160
|
+
- **Large option lists**: When users need to search through 20+ options
|
|
161
|
+
- **Dynamic data**: When options come from an API or database
|
|
162
|
+
- **User search**: Finding users, contacts, or entities by name
|
|
163
|
+
- **Location selection**: Countries, cities, addresses
|
|
164
|
+
- **Product search**: Searching product catalogs
|
|
165
|
+
- **Tag/category selection**: When users can search for categories
|
|
166
|
+
- **Form fields with many choices**: When a simple Select would be overwhelming
|
|
167
|
+
|
|
168
|
+
### ❌ When Not to Use
|
|
169
|
+
|
|
170
|
+
- **Few options (\< 10)**: Use Select or RadioGroup instead
|
|
171
|
+
- **Free text entry**: Use Input if no predefined options exist
|
|
172
|
+
- **Action menus**: Use Dropdown or MenuButton for actions
|
|
173
|
+
- **Yes/No choices**: Use Switch or Checkbox
|
|
174
|
+
- **Numeric ranges**: Use Slider or NumberInput
|
|
175
|
+
- **Date selection**: Use DatePicker
|
|
176
|
+
|
|
177
|
+
## Common Use Cases
|
|
178
|
+
|
|
179
|
+
### Country Selector
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
function CountrySelector({ value, onChange }) {
|
|
183
|
+
const countries = [
|
|
184
|
+
{ value: 'us', label: 'United States' },
|
|
185
|
+
{ value: 'uk', label: 'United Kingdom' },
|
|
186
|
+
{ value: 'ca', label: 'Canada' },
|
|
187
|
+
{ value: 'au', label: 'Australia' },
|
|
188
|
+
{ value: 'de', label: 'Germany' },
|
|
189
|
+
{ value: 'fr', label: 'France' },
|
|
190
|
+
{ value: 'jp', label: 'Japan' },
|
|
191
|
+
{ value: 'kr', label: 'South Korea' },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Autocomplete
|
|
196
|
+
label="Country"
|
|
197
|
+
placeholder="Select a country..."
|
|
198
|
+
options={countries}
|
|
199
|
+
value={value}
|
|
200
|
+
onChange={(e) => onChange(e.target.value)}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### User Search
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
function UserSearch({ onSelect }) {
|
|
210
|
+
const [users, setUsers] = useState([]);
|
|
211
|
+
const [loading, setLoading] = useState(false);
|
|
212
|
+
const [search, setSearch] = useState('');
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!search) {
|
|
216
|
+
setUsers([]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const fetchUsers = async () => {
|
|
221
|
+
setLoading(true);
|
|
222
|
+
try {
|
|
223
|
+
const response = await api.searchUsers(search);
|
|
224
|
+
setUsers(
|
|
225
|
+
response.data.map((user) => ({
|
|
226
|
+
value: user.id,
|
|
227
|
+
label: user.name,
|
|
228
|
+
secondaryText: user.email,
|
|
229
|
+
startDecorator: <Avatar src={user.avatar} size="sm" />,
|
|
230
|
+
}))
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
setLoading(false);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const debounce = setTimeout(fetchUsers, 300);
|
|
238
|
+
return () => clearTimeout(debounce);
|
|
239
|
+
}, [search]);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<Autocomplete
|
|
243
|
+
label="Search Users"
|
|
244
|
+
placeholder="Type to search..."
|
|
245
|
+
options={users}
|
|
246
|
+
loading={loading}
|
|
247
|
+
onInputChange={(e) => setSearch(e.target.value)}
|
|
248
|
+
onChange={(e) => onSelect(e.target.value)}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Product Search with Categories
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
function ProductSearch({ products, onSelect }) {
|
|
258
|
+
const productOptions = products.map((product) => ({
|
|
259
|
+
value: product.id,
|
|
260
|
+
label: product.name,
|
|
261
|
+
startDecorator: (
|
|
262
|
+
<Chip size="sm" color={product.inStock ? 'success' : 'neutral'}>
|
|
263
|
+
{product.inStock ? 'In Stock' : 'Out of Stock'}
|
|
264
|
+
</Chip>
|
|
265
|
+
),
|
|
266
|
+
endDecorator: (
|
|
267
|
+
<Typography level="body-sm" color="neutral">
|
|
268
|
+
${product.price}
|
|
269
|
+
</Typography>
|
|
270
|
+
),
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<Autocomplete
|
|
275
|
+
label="Search Products"
|
|
276
|
+
placeholder="Enter product name..."
|
|
277
|
+
options={productOptions}
|
|
278
|
+
groupBy={(option) => option.category}
|
|
279
|
+
onChange={(e) => onSelect(e.target.value)}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Address Autocomplete
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
function AddressAutocomplete({ onAddressSelect }) {
|
|
289
|
+
const [options, setOptions] = useState([]);
|
|
290
|
+
const [loading, setLoading] = useState(false);
|
|
291
|
+
|
|
292
|
+
const handleInputChange = async (e) => {
|
|
293
|
+
const query = e.target.value;
|
|
294
|
+
if (query.length < 3) {
|
|
295
|
+
setOptions([]);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setLoading(true);
|
|
300
|
+
try {
|
|
301
|
+
const results = await geocodingApi.search(query);
|
|
302
|
+
setOptions(
|
|
303
|
+
results.map((result) => ({
|
|
304
|
+
value: result.placeId,
|
|
305
|
+
label: result.formattedAddress,
|
|
306
|
+
secondaryText: result.city + ', ' + result.country,
|
|
307
|
+
}))
|
|
308
|
+
);
|
|
309
|
+
} finally {
|
|
310
|
+
setLoading(false);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<Autocomplete
|
|
316
|
+
label="Address"
|
|
317
|
+
placeholder="Start typing an address..."
|
|
318
|
+
options={options}
|
|
319
|
+
loading={loading}
|
|
320
|
+
onInputChange={handleInputChange}
|
|
321
|
+
onChange={(e) => {
|
|
322
|
+
const selected = options.find((opt) => opt.value === e.target.value);
|
|
323
|
+
onAddressSelect(selected);
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Tag Selection (Multiple)
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
function TagSelector({ availableTags, selectedTags, onChange }) {
|
|
334
|
+
const tagOptions = availableTags.map((tag) => ({
|
|
335
|
+
value: tag.id,
|
|
336
|
+
label: tag.name,
|
|
337
|
+
startDecorator: (
|
|
338
|
+
<Box
|
|
339
|
+
sx={{
|
|
340
|
+
width: 12,
|
|
341
|
+
height: 12,
|
|
342
|
+
borderRadius: '50%',
|
|
343
|
+
bgcolor: tag.color,
|
|
344
|
+
}}
|
|
345
|
+
/>
|
|
346
|
+
),
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<Autocomplete
|
|
351
|
+
label="Tags"
|
|
352
|
+
placeholder="Add tags..."
|
|
353
|
+
options={tagOptions}
|
|
354
|
+
value={selectedTags}
|
|
355
|
+
onChange={(e) => onChange(e.target.value)}
|
|
356
|
+
multiple
|
|
357
|
+
/>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Grouped Options
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
function CategorySelector() {
|
|
366
|
+
const items = [
|
|
367
|
+
{ value: 'electronics-phone', label: 'Smartphones', category: 'Electronics' },
|
|
368
|
+
{ value: 'electronics-laptop', label: 'Laptops', category: 'Electronics' },
|
|
369
|
+
{ value: 'electronics-tablet', label: 'Tablets', category: 'Electronics' },
|
|
370
|
+
{ value: 'clothing-shirt', label: 'Shirts', category: 'Clothing' },
|
|
371
|
+
{ value: 'clothing-pants', label: 'Pants', category: 'Clothing' },
|
|
372
|
+
{ value: 'clothing-shoes', label: 'Shoes', category: 'Clothing' },
|
|
373
|
+
{ value: 'home-furniture', label: 'Furniture', category: 'Home' },
|
|
374
|
+
{ value: 'home-decor', label: 'Decor', category: 'Home' },
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<Autocomplete
|
|
379
|
+
label="Category"
|
|
380
|
+
placeholder="Select a category..."
|
|
381
|
+
options={items}
|
|
382
|
+
groupBy={(option) => option.category}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### With Secondary Text
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
function ContactSelector() {
|
|
392
|
+
const contacts = [
|
|
393
|
+
{ value: 'emily', label: 'Emily Carter', secondaryText: '(415) 555-0198' },
|
|
394
|
+
{ value: 'daniel', label: 'Daniel Kim', secondaryText: '(212) 555-0421' },
|
|
395
|
+
{ value: 'sophia', label: 'Sophia Martinez', secondaryText: '(646) 555-0734' },
|
|
396
|
+
{ value: 'michael', label: 'Michael Chen', secondaryText: '(646) 555-0876' },
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<Autocomplete
|
|
401
|
+
label="Contact"
|
|
402
|
+
placeholder="Search contacts..."
|
|
403
|
+
options={contacts}
|
|
404
|
+
/>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Lazy Loading Options
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
function LazyAutocomplete({ fetchOptions }) {
|
|
413
|
+
const [options, setOptions] = useState([]);
|
|
414
|
+
const [loading, setLoading] = useState(false);
|
|
415
|
+
const [hasLoaded, setHasLoaded] = useState(false);
|
|
416
|
+
|
|
417
|
+
const handleFocus = async () => {
|
|
418
|
+
if (hasLoaded) return;
|
|
419
|
+
|
|
420
|
+
setLoading(true);
|
|
421
|
+
try {
|
|
422
|
+
const data = await fetchOptions();
|
|
423
|
+
setOptions(data);
|
|
424
|
+
setHasLoaded(true);
|
|
425
|
+
} finally {
|
|
426
|
+
setLoading(false);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<Autocomplete
|
|
432
|
+
label="Select Option"
|
|
433
|
+
placeholder="Click to load options..."
|
|
434
|
+
options={options}
|
|
435
|
+
loading={loading}
|
|
436
|
+
onFocus={handleFocus}
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Props and Customization
|
|
443
|
+
|
|
444
|
+
### Key Props
|
|
445
|
+
|
|
446
|
+
| Prop | Type | Default | Description |
|
|
447
|
+
| --------------- | ---------------------------------------- | ------- | ------------------------------------- |
|
|
448
|
+
| `options` | `string[] \| OptionObject[]` | `[]` | Array of options (strings or objects) |
|
|
449
|
+
| `value` | `string \| string[]` | - | Selected value(s) for controlled mode |
|
|
450
|
+
| `defaultValue` | `string \| string[]` | - | Initial value for uncontrolled mode |
|
|
451
|
+
| `onChange` | `(event: { target: { value } }) => void` | - | Callback when selection changes |
|
|
452
|
+
| `onInputChange` | `(event: { target: { value } }) => void` | - | Callback when input text changes |
|
|
453
|
+
| `label` | `string` | - | Label text above the input |
|
|
454
|
+
| `placeholder` | `string` | - | Placeholder text when empty |
|
|
455
|
+
| `loading` | `boolean` | `false` | Show loading indicator |
|
|
456
|
+
| `multiple` | `boolean` | `false` | Allow multiple selections |
|
|
457
|
+
| `groupBy` | `(option) => string` | - | Function to group options |
|
|
458
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size |
|
|
459
|
+
| `disabled` | `boolean` | `false` | Disable the input |
|
|
460
|
+
|
|
461
|
+
### Option Object Structure
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
interface OptionObject {
|
|
465
|
+
value: string; // Unique value identifier
|
|
466
|
+
label: string; // Display text
|
|
467
|
+
secondaryText?: string; // Secondary line of text
|
|
468
|
+
startDecorator?: ReactNode; // Content before label
|
|
469
|
+
endDecorator?: ReactNode; // Content after label
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Example option objects
|
|
473
|
+
const options = [
|
|
474
|
+
{
|
|
475
|
+
value: 'user-1',
|
|
476
|
+
label: 'John Doe',
|
|
477
|
+
secondaryText: 'john@example.com',
|
|
478
|
+
startDecorator: <Avatar src="/john.jpg" size="sm" />,
|
|
479
|
+
endDecorator: <Chip size="sm">Admin</Chip>,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
value: 'user-2',
|
|
483
|
+
label: 'Jane Smith',
|
|
484
|
+
secondaryText: 'jane@example.com',
|
|
485
|
+
startDecorator: <Avatar src="/jane.jpg" size="sm" />,
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Simple String Options
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
// Simplest usage with string array
|
|
494
|
+
<Autocomplete
|
|
495
|
+
label="Fruit"
|
|
496
|
+
options={['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']}
|
|
497
|
+
/>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Controlled vs Uncontrolled
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
// Controlled - manage value externally
|
|
504
|
+
function ControlledExample() {
|
|
505
|
+
const [value, setValue] = useState<string | undefined>();
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<Autocomplete
|
|
509
|
+
value={value}
|
|
510
|
+
onChange={(e) => setValue(e.target.value)}
|
|
511
|
+
options={options}
|
|
512
|
+
/>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Uncontrolled - internal state management
|
|
517
|
+
function UncontrolledExample() {
|
|
518
|
+
return (
|
|
519
|
+
<Autocomplete
|
|
520
|
+
defaultValue="option-1"
|
|
521
|
+
options={options}
|
|
522
|
+
/>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Size Options
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
// Small
|
|
531
|
+
<Autocomplete options={options} size="sm" />
|
|
532
|
+
|
|
533
|
+
// Medium (default)
|
|
534
|
+
<Autocomplete options={options} size="md" />
|
|
535
|
+
|
|
536
|
+
// Large
|
|
537
|
+
<Autocomplete options={options} size="lg" />
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### With Grouping
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
<Autocomplete
|
|
544
|
+
options={[
|
|
545
|
+
{ value: 'a1', label: 'Apple', category: 'Fruits' },
|
|
546
|
+
{ value: 'b1', label: 'Banana', category: 'Fruits' },
|
|
547
|
+
{ value: 'c1', label: 'Carrot', category: 'Vegetables' },
|
|
548
|
+
{ value: 'b2', label: 'Broccoli', category: 'Vegetables' },
|
|
549
|
+
]}
|
|
550
|
+
groupBy={(option) => option.category}
|
|
551
|
+
/>
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Multiple Selection
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
<Autocomplete
|
|
558
|
+
multiple
|
|
559
|
+
value={['option-1', 'option-2']}
|
|
560
|
+
options={options}
|
|
561
|
+
onChange={(e) => setValues(e.target.value)}
|
|
562
|
+
/>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Accessibility
|
|
566
|
+
|
|
567
|
+
Autocomplete includes comprehensive accessibility features:
|
|
568
|
+
|
|
569
|
+
### ARIA Attributes
|
|
570
|
+
|
|
571
|
+
- Input has `role="combobox"` with `aria-autocomplete="list"`
|
|
572
|
+
- Listbox has `role="listbox"` with proper `aria-label`
|
|
573
|
+
- Options have `role="option"` with `aria-selected` state
|
|
574
|
+
- Groups are announced with proper hierarchy
|
|
575
|
+
|
|
576
|
+
### Keyboard Navigation
|
|
577
|
+
|
|
578
|
+
- **Arrow Down**: Open dropdown / move to next option
|
|
579
|
+
- **Arrow Up**: Move to previous option
|
|
580
|
+
- **Enter**: Select focused option
|
|
581
|
+
- **Escape**: Close dropdown
|
|
582
|
+
- **Tab**: Move focus out of component
|
|
583
|
+
- **Home**: Jump to first option
|
|
584
|
+
- **End**: Jump to last option
|
|
585
|
+
- **Type ahead**: Filter options by typing
|
|
586
|
+
|
|
587
|
+
### Screen Reader Support
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
// Proper labeling for screen readers
|
|
591
|
+
<Autocomplete
|
|
592
|
+
label="Select a country" // Announces: "Select a country, combobox"
|
|
593
|
+
placeholder="Search..."
|
|
594
|
+
options={countries}
|
|
595
|
+
/>
|
|
596
|
+
|
|
597
|
+
// Options announce: "United States, option, 1 of 10"
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Focus Management
|
|
601
|
+
|
|
602
|
+
- Focus automatically moves to input when dropdown opens
|
|
603
|
+
- Focus returns to input when selecting an option
|
|
604
|
+
- Clear visual focus indicators on all interactive elements
|
|
605
|
+
|
|
606
|
+
## Best Practices
|
|
607
|
+
|
|
608
|
+
### ✅ Do
|
|
609
|
+
|
|
610
|
+
1. **Provide helpful labels and placeholders**: Guide users on what to search
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
// ✅ Good: Clear label and placeholder
|
|
614
|
+
<Autocomplete
|
|
615
|
+
label="Shipping Country"
|
|
616
|
+
placeholder="Type to search countries..."
|
|
617
|
+
options={countries}
|
|
618
|
+
/>
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
2. **Show loading state during async operations**: Keep users informed
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
// ✅ Good: Loading indicator while fetching
|
|
625
|
+
<Autocomplete
|
|
626
|
+
options={options}
|
|
627
|
+
loading={isLoading}
|
|
628
|
+
placeholder={isLoading ? 'Loading...' : 'Search...'}
|
|
629
|
+
/>
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
3. **Use grouping for better organization**: Help users scan large lists
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
// ✅ Good: Logical grouping
|
|
636
|
+
<Autocomplete
|
|
637
|
+
options={allProducts}
|
|
638
|
+
groupBy={(product) => product.category}
|
|
639
|
+
/>
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
4. **Debounce API searches**: Prevent excessive requests
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
// ✅ Good: Debounced search
|
|
646
|
+
useEffect(() => {
|
|
647
|
+
const timer = setTimeout(() => fetchResults(query), 300);
|
|
648
|
+
return () => clearTimeout(timer);
|
|
649
|
+
}, [query]);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### ❌ Don't
|
|
653
|
+
|
|
654
|
+
1. **Don't use for small option sets**: Use Select instead
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
// ❌ Bad: Too few options for Autocomplete
|
|
658
|
+
<Autocomplete options={['Yes', 'No']} />
|
|
659
|
+
|
|
660
|
+
// ✅ Good: Use Select or RadioGroup
|
|
661
|
+
<Select>
|
|
662
|
+
<Option value="yes">Yes</Option>
|
|
663
|
+
<Option value="no">No</Option>
|
|
664
|
+
</Select>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
2. **Don't hide important selection context**: Show enough information
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
// ❌ Bad: Not enough context
|
|
671
|
+
<Autocomplete
|
|
672
|
+
options={users.map((u) => ({ value: u.id, label: u.name }))}
|
|
673
|
+
/>
|
|
674
|
+
|
|
675
|
+
// ✅ Good: Include helpful secondary info
|
|
676
|
+
<Autocomplete
|
|
677
|
+
options={users.map((u) => ({
|
|
678
|
+
value: u.id,
|
|
679
|
+
label: u.name,
|
|
680
|
+
secondaryText: u.email,
|
|
681
|
+
}))}
|
|
682
|
+
/>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
3. **Don't forget empty states**: Handle no results gracefully
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
// ✅ Good: Handle empty results
|
|
689
|
+
<Autocomplete
|
|
690
|
+
options={filteredOptions}
|
|
691
|
+
noOptionsText="No matches found. Try a different search."
|
|
692
|
+
/>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
4. **Don't block interaction during initial load**: Show placeholders
|
|
696
|
+
|
|
697
|
+
```tsx
|
|
698
|
+
// ❌ Bad: Blocking the entire form
|
|
699
|
+
{loading ? <Spinner /> : <Autocomplete options={options} />}
|
|
700
|
+
|
|
701
|
+
// ✅ Good: Allow interaction with loading state
|
|
702
|
+
<Autocomplete options={options} loading={loading} />
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Performance Considerations
|
|
706
|
+
|
|
707
|
+
### Built-in Virtualization
|
|
708
|
+
|
|
709
|
+
Autocomplete automatically virtualizes long lists for performance:
|
|
710
|
+
|
|
711
|
+
```tsx
|
|
712
|
+
// Handles 1000+ options efficiently
|
|
713
|
+
<Autocomplete
|
|
714
|
+
options={Array.from({ length: 1000 }, (_, i) => `Option ${i + 1}`)}
|
|
715
|
+
/>
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Debounce Search Requests
|
|
719
|
+
|
|
720
|
+
Prevent excessive API calls:
|
|
721
|
+
|
|
722
|
+
```tsx
|
|
723
|
+
function SearchAutocomplete({ fetchOptions }) {
|
|
724
|
+
const [options, setOptions] = useState([]);
|
|
725
|
+
const [loading, setLoading] = useState(false);
|
|
726
|
+
|
|
727
|
+
const debouncedFetch = useMemo(
|
|
728
|
+
() =>
|
|
729
|
+
debounce(async (query: string) => {
|
|
730
|
+
if (!query) {
|
|
731
|
+
setOptions([]);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
setLoading(true);
|
|
735
|
+
try {
|
|
736
|
+
const results = await fetchOptions(query);
|
|
737
|
+
setOptions(results);
|
|
738
|
+
} finally {
|
|
739
|
+
setLoading(false);
|
|
740
|
+
}
|
|
741
|
+
}, 300),
|
|
742
|
+
[fetchOptions]
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
return (
|
|
746
|
+
<Autocomplete
|
|
747
|
+
options={options}
|
|
748
|
+
loading={loading}
|
|
749
|
+
onInputChange={(e) => debouncedFetch(e.target.value)}
|
|
750
|
+
/>
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Memoize Options
|
|
756
|
+
|
|
757
|
+
Prevent unnecessary re-renders:
|
|
758
|
+
|
|
759
|
+
```tsx
|
|
760
|
+
const options = useMemo(
|
|
761
|
+
() =>
|
|
762
|
+
data.map((item) => ({
|
|
763
|
+
value: item.id,
|
|
764
|
+
label: item.name,
|
|
765
|
+
startDecorator: <StatusChip status={item.status} />,
|
|
766
|
+
})),
|
|
767
|
+
[data]
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
<Autocomplete options={options} />
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Lazy Load Options
|
|
774
|
+
|
|
775
|
+
Fetch options only when needed:
|
|
776
|
+
|
|
777
|
+
```tsx
|
|
778
|
+
function LazyLoadAutocomplete() {
|
|
779
|
+
const [options, setOptions] = useState([]);
|
|
780
|
+
const [loading, setLoading] = useState(false);
|
|
781
|
+
const [initialized, setInitialized] = useState(false);
|
|
782
|
+
|
|
783
|
+
const loadOptions = async () => {
|
|
784
|
+
if (initialized) return;
|
|
785
|
+
|
|
786
|
+
setLoading(true);
|
|
787
|
+
try {
|
|
788
|
+
const data = await fetchAllOptions();
|
|
789
|
+
setOptions(data);
|
|
790
|
+
setInitialized(true);
|
|
791
|
+
} finally {
|
|
792
|
+
setLoading(false);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<Autocomplete
|
|
798
|
+
options={options}
|
|
799
|
+
loading={loading}
|
|
800
|
+
onFocus={loadOptions}
|
|
801
|
+
onOpen={loadOptions}
|
|
802
|
+
/>
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Optimize Option Rendering
|
|
808
|
+
|
|
809
|
+
For complex option content, memoize components:
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
const MemoizedOption = memo(({ option }) => (
|
|
813
|
+
<Stack direction="row" alignItems="center" gap={1}>
|
|
814
|
+
<Avatar src={option.avatar} size="sm" />
|
|
815
|
+
<Box>
|
|
816
|
+
<Typography level="body-sm">{option.name}</Typography>
|
|
817
|
+
<Typography level="body-xs" color="neutral">
|
|
818
|
+
{option.email}
|
|
819
|
+
</Typography>
|
|
820
|
+
</Box>
|
|
821
|
+
</Stack>
|
|
822
|
+
));
|
|
823
|
+
|
|
824
|
+
const options = useMemo(
|
|
825
|
+
() =>
|
|
826
|
+
users.map((user) => ({
|
|
827
|
+
value: user.id,
|
|
828
|
+
label: user.name,
|
|
829
|
+
startDecorator: <MemoizedOption option={user} />,
|
|
830
|
+
})),
|
|
831
|
+
[users]
|
|
832
|
+
);
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Autocomplete provides a powerful search-and-select experience for large option sets. Use it when users benefit from filtering options by typing, and ensure you provide clear labels, handle loading states gracefully, and optimize performance for large datasets. For simpler use cases with fewer options, prefer Select instead.
|