@sproutsocial/seeds-react-menu 1.17.0 → 1.18.1
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 +14 -22
- package/CHANGELOG.md +37 -0
- package/dist/esm/index.js +542 -229
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v3/index.js +793 -1038
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +64 -38
- package/dist/v3/index.d.ts +64 -38
- package/dist/v3/index.js +813 -1058
- package/dist/v3/index.js.map +1 -1
- package/dist/v3/menu.css +947 -0
- package/package.json +20 -21
- package/src/v3/ActionMenuStyles.tsx +169 -149
- package/src/v3/Common/ComboboxStyles.tsx +396 -511
- package/src/v3/Common/SelectIcons.tsx +17 -8
- package/src/v3/Common/SelectItem.tsx +25 -38
- package/src/v3/Common/SelectStyles.tsx +146 -144
- package/src/v3/Common/cn.ts +38 -0
- package/src/v3/MenuButtonStyles.tsx +55 -34
- package/src/v3/MultiSearchableSelect.tsx +8 -8
- package/src/v3/MultiSelect.tsx +11 -16
- package/src/v3/SingleSelect.tsx +13 -13
- package/src/v3/menu.css +947 -0
- package/tsup.config.ts +0 -1
- package/dist/esm/chunk-ROQATIB7.js +0 -357
- package/dist/esm/chunk-ROQATIB7.js.map +0 -1
- package/dist/esm/v2/index.js +0 -2089
- package/dist/esm/v2/index.js.map +0 -1
- package/dist/v2/index.d.mts +0 -108
- package/dist/v2/index.d.ts +0 -108
- package/dist/v2/index.js +0 -2280
- package/dist/v2/index.js.map +0 -1
- package/src/v2/Autocomplete/Autocomplete.stories.tsx +0 -645
- package/src/v2/Autocomplete/MultiAutocomplete.tsx +0 -487
- package/src/v2/Autocomplete/SingleAutocomplete.tsx +0 -366
- package/src/v2/Autocomplete/index.ts +0 -2
- package/src/v2/Common-V2/ComboboxContent.tsx +0 -457
- package/src/v2/Common-V2/MenuGroup.tsx +0 -60
- package/src/v2/Common-V2/MenuGroupTypes.ts +0 -30
- package/src/v2/Common-V2/MenuHeading.tsx +0 -83
- package/src/v2/Common-V2/MenuItem.tsx +0 -138
- package/src/v2/Common-V2/Popout.tsx +0 -124
- package/src/v2/Common-V2/PopoutTypes.tsx +0 -109
- package/src/v2/Common-V2/SelectToggleButton.tsx +0 -99
- package/src/v2/Common-V2/index.ts +0 -11
- package/src/v2/Common-V2/props.ts +0 -84
- package/src/v2/Common-V2/styles.tsx +0 -41
- package/src/v2/Common-V2/types.ts +0 -16
- package/src/v2/Common-V2/useHighlightedIndex.ts +0 -62
- package/src/v2/Common-V2/utils.ts +0 -238
- package/src/v2/SearchableSelect/AutocompleteContent.tsx +0 -325
- package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +0 -527
- package/src/v2/SearchableSelect/SearchableSelect.tsx +0 -395
- package/src/v2/SearchableSelect/index.ts +0 -12
- package/src/v2/Select/MultiSelect.tsx +0 -417
- package/src/v2/Select/Select.stories.tsx +0 -593
- package/src/v2/Select/SingleSelect.tsx +0 -285
- package/src/v2/Select/index.ts +0 -2
- package/src/v2/index.ts +0 -2
|
@@ -1,366 +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 } from "../Common-V2";
|
|
8
|
-
import {
|
|
9
|
-
isGroupHeading,
|
|
10
|
-
isToggleItem,
|
|
11
|
-
menuAnimationConfig,
|
|
12
|
-
getFilteredItems,
|
|
13
|
-
groupItemsWithHeadings,
|
|
14
|
-
useHighlightedIndex,
|
|
15
|
-
createItemToStringForDownshift,
|
|
16
|
-
ComboboxContent,
|
|
17
|
-
} from "../Common-V2";
|
|
18
|
-
import {
|
|
19
|
-
type DataWithId,
|
|
20
|
-
type GroupedItem,
|
|
21
|
-
type ToggleItem,
|
|
22
|
-
type BaseSingleAutocompleteProps,
|
|
23
|
-
} from "../Common-V2";
|
|
24
|
-
|
|
25
|
-
function defaultReducer(state, actionAndChanges) {
|
|
26
|
-
const { changes, type } = actionAndChanges;
|
|
27
|
-
|
|
28
|
-
switch (type) {
|
|
29
|
-
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
30
|
-
case useCombobox.stateChangeTypes.ItemClick:
|
|
31
|
-
return {
|
|
32
|
-
...changes,
|
|
33
|
-
isOpen: false, // close the menu after selection for single select
|
|
34
|
-
highlightedIndex: 0, // reset highlight
|
|
35
|
-
};
|
|
36
|
-
default:
|
|
37
|
-
return changes;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface SingleAutocompleteProps<T extends DataWithId>
|
|
42
|
-
extends BaseSingleAutocompleteProps<T> {
|
|
43
|
-
includePlaceholderItem?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function SingleAutocomplete<T extends DataWithId>({
|
|
47
|
-
id,
|
|
48
|
-
data,
|
|
49
|
-
selectedItemId: controlledSelectedItemId,
|
|
50
|
-
inputValue: controlledInputValue,
|
|
51
|
-
isOpen: controlledIsOpen,
|
|
52
|
-
itemToKey = (item: T) => item?.id,
|
|
53
|
-
itemToString,
|
|
54
|
-
itemToSearchString,
|
|
55
|
-
renderItem,
|
|
56
|
-
groupBy,
|
|
57
|
-
renderGroupHeading,
|
|
58
|
-
isVirtualized = false,
|
|
59
|
-
estimatedItemHeight,
|
|
60
|
-
EmptyState,
|
|
61
|
-
placeholder,
|
|
62
|
-
onSelectedItemIdChange,
|
|
63
|
-
onInputValueChange,
|
|
64
|
-
onIsOpenChange,
|
|
65
|
-
stateReducer = defaultReducer,
|
|
66
|
-
menuProps,
|
|
67
|
-
inputType = "icon",
|
|
68
|
-
includePlaceholderItem = false,
|
|
69
|
-
onPointerDownOutside,
|
|
70
|
-
onInteractOutside,
|
|
71
|
-
popoutPlacement,
|
|
72
|
-
}: SingleAutocompleteProps<T>) {
|
|
73
|
-
const [uncontrolledInputValue, setUncontrolledInputValue] =
|
|
74
|
-
useState<string>("");
|
|
75
|
-
const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
|
|
76
|
-
T["id"] | null
|
|
77
|
-
>(null);
|
|
78
|
-
const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
|
|
79
|
-
|
|
80
|
-
// Use controlled selectedItemId if provided, otherwise use internal state
|
|
81
|
-
const isSelectedItemIdControlled = controlledSelectedItemId !== undefined;
|
|
82
|
-
const selectedItemId = isSelectedItemIdControlled
|
|
83
|
-
? controlledSelectedItemId
|
|
84
|
-
: uncontrolledSelectedItemId;
|
|
85
|
-
|
|
86
|
-
// Convert selectedItemId to selectedItem for internal use
|
|
87
|
-
const selectedItem = useMemo<T | null>(() => {
|
|
88
|
-
if (selectedItemId === null || selectedItemId === undefined) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
return data.find((item) => itemToKey(item) === selectedItemId) ?? null;
|
|
92
|
-
}, [selectedItemId, data, itemToKey]);
|
|
93
|
-
|
|
94
|
-
// Use controlled inputValue if provided, otherwise use internal state
|
|
95
|
-
const isInputValueControlled = controlledInputValue !== undefined;
|
|
96
|
-
const inputValue = isInputValueControlled
|
|
97
|
-
? controlledInputValue
|
|
98
|
-
: uncontrolledInputValue;
|
|
99
|
-
|
|
100
|
-
// Use controlled isOpen if provided, otherwise use internal state
|
|
101
|
-
const isIsOpenControlled = controlledIsOpen !== undefined;
|
|
102
|
-
const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
|
|
103
|
-
|
|
104
|
-
// Sync inputValue with selectedItem when selectedItem changes externally and menu is closed
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (selectedItem && !isOpen) {
|
|
107
|
-
// Only update if menu is closed - when open, user might be typing
|
|
108
|
-
const selectedItemString = itemToString(selectedItem);
|
|
109
|
-
if (!isInputValueControlled) {
|
|
110
|
-
// For uncontrolled input, update internal state
|
|
111
|
-
if (uncontrolledInputValue !== selectedItemString) {
|
|
112
|
-
setUncontrolledInputValue(selectedItemString);
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
// For controlled input, notify parent if value doesn't match
|
|
116
|
-
// Parent is responsible for updating the controlled inputValue
|
|
117
|
-
if (inputValue !== selectedItemString) {
|
|
118
|
-
onInputValueChange?.(selectedItemString);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}, [
|
|
123
|
-
selectedItem,
|
|
124
|
-
isOpen,
|
|
125
|
-
isInputValueControlled,
|
|
126
|
-
itemToString,
|
|
127
|
-
uncontrolledInputValue,
|
|
128
|
-
inputValue,
|
|
129
|
-
onInputValueChange,
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
const updateSelectedItem = (newSelectedItem: T | null) => {
|
|
133
|
-
const newSelectedItemId = newSelectedItem
|
|
134
|
-
? itemToKey(newSelectedItem)
|
|
135
|
-
: null;
|
|
136
|
-
if (!isSelectedItemIdControlled) {
|
|
137
|
-
setUncontrolledSelectedItemId(newSelectedItemId);
|
|
138
|
-
}
|
|
139
|
-
onSelectedItemIdChange?.(newSelectedItemId);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const updateInputValue = (newInputValue: string) => {
|
|
143
|
-
if (!isInputValueControlled) {
|
|
144
|
-
setUncontrolledInputValue(newInputValue);
|
|
145
|
-
}
|
|
146
|
-
onInputValueChange?.(newInputValue);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const updateIsOpen = (newIsOpen: boolean) => {
|
|
150
|
-
if (!isIsOpenControlled) {
|
|
151
|
-
setUncontrolledIsOpen(newIsOpen);
|
|
152
|
-
}
|
|
153
|
-
onIsOpenChange?.(newIsOpen);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Use itemToSearchString if provided, otherwise fallback to itemToString
|
|
157
|
-
const searchStringFn =
|
|
158
|
-
itemToSearchString ?? ((item: T) => itemToString(item));
|
|
159
|
-
|
|
160
|
-
const filteredItems = useMemo<T[]>(
|
|
161
|
-
() =>
|
|
162
|
-
getFilteredItems(data, inputValue, searchStringFn, itemToString, {
|
|
163
|
-
selectedItem,
|
|
164
|
-
}),
|
|
165
|
-
[data, selectedItem, inputValue, searchStringFn, itemToString]
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const placeholderItem: ToggleItem = {
|
|
169
|
-
id: "clear-selection",
|
|
170
|
-
label: placeholder,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const items = useMemo<GroupedItem<T>[]>(() => {
|
|
174
|
-
let groupedItems: GroupedItem<T>[];
|
|
175
|
-
|
|
176
|
-
if (groupBy) {
|
|
177
|
-
groupedItems = groupItemsWithHeadings(
|
|
178
|
-
filteredItems,
|
|
179
|
-
groupBy,
|
|
180
|
-
data,
|
|
181
|
-
isVirtualized
|
|
182
|
-
);
|
|
183
|
-
} else {
|
|
184
|
-
groupedItems = filteredItems;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add placeholder item at the start if needed
|
|
188
|
-
groupedItems = includePlaceholderItem
|
|
189
|
-
? [placeholderItem, ...groupedItems]
|
|
190
|
-
: groupedItems;
|
|
191
|
-
|
|
192
|
-
return groupedItems;
|
|
193
|
-
}, [
|
|
194
|
-
filteredItems,
|
|
195
|
-
placeholder,
|
|
196
|
-
groupBy,
|
|
197
|
-
data,
|
|
198
|
-
includePlaceholderItem,
|
|
199
|
-
isVirtualized,
|
|
200
|
-
]);
|
|
201
|
-
|
|
202
|
-
// Filter out headings for downshift
|
|
203
|
-
const itemsForCombobox = useMemo<(T | ToggleItem)[]>(() => {
|
|
204
|
-
return items.filter(
|
|
205
|
-
(item): item is T | ToggleItem => !isGroupHeading(item)
|
|
206
|
-
);
|
|
207
|
-
}, [items]);
|
|
208
|
-
|
|
209
|
-
// Wrapper function to handle ToggleItem separately
|
|
210
|
-
const itemToStringForDownshift = useMemo(
|
|
211
|
-
() => createItemToStringForDownshift(itemToString),
|
|
212
|
-
[itemToString]
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const comboboxProps = useCombobox<T | ToggleItem>({
|
|
216
|
-
inputId: id,
|
|
217
|
-
items: itemsForCombobox,
|
|
218
|
-
itemToString: itemToStringForDownshift,
|
|
219
|
-
defaultHighlightedIndex: 0,
|
|
220
|
-
selectedItem: selectedItem,
|
|
221
|
-
inputValue,
|
|
222
|
-
isOpen,
|
|
223
|
-
stateReducer,
|
|
224
|
-
onStateChange({
|
|
225
|
-
inputValue: newInputValue,
|
|
226
|
-
type,
|
|
227
|
-
selectedItem: newSelectedItem,
|
|
228
|
-
isOpen: newIsOpen,
|
|
229
|
-
}) {
|
|
230
|
-
// Handle isOpen changes for any state change type
|
|
231
|
-
if (newIsOpen !== undefined) {
|
|
232
|
-
updateIsOpen(newIsOpen);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
switch (type) {
|
|
236
|
-
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
237
|
-
case useCombobox.stateChangeTypes.ItemClick:
|
|
238
|
-
if (newSelectedItem) {
|
|
239
|
-
updateSelectedItem(newSelectedItem as T);
|
|
240
|
-
if (newSelectedItem.id === "clear-selection") {
|
|
241
|
-
// Clear input if placeholder item is selected
|
|
242
|
-
updateInputValue("");
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
// Set input to selected item's string representation
|
|
246
|
-
updateInputValue(itemToString(newSelectedItem as T));
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
|
|
250
|
-
case useCombobox.stateChangeTypes.InputChange:
|
|
251
|
-
updateInputValue(newInputValue ?? "");
|
|
252
|
-
if (newInputValue === "" && selectedItem && !isInputValueControlled) {
|
|
253
|
-
// If the input is cleared and a selected item is present, clear the selection
|
|
254
|
-
// This is to ensure that the input value is cleared when the selected item is cleared
|
|
255
|
-
updateSelectedItem(null);
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
|
|
259
|
-
default:
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
const {
|
|
265
|
-
isOpen: comboboxIsOpen,
|
|
266
|
-
getToggleButtonProps,
|
|
267
|
-
getLabelProps,
|
|
268
|
-
getMenuProps,
|
|
269
|
-
getInputProps,
|
|
270
|
-
highlightedIndex: comboboxHighlightedIndex,
|
|
271
|
-
getItemProps,
|
|
272
|
-
selectedItem: comboboxSelectedItem,
|
|
273
|
-
} = comboboxProps;
|
|
274
|
-
|
|
275
|
-
// Map the highlighted index from combobox (which only knows about items) to the full items array (which includes headings)
|
|
276
|
-
const highlightedIndex = useHighlightedIndex(
|
|
277
|
-
items,
|
|
278
|
-
itemsForCombobox,
|
|
279
|
-
comboboxHighlightedIndex,
|
|
280
|
-
!!groupBy,
|
|
281
|
-
false // Single select components hardcode selectHeadings to false
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const inputProps = getInputProps();
|
|
285
|
-
// Downshift adds aria-labelledby by default, which can conflict with our Label usage
|
|
286
|
-
inputProps["aria-labelledby"] = undefined;
|
|
287
|
-
if (!comboboxIsOpen) {
|
|
288
|
-
// Remove aria-controls when the menu is closed to avoid referencing a non-existent element
|
|
289
|
-
// @ts-expect-error downshift types require aria-controls even though it is not required
|
|
290
|
-
inputProps["aria-controls"] = undefined;
|
|
291
|
-
}
|
|
292
|
-
const { ref, id: inputId, ...restProps } = inputProps as any;
|
|
293
|
-
|
|
294
|
-
return (
|
|
295
|
-
<Popout
|
|
296
|
-
open={comboboxIsOpen}
|
|
297
|
-
onOpenChange={updateIsOpen}
|
|
298
|
-
side={popoutPlacement}
|
|
299
|
-
animationConfig={menuAnimationConfig}
|
|
300
|
-
onOpenAutoFocus={(e) => {
|
|
301
|
-
// Prevent default auto-focus behavior
|
|
302
|
-
e.preventDefault();
|
|
303
|
-
}}
|
|
304
|
-
onCloseAutoFocus={(e) => {
|
|
305
|
-
// Prevent default auto-focus behavior
|
|
306
|
-
e.preventDefault();
|
|
307
|
-
}}
|
|
308
|
-
onPointerDownOutside={onPointerDownOutside}
|
|
309
|
-
onInteractOutside={onInteractOutside}
|
|
310
|
-
content={
|
|
311
|
-
<ComboboxContent
|
|
312
|
-
items={items}
|
|
313
|
-
getMenuProps={getMenuProps}
|
|
314
|
-
getItemProps={(props: { index: number }) => {
|
|
315
|
-
// Only apply getItemProps to actual items, not headings or ToggleItems
|
|
316
|
-
const item = items[props.index];
|
|
317
|
-
const dataItem = item as T;
|
|
318
|
-
// Find the index in itemsForCombobox using itemToKey for safer comparison
|
|
319
|
-
const itemIndex = itemsForCombobox.findIndex((comboboxItem) => {
|
|
320
|
-
if (isToggleItem(comboboxItem)) {
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
return itemToKey(comboboxItem as T) === itemToKey(dataItem);
|
|
324
|
-
});
|
|
325
|
-
if (itemIndex === -1) {
|
|
326
|
-
return {};
|
|
327
|
-
}
|
|
328
|
-
return getItemProps({ item: dataItem, index: itemIndex });
|
|
329
|
-
}}
|
|
330
|
-
selectedItems={selectedItem ? [selectedItem] : []}
|
|
331
|
-
highlightedIndex={highlightedIndex ?? -1}
|
|
332
|
-
renderItem={renderItem}
|
|
333
|
-
renderGroupHeading={renderGroupHeading}
|
|
334
|
-
EmptyState={EmptyState}
|
|
335
|
-
itemToKey={itemToKey}
|
|
336
|
-
itemToString={itemToString}
|
|
337
|
-
inputType={inputType}
|
|
338
|
-
selectHeadings={false}
|
|
339
|
-
groupBy={groupBy}
|
|
340
|
-
isVirtualized={isVirtualized}
|
|
341
|
-
estimatedItemHeight={estimatedItemHeight}
|
|
342
|
-
placeholderItem={includePlaceholderItem ? placeholderItem : undefined}
|
|
343
|
-
{...menuProps}
|
|
344
|
-
/>
|
|
345
|
-
}
|
|
346
|
-
>
|
|
347
|
-
<Input
|
|
348
|
-
id={id}
|
|
349
|
-
inputProps={{
|
|
350
|
-
autoComplete: "off",
|
|
351
|
-
}}
|
|
352
|
-
innerRef={ref}
|
|
353
|
-
placeholder={placeholder}
|
|
354
|
-
elemAfter={!includePlaceholderItem ? <Input.ClearButton /> : undefined}
|
|
355
|
-
clearButtonLabel="Clear selection"
|
|
356
|
-
onClear={() => {
|
|
357
|
-
// Note, by default the input clear button re-focuses the input
|
|
358
|
-
// We don't want to do that, but that would require a breaking change to Input
|
|
359
|
-
updateSelectedItem(null);
|
|
360
|
-
updateInputValue("");
|
|
361
|
-
}}
|
|
362
|
-
{...restProps}
|
|
363
|
-
/>
|
|
364
|
-
</Popout>
|
|
365
|
-
);
|
|
366
|
-
}
|