@sproutsocial/seeds-react-menu 0.2.1 → 0.2.3
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/.eslintrc.js +2 -6
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +13 -0
- package/dist/esm/index.js +12 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +34 -12
- package/dist/index.d.ts +34 -12
- package/dist/index.js +7 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/src/ActionMenu/ActionMenu.tsx +4 -19
- package/src/ActionMenu/ActionMenuTypes.ts +12 -0
- package/src/ActionMenu/index.ts +1 -0
- package/src/{ComboBox/ComboBox.feature → Autocomplete/Autocomplete.feature} +16 -16
- package/src/Autocomplete/Autocomplete.tsx +65 -0
- package/src/Autocomplete/AutocompleteTypes.ts +8 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +113 -0
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +78 -0
- package/src/Autocomplete/index.ts +2 -0
- package/src/MenuContent/MenuContent.tsx +1 -1
- package/src/MenuContent/MenuContentTypes.ts +2 -1
- package/src/MenuContext.tsx +71 -10
- package/src/MenuGroup/MenuGroupTypes.ts +0 -1
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +0 -1
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +0 -1
- package/src/MenuItem/MenuItem.tsx +0 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +4 -1
- package/src/MenuItem/styles.tsx +1 -1
- package/src/MenuItem/useOptionProps.tsx +0 -1
- package/src/MenuRoot/MenuRoot.tsx +2 -2
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +1 -2
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +0 -1
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +5 -1
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +5 -2
- package/src/MultiSelectMenu/index.ts +1 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +0 -1
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +2 -3
- package/src/hooks/useItemsFromChildren.tsx +7 -5
- package/src/index.ts +1 -0
- package/src/reducers/useComboboxStateReducer.tsx +26 -0
- package/src/types.ts +2 -5
- package/tsconfig.json +1 -1
- package/.eslintignore +0 -6
- package/src/ComboBox/TokenInput.feature +0 -84
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
"clean:modules": "rm -rf node_modules",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"test": "jest",
|
|
17
|
-
"test:watch": "jest --watch --coverage=false"
|
|
18
|
-
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
19
|
-
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
|
17
|
+
"test:watch": "jest --watch --coverage=false"
|
|
20
18
|
},
|
|
21
19
|
"dependencies": {
|
|
22
20
|
"@sproutsocial/seeds-react-box": "*",
|
|
@@ -32,7 +30,7 @@
|
|
|
32
30
|
"motion": "^11.11.17"
|
|
33
31
|
},
|
|
34
32
|
"devDependencies": {
|
|
35
|
-
"eslint-config-seeds": "*",
|
|
33
|
+
"@sproutsocial/eslint-config-seeds": "*",
|
|
36
34
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
37
35
|
"@sproutsocial/seeds-testing": "*",
|
|
38
36
|
"@types/react": "^18.0.0",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useSelect,
|
|
4
|
-
type UseSelectProps,
|
|
5
4
|
type UseSelectState,
|
|
6
5
|
type UseSelectStateChange,
|
|
7
6
|
} from "downshift";
|
|
@@ -11,22 +10,9 @@ import {
|
|
|
11
10
|
defaultUseSelectProps,
|
|
12
11
|
useSelectStateReducer,
|
|
13
12
|
} from "../reducers/useSelectStateReducer";
|
|
14
|
-
import {
|
|
15
|
-
MenuRoot,
|
|
16
|
-
type TypeMenuRootProps,
|
|
17
|
-
type TypeMenuRootOwnProps,
|
|
18
|
-
} from "../MenuRoot";
|
|
13
|
+
import { MenuRoot } from "../MenuRoot";
|
|
19
14
|
import { itemToString } from "../utils/itemToString";
|
|
20
|
-
|
|
21
|
-
type TypeInvalidMenuProps =
|
|
22
|
-
| "initialSelectedItem"
|
|
23
|
-
| "defaultSelectedItem"
|
|
24
|
-
| "selectedItem";
|
|
25
|
-
export interface TypeActionMenuProps
|
|
26
|
-
extends TypeMenuRootOwnProps,
|
|
27
|
-
Partial<Omit<UseSelectProps<TypeDefaultItemProps>, TypeInvalidMenuProps>> {
|
|
28
|
-
children: TypeMenuRootProps["children"];
|
|
29
|
-
}
|
|
15
|
+
import type { TypeActionMenuProps } from "./ActionMenuTypes";
|
|
30
16
|
|
|
31
17
|
// Manually fire the onClick handler when Enter or Space is pressed
|
|
32
18
|
const handleStateChange = (
|
|
@@ -49,7 +35,7 @@ const handleStateChange = (
|
|
|
49
35
|
if (!selectedItem) return;
|
|
50
36
|
|
|
51
37
|
// if selectedItem is disabled, do nothing
|
|
52
|
-
if (selectedItem.
|
|
38
|
+
if (selectedItem.disabled) {
|
|
53
39
|
return;
|
|
54
40
|
}
|
|
55
41
|
|
|
@@ -58,7 +44,7 @@ const handleStateChange = (
|
|
|
58
44
|
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
|
|
59
45
|
|
|
60
46
|
// selectedItem props
|
|
61
|
-
const selectedItemProps = selectedItem.
|
|
47
|
+
const selectedItemProps = selectedItem.menuItemProps;
|
|
62
48
|
|
|
63
49
|
if (selectedItemProps?.onClick) {
|
|
64
50
|
// Create a synthetic event to pass into the onClick
|
|
@@ -111,7 +97,6 @@ export const ActionMenu = ({
|
|
|
111
97
|
onStateChange: (changes) => handleStateChange(changes, currentSelectedItem),
|
|
112
98
|
stateReducer: (state, actionAndChanges) => {
|
|
113
99
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
114
|
-
// eslint-disable-next-line prefer-const, react-hooks/rules-of-hooks
|
|
115
100
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
116
101
|
return externalStateReducer
|
|
117
102
|
? externalStateReducer(state, {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TypeMenuRootProps, type TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
|
+
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
3
|
+
|
|
4
|
+
type TypeInvalidMenuProps =
|
|
5
|
+
| "initialSelectedItem"
|
|
6
|
+
| "defaultSelectedItem"
|
|
7
|
+
| "selectedItem";
|
|
8
|
+
export interface TypeActionMenuProps
|
|
9
|
+
extends TypeMenuRootOwnProps,
|
|
10
|
+
Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {
|
|
11
|
+
children: TypeMenuRootProps["children"];
|
|
12
|
+
}
|
package/src/ActionMenu/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Feature:
|
|
1
|
+
Feature: Autocomplete
|
|
2
2
|
|
|
3
3
|
A component that allows users to search, select, and manage items with autocomplete functionality.
|
|
4
4
|
|
|
@@ -8,31 +8,31 @@ Feature: ComboBox
|
|
|
8
8
|
Given an end user is using the component
|
|
9
9
|
|
|
10
10
|
Scenario: User can autocomplete text inputs
|
|
11
|
-
Given the
|
|
11
|
+
Given the Autocomplete Input is focused
|
|
12
12
|
When text is typed into the input
|
|
13
13
|
Then a list of matching items is displayed
|
|
14
14
|
And an item can be selected from the list
|
|
15
15
|
|
|
16
16
|
Scenario: User can add a new value
|
|
17
|
-
Given the
|
|
17
|
+
Given the Autocomplete Input has a value
|
|
18
18
|
When new values are allowed
|
|
19
19
|
And the value has no match
|
|
20
20
|
Then a new value can be created
|
|
21
21
|
|
|
22
22
|
Scenario: User gets a warning on error
|
|
23
|
-
Given the
|
|
23
|
+
Given the Autocomplete Input has a value
|
|
24
24
|
When the value is invalid
|
|
25
25
|
Then a warning message appears
|
|
26
26
|
|
|
27
27
|
Scenario: User can select an item from the list
|
|
28
|
-
Given the
|
|
28
|
+
Given the Autocomplete Input has text
|
|
29
29
|
When an item is selected
|
|
30
30
|
Then the item is marked as selected
|
|
31
31
|
And the input reflects the selected item
|
|
32
32
|
|
|
33
33
|
Scenario: Screen reader announces combobox
|
|
34
34
|
Given a screen reader is being used
|
|
35
|
-
When the
|
|
35
|
+
When the Autocomplete input is focused
|
|
36
36
|
Then the screen reader announces it as a "combobox"
|
|
37
37
|
|
|
38
38
|
Scenario: Component handles input change
|
|
@@ -40,7 +40,7 @@ Feature: ComboBox
|
|
|
40
40
|
Then the onChange callback fires
|
|
41
41
|
|
|
42
42
|
Scenario Outline: Custom open/close behavior
|
|
43
|
-
Given the
|
|
43
|
+
Given the Autocomplete is rendered with custom behavior
|
|
44
44
|
When the `<action>` is performed
|
|
45
45
|
Then the `<handler>` is executed
|
|
46
46
|
|
|
@@ -51,7 +51,7 @@ Feature: ComboBox
|
|
|
51
51
|
| input blurred | onBlur |
|
|
52
52
|
|
|
53
53
|
Scenario Outline: Autocomplete suggestions are displayed
|
|
54
|
-
Given the
|
|
54
|
+
Given the Autocomplete is rendered with items
|
|
55
55
|
When <text> is typed into the input
|
|
56
56
|
Then matching <suggestion> items are displayed
|
|
57
57
|
|
|
@@ -65,21 +65,21 @@ Feature: ComboBox
|
|
|
65
65
|
Background:
|
|
66
66
|
Given an engineer is developing using the component
|
|
67
67
|
|
|
68
|
-
Scenario:
|
|
69
|
-
When Menu features are used in
|
|
70
|
-
Then the
|
|
68
|
+
Scenario: Autocomplete extends Menu
|
|
69
|
+
When Menu features are used in Autocomplete
|
|
70
|
+
Then the Autocomplete responds as a Menu would
|
|
71
71
|
|
|
72
|
-
Scenario:
|
|
73
|
-
When a selected item is passed to the
|
|
72
|
+
Scenario: Autocomplete accepts selected item
|
|
73
|
+
When a selected item is passed to the Autocomplete
|
|
74
74
|
Then the input reflects the selected item
|
|
75
75
|
|
|
76
|
-
Scenario:
|
|
76
|
+
Scenario: Autocomplete hasWarning
|
|
77
77
|
When the input's validation has an error
|
|
78
78
|
Then 'hasWarning' becomes true
|
|
79
79
|
|
|
80
|
-
Scenario Outline:
|
|
80
|
+
Scenario Outline: Autocomplete checks its types
|
|
81
81
|
When <propName> is passed a value
|
|
82
|
-
Then
|
|
82
|
+
Then Autocomplete expects it to be of type <propType>
|
|
83
83
|
|
|
84
84
|
Examples: Prop Types
|
|
85
85
|
| propName | propType |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCombobox, type UseComboboxState } from "downshift";
|
|
3
|
+
import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
|
|
4
|
+
import {
|
|
5
|
+
defaultUseComboboxProps,
|
|
6
|
+
useComboboxStateReducer,
|
|
7
|
+
} from "../reducers/useComboboxStateReducer";
|
|
8
|
+
import { itemToString } from "../utils/itemToString";
|
|
9
|
+
import { MenuRoot } from "../MenuRoot";
|
|
10
|
+
import type { TypeAutocompleteProps } from "./AutocompleteTypes";
|
|
11
|
+
import type { TypeDefaultItemProps } from "../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @link https://seeds.sproutsocial.com/components/multiselect-menu/
|
|
15
|
+
*
|
|
16
|
+
* @description A Autocomplete 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.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const Autocomplete = ({
|
|
22
|
+
children,
|
|
23
|
+
stateReducer: externalStateReducer,
|
|
24
|
+
...useComboboxProps
|
|
25
|
+
}: TypeAutocompleteProps) => {
|
|
26
|
+
const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
|
|
27
|
+
|
|
28
|
+
const { isOpen, ...useComboboxReturnProps } = useCombobox({
|
|
29
|
+
...defaultUseComboboxProps,
|
|
30
|
+
// onInputValueChange({ inputValue }) {
|
|
31
|
+
// setItems(books.filter(getBooksFilter(inputValue)));
|
|
32
|
+
// },
|
|
33
|
+
items: allMenuItems,
|
|
34
|
+
itemToString,
|
|
35
|
+
selectedItem: null,
|
|
36
|
+
defaultHighlightedIndex: 0,
|
|
37
|
+
stateReducer: (state, actionAndChanges) => {
|
|
38
|
+
let newState: Partial<UseComboboxState<TypeDefaultItemProps>>;
|
|
39
|
+
newState = useComboboxStateReducer(state, actionAndChanges);
|
|
40
|
+
return externalStateReducer
|
|
41
|
+
? externalStateReducer(state, {
|
|
42
|
+
...actionAndChanges,
|
|
43
|
+
changes: newState,
|
|
44
|
+
})
|
|
45
|
+
: newState;
|
|
46
|
+
},
|
|
47
|
+
...useCombobox,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<MenuRoot
|
|
52
|
+
{...{
|
|
53
|
+
isListbox: true,
|
|
54
|
+
items: allMenuItems,
|
|
55
|
+
itemsMap: menuItemsMap,
|
|
56
|
+
itemToString,
|
|
57
|
+
isOpen,
|
|
58
|
+
...useComboboxProps,
|
|
59
|
+
...useComboboxReturnProps,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</MenuRoot>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
|
+
import type { TypeDownshiftComboboxProps } from "../MenuContext";
|
|
3
|
+
|
|
4
|
+
export interface TypeAutocompleteProps
|
|
5
|
+
extends TypeMenuRootOwnProps,
|
|
6
|
+
TypeDownshiftComboboxProps {
|
|
7
|
+
children: TypeMenuRootProps["children"];
|
|
8
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// import React from "react";
|
|
2
|
+
// import {
|
|
3
|
+
// render,
|
|
4
|
+
// screen,
|
|
5
|
+
// fireEvent,
|
|
6
|
+
// waitFor,
|
|
7
|
+
// } from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
// import { Autocomplete } from "../Autocomplete";
|
|
9
|
+
|
|
10
|
+
xdescribe("Autocomplete Component", () => {
|
|
11
|
+
it("should render the Autocomplete component", () => {
|
|
12
|
+
// render(<Autocomplete />);
|
|
13
|
+
// const autocompleteElement = screen.getByRole("combobox");
|
|
14
|
+
// expect(autocompleteElement).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should display suggestions when typing", async () => {
|
|
18
|
+
// render(<Autocomplete />);
|
|
19
|
+
// const inputElement = screen.getByRole("combobox");
|
|
20
|
+
// fireEvent.change(inputElement, { target: { value: "test" } });
|
|
21
|
+
// await waitFor(() => {
|
|
22
|
+
// const suggestions = screen.getAllByRole("option");
|
|
23
|
+
// expect(suggestions.length).toBeGreaterThan(0);
|
|
24
|
+
// });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should select a suggestion when clicked", async () => {
|
|
28
|
+
// render(<Autocomplete />);
|
|
29
|
+
// const inputElement = screen.getByRole("combobox");
|
|
30
|
+
// fireEvent.change(inputElement, { target: { value: "test" } });
|
|
31
|
+
// await waitFor(() => {
|
|
32
|
+
// const suggestion = screen.getByText("test suggestion");
|
|
33
|
+
// fireEvent.click(suggestion);
|
|
34
|
+
// expect(inputElement.value).toBe("test suggestion");
|
|
35
|
+
// });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should allow adding a new value when no match is found", async () => {
|
|
39
|
+
// render(<Autocomplete allowNewValues />);
|
|
40
|
+
// const inputElement = screen.getByRole("combobox");
|
|
41
|
+
// fireEvent.change(inputElement, { target: { value: "new value" } });
|
|
42
|
+
// await waitFor(() => {
|
|
43
|
+
// const newValueOption = screen.getByText('Add "new value"');
|
|
44
|
+
// fireEvent.click(newValueOption);
|
|
45
|
+
// expect(inputElement.value).toBe("new value");
|
|
46
|
+
// });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should display a warning message when the value is invalid", async () => {
|
|
50
|
+
// render(<Autocomplete />);
|
|
51
|
+
// const inputElement = screen.getByRole("combobox");
|
|
52
|
+
// fireEvent.change(inputElement, { target: { value: "invalid value" } });
|
|
53
|
+
// await waitFor(() => {
|
|
54
|
+
// const warningMessage = screen.getByText("Invalid value");
|
|
55
|
+
// expect(warningMessage).toBeInTheDocument();
|
|
56
|
+
// });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should mark an item as selected and reflect it in the input", async () => {
|
|
60
|
+
// render(<Autocomplete />);
|
|
61
|
+
// const inputElement = screen.getByRole("combobox");
|
|
62
|
+
// fireEvent.change(inputElement, { target: { value: "test" } });
|
|
63
|
+
// await waitFor(() => {
|
|
64
|
+
// const suggestion = screen.getByText("test suggestion");
|
|
65
|
+
// fireEvent.click(suggestion);
|
|
66
|
+
// expect(inputElement.value).toBe("test suggestion");
|
|
67
|
+
// expect(suggestion).toHaveAttribute("aria-selected", "true");
|
|
68
|
+
// });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should announce the input as a "combobox" for screen readers', () => {
|
|
72
|
+
// render(<Autocomplete />);
|
|
73
|
+
// const inputElement = screen.getByRole("combobox");
|
|
74
|
+
// expect(inputElement).toHaveAttribute("aria-haspopup", "listbox");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should fire the onChange callback when the input value changes", () => {
|
|
78
|
+
// const handleChange = jest.fn();
|
|
79
|
+
// render(<Autocomplete onChange={handleChange} />);
|
|
80
|
+
// const inputElement = screen.getByRole("combobox");
|
|
81
|
+
// fireEvent.change(inputElement, { target: { value: "test" } });
|
|
82
|
+
// expect(handleChange).toHaveBeenCalledWith("test");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each`
|
|
86
|
+
action | handler
|
|
87
|
+
${"item selected"} | ${"onSelect"}
|
|
88
|
+
${"input focused"} | ${"onFocus"}
|
|
89
|
+
${"input blurred"} | ${"onBlur"}
|
|
90
|
+
`(
|
|
91
|
+
"should execute $handler when $action is performed",
|
|
92
|
+
async ({ action, handler }) => {
|
|
93
|
+
// const handlerMock = jest.fn();
|
|
94
|
+
// const props = { [handler]: handlerMock };
|
|
95
|
+
// render(<Autocomplete {...props} />);
|
|
96
|
+
// const inputElement = screen.getByRole("combobox");
|
|
97
|
+
// if (action === "item selected") {
|
|
98
|
+
// fireEvent.change(inputElement, { target: { value: "test" } });
|
|
99
|
+
// await waitFor(() => {
|
|
100
|
+
// const suggestion = screen.getByText("test suggestion");
|
|
101
|
+
// fireEvent.click(suggestion);
|
|
102
|
+
// expect(handlerMock).toHaveBeenCalled();
|
|
103
|
+
// });
|
|
104
|
+
// } else if (action === "input focused") {
|
|
105
|
+
// fireEvent.focus(inputElement);
|
|
106
|
+
// expect(handlerMock).toHaveBeenCalled();
|
|
107
|
+
// } else if (action === "input blurred") {
|
|
108
|
+
// fireEvent.blur(inputElement);
|
|
109
|
+
// expect(handlerMock).toHaveBeenCalled();
|
|
110
|
+
// }
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
MultiSelectMenu,
|
|
4
|
+
MenuContent,
|
|
5
|
+
MenuHeader,
|
|
6
|
+
MenuFooter,
|
|
7
|
+
} from "../../index";
|
|
8
|
+
|
|
9
|
+
export const MultiSelectMenuTypeTest = () => (
|
|
10
|
+
<>
|
|
11
|
+
{/* Valid MultiSelectMenu with minimal required props */}
|
|
12
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>Test</MultiSelectMenu>
|
|
13
|
+
{/* Valid MultiSelectMenu allows MenuContent by itself as a child */}
|
|
14
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>
|
|
15
|
+
<MenuContent>Test</MenuContent>
|
|
16
|
+
</MultiSelectMenu>
|
|
17
|
+
{/* Valid MultiSelectMenu allows multiple children without fragment */}
|
|
18
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>
|
|
19
|
+
<MenuHeader>Test</MenuHeader>
|
|
20
|
+
<MenuContent>Test</MenuContent>
|
|
21
|
+
<MenuFooter>Test</MenuFooter>
|
|
22
|
+
</MultiSelectMenu>
|
|
23
|
+
{/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
|
|
24
|
+
<MultiSelectMenu
|
|
25
|
+
menuToggleElement={<>test</>}
|
|
26
|
+
onSelectedItemChange={() => {}}
|
|
27
|
+
selectedItem={null}
|
|
28
|
+
stateReducer={(c) => c}
|
|
29
|
+
onStateChange={() => {}}
|
|
30
|
+
getA11yStatusMessage={() => ""}
|
|
31
|
+
isOpen
|
|
32
|
+
>
|
|
33
|
+
Test
|
|
34
|
+
</MultiSelectMenu>
|
|
35
|
+
{/* Valid MultiSelectMenu accepts Downshift props and functions for useMultipleSelection */}
|
|
36
|
+
<MultiSelectMenu
|
|
37
|
+
menuToggleElement={<>test</>}
|
|
38
|
+
onSelectedItemsChange={() => {}}
|
|
39
|
+
selectedItems={[]}
|
|
40
|
+
multiSelectStateReducer={(c) => c}
|
|
41
|
+
onMultiSelectStateChange={() => {}}
|
|
42
|
+
getA11yMultiStatusMessage={() => ""}
|
|
43
|
+
>
|
|
44
|
+
Test
|
|
45
|
+
</MultiSelectMenu>
|
|
46
|
+
|
|
47
|
+
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
48
|
+
<MultiSelectMenu>Test</MultiSelectMenu>
|
|
49
|
+
<MultiSelectMenu
|
|
50
|
+
/* @ts-expect-error - menuToggleElement must be a valid react element */
|
|
51
|
+
menuToggleElement="test"
|
|
52
|
+
>
|
|
53
|
+
Test
|
|
54
|
+
</MultiSelectMenu>
|
|
55
|
+
{/* @ts-expect-error - when no children provided */}
|
|
56
|
+
<MultiSelectMenu menuToggleElement="test" />
|
|
57
|
+
|
|
58
|
+
<MultiSelectMenu
|
|
59
|
+
menuToggleElement={<>test</>}
|
|
60
|
+
/* @ts-expect-error - isOpen can't be a string */
|
|
61
|
+
isOpen="false"
|
|
62
|
+
/* @ts-expect-error - onSelectedItemChange can't be a string */
|
|
63
|
+
onSelectedItemChange="test"
|
|
64
|
+
/* @ts-expect-error - selectedItem can't be a string */
|
|
65
|
+
selectedItem="test"
|
|
66
|
+
/* @ts-expect-error - stateReducer must have a return value */
|
|
67
|
+
stateReducer={() => {}}
|
|
68
|
+
/* @ts-expect-error - onMultiSelectStateChange can't be a string */
|
|
69
|
+
onMultiSelectStateChange="test"
|
|
70
|
+
/* @ts-expect-error - selectedItems can't be a string */
|
|
71
|
+
selectedItems="test"
|
|
72
|
+
/* @ts-expect-error - multiSelectStateReducer must have a return value */
|
|
73
|
+
multiSelectStateReducer="test"
|
|
74
|
+
>
|
|
75
|
+
Test
|
|
76
|
+
</MultiSelectMenu>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
@@ -38,7 +38,7 @@ const MenuContent = ({
|
|
|
38
38
|
id={id}
|
|
39
39
|
tabIndex={0}
|
|
40
40
|
// apply onKeyDown logic for downshift keyboard accessibility support
|
|
41
|
-
onKeyDown={getToggleButtonProps?.()
|
|
41
|
+
onKeyDown={getToggleButtonProps?.()["onKeyDown"] || undefined}
|
|
42
42
|
// apply active descendent updates to container for proper accessibility
|
|
43
43
|
aria-activedescendant={getToggleButtonProps?.()["aria-activedescendant"]}
|
|
44
44
|
{...getMenuProps?.(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { type HTMLMotionProps } from "motion/react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
+
import type { ReactNode, RefObject } from "react";
|
|
3
4
|
|
|
4
5
|
export interface TypeMenuContentProps extends HTMLMotionProps<"ul"> {
|
|
5
6
|
children: ReactNode;
|
package/src/MenuContext.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useMemo,
|
|
4
|
+
useContext,
|
|
5
|
+
type RefObject,
|
|
6
|
+
} from "react";
|
|
2
7
|
import type {
|
|
8
|
+
UseComboboxReturnValue,
|
|
9
|
+
UseComboboxProps,
|
|
3
10
|
UseSelectReturnValue,
|
|
4
11
|
UseSelectProps,
|
|
5
12
|
UseMultipleSelectionReturnValue,
|
|
@@ -7,26 +14,80 @@ import type {
|
|
|
7
14
|
} from "downshift";
|
|
8
15
|
import type { TypeDefaultItemProps, TypeNotEmptyChildren } from "./types";
|
|
9
16
|
|
|
17
|
+
export interface TypeDownshiftComboboxProps
|
|
18
|
+
extends Partial<UseComboboxProps<TypeDefaultItemProps>> {}
|
|
19
|
+
|
|
20
|
+
export interface TypeDownshiftSelectProps
|
|
21
|
+
extends Partial<UseSelectProps<TypeDefaultItemProps>> {}
|
|
22
|
+
|
|
10
23
|
export interface TypeDownshiftMultiSelectProps
|
|
11
|
-
extends Partial<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
> {
|
|
24
|
+
extends Partial<
|
|
25
|
+
Omit<
|
|
26
|
+
UseMultipleSelectionProps<TypeDefaultItemProps>,
|
|
27
|
+
"stateReducer" | "onStateChange" | "getA11yStatusMessage"
|
|
28
|
+
>
|
|
29
|
+
> {
|
|
18
30
|
// Multi Select Props with conflicting useSelect prop names
|
|
19
31
|
getA11yMultiStatusMessage?: UseMultipleSelectionProps<TypeDefaultItemProps>["getA11yStatusMessage"];
|
|
20
32
|
onMultiSelectStateChange?: UseMultipleSelectionProps<TypeDefaultItemProps>["onStateChange"];
|
|
21
33
|
multiSelectStateReducer?: UseMultipleSelectionProps<TypeDefaultItemProps>["stateReducer"];
|
|
22
34
|
}
|
|
23
35
|
|
|
36
|
+
export interface TypeDownshiftComboboxReturnValue
|
|
37
|
+
extends Partial<UseComboboxReturnValue<TypeDefaultItemProps>> {}
|
|
38
|
+
|
|
39
|
+
export interface TypeDownshiftSelectReturnValue
|
|
40
|
+
extends Partial<UseSelectReturnValue<TypeDefaultItemProps>> {}
|
|
41
|
+
|
|
42
|
+
export interface TypeDownshiftMultiSelectReturnValue
|
|
43
|
+
extends Partial<UseMultipleSelectionReturnValue<TypeDefaultItemProps>> {}
|
|
44
|
+
|
|
45
|
+
type TypeDonwshiftSharedReturnValue =
|
|
46
|
+
| "getLabelProps"
|
|
47
|
+
| "getToggleButtonProps"
|
|
48
|
+
| "getMenuProps";
|
|
49
|
+
type TypeDonwshiftSharedProps =
|
|
50
|
+
| "onHighlightedIndexChange"
|
|
51
|
+
| "onIsOpenChange"
|
|
52
|
+
| "onSelectedItemChange"
|
|
53
|
+
| "onStateChange"
|
|
54
|
+
| "stateReducer";
|
|
24
55
|
export interface TypeMenuContextProps
|
|
25
|
-
extends
|
|
26
|
-
|
|
56
|
+
extends Omit<
|
|
57
|
+
TypeDownshiftComboboxReturnValue,
|
|
58
|
+
TypeDonwshiftSharedReturnValue
|
|
59
|
+
>,
|
|
60
|
+
Omit<TypeDownshiftComboboxProps, TypeDonwshiftSharedProps>,
|
|
61
|
+
Omit<TypeDownshiftSelectReturnValue, TypeDonwshiftSharedReturnValue>,
|
|
62
|
+
Omit<TypeDownshiftSelectProps, TypeDonwshiftSharedProps>,
|
|
63
|
+
TypeDownshiftMultiSelectReturnValue,
|
|
27
64
|
TypeDownshiftMultiSelectProps {
|
|
28
65
|
items: TypeDefaultItemProps[];
|
|
29
66
|
isListbox?: boolean;
|
|
67
|
+
onHighlightedIndexChange?:
|
|
68
|
+
| TypeDownshiftComboboxProps["onHighlightedIndexChange"]
|
|
69
|
+
| TypeDownshiftSelectProps["onHighlightedIndexChange"];
|
|
70
|
+
onIsOpenChange?:
|
|
71
|
+
| TypeDownshiftComboboxProps["onIsOpenChange"]
|
|
72
|
+
| TypeDownshiftSelectProps["onIsOpenChange"];
|
|
73
|
+
onSelectedItemChange?:
|
|
74
|
+
| TypeDownshiftComboboxProps["onSelectedItemChange"]
|
|
75
|
+
| TypeDownshiftSelectProps["onSelectedItemChange"];
|
|
76
|
+
onStateChange?:
|
|
77
|
+
| TypeDownshiftComboboxProps["onStateChange"]
|
|
78
|
+
| TypeDownshiftSelectProps["onStateChange"];
|
|
79
|
+
stateReducer?:
|
|
80
|
+
| TypeDownshiftComboboxProps["stateReducer"]
|
|
81
|
+
| TypeDownshiftSelectProps["stateReducer"];
|
|
82
|
+
getLabelProps?:
|
|
83
|
+
| TypeDownshiftComboboxReturnValue["getLabelProps"]
|
|
84
|
+
| TypeDownshiftSelectReturnValue["getLabelProps"];
|
|
85
|
+
getToggleButtonProps?:
|
|
86
|
+
| TypeDownshiftComboboxReturnValue["getToggleButtonProps"]
|
|
87
|
+
| TypeDownshiftSelectReturnValue["getToggleButtonProps"];
|
|
88
|
+
getMenuProps?:
|
|
89
|
+
| TypeDownshiftComboboxReturnValue["getMenuProps"]
|
|
90
|
+
| TypeDownshiftSelectReturnValue["getMenuProps"];
|
|
30
91
|
}
|
|
31
92
|
|
|
32
93
|
export interface TypeMenuContext extends TypeMenuContextProps {
|
|
@@ -44,7 +44,6 @@ describe("MenuGroup", () => {
|
|
|
44
44
|
const menuGroup = screen.getByRole("group", { name: menuTitle });
|
|
45
45
|
expect(menuGroup).toBeInTheDocument();
|
|
46
46
|
expect(
|
|
47
|
-
// eslint-disable-next-line testing-library/prefer-presence-queries
|
|
48
47
|
screen.queryByRole("heading", { name: menuTitle })
|
|
49
48
|
).toBeInTheDocument();
|
|
50
49
|
});
|
|
@@ -6,7 +6,6 @@ export const MenuHeaderTypeTest = () => (
|
|
|
6
6
|
title="Menu Header"
|
|
7
7
|
leftAction={<div>left action</div>}
|
|
8
8
|
rightAction={<div>right action</div>}
|
|
9
|
-
// eslint-disable-next-line react/no-children-prop
|
|
10
9
|
children={<div>children</div>}
|
|
11
10
|
/>
|
|
12
11
|
{/* left and right action accept strings */}
|
|
@@ -17,12 +17,9 @@ export const MenuItem = ({
|
|
|
17
17
|
children,
|
|
18
18
|
onClick,
|
|
19
19
|
onKeyDown,
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
21
20
|
disabled = false,
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
21
|
ref,
|
|
24
22
|
href,
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
26
23
|
color,
|
|
27
24
|
...props
|
|
28
25
|
}: TypeMenuItemProps) => {
|
|
@@ -53,7 +50,6 @@ export const MenuItem = ({
|
|
|
53
50
|
// use anchor tag if href is provided and button for menuitem
|
|
54
51
|
as={isListbox ? undefined : href ? "a" : "button"}
|
|
55
52
|
href={href}
|
|
56
|
-
// eslint-disable-next-line react/no-children-prop
|
|
57
53
|
children={children}
|
|
58
54
|
{...optionProps}
|
|
59
55
|
{...props}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
screen,
|
|
5
5
|
render,
|
|
6
6
|
waitFor,
|
|
7
|
+
act,
|
|
7
8
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
9
|
import {
|
|
9
10
|
PointerEventsCheckLevel,
|
|
@@ -394,6 +395,8 @@ describe("MenuItem when isListbox", () => {
|
|
|
394
395
|
});
|
|
395
396
|
|
|
396
397
|
describe("API", () => {
|
|
398
|
+
// This test has many tests inside it due to the `each` usage, so extend the timeout.
|
|
399
|
+
jest.setTimeout(20 * 1000);
|
|
397
400
|
test.each([INPUT_TYPES.CHECKBOX, INPUT_TYPES.RADIO, INPUT_TYPES.SWITCH])(
|
|
398
401
|
"supports %s input types",
|
|
399
402
|
(inputType) => {
|
|
@@ -491,7 +494,7 @@ describe("MenuItem when isListbox", () => {
|
|
|
491
494
|
const menuItem = screen.getByRole("option", { selected: true });
|
|
492
495
|
testByType[inputType].isSelected();
|
|
493
496
|
|
|
494
|
-
await user.click(menuItem);
|
|
497
|
+
await act(() => user.click(menuItem));
|
|
495
498
|
|
|
496
499
|
expect(
|
|
497
500
|
await screen.findByRole("option", {
|