@sproutsocial/seeds-react-menu 0.1.0 → 0.2.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.
Files changed (62) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc.js +6 -2
  3. package/.turbo/turbo-build.log +10 -10
  4. package/CHANGELOG.md +23 -1
  5. package/dist/esm/index.js +243 -217
  6. package/dist/esm/index.js.map +1 -1
  7. package/dist/index.d.mts +104 -38
  8. package/dist/index.d.ts +104 -38
  9. package/dist/index.js +272 -243
  10. package/dist/index.js.map +1 -1
  11. package/package.json +10 -7
  12. package/src/ActionMenu/ActionMenu.feature +9 -3
  13. package/src/ActionMenu/ActionMenu.tsx +76 -42
  14. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +36 -42
  15. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +13 -13
  16. package/src/MenuContent/MenuContent.tsx +31 -1
  17. package/src/MenuContent/MenuContentTypes.ts +3 -4
  18. package/src/MenuContent/styles.tsx +10 -6
  19. package/src/MenuContext.tsx +1 -6
  20. package/src/MenuFooter/MenuFooter.feature +1 -1
  21. package/src/MenuFooter/MenuFooter.tsx +8 -0
  22. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +53 -26
  23. package/src/MenuFooter/styles.tsx +1 -1
  24. package/src/MenuGroup/MenuGroup.feature +14 -26
  25. package/src/MenuGroup/MenuGroup.tsx +21 -1
  26. package/src/MenuGroup/MenuGroupTypes.ts +1 -0
  27. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +41 -6
  28. package/src/MenuGroup/styles.tsx +4 -3
  29. package/src/MenuHeader/MenuHeader.tsx +16 -5
  30. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +50 -6
  31. package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +1 -0
  32. package/src/MenuHeader/styles.tsx +21 -22
  33. package/src/MenuItem/MenuItem.feature +66 -26
  34. package/src/MenuItem/MenuItem.tsx +39 -20
  35. package/src/MenuItem/MenuItemTypes.ts +13 -1
  36. package/src/MenuItem/__tests__/MenuItem.test.tsx +431 -154
  37. package/src/MenuItem/__tests__/MenuItem.typetest.tsx +57 -38
  38. package/src/MenuItem/styles.tsx +44 -3
  39. package/src/{MenuItemSelectable/MenuItemSelectable.tsx → MenuItem/useOptionProps.tsx} +32 -54
  40. package/src/MenuRoot/MenuRoot.feature +20 -10
  41. package/src/MenuRoot/MenuRoot.tsx +14 -18
  42. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +168 -34
  43. package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +10 -10
  44. package/src/MultiSelectMenu/MultiSelectMenu.feature +2 -13
  45. package/src/MultiSelectMenu/MultiSelectMenu.tsx +9 -1
  46. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +154 -153
  47. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +10 -10
  48. package/src/SingleSelectMenu/SingleSelectMenu.feature +11 -0
  49. package/src/SingleSelectMenu/SingleSelectMenu.tsx +10 -0
  50. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +11 -14
  51. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +14 -14
  52. package/src/__tests__/Menu.test.tsx +20 -16
  53. package/src/hooks/useItemsFromChildren.tsx +2 -4
  54. package/src/index.ts +0 -1
  55. package/src/menuTestingUtils.tsx +7 -4
  56. package/tsconfig.json +1 -1
  57. package/src/MenuItemSelectable/MenuItemSelectable.feature +0 -99
  58. package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +0 -15
  59. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +0 -303
  60. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +0 -56
  61. package/src/MenuItemSelectable/index.ts +0 -5
  62. package/src/MenuItemSelectable/styles.tsx +0 -21
