@sproutsocial/seeds-react-menu 1.15.10 → 1.16.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 +12 -12
- package/CHANGELOG.md +23 -0
- package/dist/esm/v3/index.js +29 -10
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +2 -0
- package/dist/v3/index.d.ts +2 -0
- package/dist/v3/index.js +29 -10
- package/dist/v3/index.js.map +1 -1
- package/package.json +13 -13
- package/src/v3/Common/ComboboxStyles.tsx +6 -0
- package/src/v3/Common/VirtualizedComboboxList.tsx +3 -0
- package/src/v3/MultiCombobox.stories.tsx +22 -0
- package/src/v3/MultiCombobox.tsx +12 -1
- package/src/v3/__tests__/MultiCombobox.test.tsx +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,18 +36,18 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@radix-ui/react-popover": "^1.1.2",
|
|
39
|
-
"@sproutsocial/seeds-react-box": "^1.1.
|
|
40
|
-
"@sproutsocial/seeds-react-button": "^2.2.
|
|
41
|
-
"@sproutsocial/seeds-react-checkbox": "^1.3.
|
|
42
|
-
"@sproutsocial/seeds-react-icon": "^2.4.
|
|
43
|
-
"@sproutsocial/seeds-react-input": "^1.5.
|
|
44
|
-
"@sproutsocial/seeds-react-label": "^1.0.
|
|
45
|
-
"@sproutsocial/seeds-react-popout": "^2.5.
|
|
46
|
-
"@sproutsocial/seeds-react-radio": "^1.3.
|
|
47
|
-
"@sproutsocial/seeds-react-switch": "^1.2.
|
|
39
|
+
"@sproutsocial/seeds-react-box": "^1.1.22",
|
|
40
|
+
"@sproutsocial/seeds-react-button": "^2.2.2",
|
|
41
|
+
"@sproutsocial/seeds-react-checkbox": "^1.3.42",
|
|
42
|
+
"@sproutsocial/seeds-react-icon": "^2.4.1",
|
|
43
|
+
"@sproutsocial/seeds-react-input": "^1.5.27",
|
|
44
|
+
"@sproutsocial/seeds-react-label": "^1.0.23",
|
|
45
|
+
"@sproutsocial/seeds-react-popout": "^2.5.10",
|
|
46
|
+
"@sproutsocial/seeds-react-radio": "^1.3.23",
|
|
47
|
+
"@sproutsocial/seeds-react-switch": "^1.2.40",
|
|
48
48
|
"@sproutsocial/seeds-react-system-props": "^3.1.1",
|
|
49
|
-
"@sproutsocial/seeds-react-theme": "^4.1.
|
|
50
|
-
"@sproutsocial/seeds-react-token-input": "^1.4.
|
|
49
|
+
"@sproutsocial/seeds-react-theme": "^4.1.1",
|
|
50
|
+
"@sproutsocial/seeds-react-token-input": "^1.4.49",
|
|
51
51
|
"@sproutsocial/seeds-react-utilities": "^4.3.0",
|
|
52
52
|
"@base-ui/react": "^1.3.0",
|
|
53
53
|
"@tanstack/react-virtual": "^3.13.12",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"styled-system": "^5.1.5"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@sproutsocial/seeds-react-tooltip": "^1.1.
|
|
60
|
+
"@sproutsocial/seeds-react-tooltip": "^1.1.28",
|
|
61
61
|
"@sproutsocial/eslint-config-seeds": "*",
|
|
62
62
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
63
63
|
"@sproutsocial/seeds-testing": "*",
|
|
@@ -288,6 +288,12 @@ export const ComboboxGroupHeaderItem = styled(Combobox.Item)`
|
|
|
288
288
|
background: ${({ theme }) => theme.colors.listItem.background.hover};
|
|
289
289
|
color: ${({ theme }) => theme.colors.text.body};
|
|
290
290
|
}
|
|
291
|
+
|
|
292
|
+
&[data-disabled] {
|
|
293
|
+
opacity: 0.4;
|
|
294
|
+
cursor: not-allowed;
|
|
295
|
+
pointer-events: none;
|
|
296
|
+
}
|
|
291
297
|
`;
|
|
292
298
|
|
|
293
299
|
export const ComboboxSelectAllItem = styled(Combobox.Item)`
|
|
@@ -55,6 +55,7 @@ interface VirtualizedComboboxListProps<T extends DataWithId> {
|
|
|
55
55
|
inputType: "icon" | "radio" | "checkbox" | "none";
|
|
56
56
|
renderItem?: (item: T) => React.ReactNode;
|
|
57
57
|
renderGroupHeading?: (label: string) => React.ReactNode;
|
|
58
|
+
isGroupHeaderDisabled?: (groupName: string) => boolean;
|
|
58
59
|
renderCreatableItem?: (
|
|
59
60
|
sentinel: CreatableSentinel,
|
|
60
61
|
style: React.CSSProperties,
|
|
@@ -73,6 +74,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
73
74
|
inputType,
|
|
74
75
|
renderItem,
|
|
75
76
|
renderGroupHeading,
|
|
77
|
+
isGroupHeaderDisabled,
|
|
76
78
|
renderCreatableItem,
|
|
77
79
|
}: VirtualizedComboboxListProps<T>) {
|
|
78
80
|
type Row = FlatSentinelRow<T>;
|
|
@@ -198,6 +200,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
198
200
|
index={virtualItem.index}
|
|
199
201
|
data-index={virtualItem.index}
|
|
200
202
|
value={row}
|
|
203
|
+
disabled={isGroupHeaderDisabled?.(row.groupName)}
|
|
201
204
|
style={baseStyle}
|
|
202
205
|
ref={virtualizer.measureElement}
|
|
203
206
|
>
|
|
@@ -196,6 +196,28 @@ export const SelectableHeadings: Story = {
|
|
|
196
196
|
),
|
|
197
197
|
};
|
|
198
198
|
|
|
199
|
+
export const DisabledGroupHeadings: Story = {
|
|
200
|
+
name: "Disabled group headings",
|
|
201
|
+
render: () => (
|
|
202
|
+
<div style={{ width: "300px" }}>
|
|
203
|
+
<FormField label="Pick books">
|
|
204
|
+
{({ id }) => (
|
|
205
|
+
<MultiCombobox
|
|
206
|
+
id={id}
|
|
207
|
+
data={books}
|
|
208
|
+
itemToString={itemToString}
|
|
209
|
+
renderItem={renderItem}
|
|
210
|
+
placeholder="Search books..."
|
|
211
|
+
groupBy={(book) => book.author}
|
|
212
|
+
selectHeadings
|
|
213
|
+
isGroupHeaderDisabled={(groupName) => groupName === "Harper Lee"}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</FormField>
|
|
217
|
+
</div>
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
|
|
199
221
|
export const SelectAll: Story = {
|
|
200
222
|
name: "Select all",
|
|
201
223
|
render: () => (
|
package/src/v3/MultiCombobox.tsx
CHANGED
|
@@ -93,6 +93,7 @@ interface MultiComboboxDataProps<T extends DataWithId>
|
|
|
93
93
|
onSelectedItemIdsChange?: (ids: Array<T["id"]>) => void;
|
|
94
94
|
groupBy?: (item: T) => string;
|
|
95
95
|
renderGroupHeading?: (label: string) => React.ReactNode;
|
|
96
|
+
isGroupHeaderDisabled?: (groupName: string) => boolean;
|
|
96
97
|
selectHeadings?: boolean;
|
|
97
98
|
selectAll?: boolean | string;
|
|
98
99
|
isVirtualized?: boolean;
|
|
@@ -116,6 +117,7 @@ interface MultiComboboxChildrenProps extends MultiComboboxBaseProps {
|
|
|
116
117
|
inputType?: never;
|
|
117
118
|
groupBy?: never;
|
|
118
119
|
renderGroupHeading?: never;
|
|
120
|
+
isGroupHeaderDisabled?: never;
|
|
119
121
|
selectHeadings?: never;
|
|
120
122
|
selectAll?: never;
|
|
121
123
|
removeSelectedItems?: never;
|
|
@@ -184,6 +186,9 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
184
186
|
const renderGroupHeading = isChildrenMode
|
|
185
187
|
? undefined
|
|
186
188
|
: (props as MultiComboboxDataProps<T>).renderGroupHeading;
|
|
189
|
+
const isGroupHeaderDisabled = isChildrenMode
|
|
190
|
+
? undefined
|
|
191
|
+
: (props as MultiComboboxDataProps<T>).isGroupHeaderDisabled;
|
|
187
192
|
const selectHeadings = isChildrenMode
|
|
188
193
|
? false
|
|
189
194
|
: (props as MultiComboboxDataProps<T>).selectHeadings ?? false;
|
|
@@ -338,6 +343,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
338
343
|
|
|
339
344
|
let updated = [...realItems];
|
|
340
345
|
for (const sentinel of groupSentinels) {
|
|
346
|
+
if (isGroupHeaderDisabled?.(sentinel.groupName)) continue;
|
|
341
347
|
const group = groups?.find((g) => g.value === sentinel.groupName);
|
|
342
348
|
if (!group) continue;
|
|
343
349
|
const allSelected = group.items.every((item) =>
|
|
@@ -411,7 +417,11 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
411
417
|
? "all"
|
|
412
418
|
: "partial";
|
|
413
419
|
return (
|
|
414
|
-
<ComboboxGroupHeaderItem
|
|
420
|
+
<ComboboxGroupHeaderItem
|
|
421
|
+
key={`__header-${row.groupName}`}
|
|
422
|
+
value={row}
|
|
423
|
+
disabled={isGroupHeaderDisabled?.(row.groupName)}
|
|
424
|
+
>
|
|
415
425
|
<ComboboxGroupHeaderCheckbox
|
|
416
426
|
$state={state}
|
|
417
427
|
id={`__header-${row.groupName}`}
|
|
@@ -687,6 +697,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
687
697
|
inputType={inputType}
|
|
688
698
|
renderItem={renderItem}
|
|
689
699
|
renderGroupHeading={renderGroupHeading}
|
|
700
|
+
isGroupHeaderDisabled={isGroupHeaderDisabled}
|
|
690
701
|
renderCreatableItem={
|
|
691
702
|
creatableProp
|
|
692
703
|
? (sentinel, style, index, measureRef) =>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
within,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import { MultiCombobox } from "../MultiCombobox";
|
|
9
|
+
import { books, itemToString, type Book } from "../storyData";
|
|
10
|
+
|
|
11
|
+
describe("MultiCombobox — disabled group headings", () => {
|
|
12
|
+
const renderCombobox = (
|
|
13
|
+
props: {
|
|
14
|
+
onSelectedItemIdsChange?: (ids: Array<Book["id"]>) => void;
|
|
15
|
+
isGroupHeaderDisabled?: (groupName: string) => boolean;
|
|
16
|
+
} = {}
|
|
17
|
+
) =>
|
|
18
|
+
render(
|
|
19
|
+
<MultiCombobox
|
|
20
|
+
data={books}
|
|
21
|
+
itemToString={itemToString}
|
|
22
|
+
placeholder="Search books..."
|
|
23
|
+
groupBy={(book) => book.author}
|
|
24
|
+
selectHeadings
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const openPopup = () => {
|
|
30
|
+
fireEvent.click(screen.getByRole("button", { name: "Open popup" }));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The group header row is the option whose accessible text is the group name.
|
|
34
|
+
const getGroupHeader = (groupName: string) =>
|
|
35
|
+
screen.getByText(groupName).closest('[role="option"]') as HTMLElement;
|
|
36
|
+
|
|
37
|
+
test("renders a header row for each group", () => {
|
|
38
|
+
renderCombobox();
|
|
39
|
+
openPopup();
|
|
40
|
+
expect(getGroupHeader("Harper Lee")).toBeInTheDocument();
|
|
41
|
+
expect(getGroupHeader("Lev Tolstoy")).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("marks a disabled group's header with data-disabled", () => {
|
|
45
|
+
renderCombobox({
|
|
46
|
+
isGroupHeaderDisabled: (groupName) => groupName === "Harper Lee",
|
|
47
|
+
});
|
|
48
|
+
openPopup();
|
|
49
|
+
expect(getGroupHeader("Harper Lee")).toHaveAttribute("data-disabled");
|
|
50
|
+
expect(getGroupHeader("Lev Tolstoy")).not.toHaveAttribute("data-disabled");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("clicking a disabled header does not select the group's items", () => {
|
|
54
|
+
const onSelectedItemIdsChange = jest.fn();
|
|
55
|
+
renderCombobox({
|
|
56
|
+
onSelectedItemIdsChange,
|
|
57
|
+
isGroupHeaderDisabled: (groupName) => groupName === "Harper Lee",
|
|
58
|
+
});
|
|
59
|
+
openPopup();
|
|
60
|
+
fireEvent.click(getGroupHeader("Harper Lee"));
|
|
61
|
+
expect(onSelectedItemIdsChange).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("clicking an enabled header still selects the whole group", () => {
|
|
65
|
+
const onSelectedItemIdsChange = jest.fn();
|
|
66
|
+
renderCombobox({
|
|
67
|
+
onSelectedItemIdsChange,
|
|
68
|
+
isGroupHeaderDisabled: (groupName) => groupName === "Harper Lee",
|
|
69
|
+
});
|
|
70
|
+
openPopup();
|
|
71
|
+
fireEvent.click(getGroupHeader("Lev Tolstoy"));
|
|
72
|
+
// Lev Tolstoy has two books: War and Peace + Anna Karenina.
|
|
73
|
+
expect(onSelectedItemIdsChange).toHaveBeenCalledWith(
|
|
74
|
+
expect.arrayContaining(["book-2", "book-9"])
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("items inside a disabled group remain individually selectable", () => {
|
|
79
|
+
const onSelectedItemIdsChange = jest.fn();
|
|
80
|
+
renderCombobox({
|
|
81
|
+
onSelectedItemIdsChange,
|
|
82
|
+
isGroupHeaderDisabled: (groupName) => groupName === "Harper Lee",
|
|
83
|
+
});
|
|
84
|
+
openPopup();
|
|
85
|
+
const option = screen
|
|
86
|
+
.getByText("To Kill a Mockingbird")
|
|
87
|
+
.closest('[role="option"]') as HTMLElement;
|
|
88
|
+
fireEvent.click(within(option).getByText("To Kill a Mockingbird"));
|
|
89
|
+
expect(onSelectedItemIdsChange).toHaveBeenCalledWith(["book-1"]);
|
|
90
|
+
});
|
|
91
|
+
});
|