@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "1.15.10",
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.21",
40
- "@sproutsocial/seeds-react-button": "^2.2.1",
41
- "@sproutsocial/seeds-react-checkbox": "^1.3.41",
42
- "@sproutsocial/seeds-react-icon": "^2.4.0",
43
- "@sproutsocial/seeds-react-input": "^1.5.26",
44
- "@sproutsocial/seeds-react-label": "^1.0.22",
45
- "@sproutsocial/seeds-react-popout": "^2.5.9",
46
- "@sproutsocial/seeds-react-radio": "^1.3.22",
47
- "@sproutsocial/seeds-react-switch": "^1.2.39",
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.0",
50
- "@sproutsocial/seeds-react-token-input": "^1.4.48",
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.27",
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: () => (
@@ -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 key={`__header-${row.groupName}`} value={row}>
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
+ });