@ceed/ads 1.23.4 → 1.24.0

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.
@@ -8,10 +8,36 @@ Autocomplete is an enhanced input component that provides real-time suggestions
8
8
  <Autocomplete options={['Option1', 'Option2']} />
9
9
  ```
10
10
 
11
- | Field | Description | Default |
12
- | ------- | ----------- | ------- |
13
- | label | — | — |
14
- | loading | — | — |
11
+ | Field | Description | Default |
12
+ | --------------------- | ----------- | ------- |
13
+ | variant | — | — |
14
+ | color | — | — |
15
+ | size | — | — |
16
+ | placeholder | — | — |
17
+ | label | — | — |
18
+ | helperText | — | — |
19
+ | error | — | — |
20
+ | required | — | — |
21
+ | disabled | — | — |
22
+ | readOnly | — | — |
23
+ | multiple | — | — |
24
+ | freeSolo | — | — |
25
+ | disableClearable | — | — |
26
+ | autoHighlight | — | — |
27
+ | clearOnEscape | — | — |
28
+ | disableCloseOnSelect | — | — |
29
+ | openOnFocus | — | — |
30
+ | blurOnSelect | — | — |
31
+ | filterSelectedOptions | — | — |
32
+ | selectOnFocus | — | — |
33
+ | clearOnBlur | — | — |
34
+ | forcePopupIcon | — | — |
35
+ | loading | — | — |
36
+ | noOptionsText | — | — |
37
+ | loadingText | — | — |
38
+ | limitTags | — | — |
39
+ | value | — | — |
40
+ | defaultValue | — | — |
15
41
 
16
42
  > ⚠️ **Usage Warning** ⚠️
17
43
  >
@@ -47,17 +73,19 @@ function CountrySelector() {
47
73
  }
48
74
  ```
49
75
 
50
- ## Examples
76
+ ## Variants
51
77
 
52
- ### Playground
53
-
54
- Interactive example with basic string options.
78
+ The `variant` prop controls the visual style of the autocomplete input. Available variants: `outlined` (default), `soft`, `solid`, `plain`.
55
79
 
56
80
  ```tsx
57
- <Autocomplete options={['Option1', 'Option2']} />
81
+ <Stack gap={2} p={2}>
82
+ {variants.map(variant => <Autocomplete key={variant} variant={variant} options={sampleOptions} placeholder={variant} label={variant} sx={{
83
+ maxWidth: 300
84
+ }} />)}
85
+ </Stack>
58
86
  ```
59
87
 
60
- ### Sizes
88
+ ## Sizes
61
89
 
62
90
  Available size options: `sm`, `md`, `lg`.
63
91
 
@@ -72,7 +100,139 @@ Available size options: `sm`, `md`, `lg`.
72
100
  </div>
