@sproutsocial/seeds-react-menu 1.6.0 → 1.6.2

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.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -65,11 +65,22 @@ const handleOnClick = (e: React.MouseEvent<HTMLElement>, itemName: string) => {
65
65
  action(`${itemName}`)(e);
66
66
  };
67
67
 
68
- export const ActionMenuSimple: Story = {
68
+ type ActionMenuSimpleStoryArgs = ActionMenuStoryArgs & {
69
+ "Show Conditional Item"?: boolean;
70
+ };
71
+
72
+ export const ActionMenuSimple: StoryObj<ActionMenuSimpleStoryArgs> = {
69
73
  name: "Simple Action Menu",
70
74
  args: {
71
75
  "Trigger Text": "ActionMenu",
72
76
  "Trigger Appearance": "secondary",
77
+ "Show Conditional Item": false,
78
+ },
79
+ argTypes: {
80
+ "Show Conditional Item": {
81
+ control: { type: "boolean" },
82
+ description: "Toggle to show/hide the conditional menu item",
83
+ },
73
84
  },
74
85
  render: (args) => {
75
86
  return (
@@ -106,6 +117,14 @@ export const ActionMenuSimple: Story = {
106
117
  >
107
118
  This Opens a Link
108
119
  </MenuItem>
120
+ {args["Show Conditional Item"] && (
121
+ <MenuItem
122
+ onClick={(e) => handleOnClick(e, "Conditional Menu Item")}
123
+ id="conditional"
124
+ >
125
+ Conditional Menu Item
126
+ </MenuItem>
127
+ )}
109
128
  </MenuGroup>
110
129
  </MenuContent>
111
130
  </ActionMenu>
@@ -16,6 +16,7 @@ export type TypeBaseAutocompleteProps = TypeHigherOrderComponentProps &
16
16
  /** Set multiSelect to true to allow multiple selections.
17
17
  * Default: false*/
18
18
  multiSelect?: boolean;
19
+ autoCloseOnEmpty?: boolean;
19
20
  };
20
21
 
21
22
  export type TypeSingleAutocompleteProps<
@@ -37,6 +37,7 @@ export const MultiAutocomplete = <
37
37
  getA11yMultiStatusMessage,
38
38
  onMultiSelectStateChange,
39
39
  multiSelectStateReducer,
40
+ autoCloseOnEmpty = true,
40
41
  ...useComboboxProps
41
42
  }: TypeMultiAutocompleteProps<I>) => {
42
43
  /** Get a list of menuItems and children */
@@ -175,6 +176,7 @@ export const MultiAutocomplete = <
175
176
  <MenuRootComponent
176
177
  {...{
177
178
  isListbox: true,
179
+ autoCloseOnEmpty,
178
180
  items: allMenuItems,
179
181
  itemToString,
180
182
  initialSelectedItems,
@@ -192,7 +194,6 @@ export const MultiAutocomplete = <
192
194
  focusOnContent: false,
193
195
  ...userPopoutProps,
194
196
  }}
195
- autoCloseOnEmpty
196
197
  >
197
198
  {children}
198
199
  </MenuRootComponent>
@@ -20,6 +20,7 @@ export const SingleAutocomplete = <
20
20
  MenuItemComponent,
21
21
  itemToString = defaultItemToString,
22
22
  getIsItemVisible,
23
+ autoCloseOnEmpty = true,
23
24
  ...useComboboxProps
24
25
  }: TypeSingleAutocompleteProps<I>) => {
25
26
  /** Get a list of menuItems and children */
@@ -63,6 +64,7 @@ export const SingleAutocomplete = <
63
64
  <MenuRootComponent
64
65
  {...{
65
66
  isListbox: true,
67
+ autoCloseOnEmpty,
66
68
  items: allMenuItems,
67
69
  itemToString,
68
70
  ...useComboboxProps,
@@ -73,7 +75,6 @@ export const SingleAutocomplete = <
73
75
  focusOnContent: false,
74
76
  ...userPopoutProps,
75
77
  }}
76
- autoCloseOnEmpty
77
78
  >
78
79
  {children}
79
80
  </MenuRootComponent>
@@ -174,7 +174,7 @@ describe("Autocomplete Component", () => {
174
174
  expect(customFilter).toHaveBeenCalled();
175
175
  });
176
176
 
177
- it("should close the dropdown when no items match the input", async () => {
177
+ it("should close the dropdown when no items match the input by default", async () => {
178
178
  const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
179
179
  const inputElement = screen.getByRole("combobox");
180
180
 
@@ -184,11 +184,26 @@ describe("Autocomplete Component", () => {
184
184
  });
185
185
 
186
186
  await waitFor(() => {
187
- const listbox = screen.queryByRole("listbox");
188
- expect(listbox).not.toBeInTheDocument();
187
+ const emptyState = screen.queryByText("No items found");
188
+ expect(emptyState).not.toBeInTheDocument();
189
189
  });
190
190
  });
191
191
 
192
+ it("should not close the dropdown when no items match the input if supplied autoCloseOnEmpty={false}", async () => {
193
+ const { user } = render(
194
+ <Autocomplete {...defaultAutocompleteProps} autoCloseOnEmpty={false} />
195
+ );
196
+ const inputElement = screen.getByRole("combobox");
197
+
198
+ await act(async () => {
199
+ await user.click(inputElement);
200
+ await user.type(inputElement, "something not in the list");
201
+ });
202
+
203
+ const emptyState = screen.getByText("No items found");
204
+ expect(emptyState).toBeInTheDocument();
205
+ });
206
+
192
207
  it("should apply custom popoutProps.width to the popout container", async () => {
193
208
  const { user } = render(
194
209
  <Autocomplete
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  SingleSelectMenu,
10
10
  MultiSelectMenu,
11
+ ActionMenu,
11
12
  MenuContent,
12
13
  MenuGroup,
13
14
  MenuToggleButton,
@@ -284,4 +285,36 @@ describe("MenuGroup", () => {
284
285
  xit("ignores the customMenuItem prop", () => {});
285
286
  });
286
287
  });
288
+
289
+ it("supports conditionally rendered items when false", () => {
290
+ // Spy on console.error to catch and verify console errors
291
+ const consoleErrorSpy = jest
292
+ .spyOn(console, "error")
293
+ .mockImplementation(() => {});
294
+
295
+ render(
296
+ <ActionMenu
297
+ defaultIsOpen
298
+ menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
299
+ >
300
+ <MenuContent>
301
+ <MenuGroup id="group">
302
+ <MenuItem id="item-1">Item 1</MenuItem>
303
+ {false && (
304
+ <MenuItem id="conditional" onClick={() => {}}>
305
+ Conditional Menu Item
306
+ </MenuItem>
307
+ )}
308
+ <MenuItem id="item-2">Item 2</MenuItem>
309
+ </MenuGroup>
310
+ </MenuContent>
311
+ </ActionMenu>
312
+ );
313
+
314
+ // Verify no console errors occurred during render
315
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
316
+
317
+ // Restore console.error
318
+ consoleErrorSpy.mockRestore();
319
+ });
287
320
  });
@@ -10,6 +10,11 @@ export const MenuGroupTypeTest = () => (
10
10
  <MenuItem id="1">Test 1</MenuItem>
11
11
  <MenuItem id="2">Test 2</MenuItem>
12
12
  </MenuGroup>
13
+ {/* MenuGroup allows children with conditional MenuItems */}
14
+ <MenuGroup id="group">
15
+ <MenuItem id="1">Test 1</MenuItem>
16
+ {false && <MenuItem id="2">Test 2</MenuItem>}
17
+ </MenuGroup>
13
18
  {/* MenuGroup allows menuItems prop */}
14
19
  <MenuGroup id="group" menuItems={[{ id: "1", children: "1" }]} />
15
20
  {/* MenuGroup allows menuItems prop with MenuItemComponent */}
@@ -5,7 +5,7 @@ import type {
5
5
  TypeSubmenuValidMenuComponentProps,
6
6
  } from "../SubmenuItem";
7
7
  import type { TypeMenuContext } from "../MenuContext";
8
- import type { TypeMenuGroupProps, TypeMenuGroupBaseProps } from "../MenuGroup";
8
+ import type { TypeMenuGroupProps } from "../MenuGroup";
9
9
  import type { TypeMenuRootProps } from "../MenuRoot";
10
10
  import type {
11
11
  TypeNotEmptyChildren,
@@ -46,12 +46,13 @@ const walkAllChildren = (
46
46
  if (element === null || element === undefined) return;
47
47
  callback(element, parents);
48
48
  const newParents = [...parents, element];
49
- const children = (element as any).props?.children;
50
- React.Children.toArray(children).forEach((child) => {
51
- walk(child, newParents);
52
- });
49
+ if (React.isValidElement(element) && element.props?.children) {
50
+ React.Children.toArray(element.props.children).forEach((child) => {
51
+ walk(child, newParents);
52
+ });
53
+ }
53
54
  };
54
- const parents = [];
55
+ const parents: ReactNode[] = [];
55
56
  React.Children.toArray(children).forEach((child) => {
56
57
  walk(child, parents);
57
58
  });
@@ -114,22 +115,22 @@ const addItemsFromGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
114
115
  });
115
116
  });
116
117
  } else if (menuGroup.props.children) {
117
- let children = menuGroup.props.children as TypeItemElement[];
118
- // If there is only one child, it will be an object. Make it an array.
119
- if (!Array.isArray(children)) {
120
- children = [children];
121
- }
118
+ // Convert children to array to handle both arrays and iterables
119
+ const children = React.Children.toArray(menuGroup.props.children);
122
120
  children.forEach((menuItem) => {
123
- addMenuItem({
124
- itemProps: {
125
- id: menuItem.props.id,
126
- disabled: menuItem.props.disabled,
127
- menuItemProps: menuItem.props,
128
- menuGroupProps: menuGroup.props,
129
- },
130
- allMenuItems,
131
- menuItemsMap,
132
- });
121
+ // Only process React elements that have props
122
+ if (React.isValidElement(menuItem)) {
123
+ addMenuItem({
124
+ itemProps: {
125
+ id: menuItem.props.id,
126
+ disabled: menuItem.props.disabled,
127
+ menuItemProps: menuItem.props,
128
+ menuGroupProps: menuGroup.props,
129
+ },
130
+ allMenuItems,
131
+ menuItemsMap,
132
+ });
133
+ }
133
134
  });
134
135
  }
135
136
  };
@@ -269,7 +270,7 @@ export const useMenuChildren = <
269
270
  });
270
271
  children = getChildrenFromMenuItems({ menuItems, MenuItemComponent });
271
272
  } else if (childrenProp) {
272
- children = childrenProp as TypeNotEmptyChildren;
273
+ children = childrenProp;
273
274
  setMenuItemsFromChildren({
274
275
  children: childrenProp,
275
276
  allMenuItems,
package/src/types.ts CHANGED
@@ -46,10 +46,8 @@ export type TypeChildrenOrItems<
46
46
  }
47
47
  );
48
48
 
49
- export type TypeNotEmptyChildren =
50
- | React.ReactChild
51
- | React.ReactFragment
52
- | React.ReactPortal;
49
+ // Type that excludes null and undefined from React.ReactNode
50
+ export type TypeNotEmptyChildren = Exclude<React.ReactNode, null | undefined>;
53
51
 
54
52
  type TypeInputEventListeners =
55
53
  | "onKeyDown"