@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +13 -0
- package/dist/esm/v3/index.js +191 -56
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +10 -0
- package/dist/v3/index.d.ts +10 -0
- package/dist/v3/index.js +191 -56
- 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 +214 -55
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "1.12.
|
|
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.
|
|
41
|
-
"@sproutsocial/seeds-react-checkbox": "^1.3.
|
|
42
|
-
"@sproutsocial/seeds-react-icon": "^2.3.
|
|
43
|
-
"@sproutsocial/seeds-react-input": "^1.5.
|
|
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.
|
|
45
|
+
"@sproutsocial/seeds-react-popout": "^2.4.38",
|
|
46
46
|
"@sproutsocial/seeds-react-radio": "^1.3.16",
|
|
47
|
-
"@sproutsocial/seeds-react-switch": "^1.2.
|
|
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.
|
|
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.
|
|
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 {
|
|
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> =
|
|
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
|
+
};
|
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,
|
|
@@ -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 =
|
|
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:
|
|
299
|
-
|
|
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
|
|
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={
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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
|