@sproutsocial/seeds-react-menu 1.16.16 → 1.18.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 (84) hide show
  1. package/.turbo/turbo-build.log +14 -22
  2. package/CHANGELOG.md +51 -0
  3. package/dist/esm/index.js +542 -229
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/v3/index.js +870 -1081
  6. package/dist/esm/v3/index.js.map +1 -1
  7. package/dist/v3/index.d.mts +82 -38
  8. package/dist/v3/index.d.ts +82 -38
  9. package/dist/v3/index.js +887 -1098
  10. package/dist/v3/index.js.map +1 -1
  11. package/dist/v3/menu.css +947 -0
  12. package/package.json +20 -21
  13. package/src/ActionMenu/ActionMenu.stories.tsx +1 -1
  14. package/src/Autocomplete/Autocomplete.stories.tsx +1 -1
  15. package/src/MenuContent/MenuContent.stories.tsx +1 -1
  16. package/src/MenuFooter/MenuFooter.stories.tsx +1 -1
  17. package/src/MenuGroup/MenuGroup.stories.tsx +1 -1
  18. package/src/MenuHeader/MenuHeader.stories.tsx +1 -1
  19. package/src/MenuItem/MenuItem.stories.tsx +1 -1
  20. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +1 -1
  21. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +1 -1
  22. package/src/NestedMenu/NestedMenu.stories.tsx +1 -1
  23. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +1 -1
  24. package/src/SubmenuItem/SubmenuItem.stories.tsx +1 -1
  25. package/src/v3/ActionMenu.stories.tsx +1 -1
  26. package/src/v3/ActionMenuStyles.tsx +169 -149
  27. package/src/v3/Common/ComboboxStyles.tsx +396 -511
  28. package/src/v3/Common/SelectIcons.tsx +17 -8
  29. package/src/v3/Common/SelectItem.tsx +25 -38
  30. package/src/v3/Common/SelectStyles.tsx +146 -144
  31. package/src/v3/Common/cn.ts +38 -0
  32. package/src/v3/MenuButton.stories.tsx +1 -1
  33. package/src/v3/MenuButtonStyles.tsx +55 -34
  34. package/src/v3/ModalStories.stories.tsx +1 -1
  35. package/src/v3/MultiCombobox.stories.tsx +1 -1
  36. package/src/v3/MultiCombobox.tsx +7 -0
  37. package/src/v3/MultiSearchableSelect.stories.tsx +1 -1
  38. package/src/v3/MultiSearchableSelect.tsx +15 -8
  39. package/src/v3/MultiSelect.stories.tsx +1 -1
  40. package/src/v3/MultiSelect.tsx +20 -17
  41. package/src/v3/SingleCombobox.stories.tsx +1 -1
  42. package/src/v3/SingleCombobox.tsx +7 -0
  43. package/src/v3/SingleSearchableSelect.stories.tsx +1 -1
  44. package/src/v3/SingleSearchableSelect.tsx +7 -0
  45. package/src/v3/SingleSelect.stories.tsx +1 -1
  46. package/src/v3/SingleSelect.tsx +22 -14
  47. package/src/v3/__tests__/ariaLabels.test.tsx +133 -0
  48. package/src/v3/menu.css +947 -0
  49. package/tsup.config.ts +0 -1
  50. package/dist/esm/chunk-ROQATIB7.js +0 -357
  51. package/dist/esm/chunk-ROQATIB7.js.map +0 -1
  52. package/dist/esm/v2/index.js +0 -2089
  53. package/dist/esm/v2/index.js.map +0 -1
  54. package/dist/v2/index.d.mts +0 -108
  55. package/dist/v2/index.d.ts +0 -108
  56. package/dist/v2/index.js +0 -2280
  57. package/dist/v2/index.js.map +0 -1
  58. package/src/v2/Autocomplete/Autocomplete.stories.tsx +0 -645
  59. package/src/v2/Autocomplete/MultiAutocomplete.tsx +0 -487
  60. package/src/v2/Autocomplete/SingleAutocomplete.tsx +0 -366
  61. package/src/v2/Autocomplete/index.ts +0 -2
  62. package/src/v2/Common-V2/ComboboxContent.tsx +0 -457
  63. package/src/v2/Common-V2/MenuGroup.tsx +0 -60
  64. package/src/v2/Common-V2/MenuGroupTypes.ts +0 -30
  65. package/src/v2/Common-V2/MenuHeading.tsx +0 -83
  66. package/src/v2/Common-V2/MenuItem.tsx +0 -138
  67. package/src/v2/Common-V2/Popout.tsx +0 -124
  68. package/src/v2/Common-V2/PopoutTypes.tsx +0 -109
  69. package/src/v2/Common-V2/SelectToggleButton.tsx +0 -99
  70. package/src/v2/Common-V2/index.ts +0 -11
  71. package/src/v2/Common-V2/props.ts +0 -84
  72. package/src/v2/Common-V2/styles.tsx +0 -41
  73. package/src/v2/Common-V2/types.ts +0 -16
  74. package/src/v2/Common-V2/useHighlightedIndex.ts +0 -62
  75. package/src/v2/Common-V2/utils.ts +0 -238
  76. package/src/v2/SearchableSelect/AutocompleteContent.tsx +0 -325
  77. package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +0 -527
  78. package/src/v2/SearchableSelect/SearchableSelect.tsx +0 -395
  79. package/src/v2/SearchableSelect/index.ts +0 -12
  80. package/src/v2/Select/MultiSelect.tsx +0 -417
  81. package/src/v2/Select/Select.stories.tsx +0 -593
  82. package/src/v2/Select/SingleSelect.tsx +0 -285
  83. package/src/v2/Select/index.ts +0 -2
  84. package/src/v2/index.ts +0 -2
