@sproutsocial/seeds-react-menu 1.12.0 → 1.12.2
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/.turbo/turbo-build.log +16 -16
- package/CHANGELOG.md +19 -0
- package/dist/esm/v3/index.js +212 -63
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +13 -1
- package/dist/v3/index.d.ts +13 -1
- package/dist/v3/index.js +212 -63
- package/dist/v3/index.js.map +1 -1
- package/package.json +9 -9
- package/src/v3/Common/VirtualizedComboboxList.tsx +29 -2
- package/src/v3/Common/sentinels.ts +13 -0
- package/src/v3/MultiCombobox.stories.tsx +85 -1
- package/src/v3/MultiCombobox.tsx +238 -62
- package/src/v3/MultiSearchableSelect.tsx +6 -1
- package/src/v3/MultiSelect.tsx +3 -1
- package/src/v3/SingleCombobox.tsx +7 -1
- package/src/v3/SingleSearchableSelect.tsx +3 -1
- package/src/v3/SingleSelect.stories.tsx +19 -0
- package/src/v3/SingleSelect.tsx +3 -1
package/src/v3/MultiCombobox.tsx
CHANGED
|
@@ -6,6 +6,11 @@ import { useSeedsPortalContainer } from "./SeedsPortal";
|
|
|
6
6
|
import ComboboxItem from "./Common/ComboboxItem";
|
|
7
7
|
import type { DataWithId } from "./Common/types";
|
|
8
8
|
import { groupItems } from "./Common/groupItems";
|
|
9
|
+
import {
|
|
10
|
+
isCreatableSentinel,
|
|
11
|
+
makeCreatableSentinel,
|
|
12
|
+
type CreatableSentinel,
|
|
13
|
+
} from "./Common/sentinels";
|
|
9
14
|
import { Field } from "./Common/SelectStyles";
|
|
10
15
|
import {
|
|
11
16
|
ComboboxTokenInputGroup,
|
|
@@ -66,6 +71,9 @@ interface MultiComboboxBaseProps {
|
|
|
66
71
|
loadingText?: string;
|
|
67
72
|
disabled?: boolean;
|
|
68
73
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
74
|
+
inputValue?: string;
|
|
75
|
+
onInputValueChange?: (value: string) => void;
|
|
76
|
+
"aria-describedby"?: string;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
interface MultiComboboxDataProps<T extends DataWithId>
|
|
@@ -82,10 +90,12 @@ interface MultiComboboxDataProps<T extends DataWithId>
|
|
|
82
90
|
groupBy?: (item: T) => string;
|
|
83
91
|
renderGroupHeading?: (label: string) => React.ReactNode;
|
|
84
92
|
selectHeadings?: boolean;
|
|
85
|
-
selectAll?: boolean;
|
|
93
|
+
selectAll?: boolean | string;
|
|
86
94
|
isVirtualized?: boolean;
|
|
87
95
|
removeSelectedItems?: boolean;
|
|
88
96
|
maxTokens?: number;
|
|
97
|
+
creatable?: boolean;
|
|
98
|
+
onCreate?: (value: string) => T;
|
|
89
99
|
children?: never;
|
|
90
100
|
}
|
|
91
101
|
|
|
@@ -105,6 +115,8 @@ interface MultiComboboxChildrenProps extends MultiComboboxBaseProps {
|
|
|
105
115
|
selectHeadings?: never;
|
|
106
116
|
selectAll?: never;
|
|
107
117
|
removeSelectedItems?: never;
|
|
118
|
+
creatable?: never;
|
|
119
|
+
onCreate?: never;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
export type MultiComboboxProps<T extends DataWithId> =
|
|
@@ -127,7 +139,10 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
127
139
|
isLoading = false,
|
|
128
140
|
loadingText = "Loading...",
|
|
129
141
|
filter,
|
|
142
|
+
inputValue,
|
|
143
|
+
onInputValueChange,
|
|
130
144
|
} = props;
|
|
145
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
131
146
|
|
|
132
147
|
const isChildrenMode = "children" in props && props.children != null;
|
|
133
148
|
|
|
@@ -166,12 +181,21 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
166
181
|
const selectHeadings = isChildrenMode
|
|
167
182
|
? false
|
|
168
183
|
: (props as MultiComboboxDataProps<T>).selectHeadings ?? false;
|
|
169
|
-
const
|
|
184
|
+
const selectAllProp = isChildrenMode
|
|
170
185
|
? false
|
|
171
186
|
: (props as MultiComboboxDataProps<T>).selectAll ?? false;
|
|
187
|
+
const selectAll = Boolean(selectAllProp);
|
|
188
|
+
const selectAllLabel =
|
|
189
|
+
typeof selectAllProp === "string" ? selectAllProp : "Select all";
|
|
172
190
|
const isVirtualized = isChildrenMode
|
|
173
191
|
? false
|
|
174
192
|
: (props as MultiComboboxDataProps<T>).isVirtualized ?? false;
|
|
193
|
+
const creatableProp = isChildrenMode
|
|
194
|
+
? false
|
|
195
|
+
: (props as MultiComboboxDataProps<T>).creatable ?? false;
|
|
196
|
+
const onCreateProp = isChildrenMode
|
|
197
|
+
? undefined
|
|
198
|
+
: (props as MultiComboboxDataProps<T>).onCreate;
|
|
175
199
|
const children = isChildrenMode
|
|
176
200
|
? (props as MultiComboboxChildrenProps).children
|
|
177
201
|
: undefined;
|
|
@@ -182,6 +206,8 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
182
206
|
|
|
183
207
|
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
184
208
|
const [open, setOpen] = React.useState(false);
|
|
209
|
+
const [internalInputValue, setInternalInputValue] = React.useState("");
|
|
210
|
+
const skipNextInputChangeRef = React.useRef(false);
|
|
185
211
|
const virtualizerRef = React.useRef<ReturnType<
|
|
186
212
|
typeof useVirtualizer<HTMLDivElement, Element>
|
|
187
213
|
> | null>(null);
|
|
@@ -219,22 +245,45 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
219
245
|
[visibleData, groupBy]
|
|
220
246
|
);
|
|
221
247
|
|
|
248
|
+
const currentInputValue = inputValue ?? internalInputValue;
|
|
249
|
+
|
|
250
|
+
const creatableSentinel = React.useMemo<CreatableSentinel | null>(() => {
|
|
251
|
+
if (!creatableProp) return null;
|
|
252
|
+
const trimmed = currentInputValue.trim();
|
|
253
|
+
if (!trimmed) return null;
|
|
254
|
+
const lowered = trimmed.toLocaleLowerCase();
|
|
255
|
+
const exactExists = data.some(
|
|
256
|
+
(d) => itemToString(d).trim().toLocaleLowerCase() === lowered
|
|
257
|
+
);
|
|
258
|
+
if (exactExists) return null;
|
|
259
|
+
return makeCreatableSentinel(trimmed);
|
|
260
|
+
}, [creatableProp, currentInputValue, data, itemToString]);
|
|
261
|
+
|
|
222
262
|
// ─── Sentinel flat list ───────────────────────────────────────────────────
|
|
223
263
|
// Used when selectHeadings or selectAll. selectAll implies selectHeadings —
|
|
224
264
|
// group headers are always selectable when the list is flattened.
|
|
225
265
|
|
|
226
|
-
type FlatRow =
|
|
266
|
+
type FlatRow =
|
|
267
|
+
| T
|
|
268
|
+
| GroupHeaderSentinel
|
|
269
|
+
| SelectAllSentinel
|
|
270
|
+
| CreatableSentinel;
|
|
227
271
|
|
|
228
272
|
const flatItems = React.useMemo<FlatRow[] | null>(() => {
|
|
229
|
-
if (!
|
|
273
|
+
if (!selectHeadings && !selectAll) return null;
|
|
274
|
+
if (!groups && !selectAll) return null;
|
|
230
275
|
const rows: FlatRow[] = [];
|
|
231
276
|
if (selectAll) rows.push(SELECT_ALL_SENTINEL);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
277
|
+
if (groups) {
|
|
278
|
+
for (const group of groups) {
|
|
279
|
+
rows.push(makeGroupHeaderSentinel(group.value));
|
|
280
|
+
rows.push(...group.items);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
rows.push(...visibleData);
|
|
235
284
|
}
|
|
236
285
|
return rows;
|
|
237
|
-
}, [groups, selectHeadings, selectAll]);
|
|
286
|
+
}, [groups, visibleData, selectHeadings, selectAll]);
|
|
238
287
|
|
|
239
288
|
function setSelectedItems(items: T[]) {
|
|
240
289
|
if (!isControlled) setInternalValue(items);
|
|
@@ -243,6 +292,22 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
function handleValueChange(next: FlatRow[]) {
|
|
295
|
+
const creatableClicked = next.find(isCreatableSentinel);
|
|
296
|
+
if (creatableClicked) {
|
|
297
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
298
|
+
const realItems = next.filter(
|
|
299
|
+
(v): v is T =>
|
|
300
|
+
!isGroupHeaderSentinel(v) &&
|
|
301
|
+
!isSelectAllSentinel(v) &&
|
|
302
|
+
!isCreatableSentinel(v)
|
|
303
|
+
);
|
|
304
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
305
|
+
skipNextInputChangeRef.current = true;
|
|
306
|
+
setInternalInputValue("");
|
|
307
|
+
onInputValueChange?.("");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
246
311
|
const selectAllClicked = next.filter(isSelectAllSentinel);
|
|
247
312
|
const groupSentinels = next.filter(isGroupHeaderSentinel);
|
|
248
313
|
const realItems = next.filter(
|
|
@@ -283,8 +348,18 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
283
348
|
setSelectedItems(updated);
|
|
284
349
|
}
|
|
285
350
|
|
|
286
|
-
function handleSimpleValueChange(newItems:
|
|
287
|
-
|
|
351
|
+
function handleSimpleValueChange(newItems: FlatRow[]) {
|
|
352
|
+
const creatableClicked = newItems.find(isCreatableSentinel);
|
|
353
|
+
if (creatableClicked) {
|
|
354
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
355
|
+
const realItems = newItems.filter((v): v is T => !isCreatableSentinel(v));
|
|
356
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
357
|
+
skipNextInputChangeRef.current = true;
|
|
358
|
+
setInternalInputValue("");
|
|
359
|
+
onInputValueChange?.("");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
setSelectedItems(newItems as T[]);
|
|
288
363
|
}
|
|
289
364
|
|
|
290
365
|
function removeItem(item: T, e: React.MouseEvent) {
|
|
@@ -308,7 +383,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
308
383
|
return (
|
|
309
384
|
<ComboboxSelectAllItem key="__selectAll" value={row}>
|
|
310
385
|
<ComboboxGroupHeaderCheckbox $state={state} id="__selectAll" />
|
|
311
|
-
|
|
386
|
+
{selectAllLabel}
|
|
312
387
|
</ComboboxSelectAllItem>
|
|
313
388
|
);
|
|
314
389
|
}
|
|
@@ -338,6 +413,10 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
338
413
|
);
|
|
339
414
|
}
|
|
340
415
|
|
|
416
|
+
if (isCreatableSentinel(row)) {
|
|
417
|
+
return renderCreatableItem(row);
|
|
418
|
+
}
|
|
419
|
+
|
|
341
420
|
const item = row as T;
|
|
342
421
|
return (
|
|
343
422
|
<ComboboxItem
|
|
@@ -354,6 +433,33 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
354
433
|
);
|
|
355
434
|
}
|
|
356
435
|
|
|
436
|
+
function renderCreatableItem(
|
|
437
|
+
sentinel: CreatableSentinel,
|
|
438
|
+
style?: React.CSSProperties,
|
|
439
|
+
index?: number,
|
|
440
|
+
measureRef?: (el: Element | null) => void
|
|
441
|
+
) {
|
|
442
|
+
return (
|
|
443
|
+
<ComboboxItem
|
|
444
|
+
key="__creatable"
|
|
445
|
+
value={sentinel}
|
|
446
|
+
itemId="__creatable"
|
|
447
|
+
label={`Create "${sentinel.label}"`}
|
|
448
|
+
inputType="none"
|
|
449
|
+
renderItem={() => (
|
|
450
|
+
<>
|
|
451
|
+
<Icon name="plus-outline" size="mini" />
|
|
452
|
+
{`Create "${sentinel.label}"`}
|
|
453
|
+
</>
|
|
454
|
+
)}
|
|
455
|
+
style={style}
|
|
456
|
+
index={index}
|
|
457
|
+
data-index={index}
|
|
458
|
+
ref={measureRef}
|
|
459
|
+
/>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
357
463
|
// ─── Items passed to Combobox.Root ────────────────────────────────────────
|
|
358
464
|
// When selectHeadings: flatItems (flat array of group header sentinels + items).
|
|
359
465
|
// When selectAll only: prepend SELECT_ALL_SENTINEL to data so Base UI tracks
|
|
@@ -361,8 +467,12 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
361
467
|
// The sentinel is never in flatItems — it is always rendered manually above
|
|
362
468
|
// Combobox.List, so we must not prepend it when flatItems is used.
|
|
363
469
|
|
|
364
|
-
const
|
|
470
|
+
const baseItems =
|
|
365
471
|
flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
|
|
472
|
+
const rootItems =
|
|
473
|
+
creatableSentinel && Array.isArray(baseItems)
|
|
474
|
+
? ([...baseItems, creatableSentinel] as FlatRow[])
|
|
475
|
+
: baseItems;
|
|
366
476
|
|
|
367
477
|
const hasSentinels = selectAll || selectHeadings;
|
|
368
478
|
|
|
@@ -379,8 +489,36 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
379
489
|
virtualized={isVirtualized}
|
|
380
490
|
open={open}
|
|
381
491
|
disabled={props.disabled}
|
|
382
|
-
onOpenChange={
|
|
383
|
-
|
|
492
|
+
onOpenChange={(nextOpen, eventDetails) => {
|
|
493
|
+
if (!nextOpen && eventDetails.reason === "item-press") return;
|
|
494
|
+
setOpen(nextOpen);
|
|
495
|
+
}}
|
|
496
|
+
filter={
|
|
497
|
+
creatableProp
|
|
498
|
+
? (item: unknown, query: string) => {
|
|
499
|
+
if (isCreatableSentinel(item)) return true;
|
|
500
|
+
if (filter) return filter(item, query);
|
|
501
|
+
// Default Base UI filter: match against itemToStringLabel
|
|
502
|
+
if (isGroupHeaderSentinel(item) || isSelectAllSentinel(item))
|
|
503
|
+
return true;
|
|
504
|
+
const label = itemToString(item as T);
|
|
505
|
+
return label.toLowerCase().includes(query.toLowerCase());
|
|
506
|
+
}
|
|
507
|
+
: filter
|
|
508
|
+
}
|
|
509
|
+
inputValue={creatableProp ? currentInputValue : inputValue}
|
|
510
|
+
onInputValueChange={
|
|
511
|
+
creatableProp
|
|
512
|
+
? (v) => {
|
|
513
|
+
if (skipNextInputChangeRef.current) {
|
|
514
|
+
skipNextInputChangeRef.current = false;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
setInternalInputValue(v);
|
|
518
|
+
onInputValueChange?.(v);
|
|
519
|
+
}
|
|
520
|
+
: onInputValueChange
|
|
521
|
+
}
|
|
384
522
|
onItemHighlighted={
|
|
385
523
|
isVirtualized
|
|
386
524
|
? (item, { reason, index }) => {
|
|
@@ -406,20 +544,31 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
406
544
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
407
545
|
(hasSentinels ? handleValueChange : handleSimpleValueChange) as any
|
|
408
546
|
}
|
|
409
|
-
|
|
410
|
-
|
|
547
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
548
|
+
itemToStringLabel={(item: any) => {
|
|
549
|
+
if (
|
|
550
|
+
!item ||
|
|
551
|
+
isGroupHeaderSentinel(item) ||
|
|
552
|
+
isSelectAllSentinel(item) ||
|
|
553
|
+
isCreatableSentinel(item)
|
|
554
|
+
)
|
|
411
555
|
return "";
|
|
412
556
|
return itemToString(item as T);
|
|
413
557
|
}}
|
|
414
|
-
|
|
558
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
559
|
+
isItemEqualToValue={(a: any, b: any) => {
|
|
560
|
+
if (isCreatableSentinel(a) && isCreatableSentinel(b))
|
|
561
|
+
return a.label === b.label;
|
|
415
562
|
if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
|
|
416
563
|
return a.groupName === b.groupName;
|
|
417
564
|
if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
|
|
418
565
|
if (
|
|
419
566
|
!isGroupHeaderSentinel(a) &&
|
|
420
567
|
!isSelectAllSentinel(a) &&
|
|
568
|
+
!isCreatableSentinel(a) &&
|
|
421
569
|
!isGroupHeaderSentinel(b) &&
|
|
422
|
-
!isSelectAllSentinel(b)
|
|
570
|
+
!isSelectAllSentinel(b) &&
|
|
571
|
+
!isCreatableSentinel(b)
|
|
423
572
|
)
|
|
424
573
|
return (a as T).id === (b as T).id;
|
|
425
574
|
return false;
|
|
@@ -458,6 +607,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
458
607
|
<ComboboxTokenInput
|
|
459
608
|
id={id}
|
|
460
609
|
placeholder={value.length > 0 ? "" : placeholder}
|
|
610
|
+
aria-describedby={ariaDescribedBy}
|
|
461
611
|
/>
|
|
462
612
|
<ComboboxActionButtons>
|
|
463
613
|
<ComboboxClear aria-label="Clear all">
|
|
@@ -508,57 +658,83 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
508
658
|
inputType={inputType}
|
|
509
659
|
renderItem={renderItem}
|
|
510
660
|
renderGroupHeading={renderGroupHeading}
|
|
661
|
+
renderCreatableItem={
|
|
662
|
+
creatableProp
|
|
663
|
+
? (sentinel, style, index, measureRef) =>
|
|
664
|
+
renderCreatableItem(
|
|
665
|
+
sentinel,
|
|
666
|
+
style,
|
|
667
|
+
index,
|
|
668
|
+
measureRef
|
|
669
|
+
)
|
|
670
|
+
: undefined
|
|
671
|
+
}
|
|
511
672
|
/>
|
|
512
673
|
) : children ? (
|
|
513
674
|
children
|
|
514
675
|
) : flatItems ? (
|
|
515
676
|
(row: FlatRow) => renderSentinelRow(row)
|
|
516
677
|
) : groups ? (
|
|
517
|
-
(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
678
|
+
(
|
|
679
|
+
group: { value: string; items: T[] } | CreatableSentinel,
|
|
680
|
+
index: number
|
|
681
|
+
) => {
|
|
682
|
+
if (isCreatableSentinel(group)) {
|
|
683
|
+
return renderCreatableItem(group);
|
|
684
|
+
}
|
|
685
|
+
const g = group as { value: string; items: T[] };
|
|
686
|
+
return (
|
|
687
|
+
<React.Fragment key={g.value}>
|
|
688
|
+
<ComboboxGroup items={g.items}>
|
|
689
|
+
<ComboboxGroupLabel>
|
|
690
|
+
{renderGroupHeading
|
|
691
|
+
? renderGroupHeading(g.value)
|
|
692
|
+
: g.value}
|
|
693
|
+
</ComboboxGroupLabel>
|
|
694
|
+
<Combobox.Collection>
|
|
695
|
+
{(item: T) => (
|
|
696
|
+
<ComboboxItem
|
|
697
|
+
key={item.id}
|
|
698
|
+
value={item}
|
|
699
|
+
itemId={String(item.id)}
|
|
700
|
+
label={itemToString(item)}
|
|
701
|
+
disabled={item.disabled}
|
|
702
|
+
inputType={inputType}
|
|
703
|
+
renderItem={
|
|
704
|
+
renderItem
|
|
705
|
+
? () => renderItem(item)
|
|
706
|
+
: () => itemToString(item)
|
|
707
|
+
}
|
|
708
|
+
/>
|
|
709
|
+
)}
|
|
710
|
+
</Combobox.Collection>
|
|
711
|
+
</ComboboxGroup>
|
|
712
|
+
{index < groups.length - 1 && <ComboboxSeparator />}
|
|
713
|
+
</React.Fragment>
|
|
714
|
+
);
|
|
715
|
+
}
|
|
546
716
|
) : (
|
|
547
|
-
(item: T) =>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
717
|
+
(item: T | CreatableSentinel) => {
|
|
718
|
+
if (isCreatableSentinel(item)) {
|
|
719
|
+
return renderCreatableItem(item);
|
|
720
|
+
}
|
|
721
|
+
const dataItem = item as T;
|
|
722
|
+
return (
|
|
723
|
+
<ComboboxItem
|
|
724
|
+
key={dataItem.id}
|
|
725
|
+
value={dataItem}
|
|
726
|
+
itemId={String(dataItem.id)}
|
|
727
|
+
label={itemToString(dataItem)}
|
|
728
|
+
disabled={dataItem.disabled}
|
|
729
|
+
inputType={inputType}
|
|
730
|
+
renderItem={
|
|
731
|
+
renderItem
|
|
732
|
+
? () => renderItem(dataItem)
|
|
733
|
+
: () => itemToString(dataItem)
|
|
734
|
+
}
|
|
735
|
+
/>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
562
738
|
)}
|
|
563
739
|
</ComboboxList>
|
|
564
740
|
</ComboboxPopup>
|
|
@@ -73,6 +73,7 @@ interface MultiSearchableSelectBaseProps {
|
|
|
73
73
|
loadingText?: string;
|
|
74
74
|
disabled?: boolean;
|
|
75
75
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
76
|
+
"aria-describedby"?: string;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
interface MultiSearchableSelectDataProps<T extends DataWithId>
|
|
@@ -135,6 +136,7 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
135
136
|
loadingText = "Loading...",
|
|
136
137
|
filter,
|
|
137
138
|
} = props;
|
|
139
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
138
140
|
|
|
139
141
|
const isChildrenMode = "children" in props && props.children != null;
|
|
140
142
|
|
|
@@ -416,7 +418,10 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
416
418
|
}}
|
|
417
419
|
items={rootItems}
|
|
418
420
|
>
|
|
419
|
-
<SearchableSelectTokenTrigger
|
|
421
|
+
<SearchableSelectTokenTrigger
|
|
422
|
+
id={id}
|
|
423
|
+
aria-describedby={ariaDescribedBy}
|
|
424
|
+
>
|
|
420
425
|
{renderSelection ? (
|
|
421
426
|
renderSelection(value)
|
|
422
427
|
) : value.length > 0 ? (
|
package/src/v3/MultiSelect.tsx
CHANGED
|
@@ -43,6 +43,7 @@ interface MultiSelectBaseProps {
|
|
|
43
43
|
id?: string;
|
|
44
44
|
placeholder?: string;
|
|
45
45
|
disabled?: boolean;
|
|
46
|
+
"aria-describedby"?: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
interface MultiSelectDataProps<T extends DataWithId>
|
|
@@ -88,6 +89,7 @@ export type MultiSelectProps<T extends DataWithId> =
|
|
|
88
89
|
|
|
89
90
|
export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
|
|
90
91
|
const { id, placeholder = "Select..." } = props;
|
|
92
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
91
93
|
|
|
92
94
|
const isChildrenMode = "children" in props && props.children != null;
|
|
93
95
|
|
|
@@ -179,7 +181,7 @@ export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
|
|
|
179
181
|
disabled={props.disabled}
|
|
180
182
|
id={id}
|
|
181
183
|
>
|
|
182
|
-
<SelectTrigger>
|
|
184
|
+
<SelectTrigger aria-describedby={ariaDescribedBy}>
|
|
183
185
|
<SelectValue>
|
|
184
186
|
{() => {
|
|
185
187
|
if (renderSelection) return renderSelection(selectedItems);
|
|
@@ -31,6 +31,7 @@ interface SingleComboboxBaseProps {
|
|
|
31
31
|
emptyText?: string;
|
|
32
32
|
disabled?: boolean;
|
|
33
33
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
34
|
+
"aria-describedby"?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface SingleComboboxDataBaseProps<T extends DataWithId>
|
|
@@ -89,6 +90,7 @@ export function SingleCombobox<T extends DataWithId>(
|
|
|
89
90
|
emptyText = "No results found.",
|
|
90
91
|
filter,
|
|
91
92
|
} = props;
|
|
93
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
92
94
|
|
|
93
95
|
const isChildrenMode = "children" in props && props.children != null;
|
|
94
96
|
|
|
@@ -204,7 +206,11 @@ export function SingleCombobox<T extends DataWithId>(
|
|
|
204
206
|
}
|
|
205
207
|
>
|
|
206
208
|
<ComboboxInputGroup>
|
|
207
|
-
<ComboboxInput
|
|
209
|
+
<ComboboxInput
|
|
210
|
+
placeholder={placeholder}
|
|
211
|
+
id={id}
|
|
212
|
+
aria-describedby={ariaDescribedBy}
|
|
213
|
+
/>
|
|
208
214
|
<ComboboxActionButtons>
|
|
209
215
|
<ComboboxClear aria-label="Clear selection">
|
|
210
216
|
<ClearIcon />
|
|
@@ -41,6 +41,7 @@ interface SingleSearchableSelectBaseProps {
|
|
|
41
41
|
loadingText?: string;
|
|
42
42
|
disabled?: boolean;
|
|
43
43
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
44
|
+
"aria-describedby"?: string;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
interface SingleSearchableSelectDataBaseProps<T extends DataWithId>
|
|
@@ -106,6 +107,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
106
107
|
loadingText = "Loading...",
|
|
107
108
|
filter,
|
|
108
109
|
} = props;
|
|
110
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
109
111
|
|
|
110
112
|
const isChildrenMode = "children" in props && props.children != null;
|
|
111
113
|
|
|
@@ -236,7 +238,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
236
238
|
: undefined
|
|
237
239
|
}
|
|
238
240
|
>
|
|
239
|
-
<SearchableSelectTrigger id={id}>
|
|
241
|
+
<SearchableSelectTrigger id={id} aria-describedby={ariaDescribedBy}>
|
|
240
242
|
<SearchableSelectPlaceholder>
|
|
241
243
|
<Combobox.Value placeholder={placeholder}>
|
|
242
244
|
{value
|
|
@@ -37,6 +37,25 @@ export const Basic: Story = {
|
|
|
37
37
|
),
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
export const Empty: Story = {
|
|
41
|
+
name: "Empty",
|
|
42
|
+
render: () => (
|
|
43
|
+
<div style={{ width: "300px" }}>
|
|
44
|
+
<FormField label="Pick a book">
|
|
45
|
+
{({ id }) => (
|
|
46
|
+
<SingleSelect
|
|
47
|
+
id={"placeholder"}
|
|
48
|
+
disabled
|
|
49
|
+
data={[]}
|
|
50
|
+
itemToString={() => ""}
|
|
51
|
+
placeholder=""
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</FormField>
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
40
59
|
export const Disabled: Story = {
|
|
41
60
|
name: "Disabled",
|
|
42
61
|
render: () => (
|
package/src/v3/SingleSelect.tsx
CHANGED
|
@@ -39,6 +39,7 @@ interface SingleSelectBaseProps {
|
|
|
39
39
|
placeholder?: string;
|
|
40
40
|
includePlaceholderItem?: boolean;
|
|
41
41
|
disabled?: boolean;
|
|
42
|
+
"aria-describedby"?: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
interface SingleSelectDataProps<T extends DataWithId>
|
|
@@ -80,6 +81,7 @@ export function SingleSelect<T extends DataWithId>(
|
|
|
80
81
|
props: SingleSelectProps<T>
|
|
81
82
|
) {
|
|
82
83
|
const { id, placeholder, includePlaceholderItem } = props;
|
|
84
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
83
85
|
|
|
84
86
|
const isChildrenMode = "children" in props && props.children != null;
|
|
85
87
|
|
|
@@ -166,7 +168,7 @@ export function SingleSelect<T extends DataWithId>(
|
|
|
166
168
|
onValueChange={handleValueChange}
|
|
167
169
|
id={id}
|
|
168
170
|
>
|
|
169
|
-
<SelectTrigger>
|
|
171
|
+
<SelectTrigger aria-describedby={ariaDescribedBy}>
|
|
170
172
|
<StyledValue placeholder={placeholder}>
|
|
171
173
|
{renderSelection && selectedItem
|
|
172
174
|
? renderSelection(selectedItem)
|