73
101
  ```
74
102
 
75
- ### Option Groups
103
+ ## Colors
104
+
105
+ The `color` prop changes the color scheme. Available colors: `primary`, `neutral`, `danger`, `success`, `warning`.
106
+
107
+ ```tsx
108
+ <Stack gap={2} p={2}>
109
+ {colors.map(color => <Autocomplete key={color} color={color} options={sampleOptions} defaultValue="Apple" label={color} sx={{
110
+ maxWidth: 300
111
+ }} />)}
112
+ </Stack>
113
+ ```
114
+
115
+ ## States
116
+
117
+ Autocomplete supports several form states for different interaction contexts.
118
+
119
+ ### Disabled
120
+
121
+ A disabled autocomplete cannot be focused or interacted with. Use for fields that are temporarily unavailable.
122
+
123
+ ### Read Only
124
+
125
+ A read-only autocomplete can be focused and its value copied, but cannot be changed. Use when a value should be visible but not editable.
126
+
127
+ ### Error
128
+
129
+ Set `error` along with `helperText` to indicate validation failures.
130
+
131
+ ### Required
132
+
133
+ Set `required` to mark the field as required in a form.
134
+
135
+ ```tsx
136
+ <Stack gap={3} p={2} sx={{
137
+ maxWidth: 300
138
+ }}>
139
+ <Autocomplete label="Disabled" options={sampleOptions} defaultValue="Apple" disabled />
140
+ <Autocomplete label="Read Only" options={sampleOptions} defaultValue="Banana" readOnly />
141
+ <Autocomplete label="Error" options={sampleOptions} error helperText="This field is required" />
142
+ <Autocomplete label="Required" options={sampleOptions} required placeholder="Select a fruit..." />
143
+ </Stack>
144
+ ```
145
+
146
+ ## Free Solo
147
+
148
+ The `freeSolo` prop allows users to type arbitrary values that are not in the options list. This is useful for inputs where the user can either pick a suggestion or enter a custom value.
149
+
150
+ > ⚠️ **Known limitation**: The component's internal `handleChange` accesses `newValue.value`, which is `undefined` for raw strings in freeSolo mode. Typed arbitrary values may not propagate correctly through `onChange`. Consider using `onInputChange` as an alternative for capturing free-text input.
151
+
152
+ ```tsx
153
+ <Stack gap={2} p={2} sx={{
154
+ maxWidth: 300
155
+ }}>
156
+ <Autocomplete label="Free Solo" options={sampleOptions} freeSolo placeholder="Type anything..." value={value} onChange={e => setValue(e.target.value)} />
157
+ <Typography level="body-sm" textColor="text.tertiary">
158
+ Current value: {value ?? '(empty)'}
159
+ </Typography>
160
+ <Typography level="body-xs" textColor="text.tertiary">
161
+ Note: freeSolo allows arbitrary text input. However, the component&apos;s onChange handler accesses
162
+ newValue.value which is undefined for raw strings, so typed values may not propagate correctly.
163
+ </Typography>
164
+ </Stack>
165
+ ```
166
+
167
+ ## Disable Clearable
168
+
169
+ Set `disableClearable` to hide the clear (X) button. This forces the user to always have a selection.
170
+
171
+ ```tsx
172
+ <Autocomplete
173
+ options={sampleOptions}
174
+ defaultValue="Cherry"
175
+ disableClearable
176
+ label="Disable Clearable"
177
+ sx={{
178
+ maxWidth: 300
179
+ }}
180
+ />
181
+ ```
182
+
183
+ ## Custom Texts
184
+
185
+ Customize the messages shown when there are no matching options or while loading.
186
+
187
+ ```tsx
188
+ <Stack gap={3} direction="row" p={2}>
189
+ <Autocomplete label="Custom No Options Text" options={[]} noOptionsText="No fruits found — try a different search" placeholder="Search fruits..." sx={{
190
+ minWidth: 280
191
+ }} />
192
+ <Autocomplete label="Custom Loading Text" options={[]} loading loadingText="Fetching fruits from the orchard..." placeholder="Loading fruits..." sx={{
193
+ minWidth: 280
194
+ }} />
195
+ </Stack>
196
+ ```
197
+
198
+ ## Multiple Selection
199
+
200
+ Enable `multiple` to allow selecting more than one option. Selected values are shown as chips.
201
+
202
+ ```tsx
203
+ <div style={{
204
+ display: 'flex',
205
+ flexDirection: 'column',
206
+ gap: '10px'
207
+ }}>
208
+ <Autocomplete {...args} size="sm" />
209
+ <Autocomplete {...args} size="md" />
210
+ <Autocomplete {...args} size="lg" />
211
+ </div>
212
+ ```
213
+
214
+ ### Limit Tags
215
+
216
+ In multiple mode, use `limitTags` to control how many tags are visible before truncating. Use `filterSelectedOptions` to hide already-selected options from the dropdown.
217
+
218
+ > 💡 The component uses a custom `renderTags` implementation. JoyUI's `limitTags` truncates the array passed to `renderTags`, so tags will be limited, but the default "+N more" indicator text may not appear.
219
+
220
+ ```tsx
221
+ <Stack gap={3} p={2} sx={{
222
+ maxWidth: 400
223
+ }}>
224
+ <Stack gap={1}>
225
+ <Typography level="title-sm">limitTags=2</Typography>
226
+ <Autocomplete multiple options={sampleOptions} defaultValue={['Apple', 'Banana', 'Cherry', 'Date']} limitTags={2} label="Limit Tags" />
227
+ </Stack>
228
+ <Stack gap={1}>
229
+ <Typography level="title-sm">filterSelectedOptions</Typography>
230
+ <Autocomplete multiple options={sampleOptions} defaultValue={['Apple', 'Banana']} filterSelectedOptions label="Filter Selected Options" />
231
+ </Stack>
232
+ </Stack>
233
+ ```
234
+
235
+ ## Option Groups
76
236
 
77
237
  Group options into categories using the `groupBy` function.
78
238
 
@@ -100,7 +260,7 @@ Group options into categories using the `groupBy` function.
100
260
  />
101
261
  ```
