@coinbase/cds-mcp-server 8.52.2 → 8.53.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.
- package/CHANGELOG.md +4 -0
- package/mcp-docs/mobile/components/Alert.txt +1 -1
- package/mcp-docs/mobile/components/AreaChart.txt +2 -0
- package/mcp-docs/mobile/components/BarChart.txt +33 -0
- package/mcp-docs/mobile/components/CartesianChart.txt +2 -0
- package/mcp-docs/mobile/components/Combobox.txt +380 -83
- package/mcp-docs/mobile/components/LineChart.txt +232 -112
- package/mcp-docs/mobile/components/NudgeCard.txt +1 -1
- package/mcp-docs/mobile/components/Pictogram.txt +1 -1
- package/mcp-docs/mobile/components/Scrubber.txt +2 -0
- package/mcp-docs/mobile/components/SelectChipAlpha.txt +52 -40
- package/mcp-docs/web/components/Alert.txt +1 -1
- package/mcp-docs/web/components/Combobox.txt +422 -83
- package/mcp-docs/web/components/Legend.txt +6 -6
- package/mcp-docs/web/components/LineChart.txt +16 -16
- package/mcp-docs/web/components/NudgeCard.txt +1 -1
- package/mcp-docs/web/components/Pictogram.txt +1 -1
- package/mcp-docs/web/components/SegmentedControl.txt +174 -0
- package/mcp-docs/web/components/SelectChipAlpha.txt +47 -13
- package/mcp-docs/web/components/TileButton.txt +1 -1
- package/mcp-docs/web/routes.txt +1 -0
- package/package.json +1 -1
|
@@ -10,13 +10,37 @@ import { Combobox } from '@coinbase/cds-web/alpha/combobox'
|
|
|
10
10
|
|
|
11
11
|
## Examples
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Basics
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
To start, you can provide a label, an array of options, control state.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```tsx live
|
|
18
|
+
function SingleSelect() {
|
|
19
|
+
const singleSelectOptions = [
|
|
20
|
+
{ value: null, label: 'Remove selection' },
|
|
21
|
+
{ value: 'apple', label: 'Apple' },
|
|
22
|
+
{ value: 'banana', label: 'Banana' },
|
|
23
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
24
|
+
{ value: 'date', label: 'Date' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const [value, setValue] = useState('apple');
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Combobox
|
|
31
|
+
label="Favorite fruit"
|
|
32
|
+
onChange={setValue}
|
|
33
|
+
options={singleSelectOptions}
|
|
34
|
+
placeholder="Search fruits..."
|
|
35
|
+
value={value}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
#### Multiple Selections
|
|
42
|
+
|
|
43
|
+
You can also allow users to select multiple options with `type="multi"`.
|
|
20
44
|
|
|
21
45
|
```tsx live
|
|
22
46
|
function MultiSelect() {
|
|
@@ -38,7 +62,7 @@ function MultiSelect() {
|
|
|
38
62
|
{ value: 'strawberry', label: 'Strawberry' },
|
|
39
63
|
];
|
|
40
64
|
|
|
41
|
-
const { value, onChange } = useMultiSelect({ initialValue: [
|
|
65
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
42
66
|
|
|
43
67
|
return (
|
|
44
68
|
<Combobox
|
|
@@ -53,67 +77,80 @@ function MultiSelect() {
|
|
|
53
77
|
}
|
|
54
78
|
```
|
|
55
79
|
|
|
56
|
-
###
|
|
80
|
+
### Search
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
We use [fuse.js](https://www.fusejs.io/) for fuzzy search by default. You can override with `filterFunction`.
|
|
59
83
|
|
|
60
|
-
```
|
|
61
|
-
function
|
|
62
|
-
const
|
|
63
|
-
{ value:
|
|
64
|
-
{ value: '
|
|
65
|
-
{ value: '
|
|
66
|
-
{ value: '
|
|
67
|
-
{ value: 'date', label: 'Date' },
|
|
84
|
+
```tsx live
|
|
85
|
+
function CustomFilter() {
|
|
86
|
+
const cryptoOptions: SelectOption[] = [
|
|
87
|
+
{ value: 'btc', label: 'Bitcoin', description: 'BTC • Digital Gold' },
|
|
88
|
+
{ value: 'eth', label: 'Ethereum', description: 'ETH • Smart Contracts' },
|
|
89
|
+
{ value: 'usdc', label: 'USD Coin', description: 'USDC • Stablecoin' },
|
|
90
|
+
{ value: 'sol', label: 'Solana', description: 'SOL • High Performance' },
|
|
68
91
|
];
|
|
69
92
|
|
|
70
|
-
const
|
|
93
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
94
|
+
|
|
95
|
+
const filterFunction = useCallback((options: SelectOption[], searchText: string) => {
|
|
96
|
+
const search = searchText.toLowerCase().trim();
|
|
97
|
+
if (!search) return options;
|
|
98
|
+
return options.filter((option) => {
|
|
99
|
+
const label = typeof option.label === 'string' ? option.label.toLowerCase() : '';
|
|
100
|
+
const description =
|
|
101
|
+
typeof option.description === 'string' ? option.description.toLowerCase() : '';
|
|
102
|
+
return label.startsWith(search) || description.startsWith(search);
|
|
103
|
+
});
|
|
104
|
+
}, []);
|
|
71
105
|
|
|
72
106
|
return (
|
|
73
107
|
<Combobox
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
filterFunction={filterFunction}
|
|
109
|
+
label="Custom filter (starts with)"
|
|
110
|
+
onChange={onChange}
|
|
111
|
+
options={cryptoOptions}
|
|
112
|
+
placeholder="Type to filter..."
|
|
113
|
+
type="multi"
|
|
78
114
|
value={value}
|
|
79
115
|
/>
|
|
80
116
|
);
|
|
81
117
|
}
|
|
82
118
|
```
|
|
83
119
|
|
|
84
|
-
###
|
|
120
|
+
### Grouped
|
|
85
121
|
|
|
86
|
-
|
|
122
|
+
Display options under headers using `label` and `options`. Sort options by the same dimension you group by.
|
|
87
123
|
|
|
88
124
|
```tsx live
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
function GroupedOptions() {
|
|
126
|
+
const groupedOptions = [
|
|
127
|
+
{
|
|
128
|
+
label: 'Fruits',
|
|
129
|
+
options: [
|
|
130
|
+
{ value: 'apple', label: 'Apple' },
|
|
131
|
+
{ value: 'banana', label: 'Banana' },
|
|
132
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
133
|
+
{ value: 'date', label: 'Date' },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
label: 'Vegetables',
|
|
138
|
+
options: [
|
|
139
|
+
{ value: 'carrot', label: 'Carrot' },
|
|
140
|
+
{ value: 'broccoli', label: 'Broccoli' },
|
|
141
|
+
{ value: 'spinach', label: 'Spinach' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
106
144
|
];
|
|
107
145
|
|
|
108
|
-
const { value, onChange } = useMultiSelect({ initialValue: [
|
|
146
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
109
147
|
|
|
110
148
|
return (
|
|
111
149
|
<Combobox
|
|
112
|
-
|
|
113
|
-
label="Select fruits"
|
|
150
|
+
label="Category"
|
|
114
151
|
onChange={onChange}
|
|
115
|
-
options={
|
|
116
|
-
placeholder="Search
|
|
152
|
+
options={groupedOptions}
|
|
153
|
+
placeholder="Search by category..."
|
|
117
154
|
type="multi"
|
|
118
155
|
value={value}
|
|
119
156
|
/>
|
|
@@ -121,13 +158,75 @@ function HelperText() {
|
|
|
121
158
|
}
|
|
122
159
|
```
|
|
123
160
|
|
|
124
|
-
###
|
|
161
|
+
### Accessibility
|
|
162
|
+
|
|
163
|
+
Use accessibility labels to provide clear control and dropdown context. For multi-select, add remove and hidden-selection labels so screen readers can describe chip actions and +X summaries.
|
|
164
|
+
|
|
165
|
+
```tsx live
|
|
166
|
+
function AccessibilityProps() {
|
|
167
|
+
const priorityOptions: SelectOption[] = [
|
|
168
|
+
{ value: 'high', label: 'High Priority' },
|
|
169
|
+
{ value: 'medium', label: 'Medium Priority' },
|
|
170
|
+
{ value: 'low', label: 'Low Priority' },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Combobox
|
|
177
|
+
accessibilityLabel="Priority options list"
|
|
178
|
+
controlAccessibilityLabel="Task priority combobox"
|
|
179
|
+
hiddenSelectedOptionsLabel="priorities"
|
|
180
|
+
label="Task Priority"
|
|
181
|
+
maxSelectedOptionsToShow={1}
|
|
182
|
+
onChange={onChange}
|
|
183
|
+
options={priorityOptions}
|
|
184
|
+
placeholder="Choose priority..."
|
|
185
|
+
removeSelectedOptionAccessibilityLabel="Remove priority"
|
|
186
|
+
type="multi"
|
|
187
|
+
value={value}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Styling
|
|
194
|
+
|
|
195
|
+
#### Selection Display Limit
|
|
196
|
+
|
|
197
|
+
Cap visible chips with `maxSelectedOptionsToShow`; the rest show as +X more. Pair with `hiddenSelectedOptionsLabel` for screen readers.
|
|
198
|
+
|
|
199
|
+
```tsx live
|
|
200
|
+
function LimitDisplayedSelections() {
|
|
201
|
+
const countryOptions: SelectOption[] = [
|
|
202
|
+
{ value: 'us', label: 'United States', description: 'North America' },
|
|
203
|
+
{ value: 'ca', label: 'Canada', description: 'North America' },
|
|
204
|
+
{ value: 'mx', label: 'Mexico', description: 'North America' },
|
|
205
|
+
{ value: 'uk', label: 'United Kingdom', description: 'Europe' },
|
|
206
|
+
{ value: 'fr', label: 'France', description: 'Europe' },
|
|
207
|
+
{ value: 'de', label: 'Germany', description: 'Europe' },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<Combobox
|
|
214
|
+
hiddenSelectedOptionsLabel="countries"
|
|
215
|
+
label="Countries"
|
|
216
|
+
maxSelectedOptionsToShow={2}
|
|
217
|
+
onChange={onChange}
|
|
218
|
+
options={countryOptions}
|
|
219
|
+
placeholder="Select countries..."
|
|
220
|
+
type="multi"
|
|
221
|
+
value={value}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
125
226
|
|
|
126
|
-
|
|
227
|
+
#### Alignment
|
|
127
228
|
|
|
128
|
-
|
|
129
|
-
Left / right alignment is preferred for styling.
|
|
130
|
-
::::
|
|
229
|
+
Align selected values with the `align` prop.
|
|
131
230
|
|
|
132
231
|
```tsx live
|
|
133
232
|
function AlignmentExample() {
|
|
@@ -136,82 +235,322 @@ function AlignmentExample() {
|
|
|
136
235
|
{ value: 'banana', label: 'Banana' },
|
|
137
236
|
{ value: 'cherry', label: 'Cherry' },
|
|
138
237
|
{ value: 'date', label: 'Date' },
|
|
139
|
-
{ value: 'elderberry', label: 'Elderberry' },
|
|
140
|
-
{ value: 'fig', label: 'Fig' },
|
|
141
238
|
];
|
|
142
|
-
const { value
|
|
143
|
-
initialValue: ['apple', 'banana'],
|
|
144
|
-
});
|
|
239
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
145
240
|
|
|
146
241
|
return (
|
|
147
|
-
<VStack gap={
|
|
242
|
+
<VStack gap={2}>
|
|
148
243
|
<Combobox
|
|
149
|
-
|
|
150
|
-
|
|
244
|
+
align="start"
|
|
245
|
+
label="Align start"
|
|
246
|
+
onChange={onChange}
|
|
151
247
|
options={fruitOptions}
|
|
152
|
-
placeholder="
|
|
248
|
+
placeholder="Search..."
|
|
153
249
|
type="multi"
|
|
154
|
-
value={
|
|
250
|
+
value={value}
|
|
155
251
|
/>
|
|
156
252
|
<Combobox
|
|
157
253
|
align="end"
|
|
158
|
-
label="
|
|
159
|
-
onChange={
|
|
254
|
+
label="Align end"
|
|
255
|
+
onChange={onChange}
|
|
160
256
|
options={fruitOptions}
|
|
161
|
-
placeholder="
|
|
257
|
+
placeholder="Search..."
|
|
162
258
|
type="multi"
|
|
163
|
-
value={
|
|
259
|
+
value={value}
|
|
164
260
|
/>
|
|
165
261
|
</VStack>
|
|
166
262
|
);
|
|
167
263
|
}
|
|
168
264
|
```
|
|
169
265
|
|
|
170
|
-
|
|
266
|
+
#### Borderless
|
|
171
267
|
|
|
172
|
-
|
|
268
|
+
Remove the border with `bordered={false}`.
|
|
173
269
|
|
|
174
|
-
```
|
|
270
|
+
```tsx live
|
|
175
271
|
function BorderlessExample() {
|
|
176
|
-
const
|
|
177
|
-
{ value:
|
|
272
|
+
const fruitOptions = [
|
|
273
|
+
{ value: 'apple', label: 'Apple' },
|
|
274
|
+
{ value: 'banana', label: 'Banana' },
|
|
275
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
276
|
+
];
|
|
277
|
+
const [value, setValue] = useState('apple');
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<Combobox
|
|
281
|
+
bordered={false}
|
|
282
|
+
label="Borderless"
|
|
283
|
+
onChange={setValue}
|
|
284
|
+
options={fruitOptions}
|
|
285
|
+
placeholder="Search..."
|
|
286
|
+
value={value}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Compact
|
|
293
|
+
|
|
294
|
+
Use smaller sizing with `compact`.
|
|
295
|
+
|
|
296
|
+
```tsx live
|
|
297
|
+
function CompactExample() {
|
|
298
|
+
const fruitOptions = [
|
|
299
|
+
{ value: 'apple', label: 'Apple' },
|
|
300
|
+
{ value: 'banana', label: 'Banana' },
|
|
301
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
302
|
+
];
|
|
303
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<Combobox
|
|
307
|
+
compact
|
|
308
|
+
label="Compact"
|
|
309
|
+
onChange={onChange}
|
|
310
|
+
options={fruitOptions}
|
|
311
|
+
placeholder="Compact combobox..."
|
|
312
|
+
type="multi"
|
|
313
|
+
value={value}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Helper Text
|
|
320
|
+
|
|
321
|
+
Add guidance with `helperText`.
|
|
322
|
+
|
|
323
|
+
```tsx live
|
|
324
|
+
function HelperTextExample() {
|
|
325
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
326
|
+
const fruitOptions: SelectOption[] = [
|
|
178
327
|
{ value: 'apple', label: 'Apple' },
|
|
179
328
|
{ value: 'banana', label: 'Banana' },
|
|
180
329
|
{ value: 'cherry', label: 'Cherry' },
|
|
181
330
|
{ value: 'date', label: 'Date' },
|
|
182
331
|
];
|
|
183
332
|
|
|
333
|
+
return (
|
|
334
|
+
<Combobox
|
|
335
|
+
helperText="Choose more than one fruit"
|
|
336
|
+
label="Select fruits"
|
|
337
|
+
onChange={onChange}
|
|
338
|
+
options={fruitOptions}
|
|
339
|
+
placeholder="Search and select fruits..."
|
|
340
|
+
type="multi"
|
|
341
|
+
value={value}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Composed Examples
|
|
348
|
+
|
|
349
|
+
#### Country Selection
|
|
350
|
+
|
|
351
|
+
You can include flag emoji in labels to create a country selector.
|
|
352
|
+
|
|
353
|
+
```tsx live
|
|
354
|
+
function CountrySelectionExample() {
|
|
355
|
+
const getFlagEmoji = (cc) =>
|
|
356
|
+
cc
|
|
357
|
+
.toUpperCase()
|
|
358
|
+
.split('')
|
|
359
|
+
.map((c) => String.fromCodePoint(0x1f1e6 - 65 + c.charCodeAt(0)))
|
|
360
|
+
.join('');
|
|
361
|
+
|
|
362
|
+
const countryOptions = [
|
|
363
|
+
{
|
|
364
|
+
label: 'North America',
|
|
365
|
+
options: [
|
|
366
|
+
{ value: 'us', label: `${getFlagEmoji('us')} United States` },
|
|
367
|
+
{ value: 'ca', label: `${getFlagEmoji('ca')} Canada` },
|
|
368
|
+
{ value: 'mx', label: `${getFlagEmoji('mx')} Mexico` },
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
label: 'Europe',
|
|
373
|
+
options: [
|
|
374
|
+
{ value: 'uk', label: `${getFlagEmoji('gb')} United Kingdom` },
|
|
375
|
+
{ value: 'fr', label: `${getFlagEmoji('fr')} France` },
|
|
376
|
+
{ value: 'de', label: `${getFlagEmoji('de')} Germany` },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
label: 'Asia',
|
|
381
|
+
options: [
|
|
382
|
+
{ value: 'jp', label: `${getFlagEmoji('jp')} Japan` },
|
|
383
|
+
{ value: 'cn', label: `${getFlagEmoji('cn')} China` },
|
|
384
|
+
{ value: 'in', label: `${getFlagEmoji('in')} India` },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const { value, onChange } = useMultiSelect({ initialValue: [] });
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<Combobox
|
|
393
|
+
label="Country"
|
|
394
|
+
maxSelectedOptionsToShow={3}
|
|
395
|
+
onChange={onChange}
|
|
396
|
+
options={countryOptions}
|
|
397
|
+
placeholder="Select countries..."
|
|
398
|
+
type="multi"
|
|
399
|
+
value={value}
|
|
400
|
+
/>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Free Solo
|
|
406
|
+
|
|
407
|
+
You can add a dynamic option to Combobox to enable free solo where users can provide their own value.
|
|
408
|
+
|
|
409
|
+
```tsx live
|
|
410
|
+
function FreeSoloExample() {
|
|
411
|
+
const CREATE_OPTION_PREFIX = '__create__';
|
|
412
|
+
|
|
413
|
+
const FreeSoloCombobox = useMemo(() => {
|
|
414
|
+
function StableFreeSoloCombobox({
|
|
415
|
+
freeSolo = false,
|
|
416
|
+
options: initialOptions,
|
|
417
|
+
value,
|
|
418
|
+
onChange,
|
|
419
|
+
placeholder = 'Search or type to add...',
|
|
420
|
+
...comboboxProps
|
|
421
|
+
}) {
|
|
422
|
+
const [searchText, setSearchText] = useState('');
|
|
423
|
+
const [options, setOptions] = useState(initialOptions);
|
|
424
|
+
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (!freeSolo) return;
|
|
427
|
+
const initialSet = new Set(initialOptions.map((option) => option.value));
|
|
428
|
+
const valueSet = new Set(Array.isArray(value) ? value : value != null ? [value] : []);
|
|
429
|
+
setOptions((prevOptions) => {
|
|
430
|
+
const addedStillSelected = prevOptions.filter(
|
|
431
|
+
(option) => !initialSet.has(option.value) && valueSet.has(option.value),
|
|
432
|
+
);
|
|
433
|
+
return [...initialOptions, ...addedStillSelected];
|
|
434
|
+
});
|
|
435
|
+
}, [freeSolo, initialOptions, value]);
|
|
436
|
+
|
|
437
|
+
const optionsWithCreate = useMemo(() => {
|
|
438
|
+
if (!freeSolo) return options;
|
|
439
|
+
const trimmedSearch = searchText.trim();
|
|
440
|
+
if (!trimmedSearch) return options;
|
|
441
|
+
|
|
442
|
+
const alreadyExists = options.some(
|
|
443
|
+
(option) =>
|
|
444
|
+
typeof option.label === 'string' &&
|
|
445
|
+
option.label.toLowerCase() === trimmedSearch.toLowerCase(),
|
|
446
|
+
);
|
|
447
|
+
if (alreadyExists) return options;
|
|
448
|
+
|
|
449
|
+
return [
|
|
450
|
+
...options,
|
|
451
|
+
{ value: `${CREATE_OPTION_PREFIX}${trimmedSearch}`, label: `Add "${trimmedSearch}"` },
|
|
452
|
+
];
|
|
453
|
+
}, [freeSolo, options, searchText]);
|
|
454
|
+
|
|
455
|
+
const handleChange = useCallback(
|
|
456
|
+
(newValue) => {
|
|
457
|
+
if (!freeSolo) {
|
|
458
|
+
onChange(newValue);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const values = Array.isArray(newValue) ? newValue : newValue ? [newValue] : [];
|
|
463
|
+
const createValue = values.find((optionValue) =>
|
|
464
|
+
String(optionValue).startsWith(CREATE_OPTION_PREFIX),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (!createValue) {
|
|
468
|
+
onChange(newValue);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const newLabel = String(createValue).slice(CREATE_OPTION_PREFIX.length);
|
|
473
|
+
const normalizedValue = newLabel.toLowerCase();
|
|
474
|
+
const newOption = { value: normalizedValue, label: newLabel };
|
|
475
|
+
|
|
476
|
+
setOptions((prevOptions) => [...prevOptions, newOption]);
|
|
477
|
+
|
|
478
|
+
const updatedValues = values
|
|
479
|
+
.filter((optionValue) => !String(optionValue).startsWith(CREATE_OPTION_PREFIX))
|
|
480
|
+
.concat(normalizedValue);
|
|
481
|
+
|
|
482
|
+
onChange(comboboxProps.type === 'multi' ? updatedValues : normalizedValue);
|
|
483
|
+
setSearchText('');
|
|
484
|
+
},
|
|
485
|
+
[comboboxProps.type, freeSolo, onChange],
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<Combobox
|
|
490
|
+
{...comboboxProps}
|
|
491
|
+
{...(freeSolo ? { searchText, onSearch: setSearchText } : {})}
|
|
492
|
+
onChange={handleChange}
|
|
493
|
+
options={freeSolo ? optionsWithCreate : initialOptions}
|
|
494
|
+
placeholder={placeholder}
|
|
495
|
+
value={value}
|
|
496
|
+
/>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return StableFreeSoloCombobox;
|
|
501
|
+
}, [CREATE_OPTION_PREFIX]);
|
|
502
|
+
|
|
184
503
|
const fruitOptions = [
|
|
185
504
|
{ value: 'apple', label: 'Apple' },
|
|
186
505
|
{ value: 'banana', label: 'Banana' },
|
|
187
506
|
{ value: 'cherry', label: 'Cherry' },
|
|
188
507
|
{ value: 'date', label: 'Date' },
|
|
189
508
|
{ value: 'elderberry', label: 'Elderberry' },
|
|
509
|
+
{ value: 'fig', label: 'Fig' },
|
|
190
510
|
];
|
|
191
511
|
|
|
192
|
-
const [
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
});
|
|
512
|
+
const [standardSingleValue, setStandardSingleValue] = useState(null);
|
|
513
|
+
const [freeSoloSingleValue, setFreeSoloSingleValue] = useState(null);
|
|
514
|
+
const standardMulti = useMultiSelect({ initialValue: [] });
|
|
515
|
+
const freeSoloMulti = useMultiSelect({ initialValue: [] });
|
|
196
516
|
|
|
197
517
|
return (
|
|
198
518
|
<VStack gap={4}>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
label="
|
|
202
|
-
onChange={
|
|
203
|
-
options={
|
|
519
|
+
<FreeSoloCombobox
|
|
520
|
+
freeSolo={false}
|
|
521
|
+
label="Standard single"
|
|
522
|
+
onChange={setStandardSingleValue}
|
|
523
|
+
options={fruitOptions}
|
|
204
524
|
placeholder="Search fruits..."
|
|
205
|
-
|
|
525
|
+
type="single"
|
|
526
|
+
value={standardSingleValue}
|
|
206
527
|
/>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
label="
|
|
210
|
-
onChange={
|
|
528
|
+
<FreeSoloCombobox
|
|
529
|
+
freeSolo
|
|
530
|
+
label="FreeSolo single"
|
|
531
|
+
onChange={setFreeSoloSingleValue}
|
|
532
|
+
options={fruitOptions}
|
|
533
|
+
placeholder="Search or type to add..."
|
|
534
|
+
type="single"
|
|
535
|
+
value={freeSoloSingleValue}
|
|
536
|
+
/>
|
|
537
|
+
<FreeSoloCombobox
|
|
538
|
+
freeSolo={false}
|
|
539
|
+
label="Standard multi"
|
|
540
|
+
onChange={standardMulti.onChange}
|
|
211
541
|
options={fruitOptions}
|
|
212
542
|
placeholder="Search fruits..."
|
|
213
543
|
type="multi"
|
|
214
|
-
value={
|
|
544
|
+
value={standardMulti.value}
|
|
545
|
+
/>
|
|
546
|
+
<FreeSoloCombobox
|
|
547
|
+
freeSolo
|
|
548
|
+
label="FreeSolo multi"
|
|
549
|
+
onChange={freeSoloMulti.onChange}
|
|
550
|
+
options={fruitOptions}
|
|
551
|
+
placeholder="Search or type to add..."
|
|
552
|
+
type="multi"
|
|
553
|
+
value={freeSoloMulti.value}
|
|
215
554
|
/>
|
|
216
555
|
</VStack>
|
|
217
556
|
);
|
|
@@ -32,7 +32,7 @@ function BasicLegend() {
|
|
|
32
32
|
|
|
33
33
|
const chartAccessibilityLabel = `Website traffic across ${pages.length} pages showing page views and unique visitors.`;
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const getScrubberAccessibilityLabel = useCallback(
|
|
36
36
|
(index) => {
|
|
37
37
|
return `${pages[index]}: ${numberFormatter(pageViews[index])} page views, ${numberFormatter(uniqueVisitors[index])} unique visitors.`;
|
|
38
38
|
},
|
|
@@ -72,7 +72,7 @@ function BasicLegend() {
|
|
|
72
72
|
tickLabelFormatter: numberFormatter,
|
|
73
73
|
}}
|
|
74
74
|
>
|
|
75
|
-
<Scrubber accessibilityLabel={
|
|
75
|
+
<Scrubber accessibilityLabel={getScrubberAccessibilityLabel} />
|
|
76
76
|
</LineChart>
|
|
77
77
|
);
|
|
78
78
|
}
|
|
@@ -156,7 +156,7 @@ function WrappedLegend() {
|
|
|
156
156
|
|
|
157
157
|
const chartAccessibilityLabel = `Regional precipitation data across ${precipitationData.length} US regions over 12 months.`;
|
|
158
158
|
|
|
159
|
-
const
|
|
159
|
+
const getScrubberAccessibilityLabel = useCallback(
|
|
160
160
|
(index) => {
|
|
161
161
|
const month = xAxisData[index];
|
|
162
162
|
const regionValues = precipitationData
|
|
@@ -186,7 +186,7 @@ function WrappedLegend() {
|
|
|
186
186
|
showTickMarks: true,
|
|
187
187
|
}}
|
|
188
188
|
>
|
|
189
|
-
<Scrubber hideBeaconLabels hideOverlay accessibilityLabel={
|
|
189
|
+
<Scrubber hideBeaconLabels hideOverlay accessibilityLabel={getScrubberAccessibilityLabel} />
|
|
190
190
|
</LineChart>
|
|
191
191
|
);
|
|
192
192
|
}
|
|
@@ -598,7 +598,7 @@ function CustomLegendEntry() {
|
|
|
598
598
|
|
|
599
599
|
const chartAccessibilityLabel = `Candidate polling data over ${timeLabels.length} months showing support percentages for 3 candidates.`;
|
|
600
600
|
|
|
601
|
-
const
|
|
601
|
+
const getScrubberAccessibilityLabel = useCallback(
|
|
602
602
|
(index) => {
|
|
603
603
|
const month = timeLabels[index];
|
|
604
604
|
const candidateValues = series
|
|
@@ -655,7 +655,7 @@ function CustomLegendEntry() {
|
|
|
655
655
|
tickLabelFormatter: (value) => `${value}%`,
|
|
656
656
|
}}
|
|
657
657
|
>
|
|
658
|
-
<Scrubber accessibilityLabel={
|
|
658
|
+
<Scrubber accessibilityLabel={getScrubberAccessibilityLabel} />
|
|
659
659
|
</LineChart>
|
|
660
660
|
);
|
|
661
661
|
}
|