@@ -4,20 +4,20 @@ import { MenuRoot, MenuContent, MenuHeader, MenuFooter } from "../../index";
4
4
  export const MenuRootTypeTest = () => (
5
5
  <>
6
6
  {/* Valid MenuRoot with minimal required props */}
7
- <MenuRoot triggerElement={<>test</>}>Test</MenuRoot>
7
+ <MenuRoot menuToggleElement={<>test</>}>Test</MenuRoot>
8
8
  {/* Valid MenuRoot allows MenuContent by itself as a child */}
9
- <MenuRoot triggerElement={<>test</>}>
9
+ <MenuRoot menuToggleElement={<>test</>}>
10
10
  <MenuContent>Test</MenuContent>
11
11
  </MenuRoot>
12
12
  {/* Valid MenuRoot allows multiple children without fragment */}
13
- <MenuRoot triggerElement={<>test</>}>
13
+ <MenuRoot menuToggleElement={<>test</>}>
14
14
  <MenuHeader>Test</MenuHeader>
15
15
  <MenuContent>Test</MenuContent>
16
16
  <MenuFooter>Test</MenuFooter>
17
17
  </MenuRoot>
18
18
  {/* Valid MenuRoot accepts Downshift props and functions for useSelect */}
19
19
  <MenuRoot
20
- triggerElement={<>test</>}
20
+ menuToggleElement={<>test</>}
21
21
  onSelectedItemChange={() => {}}
22
22
  selectItem={() => {}}
23
23
  selectedItem={null}
@@ -30,7 +30,7 @@ export const MenuRootTypeTest = () => (
30
30
  </MenuRoot>
31
31
  {/* Valid MenuRoot accepts Downshift props and functions for useMultipleSelection */}
32
32
  <MenuRoot
33
- triggerElement={<>test</>}
33
+ menuToggleElement={<>test</>}
34
34
  onSelectedItemsChange={() => {}}
35
35
  selectedItems={[]}
36
36
  multiSelectStateReducer={(c) => c}
@@ -40,19 +40,19 @@ export const MenuRootTypeTest = () => (
40
40
  Test
41
41
  </MenuRoot>
42
42
 
43
- {/* @ts-expect-error - triggerElement is a required prop */}
43
+ {/* @ts-expect-error - menuToggleElement is a required prop */}
44
44
  <MenuRoot>Test</MenuRoot>
45
45
  <MenuRoot
46
- /* @ts-expect-error - triggerElement must be a valid react element */
47
- triggerElement="test"
46
+ /* @ts-expect-error - menuToggleElement must be a valid react element */
47
+ menuToggleElement="test"
48
48
  >
49
49
  Test
50
50
  </MenuRoot>
51
51
  {/* @ts-expect-error - when no children provided */}
52
- <MenuRoot triggerElement="test" />
52
+ <MenuRoot menuToggleElement="test" />
53
53
 
54
54
  <MenuRoot
55
- triggerElement={<>test</>}
55
+ menuToggleElement={<>test</>}
56
56
  /* @ts-expect-error - isOpen can't be a string */
57
57
  isOpen="false"
58
58
  /* @ts-expect-error - onSelectedItemChange can't be a string */
@@ -40,25 +40,14 @@ Feature: MultiSelectMenu
40
40
  Then the component renders the existing values
41
41
 
42
42
  Scenario: Component handles new selection state
43
- When the MultiSelectMenu is not passed
43
+ When the MultiSelectMenu is not passed a `selectedItems` prop
44
44
  And a new selection is created
45
- Then the 'onSelectedItemsChangee' callback is fired
45
+ Then the 'onSelectedItemsChange' callback is fired
46
46
 
47
47
  Scenario: Menu does not close on selection
48
48
  When an item is selected
49
49
  Then the 'onSelectedItemsChange' callback fires without closing the menu
50
50
 
51
- Scenario Outline: Actions can be triggered from the MultiSelectMenu
52
- When actionable elements are provided to the MultiSelectMenu
53
- And an `<action>` is clicked
54
- Then the corresponding `onClick` should be executed
55
-
56
- Examples: Action Types
57
- | action |
58
- | ---------------- |
59
- | select button |
60
- | deselect button |
61
-
62
51
  Scenario Outline: MultiSelectMenu checks its types
63
52
  When `<propName>` is passed a value
64
53
  Then MultiSelectMenu expects it to be of type `<propType>`
@@ -34,6 +34,14 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
34
34
  return changes;
35
35
  };
36
36
 
37
+ /**
38
+ * @link https://seeds.sproutsocial.com/components/multiselect-menu/
39
+ *
40
+ * @description A MultiSelectMenu allows multiple {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems} to be selected from a list or groups of lists.
41
+ *
42
+ * @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
43
+ */
44
+
37
45
  export const MultiSelectMenu = ({
38
46
  children,
39
47
  stateReducer: externalStateReducer,
@@ -74,6 +82,7 @@ export const MultiSelectMenu = ({
74
82
  defaultHighlightedIndex: 0,
75
83
  stateReducer: (state, actionAndChanges) => {
76
84
  let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
85
+ // eslint-disable-next-line react-hooks/rules-of-hooks
77
86
  newState = useSelectStateReducer(state, actionAndChanges);
78
87
  newState = selectReducerForMultiSelect(state, {
79
88
  ...actionAndChanges,
@@ -91,7 +100,6 @@ export const MultiSelectMenu = ({
91
100
  case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
92
101
  case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
93
102
  case useSelect.stateChangeTypes.ItemClick:
94
- case useSelect.stateChangeTypes.ToggleButtonBlur:
95
103
  if (!newSelectedItem) break;
96
104
  if (selectedItems?.find((item) => item?.id === newSelectedItem.id)) {
97
105
  removeSelectedItem(newSelectedItem);
@@ -1,17 +1,16 @@
1
+ /* eslint-disable testing-library/prefer-find-by */
2
+ /* eslint-disable testing-library/no-wait-for-multiple-assertions */
1
3
  /* eslint-disable testing-library/no-unnecessary-act */
2
4
  import {
3
5
  MultiSelectMenu,
4
6
  MenuContent,
5
7
  MenuToggleButton,
6
- MenuItemSelectable,
8
+ MenuItem,
7
9
  MenuGroup,
8
10
  } from "../../index";
9
11
  import {
10
12
  render,
11
13
  screen,
12
- fireEvent,
13
- act,
14
- cleanup,
15
14
  waitFor,
16
15
  } from "@sproutsocial/seeds-react-testing-library";
17
16
 
@@ -23,14 +22,10 @@ const menuItems = [
23
22
  ];
24
23
 
25
24
  describe("MultiSelectMenu", () => {
26
- afterEach(() => {
27
- cleanup();
28
- });
29
-
30
- it("should render the menuItemSelectables when provided as children", async () => {
25
+ it("should render the MenuItems when provided as children", async () => {
31
26
  const { user } = render(
32
27
  <MultiSelectMenu
33
- triggerElement={
28
+ menuToggleElement={
34
29
  <MenuToggleButton aria-label="testable menu" role="button">
35
30
  Testable Menu
36
31
  </MenuToggleButton>
@@ -39,9 +34,9 @@ describe("MultiSelectMenu", () => {
39
34
  <MenuContent>
40
35
  <MenuGroup>
41
36
  {menuItems.map((item) => (
42
- <MenuItemSelectable key={item.id} id={item.id} label={item.label}>
37
+ <MenuItem key={item.id} id={item.id} label={item.label}>
43
38
  {item.label}
44
- </MenuItemSelectable>
39
+ </MenuItem>
45
40
  ))}
46
41
  </MenuGroup>
47
42
  </MenuContent>
@@ -70,10 +65,10 @@ describe("MultiSelectMenu", () => {
70
65
  });
71
66
  });
72
67
 
73
- xit("should allow for multiple items to be selected", async () => {
68
+ it("should allow for multiple items to be selected", async () => {
74
69
  const { user } = render(
75
70
  <MultiSelectMenu
76
- triggerElement={
71
+ menuToggleElement={
77
72
  <MenuToggleButton aria-label="testable menu" role="button">
78
73
  Testable Menu
79
74
  </MenuToggleButton>
@@ -82,14 +77,14 @@ describe("MultiSelectMenu", () => {
82
77
  <MenuContent>
83
78
  <MenuGroup>
84
79
  {menuItems.map((item) => (
85
- <MenuItemSelectable
80
+ <MenuItem
86
81
  key={item.id}
87
82
  id={item.id}
88
83
  data-testid={item.id}
89
84
  label={item.label}
90
85
  >
91
86
  {item.label}
92
- </MenuItemSelectable>
87
+ </MenuItem>
93
88
  ))}
94
89
  </MenuGroup>
95
90
  </MenuContent>
@@ -113,23 +108,25 @@ describe("MultiSelectMenu", () => {
113
108
 
114
109
  await waitFor(() => {
115
110
  // Check that the first item is selected
116
- expect(item1).toHaveAttribute("aria-checked", "true");
111
+ expect(item1).toHaveAttribute("aria-selected", "true");
117
112
  });
118
113
 
119
114
  // Select second item
120
- // TODO: can this be done in the same act as the first item?
121
115
  await user.click(item2);
122
116
 
123
117
  await waitFor(() => {
124
118
  // Check that the second item is selected
125
- expect(item2).toHaveAttribute("aria-checked", "true");
119
+ expect(item2).toHaveAttribute("aria-selected", "true");
126
120
  });
121
+
122
+ // check that first item is still selected
123
+ expect(item1).toHaveAttribute("aria-selected", "true");
127
124
  });
128
125
 
129
- xit("Should maintain selected state when closed and reopened", async () => {
126
+ it("Should maintain selected state when closed and reopened", async () => {
130
127
  const { user } = render(
131
128
  <MultiSelectMenu
132
- triggerElement={
129
+ menuToggleElement={
133
130
  <MenuToggleButton aria-label="testable menu" role="button">
134
131
  Testable Menu
135
132
  </MenuToggleButton>
@@ -138,14 +135,14 @@ describe("MultiSelectMenu", () => {
138
135
  <MenuContent>
139
136
  <MenuGroup>
140
137
  {menuItems.map((item) => (
141
- <MenuItemSelectable
138
+ <MenuItem
142
139
  key={item.id}
143
140
  id={item.id}
144
141
  data-testid={item.id}
145
142
  label={item.label}
146
143
  >
147
144
  {item.label}
148
- </MenuItemSelectable>
145
+ </MenuItem>
149
146
  ))}
150
147
  </MenuGroup>
151
148
  </MenuContent>
@@ -167,30 +164,32 @@ describe("MultiSelectMenu", () => {
167
164
  await user.click(item1);
168
165
 
169
166
  await waitFor(() => {
170
- expect(item1).toHaveAttribute("aria-checked", "true");
167
+ expect(item1).toHaveAttribute("aria-selected", "true");
171
168
  });
172
169
 
173
170
  // Close Menu (MultiSelect does not close automatically)
174
171
  await user.click(toggleButton);
175
172
 
176
173
  // Make sure the menu is closed
177
- expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
174
+ await waitFor(() =>
175
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
176
+ );
178
177
 
179
178
  // Open menu again
180
179
  await user.click(toggleButton);
181
180
 
182
- await waitFor(() => {
183
- // Make sure the menu is open
184
- expect(screen.getByRole("listbox")).toBeInTheDocument();
185
- // Make sure the item is still selected
186
- expect(item1).toHaveAttribute("aria-checked", "true");
187
- });
181
+ // Make sure the menu is open
182
+ await waitFor(() =>
183
+ expect(screen.getByRole("listbox")).toBeInTheDocument()
184
+ );
185
+ // Make sure the item is still selected
186
+ expect(item1).toHaveAttribute("aria-selected", "true");
188
187
  });
189
188
 
190
- xit("should be able to navigate using the keyboard", async () => {
189
+ it("should be able to navigate using the keyboard", async () => {
191
190
  const { user } = render(
192
191
  <MultiSelectMenu
193
- triggerElement={
192
+ menuToggleElement={
194
193
  <MenuToggleButton aria-label="testable menu" role="button">
195
194
  Testable Menu
196
195
  </MenuToggleButton>
@@ -199,151 +198,153 @@ describe("MultiSelectMenu", () => {
199
198
  <MenuContent>
200
199
  <MenuGroup>
201
200
  {menuItems.map((item) => (
202
- <MenuItemSelectable
201
+ <MenuItem
203
202
  key={item.id}
204
203
  id={item.id}
205
204
  data-testid={item.id}
206
205
  label={item.label}
207
206
  >
208
207
  {item.label}
209
- </MenuItemSelectable>
208
+ </MenuItem>
210
209
  ))}
211
210
  </MenuGroup>
212
211
  </MenuContent>
213
212
  </MultiSelectMenu>
214
213
  );
215
214
 
216
- const toggleButton = screen.getByRole("button", { name: "testable menu" });
215
+ // Open Menu
216
+ await user.keyboard("{Tab}{Enter}");
217
+ expect(await screen.findByRole("listbox")).toBeInTheDocument();
217
218
 
218
- // Open Menu;
219
- fireEvent.focus(toggleButton);
220
- await user.keyboard("{Enter}");
219
+ // Check that the first item is highlighted
221
220
  await user.keyboard("{ArrowDown}");
222
-
223
- await waitFor(() => {
224
- // Check that the first item is highlighted
225
- const item1 = screen.getByRole("option", { name: "Item 1" });
226
- expect(item1).toHaveFocus();
227
- });
221
+ expect(screen.getByRole("listbox")).toHaveAttribute(
222
+ "aria-activedescendant",
223
+ screen.getByRole("option", { name: "Item 1" }).getAttribute("id")
224
+ );
228
225
 
229
226
  // Select the first item
230
227
  await user.keyboard("{Enter}");
231
- await user.keyboard("{ArrowDown}");
228
+
232
229
  // select the second item
230
+ await user.keyboard("{ArrowDown}");
233
231
  await user.keyboard(" ");
234
232
 
233
+ // Check that the second item is highlighted and selected
234
+ const item2 = screen.getByRole("option", { name: "Item 2" });
235
+ await waitFor(() => expect(item2).toHaveAttribute("aria-selected", "true"));
236
+ expect(screen.getByRole("listbox")).toHaveAttribute(
237
+ "aria-activedescendant",
238
+ item2.getAttribute("id")
239
+ );
240
+
241
+ // Check that the first item is still selected
242
+ const item1 = screen.getByRole("option", { name: "Item 1" });
243
+ expect(item1).toHaveAttribute("aria-selected", "true");
244
+ });
245
+
246
+ it("Should work with a initially selected items", async () => {
247
+ const { user } = render(
248
+ <MultiSelectMenu
249
+ menuToggleElement={
250
+ <MenuToggleButton aria-label="testable menu" role="button">
251
+ Testable Menu
252
+ </MenuToggleButton>
253
+ }
254
+ selectedItems={[menuItems[1], menuItems[2]]}
255
+ >
256
+ <MenuContent>
257
+ <MenuGroup>
258
+ {menuItems.map((item) => (
259
+ <MenuItem
260
+ key={item.id}
261
+ id={item.id}
262
+ data-testid={item.id}
263
+ label={item.label}
264
+ >
265
+ {item.label}
266
+ </MenuItem>
267
+ ))}
268
+ </MenuGroup>
269
+ </MenuContent>
270
+ </MultiSelectMenu>
271
+ );
272
+
273
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
274
+
275
+ const toggleButton = screen.getByRole("button", {
276
+ name: "testable menu",
277
+ });
278
+
279
+ // Open Menu
280
+ await user.click(toggleButton);
281
+
282
+ const item1 = screen.getByRole("option", { name: "Item 1" });
283
+ const item2 = screen.getByRole("option", { name: "Item 2" });
284
+ const item3 = screen.getByRole("option", { name: "Item 3" });
285
+
235
286
  await waitFor(() => {
236
- // Check that the second item is focused and selected
237
- const item2 = screen.getByRole("option", { name: "Item 2" });
238
- expect(item2).toHaveFocus();
239
- expect(item2).toBeChecked();
240
-
241
- // Check that the first item is still selected
242
- const item1 = screen.getByRole("menuitem", { name: "Item 1" });
243
- expect(item1).toBeChecked();
287
+ expect(item1).not.toHaveStyle("pointer-events: none");
244
288
  });
245
289
 
246
- xit("should stay open after menu item selection", async () => {
247
- const { user } = render(
248
- <MultiSelectMenu
249
- triggerElement={
250
- <MenuToggleButton aria-label="testable menu" role="button">
251
- Testable Menu
252
- </MenuToggleButton>
253
- }
254
- >
255
- <MenuContent>
256
- <MenuGroup>
257
- {menuItems.map((item) => (
258
- <MenuItemSelectable
259
- key={item.id}
260
- id={item.id}
261
- data-testid={item.id}
262
- label={item.label}
263
- >
264
- {item.label}
265
- </MenuItemSelectable>
266
- ))}
267
- </MenuGroup>
268
- </MenuContent>
269
- </MultiSelectMenu>
270
- );
271
-
272
- const menuContentContainer = screen.queryByRole("listbox");
273
-
274
- expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
275
-
276
- const toggleButton = screen.getByRole("button", {
277
- name: "testable menu",
278
- });
279
-
280
- // Open Menu
281
- await user.click(toggleButton);
282
-
283
- const item1 = screen.getByRole("option", { name: "Item 1" });
284
-
285
- await waitFor(() => {
286
- expect(item1).not.toHaveStyle("pointer-events: none");
287
- });
288
-
289
- // Select Item 1
290
- await user.click(item1);
291
-
292
- await waitFor(() => {
293
- expect(menuContentContainer).toBeInTheDocument();
294
- });
290
+ // Check that the first item is not selected
291
+ expect(item1).not.toHaveAttribute("aria-selected", "true");
292
+ // Check that the second item is selected
293
+ expect(item2).toHaveAttribute("aria-selected", "true");
294
+ // Check that the third item is selected
295
+ expect(item3).toHaveAttribute("aria-selected", "true");
296
+ });
297
+
298
+ it("should stay open after menu item selection", async () => {
299
+ const mockSelectedItemsChange = jest.fn();
300
+ const { user } = render(
301
+ <MultiSelectMenu
302
+ menuToggleElement={
303
+ <MenuToggleButton aria-label="testable menu" role="button">
304
+ Testable Menu
305
+ </MenuToggleButton>
306
+ }
307
+ onSelectedItemsChange={mockSelectedItemsChange}
308
+ >
309
+ <MenuContent>
310
+ <MenuGroup>
311
+ {menuItems.map((item) => (
312
+ <MenuItem
313
+ key={item.id}
314
+ id={item.id}
315
+ data-testid={item.id}
316
+ label={item.label}
317
+ >
318
+ {item.label}
319
+ </MenuItem>
320
+ ))}
321
+ </MenuGroup>
322
+ </MenuContent>
323
+ </MultiSelectMenu>
324
+ );
325
+
326
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
327
+
328
+ const toggleButton = screen.getByRole("button", {
329
+ name: "testable menu",
295
330
  });
296
331
 
297
- xit("Should work with a initially selected items", async () => {
298
- const { user } = render(
299
- <MultiSelectMenu
300
- triggerElement={
301
- <MenuToggleButton aria-label="testable menu" role="button">
302
- Testable Menu
303
- </MenuToggleButton>
304
- }
305
- defaultSelectedItems={[menuItems[1], menuItems[2]]}
306
- >
307
- <MenuContent>
308
- <MenuGroup>
309
- {menuItems.map((item) => (
310
- <MenuItemSelectable
311
- key={item.id}
312
- id={item.id}
313
- data-testid={item.id}
314
- label={item.label}
315
- >
316
- {item.label}
317
- </MenuItemSelectable>
318
- ))}
319
- </MenuGroup>
320
- </MenuContent>
321
- </MultiSelectMenu>
322
- );
323
-
324
- expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
325
-
326
- const toggleButton = screen.getByRole("button", {
327
- name: "testable menu",
328
- });
329
-
330
- // Open Menu
331
- await user.click(toggleButton);
332
-
333
- const item1 = screen.getByRole("option", { name: "Item 1" });
334
- const item2 = screen.getByRole("option", { name: "Item 2" });
335
- const item3 = screen.getByRole("option", { name: "Item 3" });
336
-
337
- await waitFor(() => {
338
- expect(item1).not.toHaveStyle("pointer-events: none");
339
- });
340
-
341
- act(() => {
342
- // Check that the second item is selected
343
- expect(item2).toHaveAttribute("aria-checked", "true");
344
- // Check that the third item is selected
345
- expect(item3).toHaveAttribute("aria-checked", "true");
346
- });
332
+ // Open Menu
333
+ await user.click(toggleButton);
334
+
335
+ const item1 = screen.getByRole("option", { name: "Item 1" });
336
+
337
+ await waitFor(() => {
338
+ expect(item1).not.toHaveStyle("pointer-events: none");
347
339
  });
340
+
341
+ // Select Item 1
342
+ await user.click(item1);
343
+
344
+ // Check that the menu is still open
345
+ expect(await screen.findByRole("listbox")).toBeInTheDocument();
346
+
347
+ // Check that callback called
348
+ expect(mockSelectedItemsChange).toHaveBeenCalledTimes(1);
348
349
  });
349
350
  });
@@ -9,20 +9,20 @@ import {
9
9
  export const MultiSelectMenuTypeTest = () => (
10
10
  <>
11
11
  {/* Valid MultiSelectMenu with minimal required props */}
12
- <MultiSelectMenu triggerElement={<>test</>}>Test</MultiSelectMenu>
12
+ <MultiSelectMenu menuToggleElement={<>test</>}>Test</MultiSelectMenu>
13
13
  {/* Valid MultiSelectMenu allows MenuContent by itself as a child */}
14
- <MultiSelectMenu triggerElement={<>test</>}>
14
+ <MultiSelectMenu menuToggleElement={<>test</>}>
15
15
  <MenuContent>Test</MenuContent>
16
16
  </MultiSelectMenu>
17
17
  {/* Valid MultiSelectMenu allows multiple children without fragment */}
18
- <MultiSelectMenu triggerElement={<>test</>}>
18
+ <MultiSelectMenu menuToggleElement={<>test</>}>
19
19
  <MenuHeader>Test</MenuHeader>
20
20
  <MenuContent>Test</MenuContent>
21
21
  <MenuFooter>Test</MenuFooter>
22
22
  </MultiSelectMenu>
23
23
  {/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
24
24
  <MultiSelectMenu
25
- triggerElement={<>test</>}
25
+ menuToggleElement={<>test</>}
26
26
  onSelectedItemChange={() => {}}
27
27
  selectedItem={null}
28
28
  stateReducer={(c) => c}
@@ -34,7 +34,7 @@ export const MultiSelectMenuTypeTest = () => (
34
34
  </MultiSelectMenu>
35
35
  {/* Valid MultiSelectMenu accepts Downshift props and functions for useMultipleSelection */}
36
36
  <MultiSelectMenu
37
- triggerElement={<>test</>}
37
+ menuToggleElement={<>test</>}
38
38
  onSelectedItemsChange={() => {}}
39
39
  selectedItems={[]}
40
40
  multiSelectStateReducer={(c) => c}
@@ -44,19 +44,19 @@ export const MultiSelectMenuTypeTest = () => (
44
44
  Test
45
45
  </MultiSelectMenu>
46
46
 
47
- {/* @ts-expect-error - triggerElement is a required prop */}
47
+ {/* @ts-expect-error - menuToggleElement is a required prop */}
48
48
  <MultiSelectMenu>Test</MultiSelectMenu>
49
49
  <MultiSelectMenu
50
- /* @ts-expect-error - triggerElement must be a valid react element */
51
- triggerElement="test"
50
+ /* @ts-expect-error - menuToggleElement must be a valid react element */
51
+ menuToggleElement="test"
52
52
  >
53
53
  Test
54
54
  </MultiSelectMenu>
55
55
  {/* @ts-expect-error - when no children provided */}
56
- <MultiSelectMenu triggerElement="test" />
56
+ <MultiSelectMenu menuToggleElement="test" />
57
57
 
58
58
  <MultiSelectMenu
59
- triggerElement={<>test</>}
59
+ menuToggleElement={<>test</>}
60
60
  /* @ts-expect-error - isOpen can't be a string */
61
61
  isOpen="false"
62
62
  /* @ts-expect-error - onSelectedItemChange can't be a string */
@@ -10,6 +10,17 @@ Feature: Single Select Menu
10
10
  When an item is selected
11
11
  Then a selectedItem is set
12
12
 
13
+ Scenario: SingleSelectMenu remains open when interacting with the footer
14
+ Given the menu has a footer
15
+ And the footer contains an interactive element
16
+ When an interactive element in the footer is clicked
17
+ Then the menu will remain open
18
+
19
+ Scenario: SingleSelectMenu closes when a selection is made
20
+ Given the menu is using the built in behavior
21
+ When a menu item is selected
22
+ Then the menu will close
23
+
13
24
  Rule: API
14
25
  Background:
15
26
  Given an engineer is implementing a SingleSelectMenu
@@ -10,6 +10,15 @@ import { MenuRoot } from "../MenuRoot";
10
10
  import type { TypeDefaultItemProps } from "../types";
11
11
  import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
12
12
 
13
+ /**
14
+ * @link https://seeds.sproutsocial.com/components/singleselect-menu/
15
+ *
16
+ * @description A SingleSelectMenu allows a single selection of a {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItem} from a list or groups or lists.
17
+ *
18
+ * **To trigger actions from a menu:**
19
+ * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/ActionMenu | ActionMenu}
20
+ */
21
+
13
22
  export const SingleSelectMenu = ({
14
23
  children,
15
24
  stateReducer: externalStateReducer,
@@ -27,6 +36,7 @@ export const SingleSelectMenu = ({
27
36
  itemToString,
28
37
  stateReducer: (state, actionAndChanges) => {
29
38
  let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
39
+ // eslint-disable-next-line react-hooks/rules-of-hooks, prefer-const
30
40
  newState = useSelectStateReducer(state, actionAndChanges);
31
41
  return externalStateReducer
32
42
  ? externalStateReducer(state, {