@sproutsocial/seeds-react-menu 1.12.2 → 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 +6 -0
- package/dist/esm/v3/index.js +149 -44
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +4 -0
- package/dist/v3/index.d.ts +4 -0
- package/dist/v3/index.js +148 -43
- package/dist/v3/index.js.map +1 -1
- package/package.json +1 -1
- package/src/v3/MultiCombobox.tsx +21 -12
- package/src/v3/MultiSearchableSelect.tsx +211 -54
package/package.json
CHANGED
package/src/v3/MultiCombobox.tsx
CHANGED
|
@@ -506,19 +506,28 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
506
506
|
}
|
|
507
507
|
: filter
|
|
508
508
|
}
|
|
509
|
-
inputValue={
|
|
510
|
-
|
|
511
|
-
creatableProp
|
|
512
|
-
? (v) => {
|
|
513
|
-
if (skipNextInputChangeRef.current) {
|
|
514
|
-
skipNextInputChangeRef.current = false;
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
setInternalInputValue(v);
|
|
518
|
-
onInputValueChange?.(v);
|
|
519
|
-
}
|
|
520
|
-
: onInputValueChange
|
|
509
|
+
inputValue={
|
|
510
|
+
creatableProp ? currentInputValue : inputValue ?? internalInputValue
|
|
521
511
|
}
|
|
512
|
+
onInputValueChange={(v, eventDetails) => {
|
|
513
|
+
// Preserve search query when an item is selected. Base UI fires
|
|
514
|
+
// 'input-clear' on item-press; ignore it while the popup stays open.
|
|
515
|
+
if (eventDetails.reason === "input-clear" && open) return;
|
|
516
|
+
|
|
517
|
+
if (creatableProp) {
|
|
518
|
+
if (skipNextInputChangeRef.current) {
|
|
519
|
+
skipNextInputChangeRef.current = false;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
setInternalInputValue(v);
|
|
523
|
+
onInputValueChange?.(v);
|
|
524
|
+
} else {
|
|
525
|
+
if (inputValue === undefined) {
|
|
526
|
+
setInternalInputValue(v);
|
|
527
|
+
}
|
|
528
|
+
onInputValueChange?.(v);
|
|
529
|
+
}
|
|
530
|
+
}}
|
|
522
531
|
onItemHighlighted={
|
|
523
532
|
isVirtualized
|
|
524
533
|
? (item, { reason, index }) => {
|
|
@@ -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 ────────────────────────────────────────────────────────
|
|
@@ -94,6 +100,8 @@ interface MultiSearchableSelectDataProps<T extends DataWithId>
|
|
|
94
100
|
selectAll?: boolean;
|
|
95
101
|
isVirtualized?: boolean;
|
|
96
102
|
removeSelectedItems?: boolean;
|
|
103
|
+
creatable?: boolean;
|
|
104
|
+
onCreate?: (value: string) => T;
|
|
97
105
|
children?: never;
|
|
98
106
|
}
|
|
99
107
|
|
|
@@ -116,6 +124,8 @@ interface MultiSearchableSelectChildrenProps
|
|
|
116
124
|
selectAll?: never;
|
|
117
125
|
isVirtualized?: never;
|
|
118
126
|
removeSelectedItems?: never;
|
|
127
|
+
creatable?: never;
|
|
128
|
+
onCreate?: never;
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
export type MultiSearchableSelectProps<T extends DataWithId> =
|
|
@@ -181,6 +191,12 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
181
191
|
const isVirtualized = isChildrenMode
|
|
182
192
|
? false
|
|
183
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;
|
|
184
200
|
const children = isChildrenMode
|
|
185
201
|
? (props as MultiSearchableSelectChildrenProps).children
|
|
186
202
|
: undefined;
|
|
@@ -191,6 +207,8 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
191
207
|
|
|
192
208
|
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
193
209
|
const [open, setOpen] = React.useState(false);
|
|
210
|
+
const [internalInputValue, setInternalInputValue] = React.useState("");
|
|
211
|
+
const skipNextInputChangeRef = React.useRef(false);
|
|
194
212
|
const virtualizerRef = React.useRef<ReturnType<
|
|
195
213
|
typeof useVirtualizer<HTMLDivElement, Element>
|
|
196
214
|
> | null>(null);
|
|
@@ -228,9 +246,27 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
228
246
|
[visibleData, groupBy]
|
|
229
247
|
);
|
|
230
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
|
+
|
|
231
263
|
// ─── Sentinel flat list ───────────────────────────────────────────────────
|
|
232
264
|
|
|
233
|
-
type FlatRow =
|
|
265
|
+
type FlatRow =
|
|
266
|
+
| T
|
|
267
|
+
| GroupHeaderSentinel
|
|
268
|
+
| SelectAllSentinel
|
|
269
|
+
| CreatableSentinel;
|
|
234
270
|
|
|
235
271
|
const flatItems = React.useMemo<FlatRow[] | null>(() => {
|
|
236
272
|
if (!groups || (!selectHeadings && !selectAll)) return null;
|
|
@@ -250,10 +286,28 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
250
286
|
}
|
|
251
287
|
|
|
252
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
|
+
|
|
253
304
|
const selectAllClicked = next.filter(isSelectAllSentinel);
|
|
254
305
|
const groupSentinels = next.filter(isGroupHeaderSentinel);
|
|
255
306
|
const realItems = next.filter(
|
|
256
|
-
(v): v is T =>
|
|
307
|
+
(v): v is T =>
|
|
308
|
+
!isGroupHeaderSentinel(v) &&
|
|
309
|
+
!isSelectAllSentinel(v) &&
|
|
310
|
+
!isCreatableSentinel(v)
|
|
257
311
|
);
|
|
258
312
|
|
|
259
313
|
if (selectAllClicked.length > 0) {
|
|
@@ -290,8 +344,17 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
290
344
|
setSelectedItems(updated);
|
|
291
345
|
}
|
|
292
346
|
|
|
293
|
-
function handleSimpleValueChange(newItems:
|
|
294
|
-
|
|
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[]);
|
|
295
358
|
}
|
|
296
359
|
|
|
297
360
|
// ─── Render sentinel row ──────────────────────────────────────────────────
|
|
@@ -339,6 +402,10 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
339
402
|
);
|
|
340
403
|
}
|
|
341
404
|
|
|
405
|
+
if (isCreatableSentinel(row)) {
|
|
406
|
+
return renderCreatableItem(row);
|
|
407
|
+
}
|
|
408
|
+
|
|
342
409
|
const item = row as T;
|
|
343
410
|
return (
|
|
344
411
|
<ComboboxItem
|
|
@@ -355,8 +422,39 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
355
422
|
);
|
|
356
423
|
}
|
|
357
424
|
|
|
358
|
-
|
|
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 =
|
|
359
453
|
flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
|
|
454
|
+
const rootItems =
|
|
455
|
+
creatableSentinel && Array.isArray(baseItems)
|
|
456
|
+
? ([...baseItems, creatableSentinel] as FlatRow[])
|
|
457
|
+
: baseItems;
|
|
360
458
|
const hasSentinels = selectAll || selectHeadings;
|
|
361
459
|
const rootValue = hasSentinels
|
|
362
460
|
? ([...value] as FlatRow[])
|
|
@@ -369,7 +467,29 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
369
467
|
multiple
|
|
370
468
|
id={id}
|
|
371
469
|
virtualized={isVirtualized}
|
|
372
|
-
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
|
+
}}
|
|
373
493
|
open={open}
|
|
374
494
|
disabled={props.disabled}
|
|
375
495
|
onOpenChange={setOpen}
|
|
@@ -398,20 +518,31 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
398
518
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
399
519
|
(hasSentinels ? handleValueChange : handleSimpleValueChange) as any
|
|
400
520
|
}
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
)
|
|
403
529
|
return "";
|
|
404
530
|
return itemToString(item as T);
|
|
405
531
|
}}
|
|
406
|
-
|
|
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;
|
|
407
536
|
if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
|
|
408
537
|
return a.groupName === b.groupName;
|
|
409
538
|
if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
|
|
410
539
|
if (
|
|
411
540
|
!isGroupHeaderSentinel(a) &&
|
|
412
541
|
!isSelectAllSentinel(a) &&
|
|
542
|
+
!isCreatableSentinel(a) &&
|
|
413
543
|
!isGroupHeaderSentinel(b) &&
|
|
414
|
-
!isSelectAllSentinel(b)
|
|
544
|
+
!isSelectAllSentinel(b) &&
|
|
545
|
+
!isCreatableSentinel(b)
|
|
415
546
|
)
|
|
416
547
|
return (a as T).id === (b as T).id;
|
|
417
548
|
return false;
|
|
@@ -496,57 +627,83 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
496
627
|
inputType={inputType}
|
|
497
628
|
renderItem={renderItem}
|
|
498
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
|
+
}
|
|
499
641
|
/>
|
|
500
642
|
) : children ? (
|
|
501
643
|
children
|
|
502
644
|
) : flatItems ? (
|
|
503
645
|
(row: FlatRow) => renderSentinelRow(row)
|
|
504
646
|
) : groups ? (
|
|
505
|
-
(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
+
}
|
|
534
685
|
) : (
|
|
535
|
-
(item: T) =>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
}
|
|
550
707
|
)}
|
|
551
708
|
</SearchableSelectList>
|
|
552
709
|
</SearchableSelectPopup>
|