@sproutsocial/seeds-react-menu 1.10.8 → 1.11.0

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 (50) hide show
  1. package/.turbo/turbo-build.log +21 -15
  2. package/CHANGELOG.md +22 -0
  3. package/dist/esm/index.js +45 -24
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/v2/index.js +1 -1
  6. package/dist/esm/v2/index.js.map +1 -1
  7. package/dist/esm/v3/index.js +800 -0
  8. package/dist/esm/v3/index.js.map +1 -0
  9. package/dist/index.d.mts +22 -22
  10. package/dist/index.d.ts +22 -22
  11. package/dist/index.js +35 -14
  12. package/dist/index.js.map +1 -1
  13. package/dist/v2/index.js +1 -1
  14. package/dist/v2/index.js.map +1 -1
  15. package/dist/v3/index.d.mts +295 -0
  16. package/dist/v3/index.d.ts +295 -0
  17. package/dist/v3/index.js +868 -0
  18. package/dist/v3/index.js.map +1 -0
  19. package/package.json +7 -1
  20. package/src/MenuGroup/MenuGroup.tsx +16 -3
  21. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +19 -0
  22. package/src/MenuItem/MenuItem.tsx +21 -10
  23. package/src/MenuItem/__tests__/MenuItem.test.tsx +19 -0
  24. package/src/v2/Common-V2/ComboboxContent.tsx +1 -1
  25. package/src/v3/Common/ComboboxItem.tsx +82 -0
  26. package/src/v3/Common/ComboboxStyles.tsx +475 -0
  27. package/src/v3/Common/SelectGroup.tsx +28 -0
  28. package/src/v3/Common/SelectIcons.tsx +26 -0
  29. package/src/v3/Common/SelectItem.tsx +108 -0
  30. package/src/v3/Common/SelectStyles.tsx +126 -0
  31. package/src/v3/Common/VirtualizedComboboxList.tsx +200 -0
  32. package/src/v3/Common/groupItems.ts +12 -0
  33. package/src/v3/Common/types.ts +3 -0
  34. package/src/v3/ModalStories.stories.tsx +181 -0
  35. package/src/v3/MultiCombobox.stories.tsx +322 -0
  36. package/src/v3/MultiCombobox.tsx +562 -0
  37. package/src/v3/MultiSearchableSelect.stories.tsx +389 -0
  38. package/src/v3/MultiSearchableSelect.tsx +545 -0
  39. package/src/v3/MultiSelect.stories.tsx +300 -0
  40. package/src/v3/MultiSelect.tsx +498 -0
  41. package/src/v3/SeedsPortal.tsx +35 -0
  42. package/src/v3/SingleCombobox.stories.tsx +169 -0
  43. package/src/v3/SingleCombobox.tsx +304 -0
  44. package/src/v3/SingleSearchableSelect.stories.tsx +239 -0
  45. package/src/v3/SingleSearchableSelect.tsx +319 -0
  46. package/src/v3/SingleSelect.stories.tsx +227 -0
  47. package/src/v3/SingleSelect.tsx +245 -0
  48. package/src/v3/index.ts +7 -0
  49. package/src/v3/storyData.tsx +117 -0
  50. package/tsup.config.ts +1 -0
