@sproutsocial/seeds-react-menu 1.7.1 → 1.7.5

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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +19 -11
  2. package/BUNDLE_OPTIMIZATION.md +307 -0
  3. package/CHANGELOG.md +51 -0
  4. package/dist/esm/chunk-ROQATIB7.js +357 -0
  5. package/dist/esm/chunk-ROQATIB7.js.map +1 -0
  6. package/dist/esm/index.js +261 -2271
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/v2/index.js +2000 -0
  9. package/dist/esm/v2/index.js.map +1 -0
  10. package/dist/index.d.mts +1 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +297 -1989
  13. package/dist/index.js.map +1 -1
  14. package/dist/v2/index.d.mts +96 -0
  15. package/dist/v2/index.d.ts +96 -0
  16. package/dist/v2/index.js +2191 -0
  17. package/dist/v2/index.js.map +1 -0
  18. package/package.json +30 -14
  19. package/src/MenuItem/styles.tsx +7 -0
  20. package/src/v2/Autocomplete/Autocomplete.stories.tsx +609 -0
  21. package/src/v2/Autocomplete/MultiAutocomplete.tsx +453 -0
  22. package/src/v2/Autocomplete/SingleAutocomplete.tsx +362 -0
  23. package/src/v2/Autocomplete/index.ts +2 -0
  24. package/src/v2/Common-V2/ComboboxContent.tsx +448 -0
  25. package/src/v2/Common-V2/MenuGroup.tsx +60 -0
  26. package/src/v2/Common-V2/MenuGroupTypes.ts +30 -0
  27. package/src/v2/Common-V2/MenuHeading.tsx +83 -0
  28. package/src/v2/Common-V2/MenuItem.tsx +138 -0
  29. package/src/v2/Common-V2/Popout.tsx +102 -0
  30. package/src/v2/Common-V2/PopoutTypes.tsx +92 -0
  31. package/src/v2/Common-V2/SelectToggleButton.tsx +99 -0
  32. package/src/v2/Common-V2/index.ts +11 -0
  33. package/src/v2/Common-V2/props.ts +74 -0
  34. package/src/v2/Common-V2/styles.tsx +41 -0
  35. package/src/v2/Common-V2/types.ts +16 -0
  36. package/src/v2/Common-V2/useHighlightedIndex.ts +62 -0
  37. package/src/v2/Common-V2/utils.ts +238 -0
  38. package/src/v2/SearchableSelect/AutocompleteContent.tsx +325 -0
  39. package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +521 -0
  40. package/src/v2/SearchableSelect/SearchableSelect.tsx +389 -0
  41. package/src/v2/SearchableSelect/index.ts +12 -0
  42. package/src/v2/Select/MultiSelect.tsx +404 -0
  43. package/src/v2/Select/Select.stories.tsx +593 -0
  44. package/src/v2/Select/SingleSelect.tsx +272 -0
  45. package/src/v2/Select/index.ts +2 -0
  46. package/src/v2/index.ts +2 -0
  47. package/tsup.config.ts +14 -5