102
262
 
103
- ### Custom Options
263
+ ## Custom Options
104
264
 
105
265
  Options can include decorators for rich content display.
106
266
 
@@ -121,7 +281,28 @@ Options can include decorators for rich content display.
121
281
  />
122
282
  ```
123
283
 
124
- ### Loading State
284
+ ## Secondary Text
285
+
286
+ Options can display a secondary line of text using the `secondaryText` property. This is useful for showing additional context like emails, phone numbers, or descriptions alongside the main label.
287
+
288
+ ```tsx
289
+ <Stack gap={4} direction="row" alignItems="flex-start" p={2}>
290
+ {sizes.map(size => <Stack key={size} gap={1}>
291
+ <span style={{
292
+ color: '#6366f1',
293
+ fontSize: 12
294
+ }}>{size}</span>
295
+ <Autocomplete placeholder="Placeholder" options={optionsWithSecondaryText} value={values[size]} onChange={e => setValues(prev => ({
296
+ ...prev,
297
+ [size]: e.target.value
298
+ }))} sx={{
299
+ minWidth: 200
300
+ }} size={size} />
301
+ </Stack>)}
302
+ </Stack>
303
+ ```
304
+
305
+ ## Loading State
125
306
 
126
307
  Show a loading indicator while fetching options.
127
308
 
@@ -132,6 +313,8 @@ Show a loading indicator while fetching options.
132
313
  />
133
314
  ```
134
315
 
316
+ ## Controlled / Uncontrolled
317
+
135
318
  ### Controlled
136
319
 
137
320
  Manage value externally with controlled state.
@@ -158,6 +341,67 @@ Manage value externally with controlled state.
158
341
  </Stack>