@@ -0,0 +1,245 @@
1
+ import * as React from "react";
2
+ import { Select } from "@base-ui/react/select";
3
+ import styled from "styled-components";
4
+ import { useSeedsPortalContainer } from "./SeedsPortal";
5
+ import SelectItem from "./Common/SelectItem";
6
+ import type { DataWithId } from "./Common/types";
7
+ import { groupItems } from "./Common/groupItems";
8
+ import {
9
+ Field,
10
+ SelectTrigger,
11
+ SelectIcon,
12
+ SelectGroup,
13
+ SelectGroupLabel,
14
+ SelectSeparator,
15
+ SelectPositioner,
16
+ SelectPopup,
17
+ } from "./Common/SelectStyles";
18
+ import { StyledChevron, ChevronIcon } from "./Common/SelectIcons";
19
+
20
+ // <Listbox(?!\.|B)
21
+ // ─── Styled components ────────────────────────────────────────────────────────
22
+
23
+ const StyledValue = styled(Select.Value)`
24
+ flex: 1;
25
+ text-align: left;
26
+ overflow: hidden;
27
+ text-overflow: ellipsis;
28
+ white-space: nowrap;
29
+
30
+ &[data-placeholder] {
31
+ color: ${({ theme }) => theme.colors.text.subtext};
32
+ }
33
+ `;
34
+
35
+ // ─── Props ────────────────────────────────────────────────────────────────────
36
+
37
+ interface SingleSelectBaseProps {
38
+ id?: string;
39
+ placeholder?: string;
40
+ includePlaceholderItem?: boolean;
41
+ }
42
+
43
+ interface SingleSelectDataProps<T extends DataWithId>
44
+ extends SingleSelectBaseProps {
45
+ data: T[];
46
+ itemToString: (item: T | null) => string;
47
+ renderItem?: (item: T) => React.ReactNode;
48
+ inputType?: "icon" | "radio";
49
+ selectedItemId?: T["id"] | null;
50
+ defaultSelectedItemId?: T["id"] | null;
51
+ onSelectedItemIdChange?: (id: T["id"] | null) => void;
52
+ renderSelection?: (item: T) => React.ReactNode;
53
+ groupBy?: (item: T) => string;
54
+ renderGroupHeading?: (label: string) => React.ReactNode;
55
+ children?: never;
56
+ }
57
+
58
+ interface SingleSelectChildrenProps extends SingleSelectBaseProps {
59
+ children: React.ReactNode;
60
+ selectedItemId?: string | number | null;
61
+ defaultSelectedItemId?: string | number | null;
62
+ onSelectedItemIdChange?: (id: string | number | null) => void;
63
+ data?: never;
64
+ itemToString?: never;
65
+ renderItem?: never;
66
+ inputType?: never;
67
+ renderSelection?: never;
68
+ groupBy?: never;
69
+ renderGroupHeading?: never;
70
+ }
71
+
72
+ export type SingleSelectProps<T extends DataWithId> =
73
+ | SingleSelectDataProps<T>
74
+ | SingleSelectChildrenProps;
75
+
76
+ // ─── Component ────────────────────────────────────────────────────────────────
77
+
78
+ export default function SingleSelect<T extends DataWithId>(
79
+ props: SingleSelectProps<T>
80
+ ) {
81
+ const { id, placeholder, includePlaceholderItem } = props;
82
+
83
+ const isChildrenMode = "children" in props && props.children != null;
84
+
85
+ const selectedItemId = props.selectedItemId;
86
+ const defaultSelectedItemId = props.defaultSelectedItemId;
87
+ const onSelectedItemIdChange = props.onSelectedItemIdChange;
88
+
89
+ const data = isChildrenMode ? [] : (props as SingleSelectDataProps<T>).data;
90
+ const itemToString = isChildrenMode
91
+ ? () => ""
92
+ : (props as SingleSelectDataProps<T>).itemToString;
93
+ const renderItem = isChildrenMode
94
+ ? undefined
95
+ : (props as SingleSelectDataProps<T>).renderItem;
96
+ const inputType = isChildrenMode
97
+ ? "icon"
98
+ : (props as SingleSelectDataProps<T>).inputType ?? "icon";
99
+ const renderSelection = isChildrenMode
100
+ ? undefined
101
+ : (props as SingleSelectDataProps<T>).renderSelection;
102
+ const groupBy = isChildrenMode
103
+ ? undefined
104
+ : (props as SingleSelectDataProps<T>).groupBy;
105
+ const renderGroupHeading = isChildrenMode
106
+ ? undefined
107
+ : (props as SingleSelectDataProps<T>).renderGroupHeading;
108
+ const children = isChildrenMode
109
+ ? (props as SingleSelectChildrenProps).children
110
+ : undefined;
111
+
112
+ const { containerRef, portalContainer } = useSeedsPortalContainer();
113
+ const items = React.useMemo(() => {
114
+ const dataItems = data.map((item) => ({
115
+ value: String(item.id),
116
+ label: itemToString(item),
117
+ }));
118
+ if (includePlaceholderItem) {
119
+ return [{ value: null, label: placeholder ?? "" }, ...dataItems];
120
+ }
121
+ return dataItems;
122
+ }, [data, itemToString, includePlaceholderItem, placeholder]);
123
+
124
+ const dataWithPlaceholder = includePlaceholderItem
125
+ ? [{ id: null, value: null, label: placeholder ?? "" }, ...data]
126
+ : data;
127
+
128
+ const isControlled = selectedItemId !== undefined;
129
+ const [internalValue, setInternalValue] = React.useState<string | null>(
130
+ defaultSelectedItemId != null ? String(defaultSelectedItemId) : null
131
+ );
132
+ const value = isControlled
133
+ ? selectedItemId != null
134
+ ? String(selectedItemId)
135
+ : null
136
+ : internalValue;
137
+
138
+ const selectedItem = React.useMemo(
139
+ () =>
140
+ value != null ? data.find((d) => String(d.id) === value) ?? null : null,
141
+ [data, value]
142
+ );
143
+
144
+ function handleValueChange(newValue: string | null) {
145
+ if (!isControlled) {
146
+ setInternalValue(newValue);
147
+ }
148
+ if (onSelectedItemIdChange) {
149
+ if (newValue == null) {
150
+ onSelectedItemIdChange(null);
151
+ } else {
152
+ const item = data.find((d) => String(d.id) === newValue);
153
+ onSelectedItemIdChange(item ? item.id : null);
154
+ }
155
+ }
156
+ }
157
+
158
+ return (
159
+ <Field>
160
+ <div ref={containerRef} style={{ position: "relative" }} />
161
+ <Select.Root
162
+ items={items}
163
+ value={value}
164
+ onValueChange={handleValueChange}
165
+ id={id}
166
+ >
167
+ <SelectTrigger>
168
+ <StyledValue placeholder={placeholder}>
169
+ {renderSelection && selectedItem
170
+ ? renderSelection(selectedItem)
171
+ : undefined}
172
+ </StyledValue>
173
+ <SelectIcon>
174
+ <StyledChevron data-chevron>
175
+ <ChevronIcon />
176
+ </StyledChevron>
177
+ </SelectIcon>
178
+ </SelectTrigger>
179
+ <Select.Portal container={portalContainer}>
180
+ <SelectPositioner sideOffset={8} alignItemWithTrigger={false}>
181
+ <SelectPopup>
182
+ <Select.ScrollUpArrow />
183
+ <Select.List>
184
+ {children ? (
185
+ children
186
+ ) : groupBy ? (
187
+ groupItems(data, groupBy).map((group, index, arr) => (
188
+ <React.Fragment key={group.value}>
189
+ <SelectGroup>
190
+ <SelectGroupLabel>
191
+ {renderGroupHeading
192
+ ? renderGroupHeading(group.value)
193
+ : group.value}
194
+ </SelectGroupLabel>
195
+ {group.items.map((item) => (
196
+ <SelectItem
197
+ key={item.id}
198
+ value={String(item.id)}
199
+ label={itemToString(item)}
200
+ inputType={inputType}
201
+ renderItem={
202
+ renderItem
203
+ ? () => renderItem(item)
204
+ : () => itemToString(item)
205
+ }
206
+ />
207
+ ))}
208
+ </SelectGroup>
209
+ {index < arr.length - 1 && <SelectSeparator />}
210
+ </React.Fragment>
211
+ ))
212
+ ) : (
213
+ // TODO: need to add a placeholder item when includePlaceholderItem is true
214
+ <>
215
+ {includePlaceholderItem && (
216
+ <SelectItem
217
+ key={"placeholder"}
218
+ value={null}
219
+ label={placeholder ?? ""}
220
+ />
221
+ )}
222
+ {data.map((item) => (
223
+ <SelectItem
224
+ key={item.id}
225
+ value={String(item.id)}
226
+ label={itemToString(item)}
227
+ inputType={inputType}
228
+ renderItem={
229
+ renderItem
230
+ ? () => renderItem(item)
231
+ : () => itemToString(item)
232
+ }
233
+ />
234
+ ))}
235
+ </>
236
+ )}
237
+ </Select.List>
238
+ <Select.ScrollDownArrow />
239
+ </SelectPopup>
240
+ </SelectPositioner>
241
+ </Select.Portal>
242
+ </Select.Root>
243
+ </Field>
244
+ );
245
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./SingleSelect";
2
+ export * from "./MultiSelect";
3
+ export * from "./SingleCombobox";
4
+ export * from "./MultiCombobox";
5
+ export * from "./SingleSearchableSelect";
6
+ export * from "./MultiSearchableSelect";
7
+ export * from "./Common/ComboboxStyles";
@@ -0,0 +1,117 @@
1
+ import React from "react";
2
+
3
+ export interface Book {
4
+ id: string;
5
+ title: string;
6
+ author: string;
7
+ }
8
+
9
+ export const books: Book[] = [
10
+ { id: "book-1", title: "To Kill a Mockingbird", author: "Harper Lee" },
11
+ { id: "book-2", title: "War and Peace", author: "Lev Tolstoy" },
12
+ { id: "book-3", title: "The Idiot", author: "Fyodor Dostoevsky" },
13
+ { id: "book-4", title: "A Picture of Dorian Gray", author: "Oscar Wilde" },
14
+ { id: "book-5", title: "1984", author: "George Orwell" },
15
+ { id: "book-6", title: "Pride and Prejudice", author: "Jane Austen" },
16
+ { id: "book-7", title: "Meditations", author: "Marcus Aurelius" },
17
+ {
18
+ id: "book-8",
19
+ title: "The Brothers Karamazov",
20
+ author: "Fyodor Dostoevsky",
21
+ },
22
+ { id: "book-9", title: "Anna Karenina", author: "Lev Tolstoy" },
23
+ { id: "book-10", title: "Crime and Punishment", author: "Fyodor Dostoevsky" },
24
+ ];
25
+
26
+ // Generate 1000 books for virtualization testing
27
+ const generateBooks = (count: number): Book[] => {
28
+ const authors = [
29
+ "Harper Lee",
30
+ "Lev Tolstoy",
31
+ "Fyodor Dostoevsky",
32
+ "Oscar Wilde",
33
+ "George Orwell",
34
+ "Jane Austen",
35
+ "Marcus Aurelius",
36
+ "Ernest Hemingway",
37
+ "Virginia Woolf",
38
+ "James Joyce",
39
+ "Charles Dickens",
40
+ "Mark Twain",
41
+ "J.R.R. Tolkien",
42
+ "C.S. Lewis",
43
+ "Agatha Christie",
44
+ "Arthur Conan Doyle",
45
+ "J.K. Rowling",
46
+ "Stephen King",
47
+ "Isaac Asimov",
48
+ "Ray Bradbury",
49
+ "Aldous Huxley",
50
+ "Kurt Vonnegut",
51
+ "J.D. Salinger",
52
+ "Maya Angelou",
53
+ "Toni Morrison",
54
+ "Gabriel García Márquez",
55
+ "Milan Kundera",
56
+ "Umberto Eco",
57
+ "Haruki Murakami",
58
+ "Chimamanda Ngozi Adichie",
59
+ ];
60
+
61
+ const titleWords = [
62
+ "Adventures",
63
+ "Chronicles",
64
+ "Memories",
65
+ "Journey",
66
+ "Quest",
67
+ "Tales",
68
+ "Stories",
69
+ "Legends",
70
+ "Myths",
71
+ "Dreams",
72
+ "Visions",
73
+ "Revelations",
74
+ "Secrets",
75
+ "Mysteries",
76
+ "Shadows",
77
+ "Light",
78
+ "Darkness",
79
+ "Echoes",
80
+ "Whispers",
81
+ "Voices",
82
+ "Songs",
83
+ "Poems",
84
+ ];
85
+
86
+ const books: Book[] = [];
87
+ for (let i = 0; i < count; i++) {
88
+ const author = authors[i % authors.length];
89
+ const titleWord1 =
90
+ titleWords[Math.floor(Math.random() * titleWords.length)];
91
+ const titleWord2 =
92
+ titleWords[Math.floor(Math.random() * titleWords.length)];
93
+ const number = Math.floor(i / authors.length) + 1;
94
+ const title = `${titleWord1} of ${titleWord2} ${number}`;
95
+
96
+ books.push({
97
+ id: `book-${i + 1}`,
98
+ author,
99
+ title,
100
+ });
101
+ }
102
+
103
+ return books;
104
+ };
105
+
106
+ export const largeBookSet = generateBooks(1000);
107
+
108
+ export const itemToString = (item: Book | null) => item?.title ?? "";
109
+
110
+ export const renderItem = (item: Book) => (
111
+ <>
112
+ <span style={{ fontWeight: "bold" }}>{item.title}</span>
113
+ <span style={{ fontSize: "12px", color: "#666", marginLeft: "8px" }}>
114
+ by {item.author}
115
+ </span>
116
+ </>
117
+ );
package/tsup.config.ts CHANGED
@@ -4,6 +4,7 @@ export default defineConfig((options) => ({
4
4
  entry: {
5
5
  index: "src/index.ts",
6
6
  "v2/index": "src/v2/index.ts",
7
+ "v3/index": "src/v3/index.ts",
7
8
  },
8
9
  format: ["cjs", "esm"],
9
10
  clean: true,