@@ -0,0 +1,609 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import React from "react";
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+ import { action } from "@storybook/addon-actions";
5
+
6
+ import { Box } from "@sproutsocial/seeds-react-box";
7
+ import { FormField } from "@sproutsocial/seeds-react-form-field";
8
+ import { Icon } from "@sproutsocial/seeds-react-icon";
9
+
10
+ import { MultiAutocomplete } from "./MultiAutocomplete";
11
+ import { SingleAutocomplete } from "./SingleAutocomplete";
12
+ import Button from "@sproutsocial/seeds-react-button";
13
+ import type { ToggleItem, GroupHeading } from "../Common-V2";
14
+
15
+ type CustomAutocompleteArgs = React.ComponentProps<typeof MultiAutocomplete>;
16
+
17
+ const meta: Meta<CustomAutocompleteArgs> = {
18
+ title: "Really Under Development/Autocomplete",
19
+ component: MultiAutocomplete,
20
+ parameters: {
21
+ controls: {
22
+ expanded: true,
23
+ },
24
+ },
25
+ decorators: [
26
+ (Story) => (
27
+ <Box display="flex" flexDirection="column" gap="16px" maxWidth={350}>
28
+ <Story />
29
+ </Box>
30
+ ),
31
+ ],
32
+ };
33
+
34
+ export default meta;
35
+
36
+ type Story = StoryObj<CustomAutocompleteArgs>;
37
+
38
+ interface Book {
39
+ id: string;
40
+ author: string;
41
+ title: string;
42
+ }
43
+
44
+ const books: Book[] = [
45
+ { id: "book-1", author: "Harper Lee", title: "To Kill a Mockingbird" },
46
+ { id: "book-2", author: "Lev Tolstoy", title: "War and Peace" },
47
+ { id: "book-3", author: "Fyodor Dostoyevsy", title: "The Idiot" },
48
+ { id: "book-4", author: "Oscar Wilde", title: "A Picture of Dorian Gray" },
49
+ { id: "book-5", author: "George Orwell", title: "1984" },
50
+ { id: "book-6", author: "Jane Austen", title: "Pride and Prejudice" },
51
+ { id: "book-7", author: "Marcus Aurelius", title: "Meditations" },
52
+ {
53
+ id: "book-8",
54
+ author: "Fyodor Dostoevsky",
55
+ title: "The Brothers Karamazov",
56
+ },
57
+ { id: "book-9", author: "Lev Tolstoy", title: "Anna Karenina" },
58
+ { id: "book-10", author: "Fyodor Dostoevsky", title: "Crime and Punishment" },
59
+ ];
60
+
61
+ // Generate 1000 books for virtualization testing
62
+ const generateBooks = (count: number): Book[] => {
63
+ const authors = [
64
+ "Harper Lee",
65
+ "Lev Tolstoy",
66
+ "Fyodor Dostoevsky",
67
+ "Oscar Wilde",
68
+ "George Orwell",
69
+ "Jane Austen",
70
+ "Marcus Aurelius",
71
+ "Ernest Hemingway",
72
+ "Virginia Woolf",
73
+ "James Joyce",
74
+ "Charles Dickens",
75
+ "Mark Twain",
76
+ "J.R.R. Tolkien",
77
+ "C.S. Lewis",
78
+ "Agatha Christie",
79
+ "Arthur Conan Doyle",
80
+ "J.K. Rowling",
81
+ "Stephen King",
82
+ "Isaac Asimov",
83
+ "Ray Bradbury",
84
+ "Aldous Huxley",
85
+ "Kurt Vonnegut",
86
+ "J.D. Salinger",
87
+ "Maya Angelou",
88
+ "Toni Morrison",
89
+ "Gabriel García Márquez",
90
+ "Milan Kundera",
91
+ "Umberto Eco",
92
+ "Haruki Murakami",
93
+ "Chimamanda Ngozi Adichie",
94
+ ];
95
+
96
+ const titleWords = [
97
+ "Adventures",
98
+ "Chronicles",
99
+ "Memories",
100
+ "Journey",
101
+ "Quest",
102
+ "Tales",
103
+ "Stories",
104
+ "Legends",
105
+ "Myths",
106
+ "Dreams",
107
+ "Visions",
108
+ "Revelations",
109
+ "Secrets",
110
+ "Mysteries",
111
+ "Shadows",
112
+ "Light",
113
+ "Darkness",
114
+ "Echoes",
115
+ "Whispers",
116
+ "Voices",
117
+ "Songs",
118
+ "Poems",
119
+ ];
120
+
121
+ const books: Book[] = [];
122
+ for (let i = 0; i < count; i++) {
123
+ const author = authors[i % authors.length];
124
+ const titleWord1 =
125
+ titleWords[Math.floor(Math.random() * titleWords.length)];
126
+ const titleWord2 =
127
+ titleWords[Math.floor(Math.random() * titleWords.length)];
128
+ const number = Math.floor(i / authors.length) + 1;
129
+ const title = `${titleWord1} of ${titleWord2} ${number}`;
130
+
131
+ books.push({
132
+ id: `book-${i + 1}`,
133
+ author,
134
+ title,
135
+ });
136
+ }
137
+
138
+ return books;
139
+ };
140
+ const largeBookSet = generateBooks(1000);
141
+
142
+ const renderItem = (item: Book) => (
143
+ <>
144
+ <span style={{ fontWeight: "bold" }}>{item.title}</span>
145
+ {item.author && (
146
+ <span style={{ fontSize: "12px", color: "#666", marginLeft: "8px" }}>
147
+ by {item.author}
148
+ </span>
149
+ )}
150
+ </>
151
+ );
152
+
153
+ const itemToString = (item: Book | null): string => {
154
+ if (!item) return "";
155
+ return item.title;
156
+ };
157
+
158
+ const itemToSearchString = (item: Book) => `${item.author}|${item.title}`;
159
+
160
+ const defaultEmptyState = (
161
+ <div style={{ padding: "16px", textAlign: "center", color: "#666" }}>
162
+ No books found
163
+ </div>
164
+ );
165
+
166
+ // 1. Single
167
+ export const Single: Story = {
168
+ name: "Single",
169
+ render: () => {
170
+ return (
171
+ <FormField label="Pick a book">
172
+ {({ id }) => (
173
+ <SingleAutocomplete
174
+ id={id}
175
+ data={books}
176
+ itemToString={itemToString}
177
+ renderItem={renderItem}
178
+ onSelectedItemIdChange={(itemId) => console.log(itemId)}
179
+ EmptyState={defaultEmptyState}
180
+ placeholder="Search for a book"
181
+ />
182
+ )}
183
+ </FormField>
184
+ );
185
+ },
186
+ };
187
+
188
+ // 3. Multi
189
+ export const Multi: Story = {
190
+ name: "Multi",
191
+ render: () => {
192
+ return (
193
+ <div>
194
+ <FormField label="Pick some books">
195
+ {({ id }) => (
196
+ <MultiAutocomplete
197
+ id={id}
198
+ data={books}
199
+ itemToString={itemToString}
200
+ itemToSearchString={itemToSearchString}
201
+ renderItem={renderItem}
202
+ onSelectedItemIdsChange={(itemIds) => console.log(itemIds)}
203
+ EmptyState={defaultEmptyState}
204
+ showSelectedItems={true}
205
+ placeholder="Search for books"
206
+ />
207
+ )}
208
+ </FormField>
209
+ <Button
210
+ style={{ marginTop: "16px" }}
211
+ onClick={action("Button Clicked")}
212
+ >
213
+ Example Button
214
+ </Button>
215
+ </div>
216
+ );
217
+ },
218
+ };
219
+
220
+ // 4. Multi Grouped by Author
221
+ const TestMultiGroupedByAuthor = () => {
222
+ return (
223
+ <FormField label="Pick some books">
224
+ {({ id }) => (
225
+ <MultiAutocomplete
226
+ id={id}
227
+ data={books}
228
+ itemToString={itemToString}
229
+ itemToSearchString={itemToSearchString}
230
+ renderItem={renderItem}
231
+ groupBy={(book) => book.author}
232
+ EmptyState={defaultEmptyState}
233
+ placeholder="Search for books"
234
+ />
235
+ )}
236
+ </FormField>
237
+ );
238
+ };
239
+
240
+ export const MultiGroupedByAuthor: Story = {
241
+ name: "Multi Grouped by Author",
242
+ render: () => {
243
+ return <TestMultiGroupedByAuthor />;
244
+ },
245
+ };
246
+
247
+ // 5. Multi Grouped by First Letter
248
+ const TestMultiGroupedByFirstLetter = () => {
249
+ return (
250
+ <FormField label="Pick some books">
251
+ {({ id }) => (
252
+ <MultiAutocomplete
253
+ id={id}
254
+ data={books}
255
+ itemToString={itemToString}
256
+ itemToSearchString={itemToSearchString}
257
+ renderItem={renderItem}
258
+ groupBy={(book) => book.title.charAt(0).toUpperCase()}
259
+ EmptyState={defaultEmptyState}
260
+ placeholder="Search for books"
261
+ />
262
+ )}
263
+ </FormField>
264
+ );
265
+ };
266
+
267
+ export const MultiGroupedByFirstLetter: Story = {
268
+ name: "Multi Grouped by First Letter",
269
+ render: () => {
270
+ return <TestMultiGroupedByFirstLetter />;
271
+ },
272
+ };
273
+
274
+ // 6. Multi Grouped by Author with Custom Heading
275
+ const TestMultiGroupedByAuthorWithCustomHeading = () => {
276
+ return (
277
+ <FormField label="Pick some books">
278
+ {({ id }) => (
279
+ <MultiAutocomplete
280
+ id={id}
281
+ data={books}
282
+ itemToString={itemToString}
283
+ itemToSearchString={itemToSearchString}
284
+ renderItem={renderItem}
285
+ groupBy={(book) => book.author}
286
+ renderGroupHeading={(label) => `📚 ${label.toUpperCase()}`}
287
+ EmptyState={defaultEmptyState}
288
+ placeholder="Search for books"
289
+ />
290
+ )}
291
+ </FormField>
292
+ );
293
+ };
294
+
295
+ export const MultiGroupedByAuthorWithCustomHeading: Story = {
296
+ name: "Multi Grouped by Author with Custom Heading",
297
+ render: () => {
298
+ return <TestMultiGroupedByAuthorWithCustomHeading />;
299
+ },
300
+ };
301
+
302
+ // 7. Multi with Select All
303
+ const selectAllItem: ToggleItem = {
304
+ id: "select-all",
305
+ label: "Select All",
306
+ };
307
+
308
+ const TestMultiWithSelectAll = () => {
309
+ return (
310
+ <FormField label="Pick some books">
311
+ {({ id }) => (
312
+ <MultiAutocomplete
313
+ id={id}
314
+ data={books}
315
+ itemToString={itemToString}
316
+ itemToSearchString={itemToSearchString}
317
+ renderItem={renderItem}
318
+ selectAllItem={selectAllItem}
319
+ onSelectedItemIdsChange={(itemIds) => console.log(itemIds)}
320
+ EmptyState={defaultEmptyState}
321
+ showSelectedItems={true}
322
+ placeholder="Search for books"
323
+ />
324
+ )}
325
+ </FormField>
326
+ );
327
+ };
328
+
329
+ export const MultiWithSelectAll: Story = {
330
+ name: "Multi with Select All",
331
+ render: () => {
332
+ return <TestMultiWithSelectAll />;
333
+ },
334
+ };
335
+
336
+ // 8. Virtualized Multi (with 1000 items)
337
+ const TestVirtualizedMulti = () => {
338
+ return (
339
+ <FormField label="Pick some books">
340
+ {({ id }) => (
341
+ <MultiAutocomplete
342
+ id={id}
343
+ data={largeBookSet}
344
+ itemToString={itemToString}
345
+ itemToSearchString={itemToSearchString}
346
+ renderItem={renderItem}
347
+ isVirtualized={true}
348
+ EmptyState={defaultEmptyState}
349
+ placeholder="Search for books"
350
+ menuProps={{
351
+ style: {
352
+ maxHeight: "400px",
353
+ overflow: "auto",
354
+ },
355
+ }}
356
+ />
357
+ )}
358
+ </FormField>
359
+ );
360
+ };
361
+
362
+ export const VirtualizedMulti: Story = {
363
+ name: "Virtualized Multi (with 1000 items)",
364
+ render: () => {
365
+ return <TestVirtualizedMulti />;
366
+ },
367
+ };
368
+
369
+ // 9. Virtualized Multi (with 1000 items), With Select All, Grouped by Author, with Selectable Headings
370
+ const TestVirtualizedMultiWithSelectAllGrouped = () => {
371
+ return (
372
+ <FormField label="Pick some books">
373
+ {({ id }) => (
374
+ <MultiAutocomplete
375
+ id={id}
376
+ data={largeBookSet}
377
+ itemToString={itemToString}
378
+ itemToSearchString={itemToSearchString}
379
+ renderItem={renderItem}
380
+ groupBy={(book) => book.author}
381
+ selectAllItem={selectAllItem}
382
+ selectHeadings={true}
383
+ isVirtualized={true}
384
+ renderGroupHeading={(label) => `📚 ${label.toUpperCase()}`}
385
+ onSelectedItemIdsChange={(itemIds) => console.log(itemIds)}
386
+ EmptyState={defaultEmptyState}
387
+ showSelectedItems={true}
388
+ placeholder="Search for books"
389
+ menuProps={{
390
+ style: {
391
+ maxHeight: "400px",
392
+ overflow: "auto",
393
+ },
394
+ }}
395
+ />
396
+ )}
397
+ </FormField>
398
+ );
399
+ };
400
+
401
+ export const VirtualizedMultiWithSelectAllGrouped: Story = {
402
+ name: "Virtualized Multi (with 1000 items), With Select All, Grouped by Author, with Selectable Headings",
403
+ render: () => {
404
+ return <TestVirtualizedMultiWithSelectAllGrouped />;
405
+ },
406
+ };
407
+
408
+ // 10. FullyControlled Single
409
+ const TestFullyControlledSingle = () => {
410
+ const [inputValue, setInputValue] = React.useState("");
411
+ const [selectedItemId, setSelectedItemId] = React.useState<string | null>(
412
+ books[0].id
413
+ );
414
+ const [isOpen, setIsOpen] = React.useState(false);
415
+
416
+ const selectedItem = books.find((book) => book.id === selectedItemId) ?? null;
417
+
418
+ return (
419
+ <>
420
+ <FormField label="Pick a book">
421
+ {({ id }) => (
422
+ <SingleAutocomplete
423
+ id={id}
424
+ data={books}
425
+ inputValue={inputValue}
426
+ itemToString={itemToString}
427
+ itemToSearchString={itemToSearchString}
428
+ onInputValueChange={(newInputValue) => {
429
+ console.log("Input value changed:", newInputValue);
430
+ setInputValue(newInputValue);
431
+ }}
432
+ selectedItemId={selectedItemId}
433
+ onSelectedItemIdChange={(newItemId) => {
434
+ console.log("Selected item changed:", newItemId);
435
+ setSelectedItemId(newItemId);
436
+ }}
437
+ isOpen={isOpen}
438
+ onIsOpenChange={(newIsOpen) => {
439
+ console.log("isOpen changed:", newIsOpen);
440
+ setIsOpen(newIsOpen);
441
+ }}
442
+ renderItem={renderItem}
443
+ EmptyState={defaultEmptyState}
444
+ placeholder="Search for a book"
445
+ />
446
+ )}
447
+ </FormField>
448
+ <div
449
+ style={{
450
+ marginTop: "16px",
451
+ display: "flex",
452
+ flexDirection: "column",
453
+ gap: "8px",
454
+ }}
455
+ >
456
+ <div>
457
+ <Button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</Button>
458
+ <Button
459
+ onClick={() => setInputValue("War")}
460
+ style={{ marginLeft: "8px" }}
461
+ >
462
+ Set Input to "War"
463
+ </Button>
464
+ <Button
465
+ onClick={() => {
466
+ const randomBook =
467
+ books[Math.floor(Math.random() * books.length)];
468
+ setSelectedItemId(randomBook.id);
469
+ }}
470
+ style={{ marginLeft: "8px" }}
471
+ >
472
+ Select Random Book
473
+ </Button>
474
+ </div>
475
+ <div style={{ fontSize: "12px", color: "#666" }}>
476
+ Input: "{inputValue}" | Menu: {isOpen ? "OPEN" : "CLOSED"} | Selected:{" "}
477
+ {selectedItem ? selectedItem.title : "None"}
478
+ </div>
479
+ </div>
480
+ </>
481
+ );
482
+ };
483
+
484
+ export const FullyControlledSingle: Story = {
485
+ name: "FullyControlled Single",
486
+ render: () => {
487
+ return <TestFullyControlledSingle />;
488
+ },
489
+ };
490
+
491
+ // 11. FullyControlled Multi
492
+ const TestFullyControlledMulti = () => {
493
+ const [inputValue, setInputValue] = React.useState("");
494
+ const [selectedItemIds, setSelectedItemIds] = React.useState<string[]>([
495
+ books[0].id,
496
+ ]);
497
+ const [isOpen, setIsOpen] = React.useState(false);
498
+
499
+ const selectedItems = books.filter((book) =>
500
+ selectedItemIds.includes(book.id)
501
+ );
502
+
503
+ return (
504
+ <>
505
+ <FormField label="Pick some books">
506
+ {({ id }) => (
507
+ <MultiAutocomplete
508
+ id={id}
509
+ data={books}
510
+ inputValue={inputValue}
511
+ itemToString={itemToString}
512
+ itemToSearchString={itemToSearchString}
513
+ onInputValueChange={(newInputValue) => {
514
+ console.log("Input value changed:", newInputValue);
515
+ setInputValue(newInputValue);
516
+ }}
517
+ selectedItemIds={selectedItemIds}
518
+ onSelectedItemIdsChange={(newItemIds) => {
519
+ console.log("Selected items changed:", newItemIds);
520
+ setSelectedItemIds(newItemIds);
521
+ }}
522
+ isOpen={isOpen}
523
+ onIsOpenChange={(newIsOpen) => {
524
+ console.log("isOpen changed:", newIsOpen);
525
+ setIsOpen(newIsOpen);
526
+ }}
527
+ renderItem={renderItem}
528
+ EmptyState={defaultEmptyState}
529
+ placeholder="Search for books"
530
+ />
531
+ )}
532
+ </FormField>
533
+ <div
534
+ style={{
535
+ marginTop: "16px",
536
+ display: "flex",
537
+ flexDirection: "column",
538
+ gap: "8px",
539
+ }}
540
+ >
541
+ <div>
542
+ <Button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</Button>
543
+ <Button
544
+ onClick={() => setInputValue("War")}
545
+ style={{ marginLeft: "8px" }}
546
+ >
547
+ Set Input to "War"
548
+ </Button>
549
+ <Button
550
+ onClick={() => {
551
+ const randomBook =
552
+ books[Math.floor(Math.random() * books.length)];
553
+ setSelectedItemIds((prev) => {
554
+ if (!prev.includes(randomBook.id)) {
555
+ return [...prev, randomBook.id];
556
+ }
557
+ return prev;
558
+ });
559
+ }}
560
+ style={{ marginLeft: "8px" }}
561
+ >
562
+ Add Random Book
563
+ </Button>
564
+ </div>
565
+ <div style={{ fontSize: "12px", color: "#666" }}>
566
+ Input: "{inputValue}" | Menu: {isOpen ? "OPEN" : "CLOSED"} | Selected:{" "}
567
+ {selectedItems.map((item) => item.title).join(", ") || "None"}
568
+ </div>
569
+ </div>
570
+ </>
571
+ );
572
+ };
573
+
574
+ export const FullyControlledMulti: Story = {
575
+ name: "FullyControlled Multi",
576
+ render: () => {
577
+ return <TestFullyControlledMulti />;
578
+ },
579
+ };
580
+
581
+ // Single Custom Search string
582
+ const TestCustomSearchString = () => {
583
+ const customItemToSearchString = (item: Book) =>
584
+ `${item.author}|${item.title}`;
585
+
586
+ return (
587
+ <FormField label="Pick a book">
588
+ {({ id }) => (
589
+ <SingleAutocomplete
590
+ id={id}
591
+ data={books}
592
+ itemToString={itemToString}
593
+ itemToSearchString={customItemToSearchString}
594
+ renderItem={renderItem}
595
+ onSelectedItemIdChange={(itemId) => console.log(itemId)}
596
+ EmptyState={defaultEmptyState}
597
+ placeholder="Search for a book"
598
+ />
599
+ )}
600
+ </FormField>
601
+ );
602
+ };
603
+
604
+ export const CustomSearchString: Story = {
605
+ name: "Custom Search String",
606
+ render: () => {
607
+ return <TestCustomSearchString />;
608
+ },
609
+ };