@sproutsocial/seeds-react-menu 1.12.1 → 1.12.3
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 +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/esm/v3/index.js +332 -92
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +14 -0
- package/dist/v3/index.d.ts +14 -0
- package/dist/v3/index.js +331 -91
- 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 +34 -0
- package/src/v3/MultiCombobox.tsx +223 -55
- package/src/v3/MultiSearchableSelect.tsx +217 -55
- 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
|
@@ -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
|
ComboboxGroup,
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
SearchableSelectList,
|
|
27
32
|
} from "./Common/ComboboxStyles";
|
|
28
33
|
import { ClearIcon, StyledChevron, ChevronIcon } from "./Common/SelectIcons";
|
|
34
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
29
35
|
import styled from "styled-components";
|
|
30
36
|
|
|
31
37
|
// ─── Styled components ────────────────────────────────────────────────────────
|
|
@@ -73,6 +79,7 @@ interface MultiSearchableSelectBaseProps {
|
|
|
73
79
|
loadingText?: string;
|
|
74
80
|
disabled?: boolean;
|
|
75
81
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
82
|
+
"aria-describedby"?: string;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
interface MultiSearchableSelectDataProps<T extends DataWithId>
|
|
@@ -93,6 +100,8 @@ interface MultiSearchableSelectDataProps<T extends DataWithId>
|
|
|
93
100
|
selectAll?: boolean;
|
|
94
101
|
isVirtualized?: boolean;
|
|
95
102
|
removeSelectedItems?: boolean;
|
|
103
|
+
creatable?: boolean;
|
|
104
|
+
onCreate?: (value: string) => T;
|
|
96
105
|
children?: never;
|
|
97
106
|
}
|
|
98
107
|
|
|
@@ -115,6 +124,8 @@ interface MultiSearchableSelectChildrenProps
|
|
|
115
124
|
selectAll?: never;
|
|
116
125
|
isVirtualized?: never;
|
|
117
126
|
removeSelectedItems?: never;
|
|
127
|
+
creatable?: never;
|
|
128
|
+
onCreate?: never;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
export type MultiSearchableSelectProps<T extends DataWithId> =
|
|
@@ -135,6 +146,7 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
135
146
|
loadingText = "Loading...",
|
|
136
147
|
filter,
|
|
137
148
|
} = props;
|
|
149
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
138
150
|
|
|
139
151
|
const isChildrenMode = "children" in props && props.children != null;
|
|
140
152
|
|
|
@@ -179,6 +191,12 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
179
191
|
const isVirtualized = isChildrenMode
|
|
180
192
|
? false
|
|
181
193
|
: (props as MultiSearchableSelectDataProps<T>).isVirtualized ?? false;
|
|
194
|
+
const creatableProp = isChildrenMode
|
|
195
|
+
? false
|
|
196
|
+
: (props as MultiSearchableSelectDataProps<T>).creatable ?? false;
|
|
197
|
+
const onCreateProp = isChildrenMode
|
|
198
|
+
? undefined
|
|
199
|
+
: (props as MultiSearchableSelectDataProps<T>).onCreate;
|
|
182
200
|
const children = isChildrenMode
|
|
183
201
|
? (props as MultiSearchableSelectChildrenProps).children
|
|
184
202
|
: undefined;
|
|
@@ -189,6 +207,8 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
189
207
|
|
|
190
208
|
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
191
209
|
const [open, setOpen] = React.useState(false);
|
|
210
|
+
const [internalInputValue, setInternalInputValue] = React.useState("");
|
|
211
|
+
const skipNextInputChangeRef = React.useRef(false);
|
|
192
212
|
const virtualizerRef = React.useRef<ReturnType<
|
|
193
213
|
typeof useVirtualizer<HTMLDivElement, Element>
|
|
194
214
|
> | null>(null);
|
|
@@ -226,9 +246,27 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
226
246
|
[visibleData, groupBy]
|
|
227
247
|
);
|
|
228
248
|
|
|
249
|
+
const currentInputValue = internalInputValue;
|
|
250
|
+
|
|
251
|
+
const creatableSentinel = React.useMemo<CreatableSentinel | null>(() => {
|
|
252
|
+
if (!creatableProp) return null;
|
|
253
|
+
const trimmed = currentInputValue.trim();
|
|
254
|
+
if (!trimmed) return null;
|
|
255
|
+
const lowered = trimmed.toLocaleLowerCase();
|
|
256
|
+
const exactExists = data.some(
|
|
257
|
+
(d) => itemToString(d).trim().toLocaleLowerCase() === lowered
|
|
258
|
+
);
|
|
259
|
+
if (exactExists) return null;
|
|
260
|
+
return makeCreatableSentinel(trimmed);
|
|
261
|
+
}, [creatableProp, currentInputValue, data, itemToString]);
|
|
262
|
+
|
|
229
263
|
// ─── Sentinel flat list ───────────────────────────────────────────────────
|
|
230
264
|
|
|
231
|
-
type FlatRow =
|
|
265
|
+
type FlatRow =
|
|
266
|
+
| T
|
|
267
|
+
| GroupHeaderSentinel
|
|
268
|
+
| SelectAllSentinel
|
|
269
|
+
| CreatableSentinel;
|
|
232
270
|
|
|
233
271
|
const flatItems = React.useMemo<FlatRow[] | null>(() => {
|
|
234
272
|
if (!groups || (!selectHeadings && !selectAll)) return null;
|
|
@@ -248,10 +286,28 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
function handleValueChange(next: FlatRow[]) {
|
|
289
|
+
const creatableClicked = next.find(isCreatableSentinel);
|
|
290
|
+
if (creatableClicked) {
|
|
291
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
292
|
+
const realItems = next.filter(
|
|
293
|
+
(v): v is T =>
|
|
294
|
+
!isGroupHeaderSentinel(v) &&
|
|
295
|
+
!isSelectAllSentinel(v) &&
|
|
296
|
+
!isCreatableSentinel(v)
|
|
297
|
+
);
|
|
298
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
299
|
+
skipNextInputChangeRef.current = true;
|
|
300
|
+
setInternalInputValue("");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
251
304
|
const selectAllClicked = next.filter(isSelectAllSentinel);
|
|
252
305
|
const groupSentinels = next.filter(isGroupHeaderSentinel);
|
|
253
306
|
const realItems = next.filter(
|
|
254
|
-
(v): v is T =>
|
|
307
|
+
(v): v is T =>
|
|
308
|
+
!isGroupHeaderSentinel(v) &&
|
|
309
|
+
!isSelectAllSentinel(v) &&
|
|
310
|
+
!isCreatableSentinel(v)
|
|
255
311
|
);
|
|
256
312
|
|
|
257
313
|
if (selectAllClicked.length > 0) {
|
|
@@ -288,8 +344,17 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
288
344
|
setSelectedItems(updated);
|
|
289
345
|
}
|
|
290
346
|
|
|
291
|
-
function handleSimpleValueChange(newItems:
|
|
292
|
-
|
|
347
|
+
function handleSimpleValueChange(newItems: FlatRow[]) {
|
|
348
|
+
const creatableClicked = newItems.find(isCreatableSentinel);
|
|
349
|
+
if (creatableClicked) {
|
|
350
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
351
|
+
const realItems = newItems.filter((v): v is T => !isCreatableSentinel(v));
|
|
352
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
353
|
+
skipNextInputChangeRef.current = true;
|
|
354
|
+
setInternalInputValue("");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
setSelectedItems(newItems as T[]);
|
|
293
358
|
}
|
|
294
359
|
|
|
295
360
|
// ─── Render sentinel row ──────────────────────────────────────────────────
|
|
@@ -337,6 +402,10 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
337
402
|
);
|
|
338
403
|
}
|
|
339
404
|
|
|
405
|
+
if (isCreatableSentinel(row)) {
|
|
406
|
+
return renderCreatableItem(row);
|
|
407
|
+
}
|
|
408
|
+
|
|
340
409
|
const item = row as T;
|
|
341
410
|
return (
|
|
342
411
|
<ComboboxItem
|
|
@@ -353,8 +422,39 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
353
422
|
);
|
|
354
423
|
}
|
|
355
424
|
|
|
356
|
-
|
|
425
|
+
function renderCreatableItem(
|
|
426
|
+
sentinel: CreatableSentinel,
|
|
427
|
+
style?: React.CSSProperties,
|
|
428
|
+
index?: number,
|
|
429
|
+
measureRef?: (el: Element | null) => void
|
|
430
|
+
) {
|
|
431
|
+
return (
|
|
432
|
+
<ComboboxItem
|
|
433
|
+
key="__creatable"
|
|
434
|
+
value={sentinel}
|
|
435
|
+
itemId="__creatable"
|
|
436
|
+
label={`Create "${sentinel.label}"`}
|
|
437
|
+
inputType="none"
|
|
438
|
+
renderItem={() => (
|
|
439
|
+
<>
|
|
440
|
+
<Icon name="plus-outline" size="mini" />
|
|
441
|
+
{`Create "${sentinel.label}"`}
|
|
442
|
+
</>
|
|
443
|
+
)}
|
|
444
|
+
style={style}
|
|
445
|
+
index={index}
|
|
446
|
+
data-index={index}
|
|
447
|
+
ref={measureRef}
|
|
448
|
+
/>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const baseItems =
|
|
357
453
|
flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
|
|
454
|
+
const rootItems =
|
|
455
|
+
creatableSentinel && Array.isArray(baseItems)
|
|
456
|
+
? ([...baseItems, creatableSentinel] as FlatRow[])
|
|
457
|
+
: baseItems;
|
|
358
458
|
const hasSentinels = selectAll || selectHeadings;
|
|
359
459
|
const rootValue = hasSentinels
|
|
360
460
|
? ([...value] as FlatRow[])
|
|
@@ -367,7 +467,29 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
367
467
|
multiple
|
|
368
468
|
id={id}
|
|
369
469
|
virtualized={isVirtualized}
|
|
370
|
-
filter={
|
|
470
|
+
filter={
|
|
471
|
+
creatableProp
|
|
472
|
+
? (item: unknown, query: string) => {
|
|
473
|
+
if (isCreatableSentinel(item)) return true;
|
|
474
|
+
if (filter) return filter(item, query);
|
|
475
|
+
if (isGroupHeaderSentinel(item) || isSelectAllSentinel(item))
|
|
476
|
+
return true;
|
|
477
|
+
const label = itemToString(item as T);
|
|
478
|
+
return label.toLowerCase().includes(query.toLowerCase());
|
|
479
|
+
}
|
|
480
|
+
: filter
|
|
481
|
+
}
|
|
482
|
+
inputValue={internalInputValue}
|
|
483
|
+
onInputValueChange={(v, eventDetails) => {
|
|
484
|
+
// Preserve search query when an item is selected. Base UI fires
|
|
485
|
+
// 'input-clear' on item-press; ignore it while the popup stays open.
|
|
486
|
+
if (eventDetails.reason === "input-clear" && open) return;
|
|
487
|
+
if (skipNextInputChangeRef.current) {
|
|
488
|
+
skipNextInputChangeRef.current = false;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
setInternalInputValue(v);
|
|
492
|
+
}}
|
|
371
493
|
open={open}
|
|
372
494
|
disabled={props.disabled}
|
|
373
495
|
onOpenChange={setOpen}
|
|
@@ -396,27 +518,41 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
396
518
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
519
|
(hasSentinels ? handleValueChange : handleSimpleValueChange) as any
|
|
398
520
|
}
|
|
399
|
-
|
|
400
|
-
|
|
521
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
522
|
+
itemToStringLabel={(item: any) => {
|
|
523
|
+
if (
|
|
524
|
+
!item ||
|
|
525
|
+
isGroupHeaderSentinel(item) ||
|
|
526
|
+
isSelectAllSentinel(item) ||
|
|
527
|
+
isCreatableSentinel(item)
|
|
528
|
+
)
|
|
401
529
|
return "";
|
|
402
530
|
return itemToString(item as T);
|
|
403
531
|
}}
|
|
404
|
-
|
|
532
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
533
|
+
isItemEqualToValue={(a: any, b: any) => {
|
|
534
|
+
if (isCreatableSentinel(a) && isCreatableSentinel(b))
|
|
535
|
+
return a.label === b.label;
|
|
405
536
|
if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
|
|
406
537
|
return a.groupName === b.groupName;
|
|
407
538
|
if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
|
|
408
539
|
if (
|
|
409
540
|
!isGroupHeaderSentinel(a) &&
|
|
410
541
|
!isSelectAllSentinel(a) &&
|
|
542
|
+
!isCreatableSentinel(a) &&
|
|
411
543
|
!isGroupHeaderSentinel(b) &&
|
|
412
|
-
!isSelectAllSentinel(b)
|
|
544
|
+
!isSelectAllSentinel(b) &&
|
|
545
|
+
!isCreatableSentinel(b)
|
|
413
546
|
)
|
|
414
547
|
return (a as T).id === (b as T).id;
|
|
415
548
|
return false;
|
|
416
549
|
}}
|
|
417
550
|
items={rootItems}
|
|
418
551
|
>
|
|
419
|
-
<SearchableSelectTokenTrigger
|
|
552
|
+
<SearchableSelectTokenTrigger
|
|
553
|
+
id={id}
|
|
554
|
+
aria-describedby={ariaDescribedBy}
|
|
555
|
+
>
|
|
420
556
|
{renderSelection ? (
|
|
421
557
|
renderSelection(value)
|
|
422
558
|
) : value.length > 0 ? (
|
|
@@ -491,57 +627,83 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
491
627
|
inputType={inputType}
|
|
492
628
|
renderItem={renderItem}
|
|
493
629
|
renderGroupHeading={renderGroupHeading}
|
|
630
|
+
renderCreatableItem={
|
|
631
|
+
creatableProp
|
|
632
|
+
? (sentinel, style, index, measureRef) =>
|
|
633
|
+
renderCreatableItem(
|
|
634
|
+
sentinel,
|
|
635
|
+
style,
|
|
636
|
+
index,
|
|
637
|
+
measureRef
|
|
638
|
+
)
|
|
639
|
+
: undefined
|
|
640
|
+
}
|
|
494
641
|
/>
|
|
495
642
|
) : children ? (
|
|
496
643
|
children
|
|
497
644
|
) : flatItems ? (
|
|
498
645
|
(row: FlatRow) => renderSentinelRow(row)
|
|
499
646
|
) : groups ? (
|
|
500
|
-
(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
647
|
+
(
|
|
648
|
+
group: { value: string; items: T[] } | CreatableSentinel,
|
|
649
|
+
index: number
|
|
650
|
+
) => {
|
|
651
|
+
if (isCreatableSentinel(group)) {
|
|
652
|
+
return renderCreatableItem(group);
|
|
653
|
+
}
|
|
654
|
+
const g = group as { value: string; items: T[] };
|
|
655
|
+
return (
|
|
656
|
+
<React.Fragment key={g.value}>
|
|
657
|
+
<ComboboxGroup items={g.items}>
|
|
658
|
+
<ComboboxGroupLabel>
|
|
659
|
+
{renderGroupHeading
|
|
660
|
+
? renderGroupHeading(g.value)
|
|
661
|
+
: g.value}
|
|
662
|
+
</ComboboxGroupLabel>
|
|
663
|
+
<Combobox.Collection>
|
|
664
|
+
{(item: T) => (
|
|
665
|
+
<ComboboxItem
|
|
666
|
+
key={item.id}
|
|
667
|
+
value={item}
|
|
668
|
+
itemId={String(item.id)}
|
|
669
|
+
label={itemToString(item)}
|
|
670
|
+
disabled={item.disabled}
|
|
671
|
+
inputType={inputType}
|
|
672
|
+
renderItem={
|
|
673
|
+
renderItem
|
|
674
|
+
? () => renderItem(item)
|
|
675
|
+
: () => itemToString(item)
|
|
676
|
+
}
|
|
677
|
+
/>
|
|
678
|
+
)}
|
|
679
|
+
</Combobox.Collection>
|
|
680
|
+
</ComboboxGroup>
|
|
681
|
+
{index < groups.length - 1 && <ComboboxSeparator />}
|
|
682
|
+
</React.Fragment>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
529
685
|
) : (
|
|
530
|
-
(item: T) =>
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
686
|
+
(item: T | CreatableSentinel) => {
|
|
687
|
+
if (isCreatableSentinel(item)) {
|
|
688
|
+
return renderCreatableItem(item);
|
|
689
|
+
}
|
|
690
|
+
const dataItem = item as T;
|
|
691
|
+
return (
|
|
692
|
+
<ComboboxItem
|
|
693
|
+
key={dataItem.id}
|
|
694
|
+
value={dataItem}
|
|
695
|
+
itemId={String(dataItem.id)}
|
|
696
|
+
label={itemToString(dataItem)}
|
|
697
|
+
disabled={dataItem.disabled}
|
|
698
|
+
inputType={inputType}
|
|
699
|
+
renderItem={
|
|
700
|
+
renderItem
|
|
701
|
+
? () => renderItem(dataItem)
|
|
702
|
+
: () => itemToString(dataItem)
|
|
703
|
+
}
|
|
704
|
+
/>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
545
707
|
)}
|
|
546
708
|
</SearchableSelectList>
|
|
547
709
|
</SearchableSelectPopup>
|
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)
|