@sproutsocial/seeds-react-menu 1.12.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -37,17 +37,17 @@
37
37
  "dependencies": {
38
38
  "@radix-ui/react-popover": "^1.1.2",
39
39
  "@sproutsocial/seeds-react-box": "^1.1.16",
40
- "@sproutsocial/seeds-react-button": "^2.0.4",
41
- "@sproutsocial/seeds-react-checkbox": "^1.3.32",
42
- "@sproutsocial/seeds-react-icon": "^2.3.6",
43
- "@sproutsocial/seeds-react-input": "^1.5.15",
40
+ "@sproutsocial/seeds-react-button": "^2.0.5",
41
+ "@sproutsocial/seeds-react-checkbox": "^1.3.33",
42
+ "@sproutsocial/seeds-react-icon": "^2.3.7",
43
+ "@sproutsocial/seeds-react-input": "^1.5.16",
44
44
  "@sproutsocial/seeds-react-label": "^1.0.16",
45
- "@sproutsocial/seeds-react-popout": "^2.4.37",
45
+ "@sproutsocial/seeds-react-popout": "^2.4.38",
46
46
  "@sproutsocial/seeds-react-radio": "^1.3.16",
47
- "@sproutsocial/seeds-react-switch": "^1.2.31",
47
+ "@sproutsocial/seeds-react-switch": "^1.2.32",
48
48
  "@sproutsocial/seeds-react-system-props": "^3.0.2",
49
49
  "@sproutsocial/seeds-react-theme": "^3.6.2",
50
- "@sproutsocial/seeds-react-token-input": "^1.4.37",
50
+ "@sproutsocial/seeds-react-token-input": "^1.4.38",
51
51
  "@sproutsocial/seeds-react-utilities": "^4.3.0",
52
52
  "@base-ui/react": "^1.3.0",
53
53
  "@tanstack/react-virtual": "^3.13.12",
@@ -57,7 +57,7 @@
57
57
  "styled-system": "^5.1.5"
58
58
  },