159
342
  ```
160
343
 
344
+ ### Uncontrolled
345
+
346
+ Let the component manage its own state with `defaultValue`.
347
+
348
+ ```tsx
349
+ <Autocomplete
350
+ options={['Uncontrolled', 'Controlled']}
351
+ label="Component Type"
352
+ defaultValue="Controlled"
353
+ />
354
+ ```
355
+
356
+ ## Virtualization
357
+
358
+ Autocomplete automatically virtualizes long option lists for performance. This example renders 1,000 options efficiently.
359
+
360
+ ```tsx
361
+ <Autocomplete
362
+ options={(() => {
363
+ const res: any[] = [];
364
+ for (let i = 0; i < 1000; i++) {
365
+ res.push(i);
366
+ }
367
+ return res;
368
+ })()}
369
+ />
370
+ ```
371
+
372
+ ## Behavior Props
373
+
374
+ These boolean props fine-tune interaction behavior. Toggle them in the Controls panel below to see their effects.
375
+
376
+ | Prop | Default | Effect |
377
+ | ---------------------- | -------- | -------------------------------------------------------------- |
378
+ | `autoHighlight` | `false` | Automatically highlights the first option when the popup opens |
379
+ | `clearOnEscape` | `false` | Clears the input value when the Escape key is pressed |
380
+ | `disableCloseOnSelect` | `false` | Keeps the popup open after selecting an option |
381
+ | `openOnFocus` | `false` | Opens the popup when the input receives focus |
382
+ | `blurOnSelect` | `false` | Blurs the input after an option is selected |
383
+ | `selectOnFocus` | `false` | Selects all input text when the input is focused |
384
+ | `clearOnBlur` | `true` | Clears the input text on blur if it doesn't match any option |
385
+ | `forcePopupIcon` | `'auto'` | Controls whether the popup icon is always shown |
386
+
387
+ ```tsx
388
+ <Autocomplete
389
+ options={sampleOptions}
390
+ label="Behavior Props"
391
+ placeholder="Toggle props in the Controls panel..."
392
+ autoHighlight={false}
393
+ clearOnEscape={false}
394
+ disableCloseOnSelect={false}
395
+ openOnFocus={false}
396
+ blurOnSelect={false}
397
+ selectOnFocus={false}
398
+ clearOnBlur
399
+ sx={{
400
+ maxWidth: 400
401
+ }}
402
+ />
403
+ ```
404
+
161
405
  ## When to Use
162
406
 
163
407
  ### ✅ Good Use Cases
@@ -232,7 +476,7 @@ function UserSearch({ onSelect }) {
232
476
  label: user.name,
233
477
  secondaryText: user.email,
234
478
  startDecorator: <Avatar src={user.avatar} size="sm" />,
235
- }))
479
+ })),
236
480
  );
237
481
  } finally {
238
482
  setLoading(false);
@@ -309,7 +553,7 @@ function AddressAutocomplete({ onAddressSelect }) {
309
553
  value: result.placeId,
310
554
  label: result.formattedAddress,
311
555
  secondaryText: result.city + ', ' + result.country,
312
- }))
556
+ })),
313
557
  );
314
558
  } finally {
315
559
  setLoading(false);
@@ -401,13 +645,7 @@ function ContactSelector() {
401
645
  { value: 'michael', label: 'Michael Chen', secondaryText: '(646) 555-0876' },
402
646
  ];
403
647
 
404
- return (
405
- <Autocomplete
406
- label="Contact"
407
- placeholder="Search contacts..."
408
- options={contacts}
409
- />
410
- );
648
+ return <Autocomplete label="Contact" placeholder="Search contacts..." options={contacts} />;
411
649
  }
412
650
  ```
413
651
 
