@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.
- package/.turbo/turbo-build.log +21 -15
- package/CHANGELOG.md +22 -0
- package/dist/esm/index.js +45 -24
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v2/index.js +1 -1
- package/dist/esm/v2/index.js.map +1 -1
- package/dist/esm/v3/index.js +800 -0
- package/dist/esm/v3/index.js.map +1 -0
- package/dist/index.d.mts +22 -22
- package/dist/index.d.ts +22 -22
- package/dist/index.js +35 -14
- package/dist/index.js.map +1 -1
- package/dist/v2/index.js +1 -1
- package/dist/v2/index.js.map +1 -1
- package/dist/v3/index.d.mts +295 -0
- package/dist/v3/index.d.ts +295 -0
- package/dist/v3/index.js +868 -0
- package/dist/v3/index.js.map +1 -0
- package/package.json +7 -1
- package/src/MenuGroup/MenuGroup.tsx +16 -3
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +19 -0
- package/src/MenuItem/MenuItem.tsx +21 -10
- package/src/MenuItem/__tests__/MenuItem.test.tsx +19 -0
- package/src/v2/Common-V2/ComboboxContent.tsx +1 -1
- package/src/v3/Common/ComboboxItem.tsx +82 -0
- package/src/v3/Common/ComboboxStyles.tsx +475 -0
- package/src/v3/Common/SelectGroup.tsx +28 -0
- package/src/v3/Common/SelectIcons.tsx +26 -0
- package/src/v3/Common/SelectItem.tsx +108 -0
- package/src/v3/Common/SelectStyles.tsx +126 -0
- package/src/v3/Common/VirtualizedComboboxList.tsx +200 -0
- package/src/v3/Common/groupItems.ts +12 -0
- package/src/v3/Common/types.ts +3 -0
- package/src/v3/ModalStories.stories.tsx +181 -0
- package/src/v3/MultiCombobox.stories.tsx +322 -0
- package/src/v3/MultiCombobox.tsx +562 -0
- package/src/v3/MultiSearchableSelect.stories.tsx +389 -0
- package/src/v3/MultiSearchableSelect.tsx +545 -0
- package/src/v3/MultiSelect.stories.tsx +300 -0
- package/src/v3/MultiSelect.tsx +498 -0
- package/src/v3/SeedsPortal.tsx +35 -0
- package/src/v3/SingleCombobox.stories.tsx +169 -0
- package/src/v3/SingleCombobox.tsx +304 -0
- package/src/v3/SingleSearchableSelect.stories.tsx +239 -0
- package/src/v3/SingleSearchableSelect.tsx +319 -0
- package/src/v3/SingleSelect.stories.tsx +227 -0
- package/src/v3/SingleSelect.tsx +245 -0
- package/src/v3/index.ts +7 -0
- package/src/v3/storyData.tsx +117 -0
- 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
|
+
}
|
package/src/v3/index.ts
ADDED
|
@@ -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
|
+
);
|