59
59
  "devDependencies": {
60
- "@sproutsocial/seeds-react-tooltip": "^1.1.19",
60
+ "@sproutsocial/seeds-react-tooltip": "^1.1.20",
61
61
  "@sproutsocial/eslint-config-seeds": "*",
62
62
  "@sproutsocial/seeds-react-testing-library": "*",
63
63
  "@sproutsocial/seeds-testing": "*",
@@ -8,7 +8,11 @@ import {
8
8
  ComboboxSelectAllItem,
9
9
  } from "./ComboboxStyles";
10
10
  import type { DataWithId } from "./types";
11
- import { isPlaceholderSentinel } from "./sentinels";
11
+ import {
12
+ isPlaceholderSentinel,
13
+ isCreatableSentinel,
14
+ type CreatableSentinel,
15
+ } from "./sentinels";
12
16
 
13
17
  export const ITEM_HEIGHT = 32;
14
18
 
@@ -29,7 +33,11 @@ export function isSelectAllSentinel(v: unknown): v is SelectAllSentinel {
29
33
  return typeof v === "object" && v !== null && "__selectAll" in v;
30
34
  }
31
35
 
32
- export type FlatSentinelRow<T> = T | GroupHeaderSentinel | SelectAllSentinel;
36
+ export type FlatSentinelRow<T> =
37
+ | T
38
+ | GroupHeaderSentinel
39
+ | SelectAllSentinel
40
+ | CreatableSentinel;
33
41
 
34
42
  interface VirtualizedComboboxListProps<T extends DataWithId> {
35
43
  open: boolean;
@@ -47,6 +55,12 @@ interface VirtualizedComboboxListProps<T extends DataWithId> {
47
55
  inputType: "icon" | "radio" | "checkbox" | "none";
48
56
  renderItem?: (item: T) => React.ReactNode;
49
57
  renderGroupHeading?: (label: string) => React.ReactNode;
58
+ renderCreatableItem?: (
59
+ sentinel: CreatableSentinel,
60
+ style: React.CSSProperties,
61
+ index: number,
62
+ measureRef: (el: Element | null) => void
63
+ ) => React.ReactNode;
50
64
  }
51
65
 
52
66
  export default function VirtualizedComboboxList<T extends DataWithId>({
@@ -59,6 +73,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
59
73
  inputType,
60
74
  renderItem,
61
75
  renderGroupHeading,
76
+ renderCreatableItem,
62
77
  }: VirtualizedComboboxListProps<T>) {
63
78
  type Row = FlatSentinelRow<T>;
64
79
 
@@ -197,6 +212,18 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
197
212
  );
198
213
  }
199
214
 
215
+ if (isCreatableSentinel(row)) {
216
+ if (renderCreatableItem) {
217
+ return renderCreatableItem(
218
+ row,
219
+ baseStyle,
220
+ virtualItem.index,
221
+ virtualizer.measureElement
222
+ );
223
+ }
224
+ return null;
225
+ }
226
+
200
227
  const item = row as T;
201
228
  return (
202
229
  <ComboboxItem
@@ -15,3 +15,16 @@ export function makePlaceholderSentinel(label: string): PlaceholderSentinel {
15
15
  export function isPlaceholderSentinel(v: unknown): v is PlaceholderSentinel {
16
16
  return typeof v === "object" && v !== null && "__placeholder" in v;
17
17
  }
18
+
19
+ export interface CreatableSentinel {
20
+ __creatable: true;
21
+ label: string;
22
+ }
23
+
24
+ export function makeCreatableSentinel(label: string): CreatableSentinel {
25
+ return { __creatable: true, label };
26
+ }
27
+
28
+ export function isCreatableSentinel(v: unknown): v is CreatableSentinel {
29
+ return typeof v === "object" && v !== null && "__creatable" in v;
30
+ }
@@ -8,6 +8,7 @@ import {
8
8
  largeBookSet,
9
9
  itemToString,
10
10
  renderItem,
11
+ type Book,
11
12
  } from "./storyData";
12
13
 
13
14
  const meta: Meta<typeof MultiCombobox> = {
@@ -421,3 +422,36 @@ export const CustomSearch: Story = {
421
422
  </div>
422
423
  ),
423
424
  };
425
+
426
+ export const Creatable: Story = {
427
+ name: "Creatable",
428
+ render: () => {
429
+ const [bookList, setBookList] = React.useState<Book[]>(books);
430
+
431
+ return (
432
+ <div style={{ width: "300px" }}>
433
+ <FormField label="Pick or create books">
434
+ {({ id }) => (
435
+ <MultiCombobox
436
+ id={id}
437
+ data={bookList}
438
+ itemToString={itemToString}
439
+ placeholder="Search or create..."
440
+ isVirtualized
441
+ creatable
442
+ onCreate={(value) => {
443
+ const newBook: Book = {
444
+ id: `book-custom-${value.toLowerCase().replace(/\s+/g, "-")}`,
445
+ title: value,
446
+ author: "Unknown",
447
+ };
448
+ setBookList((prev) => [...prev, newBook]);
449
+ return newBook;
450
+ }}
451
+ />
452
+ )}
453
+ </FormField>
454
+ </div>
455
+ );
456
+ },
457
+ };
@@ -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,
@@ -68,6 +73,7 @@ interface MultiComboboxBaseProps {
68
73
  filter?: ((item: unknown, query: string) => boolean) | null;
69
74
  inputValue?: string;
70
75
  onInputValueChange?: (value: string) => void;
76
+ "aria-describedby"?: string;
71
77
  }
72
78
 
73
79
  interface MultiComboboxDataProps<T extends DataWithId>
@@ -88,6 +94,8 @@ interface MultiComboboxDataProps<T extends DataWithId>
88
94
  isVirtualized?: boolean;
89
95
  removeSelectedItems?: boolean;
90
96
  maxTokens?: number;
97
+ creatable?: boolean;
98
+ onCreate?: (value: string) => T;
91
99
  children?: never;
92
100
  }
93
101
 
@@ -107,6 +115,8 @@ interface MultiComboboxChildrenProps extends MultiComboboxBaseProps {
107
115
  selectHeadings?: never;
108
116
  selectAll?: never;
109
117
  removeSelectedItems?: never;
118
+ creatable?: never;
119
+ onCreate?: never;
110
120
  }
111
121
 
112
122
  export type MultiComboboxProps<T extends DataWithId> =
@@ -132,6 +142,7 @@ export function MultiCombobox<T extends DataWithId>(
132
142
  inputValue,
133
143
  onInputValueChange,
134
144
  } = props;
145
+ const ariaDescribedBy = props["aria-describedby"];
135
146
 
136
147
  const isChildrenMode = "children" in props && props.children != null;
137
148
 
@@ -179,6 +190,12 @@ export function MultiCombobox<T extends DataWithId>(
179
190
  const isVirtualized = isChildrenMode
180
191
  ? false
181
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;
182
199
  const children = isChildrenMode
183
200
  ? (props as MultiComboboxChildrenProps).children
184
201
  : undefined;
@@ -189,6 +206,8 @@ export function MultiCombobox<T extends DataWithId>(
189
206
 
190
207
  const { containerRef, portalContainer } = useSeedsPortalContainer();
191
208
  const [open, setOpen] = React.useState(false);
209
+ const [internalInputValue, setInternalInputValue] = React.useState("");
210
+ const skipNextInputChangeRef = React.useRef(false);
192
211
  const virtualizerRef = React.useRef<ReturnType<
193
212
  typeof useVirtualizer<HTMLDivElement, Element>
194
213
  > | null>(null);
@@ -226,11 +245,29 @@ export function MultiCombobox<T extends DataWithId>(
226
245
  [visibleData, groupBy]
227
246
  );
228
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
+
229
262
  // ─── Sentinel flat list ───────────────────────────────────────────────────
230
263
  // Used when selectHeadings or selectAll. selectAll implies selectHeadings —
231
264
  // group headers are always selectable when the list is flattened.
232
265
 
233
- type FlatRow = T | GroupHeaderSentinel | SelectAllSentinel;
266
+ type FlatRow =
267
+ | T
268
+ | GroupHeaderSentinel
269
+ | SelectAllSentinel
270
+ | CreatableSentinel;
234
271
 
235
272
  const flatItems = React.useMemo<FlatRow[] | null>(() => {
236
273
  if (!selectHeadings && !selectAll) return null;
@@ -255,6 +292,22 @@ export function MultiCombobox<T extends DataWithId>(
255
292
  }
256
293
 
257
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
+
258
311
  const selectAllClicked = next.filter(isSelectAllSentinel);
259
312
  const groupSentinels = next.filter(isGroupHeaderSentinel);
260
313
  const realItems = next.filter(
@@ -295,8 +348,18 @@ export function MultiCombobox<T extends DataWithId>(
295
348
  setSelectedItems(updated);
296
349
  }
297
350
 
298
- function handleSimpleValueChange(newItems: T[]) {
299
- setSelectedItems(newItems);
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[]);
300
363
  }
301
364
 
302
365
  function removeItem(item: T, e: React.MouseEvent) {
@@ -350,6 +413,10 @@ export function MultiCombobox<T extends DataWithId>(
350
413
  );
351
414
  }
352
415
 
416
+ if (isCreatableSentinel(row)) {
417
+ return renderCreatableItem(row);
418
+ }
419
+
353
420
  const item = row as T;
354
421
  return (
355
422
  <ComboboxItem
@@ -366,6 +433,33 @@ export function MultiCombobox<T extends DataWithId>(
366
433
  );
367
434
  }
368
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
+
369
463
  // ─── Items passed to Combobox.Root ────────────────────────────────────────
370
464
  // When selectHeadings: flatItems (flat array of group header sentinels + items).
371
465
  // When selectAll only: prepend SELECT_ALL_SENTINEL to data so Base UI tracks
@@ -373,8 +467,12 @@ export function MultiCombobox<T extends DataWithId>(
373
467
  // The sentinel is never in flatItems — it is always rendered manually above
374
468
  // Combobox.List, so we must not prepend it when flatItems is used.
375
469
 
376
- const rootItems =
470
+ const baseItems =
377
471
  flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
472
+ const rootItems =
473
+ creatableSentinel && Array.isArray(baseItems)
474
+ ? ([...baseItems, creatableSentinel] as FlatRow[])
475
+ : baseItems;
378
476
 
379
477
  const hasSentinels = selectAll || selectHeadings;
380
478
 
@@ -395,9 +493,32 @@ export function MultiCombobox<T extends DataWithId>(
395
493
  if (!nextOpen && eventDetails.reason === "item-press") return;
396
494
  setOpen(nextOpen);
397
495
  }}
398
- filter={filter}
399
- inputValue={inputValue}
400
- onInputValueChange={onInputValueChange}
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
+ }
401
522
  onItemHighlighted={
402
523
  isVirtualized
403
524
  ? (item, { reason, index }) => {
@@ -423,20 +544,31 @@ export function MultiCombobox<T extends DataWithId>(
423
544
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
424
545
  (hasSentinels ? handleValueChange : handleSimpleValueChange) as any
425
546
  }
426
- itemToStringLabel={(item: FlatRow | null) => {
427
- if (!item || isGroupHeaderSentinel(item) || isSelectAllSentinel(item))
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
+ )
428
555
  return "";
429
556
  return itemToString(item as T);
430
557
  }}
431
- isItemEqualToValue={(a: FlatRow, b: FlatRow) => {
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;
432
562
  if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
433
563
  return a.groupName === b.groupName;
434
564
  if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
435
565
  if (
436
566
  !isGroupHeaderSentinel(a) &&
437
567
  !isSelectAllSentinel(a) &&
568
+ !isCreatableSentinel(a) &&
438
569
  !isGroupHeaderSentinel(b) &&
439
- !isSelectAllSentinel(b)
570
+ !isSelectAllSentinel(b) &&
571
+ !isCreatableSentinel(b)
440
572
  )
441
573
  return (a as T).id === (b as T).id;
442
574
  return false;
@@ -475,6 +607,7 @@ export function MultiCombobox<T extends DataWithId>(
475
607
  <ComboboxTokenInput
476
608
  id={id}
477
609
  placeholder={value.length > 0 ? "" : placeholder}
610
+ aria-describedby={ariaDescribedBy}
478
611
  />
479
612
  <ComboboxActionButtons>
480
613
  <ComboboxClear aria-label="Clear all">
@@ -525,57 +658,83 @@ export function MultiCombobox<T extends DataWithId>(
525
658
  inputType={inputType}
526
659
  renderItem={renderItem}
527
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
+ }
528
672
  />
529
673
  ) : children ? (
530
674
  children
531
675
  ) : flatItems ? (
532
676
  (row: FlatRow) => renderSentinelRow(row)
533
677
  ) : groups ? (
534
- (group: { value: string; items: T[] }, index: number) => (
535
- <React.Fragment key={group.value}>
536
- <ComboboxGroup items={group.items}>
537
- <ComboboxGroupLabel>
538
- {renderGroupHeading
539
- ? renderGroupHeading(group.value)
540
- : group.value}
541
- </ComboboxGroupLabel>
542
- <Combobox.Collection>
543
- {(item: T) => (
544
- <ComboboxItem
545
- key={item.id}
546
- value={item}
547
- itemId={String(item.id)}
548
- label={itemToString(item)}
549
- disabled={item.disabled}
550
- inputType={inputType}
551
- renderItem={
552
- renderItem
553
- ? () => renderItem(item)
554
- : () => itemToString(item)
555
- }
556
- />
557
- )}
558
- </Combobox.Collection>
559
- </ComboboxGroup>
560
- {index < groups.length - 1 && <ComboboxSeparator />}
561
- </React.Fragment>
562
- )
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
+ }
563
716
  ) : (
564
- (item: T) => (
565
- <ComboboxItem
566
- key={item.id}
567
- value={item}
568
- itemId={String(item.id)}
569
- label={itemToString(item)}
570
- disabled={item.disabled}
571
- inputType={inputType}
572
- renderItem={
573
- renderItem
574
- ? () => renderItem(item)
575
- : () => itemToString(item)
576
- }
577
- />
578
- )
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
+ }
579
738
  )}
580
739
  </ComboboxList>
581
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 id={id}>
421
+ <SearchableSelectTokenTrigger
422
+ id={id}
423
+ aria-describedby={ariaDescribedBy}
424
+ >
420
425
  {renderSelection ? (
421
426
  renderSelection(value)
422
427
  ) : value.length > 0 ? (
@@ -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 placeholder={placeholder} id={id} />
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