@@ -448,30 +686,46 @@ function LazyAutocomplete({ fetchOptions }) {
448
686
 
449
687
  ### Key Props
450
688
 
451
- | Prop | Type | Default | Description |
452
- | --------------- | ---------------------------------------- | ------- | ------------------------------------- |
453
- | `options` | `string[] \| OptionObject[]` | `[]` | Array of options (strings or objects) |
454
- | `value` | `string \| string[]` | - | Selected value(s) for controlled mode |
455
- | `defaultValue` | `string \| string[]` | - | Initial value for uncontrolled mode |
456
- | `onChange` | `(event: { target: { value } }) => void` | - | Callback when selection changes |
457
- | `onInputChange` | `(event: { target: { value } }) => void` | - | Callback when input text changes |
458
- | `label` | `string` | - | Label text above the input |
459
- | `placeholder` | `string` | - | Placeholder text when empty |
460
- | `loading` | `boolean` | `false` | Show loading indicator |
461
- | `multiple` | `boolean` | `false` | Allow multiple selections |
462
- | `groupBy` | `(option) => string` | - | Function to group options |
463
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size |
464
- | `disabled` | `boolean` | `false` | Disable the input |
689
+ | Prop | Type | Default | Description |
690
+ | ----------------------- | -------------------------------------------------------------- | -------------- | ----------------------------------------------------- |
691
+ | `options` | `string[] \| OptionObject[]` | `[]` | Array of options (strings or objects) |
692
+ | `value` | `string \| string[]` | - | Selected value(s) for controlled mode |
693
+ | `defaultValue` | `string \| string[]` | - | Initial value for uncontrolled mode |
694
+ | `onChange` | `(event: { target: { value } }) => void` | - | Callback when selection changes |
695
+ | `onInputChange` | `(event: { target: { value } }) => void` | - | Callback when input text changes |
696
+ | `label` | `ReactNode` | - | Label text above the input |
697
+ | `placeholder` | `string` | - | Placeholder text when empty |
698
+ | `loading` | `boolean` | `false` | Show loading indicator |
699
+ | `multiple` | `boolean` | `false` | Allow multiple selections |
700
+ | `groupBy` | `(option) => string` | - | Function to group options |
701
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size |
702
+ | `variant` | `'outlined' \| 'soft' \| 'solid' \| 'plain'` | `'outlined'` | Visual style variant |
703
+ | `color` | `'primary' \| 'neutral' \| 'danger' \| 'success' \| 'warning'` | `'neutral'` | Color scheme |
704
+ | `disabled` | `boolean` | `false` | Disable the input |
705
+ | `readOnly` | `boolean` | `false` | Make the input read-only (focusable but not editable) |
706
+ | `required` | `boolean` | `false` | Mark the field as required |
707
+ | `error` | `boolean` | `false` | Indicate an error state |
708
+ | `helperText` | `ReactNode` | - | Helper text below the input |
709
+ | `freeSolo` | `boolean` | `false` | Allow arbitrary values not in the options list |
710
+ | `disableClearable` | `boolean` | `false` | Hide the clear (X) button |
711
+ | `noOptionsText` | `ReactNode` | `'No options'` | Text when no options match |
712
+ | `loadingText` | `ReactNode` | `'Loading…'` | Text while loading |
713
+ | `limitTags` | `number` | `-1` | Max visible tags in multiple mode (-1 for unlimited) |
714
+ | `autoHighlight` | `boolean` | `false` | Auto-highlight first option |
715
+ | `clearOnEscape` | `boolean` | `false` | Clear value on Escape key |
716
+ | `disableCloseOnSelect` | `boolean` | `false` | Keep popup open after selection |
717
+ | `openOnFocus` | `boolean` | `false` | Open popup on focus |
718
+ | `filterSelectedOptions` | `boolean` | `false` | Hide selected options from dropdown (multiple mode) |
465
719
 
466
720
  ### Option Object Structure
467
721
 
468
722
  ```tsx
469
723
  interface OptionObject {
470
- value: string; // Unique value identifier
471
- label: string; // Display text
472
- secondaryText?: string; // Secondary line of text
724
+ value: string; // Unique value identifier
725
+ label: string; // Display text
726
+ secondaryText?: string; // Secondary line of text
473
727
  startDecorator?: ReactNode; // Content before label
474
- endDecorator?: ReactNode; // Content after label
728
+ endDecorator?: ReactNode; // Content after label
475
729
  }
476
730
 
477
731
  // Example option objects
@@ -496,10 +750,7 @@ const options = [
496
750
 
497
751
  ```tsx
498
752
  // Simplest usage with string array
499
- <Autocomplete
500
- label="Fruit"
501
- options={['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']}
502
- />
753
+ <Autocomplete label="Fruit" options={['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']} />
503
754
  ```
504
755
 
505
756
  ### Controlled vs Uncontrolled
@@ -509,23 +760,12 @@ const options = [
509
760
  function ControlledExample() {
510
761
  const [value, setValue] = useState<string | undefined>();
511
762
 
512
- return (
513
- <Autocomplete
514
- value={value}
515
- onChange={(e) => setValue(e.target.value)}
516
- options={options}
517
- />
518
- );
763
+ return <Autocomplete value={value} onChange={(e) => setValue(e.target.value)} options={options} />;
519
764
  }
520
765
 
521
766
  // Uncontrolled - internal state management
522
767
  function UncontrolledExample() {
523
- return (
524
- <Autocomplete
525
- defaultValue="option-1"
526
- options={options}
527
- />
528
- );
768
+ return <Autocomplete defaultValue="option-1" options={options} />;
529
769
  }
530
770
  ```
531
771
 
@@ -559,12 +799,7 @@ function UncontrolledExample() {
559
799
  ### Multiple Selection
560
800
 
561
801
  ```tsx
562
- <Autocomplete
563
- multiple
564
- value={['option-1', 'option-2']}
565
- options={options}
566
- onChange={(e) => setValues(e.target.value)}
567
- />
802
+ <Autocomplete multiple value={['option-1', 'option-2']} options={options} onChange={(e) => setValues(e.target.value)} />
568
803
  ```
569
804
 
570
805
  ## Accessibility
@@ -583,7 +818,7 @@ Autocomplete includes comprehensive accessibility features:
583
818
  - **Arrow Down**: Open dropdown / move to next option
584
819
  - **Arrow Up**: Move to previous option
585
820
  - **Enter**: Select focused option
586
- - **Escape**: Close dropdown
821
+ - **Escape**: Close dropdown (clears value if `clearOnEscape` is enabled)
587
822
  - **Tab**: Move focus out of component
588
823
  - **Home**: Jump to first option
589
824
  - **End**: Jump to last option
@@ -594,7 +829,7 @@ Autocomplete includes comprehensive accessibility features:
594
829
  ```tsx
595
830
  // Proper labeling for screen readers
596
831
  <Autocomplete
597
- label="Select a country" // Announces: "Select a country, combobox"
832
+ label="Select a country" // Announces: "Select a country, combobox"
598
833
  placeholder="Search..."
599
834
  options={countries}
600
835
  />
@@ -608,6 +843,11 @@ Autocomplete includes comprehensive accessibility features:
608
843
  - Focus returns to input when selecting an option
609
844
  - Clear visual focus indicators on all interactive elements
610
845
 
846
+ ### Read Only vs Disabled Semantics
847
+
848
+ - **`readOnly`**: The input is focusable, its value can be copied, and screen readers announce it as read-only. Use when a value should be visible but not editable.
849
+ - **`disabled`**: The input is not focusable and is skipped in tab order. Use when the field is temporarily unavailable.
850
+
611
851
  ## Best Practices
612
852
 
613
853
  ### ✅ Do
@@ -616,32 +856,21 @@ Autocomplete includes comprehensive accessibility features:
616
856
 
617
857
  ```tsx
618
858
  // ✅ Good: Clear label and placeholder
619
- <Autocomplete
620
- label="Shipping Country"
621
- placeholder="Type to search countries..."
622
- options={countries}
623
- />
859
+ <Autocomplete label="Shipping Country" placeholder="Type to search countries..." options={countries} />
624
860
  ```
625
861
 
626
862
  2. **Show loading state during async operations**: Keep users informed
627
863
 
628
864
  ```tsx
629
865
  // ✅ Good: Loading indicator while fetching
630
- <Autocomplete
631
- options={options}
632
- loading={isLoading}
633
- placeholder={isLoading ? 'Loading...' : 'Search...'}
634
- />
866
+ <Autocomplete options={options} loading={isLoading} placeholder={isLoading ? 'Loading...' : 'Search...'} />
635
867
  ```
636
868
 
637
869
  3. **Use grouping for better organization**: Help users scan large lists
638
870
 
639
871
  ```tsx
640
872
  // ✅ Good: Logical grouping
641
- <Autocomplete
642
- options={allProducts}
643
- groupBy={(product) => product.category}
644
- />
873
+ <Autocomplete options={allProducts} groupBy={(product) => product.category} />
645
874
  ```
646
875
 
647
876
  4. **Debounce API searches**: Prevent excessive requests
@@ -654,6 +883,20 @@ useEffect(() => {
654
883
  }, [query]);
655
884
  ```
656
885
 
886
+ 5. **Use `disableCloseOnSelect` with `multiple`**: Keep the popup open for batch selection
887
+
888
+ ```tsx
889
+ // ✅ Good: Popup stays open for multiple selections
890
+ <Autocomplete multiple disableCloseOnSelect options={tags} />
891
+ ```
892
+
893
+ 6. **Set `limitTags` for constrained layouts**: Prevent tag overflow
894
+
895
+ ```tsx
896
+ // ✅ Good: Show at most 3 tags, truncate the rest
897
+ <Autocomplete multiple limitTags={3} options={allTags} />
898
+ ```
899
+
657
900
  ### ❌ Don't
658
901
 
659
902
  1. **Don't use for small option sets**: Use Select instead
@@ -691,20 +934,36 @@ useEffect(() => {
691
934
 
692
935
  ```tsx
693
936
  // ✅ Good: Handle empty results
694
- <Autocomplete
695
- options={filteredOptions}
696
- noOptionsText="No matches found. Try a different search."
697
- />
937
+ <Autocomplete options={filteredOptions} noOptionsText="No matches found. Try a different search." />
698
938
  ```
699
939
 
700
940
  4. **Don't block interaction during initial load**: Show placeholders
701
941
 
702
942
  ```tsx
703
943
  // ❌ Bad: Blocking the entire form
704
- {loading ? <Spinner /> : <Autocomplete options={options} />}
944
+ {
945
+ loading ? <Spinner /> : <Autocomplete options={options} />;
946
+ }
705
947
 
706
948
  // ✅ Good: Allow interaction with loading state
707
- <Autocomplete options={options} loading={loading} />
949
+ <Autocomplete options={options} loading={loading} />;
950
+ ```
951
+
952
+ 5. **Don't rely on `freeSolo` for validated input without extra handling**: Validate free-text values
953
+
954
+ ```tsx
955
+ // ❌ Bad: No validation for free-text input
956
+ <Autocomplete freeSolo options={emails} />
957
+
958
+ // ✅ Good: Validate free-text input via onInputChange
959
+ <Autocomplete
960
+ freeSolo
961
+ options={emails}
962
+ onInputChange={(e) => {
963
+ const value = e.target.value;
964
+ setError(!isValidEmail(value));
965
+ }}
966
+ />
708
967
  ```
709
968
 
710
969
  ## Performance Considerations
@@ -715,9 +974,7 @@ Autocomplete automatically virtualizes long lists for performance:
715
974
 
716
975
  ```tsx
717
976
  // Handles 1000+ options efficiently
718
- <Autocomplete
719
- options={Array.from({ length: 1000 }, (_, i) => `Option ${i + 1}`)}
720
- />
977
+ <Autocomplete options={Array.from({ length: 1000 }, (_, i) => `Option ${i + 1}`)} />
721
978
  ```
722
979
 
723
980
  ### Debounce Search Requests
@@ -744,16 +1001,10 @@ function SearchAutocomplete({ fetchOptions }) {
744
1001
  setLoading(false);
745
1002
  }
746
1003
  }, 300),
747
- [fetchOptions]
1004
+ [fetchOptions],
748
1005
  );
749
1006
 
750
- return (
751
- <Autocomplete
752
- options={options}
753
- loading={loading}
754
- onInputChange={(e) => debouncedFetch(e.target.value)}
755
- />
756
- );
1007
+ return <Autocomplete options={options} loading={loading} onInputChange={(e) => debouncedFetch(e.target.value)} />;
757
1008
  }
758
1009
  ```
759
1010
 
@@ -769,10 +1020,10 @@ const options = useMemo(
769
1020
  label: item.name,
770
1021
  startDecorator: <StatusChip status={item.status} />,
771
1022
  })),
772
- [data]
1023
+ [data],
773
1024
  );
774
1025
 
775
- <Autocomplete options={options} />
1026
+ <Autocomplete options={options} />;
776
1027
  ```
777
1028
 
778
1029
  ### Lazy Load Options
@@ -798,14 +1049,7 @@ function LazyLoadAutocomplete() {
798
1049
  }
799
1050
  };
800
1051
 
801
- return (
802
- <Autocomplete
803
- options={options}
804
- loading={loading}
805
- onFocus={loadOptions}
806
- onOpen={loadOptions}
807
- />
808
- );
1052
+ return <Autocomplete options={options} loading={loading} onFocus={loadOptions} onOpen={loadOptions} />;
809
1053
  }
810
1054
  ```
811
1055
 
@@ -833,7 +1077,7 @@ const options = useMemo(
833
1077
  label: user.name,
834
1078
  startDecorator: <MemoizedOption option={user} />,
835
1079
  })),
836
- [users]
1080
+ [users],
837
1081
  );
838
1082
  ```
839
1083