@sproutsocial/seeds-react-menu 1.7.2 → 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 +39 -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,272 @@
1
+ import { useState, useMemo } from "react";
2
+ import { useSelect } from "downshift";
3
+ import type { ReactNode } from "react";
4
+ import { Label } from "@sproutsocial/seeds-react-label";
5
+ // This will eventually be imported from seeds-react-popout
6
+ import { Popout } from "../Common-V2";
7
+ import {
8
+ isGroupHeading,
9
+ isToggleItem,
10
+ menuAnimationConfig,
11
+ groupItemsWithHeadings,
12
+ useHighlightedIndex,
13
+ createItemToStringForDownshift,
14
+ ComboboxContent,
15
+ SelectToggleButton,
16
+ } from "../Common-V2";
17
+ import {
18
+ type DataWithId,
19
+ type GroupedItem,
20
+ type ToggleItem,
21
+ type BaseSingleSelectProps,
22
+ } from "../Common-V2";
23
+
24
+ function defaultReducer(state, actionAndChanges) {
25
+ const { changes, type } = actionAndChanges;
26
+
27
+ switch (type) {
28
+ case useSelect.stateChangeTypes.ItemClick:
29
+ return {
30
+ ...changes,
31
+ isOpen: false, // close the menu after selection for single select
32
+ highlightedIndex: 0, // reset highlight
33
+ };
34
+ default:
35
+ return changes;
36
+ }
37
+ }
38
+
39
+ export interface SingleSelectProps<T extends DataWithId>
40
+ extends BaseSingleSelectProps<T> {
41
+ itemToSelection?: (item: T) => ReactNode;
42
+ renderSelection?: (item: T | null) => ReactNode;
43
+ triggerButtonProps?: any;
44
+ }
45
+
46
+ export function SingleSelect<T extends DataWithId>({
47
+ id,
48
+ data,
49
+ selectedItemId: controlledSelectedItemId,
50
+ isOpen: controlledIsOpen,
51
+ itemToKey = (item: T) => item?.id,
52
+ itemToString,
53
+ itemToSelection,
54
+ renderSelection,
55
+ renderItem,
56
+ groupBy,
57
+ renderGroupHeading,
58
+ isVirtualized = false,
59
+ placeholder,
60
+ onSelectedItemIdChange,
61
+ onIsOpenChange,
62
+ stateReducer = defaultReducer,
63
+ triggerButtonProps,
64
+ menuProps,
65
+ inputType = "icon",
66
+ removePlaceholderItem = false,
67
+ }: SingleSelectProps<T>) {
68
+ const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
69
+ T["id"] | null
70
+ >(null);
71
+ const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
72
+
73
+ // Use controlled selectedItemId if provided, otherwise use internal state
74
+ const isSelectedItemIdControlled = controlledSelectedItemId !== undefined;
75
+ const selectedItemId = isSelectedItemIdControlled
76
+ ? controlledSelectedItemId
77
+ : uncontrolledSelectedItemId;
78
+
79
+ // Convert selectedItemId to selectedItem for internal use
80
+ const selectedItem = useMemo<T | null>(() => {
81
+ if (selectedItemId === null || selectedItemId === undefined) {
82
+ return null;
83
+ }
84
+ return data.find((item) => itemToKey(item) === selectedItemId) ?? null;
85
+ }, [selectedItemId, data, itemToKey]);
86
+
87
+ // Use controlled isOpen if provided, otherwise use internal state
88
+ const isIsOpenControlled = controlledIsOpen !== undefined;
89
+ const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
90
+
91
+ const updateSelectedItem = (newSelectedItem: T | null) => {
92
+ const newSelectedItemId = newSelectedItem
93
+ ? itemToKey(newSelectedItem)
94
+ : null;
95
+ if (!isSelectedItemIdControlled) {
96
+ setUncontrolledSelectedItemId(newSelectedItemId);
97
+ }
98
+ onSelectedItemIdChange?.(newSelectedItemId);
99
+ };
100
+
101
+ const updateIsOpen = (newIsOpen: boolean) => {
102
+ if (!isIsOpenControlled) {
103
+ setUncontrolledIsOpen(newIsOpen);
104
+ }
105
+ onIsOpenChange?.(newIsOpen);
106
+ };
107
+
108
+ // TODO: This could be made generic as a hook and used in other components
109
+ // For Select, we show all items (no filtering)
110
+ const items = useMemo<GroupedItem<T>[]>(() => {
111
+ let groupedItems: GroupedItem<T>[];
112
+
113
+ const placeholderItem: ToggleItem = {
114
+ id: "clear-selection",
115
+ label: placeholder,
116
+ };
117
+
118
+ if (groupBy) {
119
+ // Group first, then add placeholderItem at the beginning
120
+ groupedItems = groupItemsWithHeadings(data, groupBy, data, true);
121
+ } else {
122
+ groupedItems = data;
123
+ }
124
+ if (!removePlaceholderItem) {
125
+ groupedItems = [placeholderItem, ...groupedItems];
126
+ }
127
+
128
+ return groupedItems;
129
+ }, [data, groupBy, placeholder]);
130
+
131
+ // Filter out headings for downshift
132
+ const itemsForSelect = useMemo<(T | ToggleItem)[]>(() => {
133
+ // This is unnecessary for single select because we don't have group headings
134
+ return items.filter((item): item is T => !isGroupHeading(item));
135
+ }, [items]);
136
+
137
+ // Wrapper function to handle ToggleItem separately
138
+ const itemToStringForDownshift = useMemo(
139
+ () => createItemToStringForDownshift(itemToString),
140
+ [itemToString]
141
+ );
142
+
143
+ const selectProps = useSelect<T | ToggleItem>({
144
+ toggleButtonId: id,
145
+ items: itemsForSelect,
146
+ itemToString: itemToStringForDownshift,
147
+ // defaultHighlightedIndex: 0,
148
+ selectedItem: selectedItem,
149
+ isOpen,
150
+ stateReducer,
151
+ onStateChange({ type, selectedItem: newSelectedItem, isOpen: newIsOpen }) {
152
+ // Handle isOpen changes for any state change type
153
+ if (newIsOpen !== undefined) {
154
+ updateIsOpen(newIsOpen);
155
+ }
156
+
157
+ // Handle item selection for both click and keyboard (Enter/Space) interactions
158
+ // When Enter/Space is pressed on a highlighted item, useSelect sets selectedItem
159
+ // We detect selection when selectedItem changes and it's a user interaction
160
+ const isSelectionEvent =
161
+ type === useSelect.stateChangeTypes.ItemClick ||
162
+ type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
163
+
164
+ if (isSelectionEvent && newSelectedItem && newSelectedItem !== null) {
165
+ // If placeholderItem is selected, set selectedItem to null
166
+ // Note: placeholderItem won't be in itemsForSelect, so this check is for safety
167
+ if (newSelectedItem.id === "clear-selection") {
168
+ updateSelectedItem(null);
169
+ } else {
170
+ updateSelectedItem(newSelectedItem as T);
171
+ }
172
+ }
173
+ },
174
+ });
175
+
176
+ const {
177
+ isOpen: selectIsOpen,
178
+ getToggleButtonProps,
179
+ getLabelProps,
180
+ getMenuProps,
181
+ highlightedIndex: selectHighlightedIndex,
182
+ getItemProps,
183
+ } = selectProps;
184
+
185
+ // Map the highlighted index from select (which only knows about items) to the full items array (which includes headings)
186
+ const highlightedIndex = useHighlightedIndex(
187
+ items,
188
+ itemsForSelect,
189
+ selectHighlightedIndex,
190
+ !!groupBy,
191
+ false // Single select components hardcode selectHeadings to false
192
+ );
193
+
194
+ const buttonContent = renderSelection
195
+ ? renderSelection(selectedItem)
196
+ : selectedItem
197
+ ? itemToSelection
198
+ ? itemToSelection(selectedItem)
199
+ : itemToString(selectedItem)
200
+ : placeholder;
201
+
202
+ const toggleButtonProps = getToggleButtonProps();
203
+ // Downshift adds aria-labelledby by default, which can conflict with our Label usage
204
+ toggleButtonProps["aria-labelledby"] = undefined;
205
+ if (!selectIsOpen) {
206
+ // Remove aria-controls when the menu is closed to avoid referencing a non-existent element
207
+ // @ts-expect-error downshift types require aria-controls even though it is not required
208
+ toggleButtonProps["aria-controls"] = undefined;
209
+ }
210
+
211
+ return (
212
+ <Popout
213
+ open={selectIsOpen}
214
+ onOpenChange={updateIsOpen}
215
+ animationConfig={menuAnimationConfig}
216
+ onOpenAutoFocus={(e) => {
217
+ // Prevent default auto-focus behavior
218
+ e.preventDefault();
219
+ }}
220
+ onCloseAutoFocus={(e) => {
221
+ // Prevent default auto-focus behavior
222
+ e.preventDefault();
223
+ }}
224
+ content={
225
+ <ComboboxContent
226
+ items={items}
227
+ getMenuProps={getMenuProps}
228
+ getItemProps={(props: { index: number }) => {
229
+ // Only apply getItemProps to actual items, not headings or ToggleItems
230
+ const item = items[props.index];
231
+ if (isToggleItem(item)) {
232
+ // Handle ToggleItem (placeholderItem) clicks manually
233
+ return {
234
+ onClick: () => {
235
+ if (item.id === "clear-selection") {
236
+ updateSelectedItem(null);
237
+ }
238
+ },
239
+ };
240
+ }
241
+ const dataItem = item as T;
242
+ // Find the index in itemsForSelect
243
+ const itemIndex = itemsForSelect.indexOf(dataItem);
244
+ if (itemIndex === -1) {
245
+ return {};
246
+ }
247
+ return getItemProps({ item: dataItem, index: itemIndex });
248
+ }}
249
+ selectedItems={selectedItem ? [selectedItem] : []}
250
+ highlightedIndex={highlightedIndex ?? -1}
251
+ renderItem={renderItem}
252
+ renderGroupHeading={renderGroupHeading}
253
+ itemToKey={itemToKey}
254
+ itemToString={itemToString}
255
+ inputType={inputType}
256
+ isVirtualized={isVirtualized}
257
+ groupBy={groupBy}
258
+ selectHeadings={false}
259
+ {...menuProps}
260
+ />
261
+ }
262
+ >
263
+ <SelectToggleButton
264
+ {...toggleButtonProps}
265
+ {...triggerButtonProps}
266
+ isOpen={selectIsOpen}
267
+ >
268
+ {buttonContent}
269
+ </SelectToggleButton>
270
+ </Popout>
271
+ );
272
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SingleSelect";
2
+ export * from "./MultiSelect";
@@ -0,0 +1,2 @@
1
+ export * from "./Select";
2
+ export * from "./Autocomplete";
package/tsup.config.ts CHANGED
@@ -1,11 +1,20 @@
1
1
  import { defineConfig } from "tsup";
2
2
 
3
- export default defineConfig({
4
- entry: ["src/index.ts"],
3
+ export default defineConfig((options) => ({
4
+ entry: {
5
+ index: "src/index.ts",
6
+ "v2/index": "src/v2/index.ts",
7
+ },
5
8
  format: ["cjs", "esm"],
6
9
  clean: true,
7
10
  legacyOutput: true,
8
- dts: false,
11
+ dts: options.dts,
9
12
  sourcemap: true,
10
- external: ["react"],
11
- });
13
+ external: [
14
+ "react",
15
+ "styled-components",
16
+ "styled-system",
17
+ /^@sproutsocial\//, // All internal Seeds packages (monorepo optimization)
18
+ ],
19
+ metafile: options.metafile,
20
+ }));