@@ -1,395 +0,0 @@
1
- import { useState, useMemo, useEffect } from "react";
2
- import { useCombobox } from "downshift";
3
- import type { ReactNode } from "react";
4
- import { Label } from "@sproutsocial/seeds-react-label";
5
- import { Input } from "@sproutsocial/seeds-react-input";
6
- // This will eventually be imported from seeds-react-popout
7
- import { Popout, SelectToggleButton } from "../Common-V2";
8
- import {
9
- VirtualizedAutocompleteContent,
10
- NonVirtualizedAutocompleteContent,
11
- } from "./AutocompleteContent";
12
- import {
13
- isGroupHeading,
14
- isToggleItem,
15
- menuAnimationConfig,
16
- getFilteredItems,
17
- groupItemsWithHeadings,
18
- useHighlightedIndex,
19
- createItemToStringForDownshift,
20
- } from "../Common-V2";
21
- import {
22
- type DataWithId,
23
- type GroupedItem,
24
- type ToggleItem,
25
- type BaseSingleAutocompleteProps,
26
- } from "../Common-V2";
27
-
28
- function defaultReducer(state, actionAndChanges) {
29
- const { changes, type } = actionAndChanges;
30
-
31
- switch (type) {
32
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
33
- case useCombobox.stateChangeTypes.ItemClick:
34
- return {
35
- ...changes,
36
- inputValue: "",
37
- isOpen: false, // close the menu after selection for single select
38
- highlightedIndex: 0, // reset highlight
39
- };
40
- default:
41
- return changes;
42
- }
43
- }
44
-
45
- export interface SearchableSelectProps<T extends DataWithId>
46
- extends BaseSingleAutocompleteProps<T> {
47
- name: string;
48
- renderSelection?: (item: T | null) => ReactNode;
49
- triggerButtonProps?: any;
50
- }
51
-
52
- export function SearchableSelect<T extends DataWithId>({
53
- id,
54
- data,
55
- selectedItemId: controlledSelectedItemId,
56
- inputValue: controlledInputValue,
57
- isOpen: controlledIsOpen,
58
- itemToKey = (item: T) => item?.id,
59
- itemToString,
60
- itemToSearchString,
61
- renderSelection,
62
- renderItem,
63
- groupBy,
64
- renderGroupHeading,
65
- isVirtualized = false,
66
- EmptyState,
67
- name,
68
- placeholder,
69
- placeholderItem,
70
- onSelectedItemIdChange,
71
- onInputValueChange,
72
- onIsOpenChange,
73
- stateReducer = defaultReducer,
74
- triggerButtonProps,
75
- onPointerDownOutside,
76
- onInteractOutside,
77
- popoutPlacement,
78
- }: SearchableSelectProps<T>) {
79
- const [uncontrolledInputValue, setUncontrolledInputValue] =
80
- useState<string>("");
81
- const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
82
- T["id"] | null
83
- >(null);
84
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
85
-
86
- // Use controlled selectedItemId if provided, otherwise use internal state
87
- const isSelectedItemIdControlled = controlledSelectedItemId !== undefined;
88
- const selectedItemId = isSelectedItemIdControlled
89
- ? controlledSelectedItemId
90
- : uncontrolledSelectedItemId;
91
-
92
- // Convert selectedItemId to selectedItem for internal use
93
- const selectedItem = useMemo<T | null>(() => {
94
- if (selectedItemId === null || selectedItemId === undefined) {
95
- return null;
96
- }
97
- return data.find((item) => itemToKey(item) === selectedItemId) ?? null;
98
- }, [selectedItemId, data, itemToKey]);
99
-
100
- // Use controlled inputValue if provided, otherwise use internal state
101
- const isInputValueControlled = controlledInputValue !== undefined;
102
- const inputValue = isInputValueControlled
103
- ? controlledInputValue
104
- : uncontrolledInputValue;
105
-
106
- // Use controlled isOpen if provided, otherwise use internal state
107
- const isIsOpenControlled = controlledIsOpen !== undefined;
108
- const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
109
-
110
- const updateSelectedItem = (newSelectedItem: T | null) => {
111
- const newSelectedItemId = newSelectedItem
112
- ? itemToKey(newSelectedItem)
113
- : null;
114
- if (!isSelectedItemIdControlled) {
115
- setUncontrolledSelectedItemId(newSelectedItemId);
116
- }
117
- onSelectedItemIdChange?.(newSelectedItemId);
118
- };
119
-
120
- const updateInputValue = (newInputValue: string) => {
121
- if (!isInputValueControlled) {
122
- setUncontrolledInputValue(newInputValue);
123
- }
124
- onInputValueChange?.(newInputValue);
125
- };
126
-
127
- const updateIsOpen = (newIsOpen: boolean) => {
128
- if (!isIsOpenControlled) {
129
- setUncontrolledIsOpen(newIsOpen);
130
- }
131
- onIsOpenChange?.(newIsOpen);
132
- };
133
-
134
- // Use itemToSearchString if provided, otherwise fallback to itemToString
135
- const searchStringFn =
136
- itemToSearchString ?? ((item: T) => itemToString(item));
137
-
138
- const filteredItems = useMemo<T[]>(
139
- () =>
140
- getFilteredItems(data, inputValue, searchStringFn, itemToString, {
141
- selectedItem,
142
- }),
143
- [data, selectedItem, inputValue, searchStringFn, itemToString]
144
- );
145
-
146
- const items = useMemo<GroupedItem<T>[]>(() => {
147
- let groupedItems: GroupedItem<T>[];
148
-
149
- if (groupBy) {
150
- // Group first, then add placeholderItem at the beginning
151
- groupedItems = groupItemsWithHeadings(filteredItems, groupBy, data);
152
- } else {
153
- groupedItems = filteredItems;
154
- }
155
-
156
- // Add placeholderItem at the beginning if provided
157
- if (placeholderItem) {
158
- return [placeholderItem, ...groupedItems];
159
- }
160
-
161
- return groupedItems;
162
- }, [filteredItems, groupBy, placeholderItem, data]);
163
-
164
- // Filter out headings for downshift
165
- const itemsForCombobox = useMemo<(T | ToggleItem)[]>(() => {
166
- return items.filter(
167
- (item): item is T | ToggleItem => !isGroupHeading(item)
168
- );
169
- }, [items]);
170
-
171
- // Wrapper function to handle ToggleItem separately
172
- const itemToStringForDownshift = useMemo(
173
- () => createItemToStringForDownshift(itemToString),
174
- [itemToString]
175
- );
176
-
177
- const comboboxProps = useCombobox<T | ToggleItem>({
178
- inputId: id,
179
- items: itemsForCombobox,
180
- itemToString: itemToStringForDownshift,
181
- defaultHighlightedIndex: 0,
182
- selectedItem: selectedItem,
183
- inputValue,
184
- isOpen,
185
- stateReducer,
186
- onStateChange({
187
- inputValue: newInputValue,
188
- type,
189
- selectedItem: newSelectedItem,
190
- isOpen: newIsOpen,
191
- }) {
192
- // Handle isOpen changes for any state change type
193
- if (
194
- newIsOpen !== undefined &&
195
- type !== useCombobox.stateChangeTypes.InputBlur
196
- ) {
197
- updateIsOpen(newIsOpen);
198
- }
199
-
200
- switch (type) {
201
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
202
- case useCombobox.stateChangeTypes.ItemClick:
203
- if (newSelectedItem) {
204
- updateInputValue("");
205
- // If placeholderItem is selected, set selectedItem to null
206
- // Note: placeholderItem won't be in itemsForCombobox, so this check is for safety
207
- if (
208
- placeholderItem &&
209
- isToggleItem(newSelectedItem) &&
210
- newSelectedItem.id === placeholderItem.id
211
- ) {
212
- updateSelectedItem(null);
213
- } else {
214
- updateSelectedItem(newSelectedItem as T);
215
- }
216
- }
217
- break;
218
-
219
- case useCombobox.stateChangeTypes.InputChange:
220
- updateInputValue(newInputValue ?? "");
221
- break;
222
-
223
- default:
224
- break;
225
- }
226
- },
227
- });
228
- const {
229
- isOpen: comboboxIsOpen,
230
- getLabelProps,
231
- getMenuProps,
232
- getInputProps,
233
- highlightedIndex: comboboxHighlightedIndex,
234
- getItemProps,
235
- } = comboboxProps;
236
-
237
- // Map the highlighted index from combobox (which only knows about items) to the full items array (which includes headings)
238
- const highlightedIndex = useHighlightedIndex(
239
- items,
240
- itemsForCombobox,
241
- comboboxHighlightedIndex,
242
- !!groupBy,
243
- false // Single select components hardcode selectHeadings to false
244
- );
245
-
246
- const buttonContent = renderSelection
247
- ? renderSelection(selectedItem)
248
- : selectedItem
249
- ? itemToString(selectedItem)
250
- : placeholder;
251
-
252
- // Handle button click to toggle popout
253
- const handleButtonClick = () => {
254
- updateIsOpen(!comboboxIsOpen);
255
- };
256
-
257
- return (
258
- <Popout
259
- open={comboboxIsOpen}
260
- onOpenChange={updateIsOpen}
261
- side={popoutPlacement}
262
- animationConfig={menuAnimationConfig}
263
- onOpenAutoFocus={(e) => {
264
- // Prevent default auto-focus behavior - we'll focus the input manually
265
- e.preventDefault();
266
- // Focus the input when popout opens
267
- const inputElement = document.querySelector(
268
- `input[name="${name}"]`
269
- ) as HTMLInputElement;
270
- if (inputElement) {
271
- setTimeout(() => {
272
- inputElement.focus();
273
- }, 0);
274
- }
275
- }}
276
- onCloseAutoFocus={(e) => {
277
- // Prevent default auto-focus behavior
278
- e.preventDefault();
279
- }}
280
- onPointerDownOutside={onPointerDownOutside}
281
- onInteractOutside={onInteractOutside}
282
- content={
283
- <div>
284
- {/* Input inside the Popout */}
285
- <div style={{ marginBottom: "8px" }}>
286
- {(() => {
287
- const inputProps = getInputProps();
288
- const { ref, id, ...restProps } = inputProps as any;
289
- return (
290
- <Input
291
- id={id}
292
- name={name}
293
- inputProps={{
294
- autoComplete: "off",
295
- ...restProps,
296
- }}
297
- innerRef={ref}
298
- placeholder={placeholder}
299
- />
300
- );
301
- })()}
302
- </div>
303
- {/* Menu inside the Popout */}
304
- {isVirtualized ? (
305
- <VirtualizedAutocompleteContent
306
- items={items}
307
- getMenuProps={getMenuProps}
308
- getItemProps={(props: { index: number }) => {
309
- // Only apply getItemProps to actual items, not headings or ToggleItems
310
- const item = items[props.index];
311
- if (isGroupHeading(item)) {
312
- return {};
313
- }
314
- if (isToggleItem(item)) {
315
- // Handle ToggleItem (placeholderItem) clicks manually
316
- return {
317
- onClick: () => {
318
- if (placeholderItem && item.id === placeholderItem.id) {
319
- updateSelectedItem(null);
320
- }
321
- },
322
- };
323
- }
324
- const dataItem = item as T;
325
- // Find the index in itemsForCombobox
326
- const itemIndex = itemsForCombobox.indexOf(dataItem);
327
- if (itemIndex === -1) {
328
- return {};
329
- }
330
- return getItemProps({ item: dataItem, index: itemIndex });
331
- }}
332
- selectedItems={selectedItem ? [selectedItem] : []}
333
- highlightedIndex={highlightedIndex ?? -1}
334
- renderItem={renderItem}
335
- renderGroupHeading={renderGroupHeading}
336
- EmptyState={EmptyState}
337
- itemToKey={itemToKey}
338
- itemToString={itemToString}
339
- inputType="radio"
340
- />
341
- ) : (
342
- <NonVirtualizedAutocompleteContent
343
- items={items}
344
- getMenuProps={getMenuProps}
345
- getItemProps={(props: { index: number }) => {
346
- // Only apply getItemProps to actual items, not headings or ToggleItems
347
- const item = items[props.index];
348
- if (isGroupHeading(item)) {
349
- return {};
350
- }
351
- if (isToggleItem(item)) {
352
- // Handle ToggleItem (placeholderItem) clicks manually
353
- return {
354
- onClick: () => {
355
- if (placeholderItem && item.id === placeholderItem.id) {
356
- updateSelectedItem(null);
357
- }
358
- },
359
- };
360
- }
361
- const dataItem = item as T;
362
- // Find the index in itemsForCombobox
363
- const itemIndex = itemsForCombobox.indexOf(dataItem);
364
- if (itemIndex === -1) {
365
- return {};
366
- }
367
- return getItemProps({ item: dataItem, index: itemIndex });
368
- }}
369
- selectedItems={selectedItem ? [selectedItem] : []}
370
- highlightedIndex={highlightedIndex ?? -1}
371
- renderItem={renderItem}
372
- renderGroupHeading={renderGroupHeading}
373
- EmptyState={EmptyState}
374
- itemToKey={itemToKey}
375
- itemToString={itemToString}
376
- inputType="radio"
377
- />
378
- )}
379
- </div>
380
- }
381
- >
382
- <div>
383
- <SelectToggleButton
384
- onClick={handleButtonClick}
385
- {...triggerButtonProps}
386
- name={name}
387
- isOpen={comboboxIsOpen}
388
- type="button"
389
- >
390
- {buttonContent}
391
- </SelectToggleButton>
392
- </div>
393
- </Popout>
394
- );
395
- }
@@ -1,12 +0,0 @@
1
- // These are currently experimental components
2
- // These use the combobox pattern, but have a "dialog" popup.
3
- // The dialog content includes an inline autocomplete which is always open.
4
- // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
5
- // Still need to determine wether we should have
6
- // - The Root component be the "Combobox" and the popup be a dialog with another combobox inside
7
- // - Or the Root component simply be a "Dialog" with a combobox inside
8
- // currently it is the latter.
9
- export { SearchableSelect } from "./SearchableSelect";
10
- export type { SearchableSelectProps } from "./SearchableSelect";
11
- export { SearchableMultiSelect } from "./SearchableMultiSelect";
12
- export type { SearchableMultiSelectProps } from "./SearchableMultiSelect";