@sproutsocial/seeds-react-menu 0.6.0 → 1.0.0
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 +10 -10
- package/CHANGELOG.md +57 -0
- package/dist/esm/index.js +73 -30
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +34 -7
- package/dist/index.d.ts +34 -7
- package/dist/index.js +68 -25
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
- package/src/ActionMenu/ActionMenu.stories.tsx +20 -24
- package/src/Autocomplete/MultiAutocomplete.tsx +0 -1
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +36 -0
- package/src/MenuContent/MenuContent.tsx +2 -0
- package/src/MenuContext.tsx +2 -1
- package/src/MenuFooter/styles.tsx +1 -1
- package/src/MenuGroup/MenuGroup.tsx +5 -1
- package/src/MenuGroup/styles.tsx +1 -1
- package/src/MenuHeader/MenuHeader.tsx +10 -7
- package/src/MenuHeader/styles.tsx +4 -0
- package/src/MenuItem/MenuItem.tsx +5 -1
- package/src/MenuItem/styles.tsx +4 -2
- package/src/MenuItem/useOptionProps.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +17 -3
- package/src/MenuSearchInput/MenuSearchInput.tsx +30 -8
- package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +13 -3
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +51 -43
- package/src/NestedMenu/NestedMenuContent.tsx +5 -1
- package/src/NestedMenu/NestedMenuItem.tsx +5 -1
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +34 -28
- package/src/SubmenuItem/SubmenuItem.tsx +5 -1
- package/src/hooks/useDownshiftDefaults.tsx +11 -1
- package/src/hooks/useItemFiltering.tsx +2 -0
- package/src/hooks/useMenuChildren.tsx +11 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,19 +17,19 @@
|
|
|
17
17
|
"test:watch": "jest --watch --coverage=false"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@sproutsocial/seeds-react-box": "^1.1.
|
|
21
|
-
"@sproutsocial/seeds-react-button": "^1.2.
|
|
22
|
-
"@sproutsocial/seeds-react-checkbox": "^1.3.
|
|
23
|
-
"@sproutsocial/seeds-react-icon": "^1.1.
|
|
24
|
-
"@sproutsocial/seeds-react-input": "^1.
|
|
25
|
-
"@sproutsocial/seeds-react-label": "^1.0.
|
|
26
|
-
"@sproutsocial/seeds-react-popout": "^2.1.
|
|
27
|
-
"@sproutsocial/seeds-react-radio": "^1.
|
|
28
|
-
"@sproutsocial/seeds-react-switch": "^1.2.
|
|
29
|
-
"@sproutsocial/seeds-react-system-props": "^3.0.
|
|
30
|
-
"@sproutsocial/seeds-react-theme": "^2.
|
|
31
|
-
"@sproutsocial/seeds-react-token-input": "^1.0
|
|
32
|
-
"@sproutsocial/seeds-react-utilities": "^4.
|
|
20
|
+
"@sproutsocial/seeds-react-box": "^1.1.2",
|
|
21
|
+
"@sproutsocial/seeds-react-button": "^1.2.1",
|
|
22
|
+
"@sproutsocial/seeds-react-checkbox": "^1.3.1",
|
|
23
|
+
"@sproutsocial/seeds-react-icon": "^1.1.2",
|
|
24
|
+
"@sproutsocial/seeds-react-input": "^1.4.1",
|
|
25
|
+
"@sproutsocial/seeds-react-label": "^1.0.1",
|
|
26
|
+
"@sproutsocial/seeds-react-popout": "^2.1.1",
|
|
27
|
+
"@sproutsocial/seeds-react-radio": "^1.3.0",
|
|
28
|
+
"@sproutsocial/seeds-react-switch": "^1.2.1",
|
|
29
|
+
"@sproutsocial/seeds-react-system-props": "^3.0.2",
|
|
30
|
+
"@sproutsocial/seeds-react-theme": "^2.3.0",
|
|
31
|
+
"@sproutsocial/seeds-react-token-input": "^1.3.0",
|
|
32
|
+
"@sproutsocial/seeds-react-utilities": "^4.2.0",
|
|
33
33
|
"downshift": "9.0.4",
|
|
34
34
|
"motion": "^11.11.17"
|
|
35
35
|
},
|
|
@@ -160,6 +160,7 @@ export const ActionMenuWithIcons: Story = {
|
|
|
160
160
|
"pencil-to-square-outline",
|
|
161
161
|
"envelope-outline",
|
|
162
162
|
"chart-area-outline",
|
|
163
|
+
"circle-question-outline",
|
|
163
164
|
],
|
|
164
165
|
"Trigger Appearance": "secondary",
|
|
165
166
|
},
|
|
@@ -181,13 +182,9 @@ export const ActionMenuWithIcons: Story = {
|
|
|
181
182
|
key={0}
|
|
182
183
|
id="Compose"
|
|
183
184
|
>
|
|
184
|
-
<Box
|
|
185
|
-
display="flex"
|
|
186
|
-
alignItems="center"
|
|
187
|
-
justifyContent="space-between"
|
|
188
|
-
>
|
|
189
|
-
Compose
|
|
185
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
190
186
|
<Icon name={args["icons"][0] as TypeIconName} />
|
|
187
|
+
Compose
|
|
191
188
|
</Box>
|
|
192
189
|
</MenuItem>
|
|
193
190
|
<MenuItem
|
|
@@ -195,13 +192,9 @@ export const ActionMenuWithIcons: Story = {
|
|
|
195
192
|
key={1}
|
|
196
193
|
id="View Messages"
|
|
197
194
|
>
|
|
198
|
-
<Box
|
|
199
|
-
display="flex"
|
|
200
|
-
alignItems="center"
|
|
201
|
-
justifyContent="space-between"
|
|
202
|
-
>
|
|
203
|
-
View Messages
|
|
195
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
204
196
|
<Icon name={args["icons"][1] as TypeIconName} />
|
|
197
|
+
View Messages
|
|
205
198
|
</Box>
|
|
206
199
|
</MenuItem>
|
|
207
200
|
<MenuItem
|
|
@@ -209,13 +202,20 @@ export const ActionMenuWithIcons: Story = {
|
|
|
209
202
|
key={2}
|
|
210
203
|
id="Analyze post"
|
|
211
204
|
>
|
|
212
|
-
<Box
|
|
213
|
-
display="flex"
|
|
214
|
-
alignItems="center"
|
|
215
|
-
justifyContent="space-between"
|
|
216
|
-
>
|
|
217
|
-
Analyze Post
|
|
205
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
218
206
|
<Icon name={args["icons"][2] as TypeIconName} />
|
|
207
|
+
Analyze Post
|
|
208
|
+
</Box>
|
|
209
|
+
</MenuItem>
|
|
210
|
+
<MenuItem href="https://sproutsocial.com" target="_blank" id="help">
|
|
211
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
212
|
+
<Icon name={args["icons"][3] as TypeIconName} />
|
|
213
|
+
Visit Help Center
|
|
214
|
+
<Icon
|
|
215
|
+
name="arrow-right-up"
|
|
216
|
+
aria-label="open in a new window"
|
|
217
|
+
style={{ marginLeft: "auto" }}
|
|
218
|
+
/>
|
|
219
219
|
</Box>
|
|
220
220
|
</MenuItem>
|
|
221
221
|
</MenuGroup>
|
|
@@ -225,13 +225,9 @@ export const ActionMenuWithIcons: Story = {
|
|
|
225
225
|
key={3}
|
|
226
226
|
id="Settings"
|
|
227
227
|
>
|
|
228
|
-
<Box
|
|
229
|
-
display="flex"
|
|
230
|
-
alignItems="center"
|
|
231
|
-
justifyContent="space-between"
|
|
232
|
-
>
|
|
233
|
-
Settings
|
|
228
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
234
229
|
<Icon name="gear-outline" />
|
|
230
|
+
Settings
|
|
235
231
|
</Box>
|
|
236
232
|
</MenuItem>
|
|
237
233
|
</MenuGroup>
|
|
@@ -17,7 +17,6 @@ import { getSelectedItemIds } from "../MenuContext";
|
|
|
17
17
|
import { MenuRoot } from "../MenuRoot";
|
|
18
18
|
|
|
19
19
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
20
|
-
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
21
20
|
import type { TypeMultiAutocompleteProps } from "./AutocompleteTypes";
|
|
22
21
|
|
|
23
22
|
export const MultiAutocomplete = <
|
|
@@ -174,6 +174,21 @@ 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 () => {
|
|
178
|
+
const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
|
|
179
|
+
const inputElement = screen.getByRole("combobox");
|
|
180
|
+
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await user.click(inputElement);
|
|
183
|
+
await user.type(inputElement, "something not in the list");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
const listbox = screen.queryByRole("listbox");
|
|
188
|
+
expect(listbox).not.toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
177
192
|
describe("in multiSelect mode", () => {
|
|
178
193
|
it("should render multiple selected items", () => {
|
|
179
194
|
render(
|
|
@@ -330,5 +345,26 @@ describe("Autocomplete Component", () => {
|
|
|
330
345
|
})
|
|
331
346
|
).toBeInTheDocument();
|
|
332
347
|
});
|
|
348
|
+
|
|
349
|
+
it("should close the dropdown when no items match the input", async () => {
|
|
350
|
+
const { user } = render(
|
|
351
|
+
<Autocomplete
|
|
352
|
+
{...defaultAutocompleteProps}
|
|
353
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
354
|
+
multiSelect
|
|
355
|
+
/>
|
|
356
|
+
);
|
|
357
|
+
const inputElement = screen.getByRole("combobox");
|
|
358
|
+
|
|
359
|
+
await act(async () => {
|
|
360
|
+
await user.click(inputElement);
|
|
361
|
+
await user.type(inputElement, "something not in the list");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await waitFor(() => {
|
|
365
|
+
const listbox = screen.queryByRole("listbox");
|
|
366
|
+
expect(listbox).not.toBeInTheDocument();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
333
369
|
});
|
|
334
370
|
});
|
package/src/MenuContext.tsx
CHANGED
|
@@ -57,6 +57,7 @@ export interface TypeMenuContextBaseProps {
|
|
|
57
57
|
itemsMap?: { [key: string]: TypeInternalItemProps };
|
|
58
58
|
isListbox?: boolean;
|
|
59
59
|
hiddenItemsMap?: Record<string, boolean>;
|
|
60
|
+
hiddenItemsCount?: number;
|
|
60
61
|
setHiddenItemsMap?: React.Dispatch<
|
|
61
62
|
React.SetStateAction<Record<string, boolean>>
|
|
62
63
|
>;
|
|
@@ -202,7 +203,7 @@ export const useMenu = ({
|
|
|
202
203
|
|
|
203
204
|
const isItemSelected = React.useCallback(
|
|
204
205
|
(id: string) => checkIfItemSelected({ id, selectedItem, selectedItemIds }),
|
|
205
|
-
[selectedItem, selectedItemIds]
|
|
206
|
+
[selectedItem?.id, selectedItemIds]
|
|
206
207
|
);
|
|
207
208
|
|
|
208
209
|
const getInputComponentProps = React.useCallback<
|
|
@@ -2,6 +2,6 @@ import styled from "styled-components";
|
|
|
2
2
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
3
|
|
|
4
4
|
export const StyledMenuFooterBox = styled(Box)`
|
|
5
|
-
padding: ${(
|
|
5
|
+
padding: ${({ theme }) => theme.space[350]} ${({ theme }) => theme.space[400]};
|
|
6
6
|
border-top: 1px solid ${(p) => p.theme.colors.container.border.base};
|
|
7
7
|
`;
|
|
@@ -25,7 +25,7 @@ import { mapMenuItems } from "../hooks/useMenuChildren";
|
|
|
25
25
|
* @see {@link https://seeds.sproutsocial.com/components/menu-item/ | MenuItem}
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const MenuGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
29
29
|
titleAs = "h3",
|
|
30
30
|
children: childrenProp,
|
|
31
31
|
title,
|
|
@@ -68,3 +68,7 @@ export const MenuGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
|
68
68
|
</StyledMenuGroup>
|
|
69
69
|
);
|
|
70
70
|
};
|
|
71
|
+
|
|
72
|
+
MenuGroup.__MENU_PRIMITIVE_TYPE = "MenuGroup";
|
|
73
|
+
|
|
74
|
+
export { MenuGroup };
|
package/src/MenuGroup/styles.tsx
CHANGED
|
@@ -29,7 +29,7 @@ export const StyledMenuGroup = styled.li<TypeMenuGroupBaseProps>`
|
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
export const StyledTitle = styled(Text.SmallSubHeadline)`
|
|
32
|
-
font-size: ${({ theme }) => theme.typography[
|
|
32
|
+
font-size: ${({ theme }) => theme.typography[100].fontSize};
|
|
33
33
|
margin: ${({ theme }) => theme.space[400]} ${({ theme }) => theme.space[400]}
|
|
34
34
|
0px ${({ theme }) => theme.space[400]};
|
|
35
35
|
`;
|
|
@@ -22,17 +22,20 @@ const MenuHeader = ({
|
|
|
22
22
|
rightAction,
|
|
23
23
|
...rest
|
|
24
24
|
}: TypeMenuHeaderProps) => {
|
|
25
|
+
const showTopRow = title || leftAction || rightAction;
|
|
25
26
|
return (
|
|
26
27
|
<>
|
|
27
28
|
<MenuHeaderContainer $leftAction={!!leftAction} {...rest}>
|
|
28
|
-
|
|
29
|
-
{leftAction
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
{showTopRow && (
|
|
30
|
+
<MenuHeaderFeatures $leftAction={!!leftAction}>
|
|
31
|
+
{leftAction && leftAction}
|
|
32
|
+
{title && (
|
|
33
|
+
<MenuTitleText $leftAction={!!leftAction}>{title}</MenuTitleText>
|
|
34
|
+
)}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
{rightAction && <div className="right-action">{rightAction}</div>}
|
|
37
|
+
</MenuHeaderFeatures>
|
|
38
|
+
)}
|
|
36
39
|
|
|
37
40
|
{children && <MenuChildrenContainer>{children}</MenuChildrenContainer>}
|
|
38
41
|
</MenuHeaderContainer>
|
|
@@ -52,6 +52,10 @@ export const MenuTitleText = styled.h3<TypeMenuHeaderContainerProps>`
|
|
|
52
52
|
export const MenuChildrenContainer = styled.div`
|
|
53
53
|
grid-column: 1 / -1;
|
|
54
54
|
margin-top: ${({ theme }) => theme.space[300]};
|
|
55
|
+
|
|
56
|
+
&:first-child {
|
|
57
|
+
margin-top: 0;
|
|
58
|
+
}
|
|
55
59
|
`;
|
|
56
60
|
|
|
57
61
|
export const MenuHeaderFeatures = styled.div<TypeMenuHeaderContainerProps>`
|
|
@@ -11,7 +11,7 @@ import { useOptionProps } from "./useOptionProps";
|
|
|
11
11
|
// radio: "menuitemradio",
|
|
12
12
|
// } as const;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const MenuItem = ({
|
|
15
15
|
id,
|
|
16
16
|
children,
|
|
17
17
|
onClick,
|
|
@@ -69,3 +69,7 @@ export const MenuItem = ({
|
|
|
69
69
|
</MenuItemContainer>
|
|
70
70
|
);
|
|
71
71
|
};
|
|
72
|
+
|
|
73
|
+
MenuItem.__MENU_PRIMITIVE_TYPE = "MenuItem";
|
|
74
|
+
|
|
75
|
+
export { MenuItem };
|
package/src/MenuItem/styles.tsx
CHANGED
|
@@ -19,8 +19,10 @@ const highlightedStyles = css`
|
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
21
|
const activeStyles = css`
|
|
22
|
-
|
|
23
|
-
color: ${({ theme }) => theme.colors.
|
|
22
|
+
border: 1px solid ${({ theme }) => theme.colors.listItem.border.base};
|
|
23
|
+
background-color: ${({ theme }) => theme.colors.menuItem.background.active};
|
|
24
|
+
color: ${({ theme }) => theme.colors.text.body};
|
|
25
|
+
text-decoration: none;
|
|
24
26
|
`;
|
|
25
27
|
|
|
26
28
|
export const MenuItemContainer = styled.li`
|
|
@@ -83,11 +83,11 @@ export const useOptionProps = ({
|
|
|
83
83
|
inputLabelId,
|
|
84
84
|
...props
|
|
85
85
|
}: TypeMenuItemProps) => {
|
|
86
|
-
const { selectedItem,
|
|
86
|
+
const { selectedItem, selectedItemIds, isListbox, isItemSelected } =
|
|
87
87
|
useMenuContentContext();
|
|
88
88
|
const selected = useMemo(
|
|
89
89
|
() => isItemSelected(id),
|
|
90
|
-
[selectedItem,
|
|
90
|
+
[selectedItem?.id, selectedItemIds]
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
const label = inputLabelId || `menu-item-child-${id}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback } from "react";
|
|
1
|
+
import React, { useCallback, useEffect } from "react";
|
|
2
2
|
import styled from "styled-components";
|
|
3
3
|
import { Popout, type TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
4
4
|
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
type TypeMenuContextProps,
|
|
10
10
|
type TypeMenuProviderProps,
|
|
11
11
|
} from "../MenuContext";
|
|
12
|
-
import type { TypeInternalItemProps } from "../types";
|
|
13
12
|
|
|
14
13
|
export interface TypeMenuRootOwnProps {
|
|
15
14
|
menuToggleElement: React.ReactElement<any>;
|
|
@@ -42,7 +41,14 @@ export const MenuRoot = ({
|
|
|
42
41
|
width = "300px",
|
|
43
42
|
...contextProps
|
|
44
43
|
}: TypeMenuRootProps) => {
|
|
45
|
-
const {
|
|
44
|
+
const {
|
|
45
|
+
isOpen,
|
|
46
|
+
openMenu,
|
|
47
|
+
closeMenu,
|
|
48
|
+
inputValue,
|
|
49
|
+
hiddenItemsCount = 0,
|
|
50
|
+
items,
|
|
51
|
+
} = contextProps;
|
|
46
52
|
const setIsOpen = useCallback(
|
|
47
53
|
(newIsOpen: boolean) => {
|
|
48
54
|
newIsOpen ? openMenu?.() : closeMenu?.();
|
|
@@ -50,6 +56,14 @@ export const MenuRoot = ({
|
|
|
50
56
|
[openMenu, closeMenu]
|
|
51
57
|
);
|
|
52
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!inputValue) return;
|
|
61
|
+
|
|
62
|
+
if (items && hiddenItemsCount >= items.length && isOpen) {
|
|
63
|
+
closeMenu?.();
|
|
64
|
+
}
|
|
65
|
+
}, [hiddenItemsCount, items?.length, isOpen, inputValue]);
|
|
66
|
+
|
|
53
67
|
const menuContext = useMenu(contextProps);
|
|
54
68
|
|
|
55
69
|
return (
|
|
@@ -4,22 +4,34 @@ import {
|
|
|
4
4
|
useItemFiltering,
|
|
5
5
|
type TypeGetIsItemVisibleFn,
|
|
6
6
|
} from "../hooks/useItemFiltering";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useMenuContentContext,
|
|
9
|
+
type TypeDownshiftSelectReturnValue,
|
|
10
|
+
} from "../MenuContext";
|
|
8
11
|
|
|
9
12
|
export interface TypeMenuSearchInputProps extends TypeInputProps {
|
|
10
13
|
/* A function that can be passed to override the default filtering logic */
|
|
11
14
|
getIsItemVisible?: TypeGetIsItemVisibleFn;
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* <input
|
|
19
|
+
* aria-activedescendant=""
|
|
20
|
+
* aria-autocomplete="list"
|
|
21
|
+
* aria-controls="downshift-:r1:-menu"
|
|
22
|
+
* aria-expanded="true"
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* @link https://seeds.sproutsocial.com/components/menu-header/
|
|
16
28
|
*
|
|
17
29
|
* @description MenuSearchInput is search input that can be added to the MenuHeader to filter the items.
|
|
18
30
|
*/
|
|
19
|
-
|
|
20
31
|
export const MenuSearchInput = ({
|
|
21
32
|
getIsItemVisible,
|
|
22
|
-
|
|
33
|
+
inputProps,
|
|
34
|
+
...restInputProps
|
|
23
35
|
}: TypeMenuSearchInputProps) => {
|
|
24
36
|
const {
|
|
25
37
|
items,
|
|
@@ -37,16 +49,20 @@ export const MenuSearchInput = ({
|
|
|
37
49
|
|
|
38
50
|
React.useEffect(() => {
|
|
39
51
|
// handles when open/close state changes or if input is controlled
|
|
40
|
-
updateFilteredItems({ inputValue:
|
|
41
|
-
}, [isOpen,
|
|
52
|
+
updateFilteredItems({ inputValue: restInputProps.value || "" });
|
|
53
|
+
}, [isOpen, restInputProps.value]);
|
|
54
|
+
|
|
55
|
+
const toggleButtonProps: Partial<
|
|
56
|
+
ReturnType<Required<TypeDownshiftSelectReturnValue>["getToggleButtonProps"]>
|
|
57
|
+
> = getToggleButtonProps?.() || {};
|
|
42
58
|
|
|
43
59
|
const handleKeyDown = React.useCallback(
|
|
44
60
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
45
61
|
// prevents selecting items with space key when input is focused
|
|
46
62
|
if (e.key === " ") return;
|
|
47
|
-
|
|
63
|
+
toggleButtonProps.onKeyDown?.(e);
|
|
48
64
|
},
|
|
49
|
-
[
|
|
65
|
+
[toggleButtonProps.onKeyDown]
|
|
50
66
|
);
|
|
51
67
|
|
|
52
68
|
return (
|
|
@@ -57,7 +73,13 @@ export const MenuSearchInput = ({
|
|
|
57
73
|
// apply onKeyDown logic for downshift keyboard accessibility support
|
|
58
74
|
onKeyDown={handleKeyDown}
|
|
59
75
|
autoComplete={false}
|
|
60
|
-
|
|
76
|
+
aria-autocomplete="list"
|
|
77
|
+
inputProps={{
|
|
78
|
+
["aria-activedescendant"]: toggleButtonProps["aria-activedescendant"],
|
|
79
|
+
["aria-controls"]: toggleButtonProps["aria-controls"],
|
|
80
|
+
...inputProps,
|
|
81
|
+
}}
|
|
82
|
+
{...restInputProps}
|
|
61
83
|
/>
|
|
62
84
|
);
|
|
63
85
|
};
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
useItemFiltering,
|
|
4
4
|
type TypeGetIsItemVisibleFn,
|
|
5
5
|
} from "../hooks/useItemFiltering";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MenuContentContext,
|
|
8
|
+
useMenuContentContext,
|
|
9
|
+
type TypeDownshiftSelectReturnValue,
|
|
10
|
+
} from "../MenuContext";
|
|
7
11
|
import {
|
|
8
12
|
MenuTokenInput,
|
|
9
13
|
type TypeMenuTokenInputProps,
|
|
@@ -70,13 +74,17 @@ export const MenuSearchTokenInput = ({
|
|
|
70
74
|
updateFilteredState(controlledValue);
|
|
71
75
|
}, [isOpen]);
|
|
72
76
|
|
|
77
|
+
const toggleButtonProps: Partial<
|
|
78
|
+
ReturnType<Required<TypeDownshiftSelectReturnValue>["getToggleButtonProps"]>
|
|
79
|
+
> = getToggleButtonProps?.() || {};
|
|
80
|
+
|
|
73
81
|
const handleKeyDown = React.useCallback(
|
|
74
82
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
75
83
|
// prevents selecting items with space key when input is focused
|
|
76
84
|
if (e.key === " ") return;
|
|
77
|
-
|
|
85
|
+
toggleButtonProps.onKeyDown?.(e);
|
|
78
86
|
},
|
|
79
|
-
[
|
|
87
|
+
[toggleButtonProps.onKeyDown]
|
|
80
88
|
);
|
|
81
89
|
|
|
82
90
|
return (
|
|
@@ -89,6 +97,8 @@ export const MenuSearchTokenInput = ({
|
|
|
89
97
|
onKeyDown={handleKeyDown}
|
|
90
98
|
inputProps={{
|
|
91
99
|
value: internalValue,
|
|
100
|
+
["aria-activedescendant"]: toggleButtonProps["aria-activedescendant"],
|
|
101
|
+
["aria-controls"]: toggleButtonProps["aria-controls"],
|
|
92
102
|
...inputProps,
|
|
93
103
|
}}
|
|
94
104
|
MenuContext={MenuContentContext}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
|
|
4
4
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
5
|
+
import { action } from "@storybook/addon-actions";
|
|
5
6
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
6
7
|
import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
|
|
7
8
|
import { TEST_BOOKS, EXTRA_BOOKS } from "../__fixtures__/menu-test-data";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
MenuSearchTokenInput,
|
|
16
17
|
type TypeInternalItemProps,
|
|
17
18
|
type TypeMultiSelectMenuProps,
|
|
19
|
+
type TypeMultiSelectMenuBaseProps,
|
|
18
20
|
} from "../index";
|
|
19
21
|
|
|
20
22
|
type TypeMultiSelectMenuStoryProps = TypeMultiSelectMenuProps & {
|
|
@@ -135,54 +137,60 @@ export const MultiSelectMenuWithGroupsExample: Story = {
|
|
|
135
137
|
},
|
|
136
138
|
};
|
|
137
139
|
|
|
140
|
+
const TestControlledMultiSelectMenu = ({
|
|
141
|
+
inputType,
|
|
142
|
+
...args
|
|
143
|
+
}: TypeMultiSelectMenuBaseProps & {
|
|
144
|
+
inputType: "checkbox" | "switch";
|
|
145
|
+
}) => {
|
|
146
|
+
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
147
|
+
const [selectedItems, setSelectedItems] = React.useState<
|
|
148
|
+
TypeInternalItemProps[]
|
|
149
|
+
>([]);
|
|
150
|
+
return (
|
|
151
|
+
<MultiSelectMenu
|
|
152
|
+
{...args}
|
|
153
|
+
isOpen={isOpen}
|
|
154
|
+
onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
|
|
155
|
+
selectedItems={selectedItems}
|
|
156
|
+
onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
|
|
157
|
+
setSelectedItems(newSelectedItems);
|
|
158
|
+
}}
|
|
159
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
160
|
+
>
|
|
161
|
+
<MenuContent>
|
|
162
|
+
<MenuGroup title="Classics" id="classics">
|
|
163
|
+
{TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
164
|
+
<MenuItem
|
|
165
|
+
inputType={inputType}
|
|
166
|
+
key={itemsToRender.id}
|
|
167
|
+
id={itemsToRender.id}
|
|
168
|
+
>
|
|
169
|
+
{itemsToRender.title}
|
|
170
|
+
</MenuItem>
|
|
171
|
+
))}
|
|
172
|
+
</MenuGroup>
|
|
173
|
+
<MenuGroup title="Essentials" id="essentials">
|
|
174
|
+
{EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
175
|
+
<MenuItem
|
|
176
|
+
inputType={inputType}
|
|
177
|
+
key={itemsToRender.id}
|
|
178
|
+
id={itemsToRender.id}
|
|
179
|
+
>
|
|
180
|
+
{itemsToRender.title}
|
|
181
|
+
</MenuItem>
|
|
182
|
+
))}
|
|
183
|
+
</MenuGroup>
|
|
184
|
+
</MenuContent>
|
|
185
|
+
</MultiSelectMenu>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
138
188
|
export const MultiSelectMenuControlled: Story = {
|
|
139
189
|
name: "Controlled State",
|
|
140
190
|
args: {
|
|
141
191
|
inputType: "checkbox",
|
|
142
192
|
},
|
|
143
|
-
render: (
|
|
144
|
-
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
145
|
-
const [selectedItems, setSelectedItems] = React.useState<
|
|
146
|
-
TypeInternalItemProps[]
|
|
147
|
-
>([]);
|
|
148
|
-
return (
|
|
149
|
-
<MultiSelectMenu
|
|
150
|
-
isOpen={isOpen}
|
|
151
|
-
onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
|
|
152
|
-
selectedItems={selectedItems}
|
|
153
|
-
onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
|
|
154
|
-
setSelectedItems(newSelectedItems);
|
|
155
|
-
}}
|
|
156
|
-
menuToggleElement={<StorybookMenuToggleButton />}
|
|
157
|
-
{...args}
|
|
158
|
-
>
|
|
159
|
-
<MenuContent>
|
|
160
|
-
<MenuGroup title="Classics" id="classics">
|
|
161
|
-
{TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
162
|
-
<MenuItem
|
|
163
|
-
inputType={inputType}
|
|
164
|
-
key={itemsToRender.id}
|
|
165
|
-
id={itemsToRender.id}
|
|
166
|
-
>
|
|
167
|
-
{itemsToRender.title}
|
|
168
|
-
</MenuItem>
|
|
169
|
-
))}
|
|
170
|
-
</MenuGroup>
|
|
171
|
-
<MenuGroup title="Essentials" id="essentials">
|
|
172
|
-
{EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
173
|
-
<MenuItem
|
|
174
|
-
inputType={inputType}
|
|
175
|
-
key={itemsToRender.id}
|
|
176
|
-
id={itemsToRender.id}
|
|
177
|
-
>
|
|
178
|
-
{itemsToRender.title}
|
|
179
|
-
</MenuItem>
|
|
180
|
-
))}
|
|
181
|
-
</MenuGroup>
|
|
182
|
-
</MenuContent>
|
|
183
|
-
</MultiSelectMenu>
|
|
184
|
-
);
|
|
185
|
-
},
|
|
193
|
+
render: (args) => <TestControlledMultiSelectMenu {...args} />,
|
|
186
194
|
};
|
|
187
195
|
|
|
188
196
|
export const MultiSelectMenuWithSearch: Story = {
|
|
@@ -8,7 +8,7 @@ import { useEffect, useRef } from "react";
|
|
|
8
8
|
import { mergeRefs } from "@sproutsocial/seeds-react-utilities/src/index";
|
|
9
9
|
import { NestedMenuItem } from "./NestedMenuItem";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const NestedMenuContent = <
|
|
12
12
|
I extends TypeMenuItemProps = TypeNestedMenuItemProps
|
|
13
13
|
>({
|
|
14
14
|
innerRef: innerRefProp = null,
|
|
@@ -69,3 +69,7 @@ export const NestedMenuContent = <
|
|
|
69
69
|
/>
|
|
70
70
|
);
|
|
71
71
|
};
|
|
72
|
+
|
|
73
|
+
NestedMenuContent.__MENU_PRIMITIVE_TYPE = "MenuContent";
|
|
74
|
+
|
|
75
|
+
export { NestedMenuContent };
|
|
@@ -4,7 +4,7 @@ import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
|
4
4
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
5
5
|
import type { TypeNestedMenuItemProps } from "./NestedMenuTypes";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const NestedMenuItem = ({
|
|
8
8
|
children,
|
|
9
9
|
childMenuId,
|
|
10
10
|
onClick: onClickProp,
|
|
@@ -35,3 +35,7 @@ export const NestedMenuItem = ({
|
|
|
35
35
|
</MenuItem>
|
|
36
36
|
);
|
|
37
37
|
};
|
|
38
|
+
|
|
39
|
+
NestedMenuItem.__MENU_PRIMITIVE_TYPE = "MenuItem";
|
|
40
|
+
|
|
41
|
+
export { NestedMenuItem };
|