@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/src/MenuItem/styles.tsx
CHANGED
|
@@ -23,7 +23,6 @@ export const MenuItemContainer = styled.li`
|
|
|
23
23
|
list-style-type: none;
|
|
24
24
|
margin-left: ${({ theme }) => theme.space[300]};
|
|
25
25
|
margin-right: ${({ theme }) => theme.space[300]};
|
|
26
|
-
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
27
26
|
|
|
28
27
|
&:first-child {
|
|
29
28
|
margin-top: ${({ theme }) => theme.space[300]};
|
|
@@ -49,6 +48,7 @@ export const StyledMenuItem = styled.div<{ $highlighted: boolean }>`
|
|
|
49
48
|
padding: ${({ theme }) => theme.space[300]};
|
|
50
49
|
outline: 0;
|
|
51
50
|
${({ theme }) => theme.typography[200]};
|
|
51
|
+
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
52
52
|
|
|
53
53
|
/* Highlighted */
|
|
54
54
|
${({ $highlighted }) => $highlighted && highlightedStyles}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import React, { useCallback } from "react";
|
|
1
|
+
import React, { useCallback, useRef } from "react";
|
|
2
2
|
import styled from "styled-components";
|
|
3
3
|
import { Popout, type TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
4
4
|
|
|
5
5
|
import { MenuProvider, type TypeMenuProviderProps } from "../MenuContext";
|
|
6
6
|
|
|
7
7
|
export interface TypeMenuRootOwnProps {
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
8
|
menuToggleElement: React.ReactElement<any>;
|
|
10
9
|
popoutProps?: Partial<
|
|
11
10
|
Omit<TypePopoutProps, "isOpen" | "setIsOpen" | "content" | "children">
|
|
@@ -46,6 +45,7 @@ export const MenuRoot = ({
|
|
|
46
45
|
return (
|
|
47
46
|
<MenuProvider {...contextProps}>
|
|
48
47
|
<Popout
|
|
48
|
+
menuPopout
|
|
49
49
|
isOpen={!!isOpen}
|
|
50
50
|
setIsOpen={setIsOpen}
|
|
51
51
|
content={
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
screen,
|
|
6
6
|
waitFor,
|
|
7
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
8
9
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
9
10
|
import {
|
|
10
11
|
MenuRoot,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
type TypeMenuContextProps,
|
|
18
19
|
} from "../../index";
|
|
19
20
|
|
|
20
|
-
// eslint-disable-next-line react/display-name
|
|
21
21
|
jest.mock("@sproutsocial/seeds-react-portal", () => ({ children }) => (
|
|
22
22
|
<div>{children}</div>
|
|
23
23
|
));
|
|
@@ -197,7 +197,6 @@ describe("MenuRoot", () => {
|
|
|
197
197
|
const { user } = render(
|
|
198
198
|
<MenuRoot
|
|
199
199
|
menuToggleElement={<CustomToggleElement />}
|
|
200
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
201
200
|
// @ts-ignore - mock contextProps may be imperfect
|
|
202
201
|
getToggleButtonProps={() => ({ onClick: mockToggleClick })}
|
|
203
202
|
>
|
|
@@ -82,7 +82,6 @@ export const MultiSelectMenu = ({
|
|
|
82
82
|
defaultHighlightedIndex: 0,
|
|
83
83
|
stateReducer: (state, actionAndChanges) => {
|
|
84
84
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
85
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
86
85
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
87
86
|
newState = selectReducerForMultiSelect(state, {
|
|
88
87
|
...actionAndChanges,
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypeDownshiftSelectProps,
|
|
4
|
+
TypeDownshiftMultiSelectProps,
|
|
5
|
+
} from "../MenuContext";
|
|
3
6
|
|
|
4
7
|
export interface TypeMultiSelectMenuProps
|
|
5
8
|
extends TypeMenuRootOwnProps,
|
|
9
|
+
TypeDownshiftSelectProps,
|
|
6
10
|
TypeDownshiftMultiSelectProps {
|
|
7
11
|
children: TypeMenuRootProps["children"];
|
|
8
12
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable testing-library/prefer-find-by */
|
|
2
|
-
/* eslint-disable testing-library/no-wait-for-multiple-assertions */
|
|
3
1
|
/* eslint-disable testing-library/no-unnecessary-act */
|
|
4
2
|
import {
|
|
5
3
|
MultiSelectMenu,
|
|
@@ -11,6 +9,7 @@ import {
|
|
|
11
9
|
import {
|
|
12
10
|
render,
|
|
13
11
|
screen,
|
|
12
|
+
cleanup,
|
|
14
13
|
waitFor,
|
|
15
14
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
16
15
|
|
|
@@ -22,6 +21,10 @@ const menuItems = [
|
|
|
22
21
|
];
|
|
23
22
|
|
|
24
23
|
describe("MultiSelectMenu", () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
25
28
|
it("should render the MenuItems when provided as children", async () => {
|
|
26
29
|
const { user } = render(
|
|
27
30
|
<MultiSelectMenu
|
|
@@ -36,7 +36,6 @@ export const SingleSelectMenu = ({
|
|
|
36
36
|
itemToString,
|
|
37
37
|
stateReducer: (state, actionAndChanges) => {
|
|
38
38
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
39
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks, prefer-const
|
|
40
39
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
41
40
|
return externalStateReducer
|
|
42
41
|
? externalStateReducer(state, {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { UseSelectProps } from "downshift";
|
|
2
1
|
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
3
|
-
import type {
|
|
2
|
+
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
4
3
|
|
|
5
4
|
export interface TypeSingleSelectMenuProps
|
|
6
5
|
extends TypeMenuRootOwnProps,
|
|
7
|
-
|
|
6
|
+
TypeDownshiftSelectProps {
|
|
8
7
|
children: TypeMenuRootProps["children"];
|
|
9
8
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
3
|
import type { TypeMenuContext } from "../MenuContext";
|
|
4
4
|
|
|
5
5
|
export interface TypeUseItemsFromChildrenProps {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
}
|
|
8
|
+
export interface TypeItemElements {
|
|
9
|
+
element: React.ReactElement<TypeMenuItemProps>;
|
|
10
|
+
parents: readonly React.ReactNode[];
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
10
14
|
const walkAllChildren = (
|
|
@@ -16,7 +20,6 @@ const walkAllChildren = (
|
|
|
16
20
|
// console.log("element", element);
|
|
17
21
|
callback(element, parents);
|
|
18
22
|
const newParents = [...parents, element];
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
23
|
const children = (element as any).props?.children;
|
|
21
24
|
React.Children.toArray(children).forEach((child) => {
|
|
22
25
|
walk(child, newParents);
|
|
@@ -60,13 +63,12 @@ export const useItemsFromChildren = ({
|
|
|
60
63
|
const allMenuItemElements = getAllMenuItems(children);
|
|
61
64
|
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
62
65
|
const allMenuItems: TypeMenuContext["items"] = allMenuItemElements.map(
|
|
63
|
-
({ element
|
|
66
|
+
({ element }, index) => {
|
|
64
67
|
const item = {
|
|
65
68
|
id: element.props.id,
|
|
66
69
|
disabled: element.props.disabled,
|
|
67
70
|
index,
|
|
68
|
-
element,
|
|
69
|
-
parents,
|
|
71
|
+
menuItemProps: element.props,
|
|
70
72
|
};
|
|
71
73
|
menuItemsMap[`${item.id}`] = item;
|
|
72
74
|
return item;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCombobox } from "downshift";
|
|
2
|
+
import type { UseComboboxProps } from "downshift";
|
|
3
|
+
import type { TypeDefaultItemProps } from "../types";
|
|
4
|
+
|
|
5
|
+
export const useComboboxStateReducer: Required<
|
|
6
|
+
UseComboboxProps<TypeDefaultItemProps>
|
|
7
|
+
>["stateReducer"] = (state, actionAndChanges) => {
|
|
8
|
+
const { type, changes } = actionAndChanges;
|
|
9
|
+
switch (type) {
|
|
10
|
+
/** If downshift is blurred by tabbing within the menu, stay open */
|
|
11
|
+
// case useCombobox.stateChangeTypes.InputBlur:
|
|
12
|
+
// return {
|
|
13
|
+
// ...changes,
|
|
14
|
+
// // keep menu open on blur
|
|
15
|
+
// isOpen: true,
|
|
16
|
+
// };
|
|
17
|
+
|
|
18
|
+
/** Return standard change payload */
|
|
19
|
+
default:
|
|
20
|
+
return changes;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const defaultUseComboboxProps = {
|
|
25
|
+
isItemDisabled: (item) => !!item.disabled,
|
|
26
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { TypeMenuItemProps } from "./MenuItem";
|
|
2
2
|
|
|
3
|
-
export interface
|
|
4
|
-
element: React.ReactElement<TypeMenuItemProps>;
|
|
5
|
-
parents: readonly React.ReactNode[];
|
|
6
|
-
}
|
|
7
|
-
export interface TypeDefaultItemProps extends Partial<TypeItemElements> {
|
|
3
|
+
export interface TypeDefaultItemProps {
|
|
8
4
|
id: string;
|
|
9
5
|
index: number;
|
|
10
6
|
disabled?: boolean;
|
|
7
|
+
menuItemProps?: TypeMenuItemProps;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
10
|
export type TypeNotEmptyChildren =
|
package/tsconfig.json
CHANGED
package/.eslintignore
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
Feature: TokenInput
|
|
2
|
-
|
|
3
|
-
A component that allows users to search, select, and manage tokens with autocomplete functionality.
|
|
4
|
-
|
|
5
|
-
Rule: User behavior
|
|
6
|
-
|
|
7
|
-
Background:
|
|
8
|
-
Given an end user is using the component
|
|
9
|
-
|
|
10
|
-
Scenario: User can add tokens
|
|
11
|
-
Given the ComboBox accepts new values
|
|
12
|
-
When text is typed
|
|
13
|
-
And a delimiter key is pressed
|
|
14
|
-
Then a new token is created from the input text
|
|
15
|
-
|
|
16
|
-
Scenario: User can not add tokens
|
|
17
|
-
Given the ComboBox does not accept new values
|
|
18
|
-
When text is typed
|
|
19
|
-
And a delimiter key is pressed
|
|
20
|
-
Then no new token is added
|
|
21
|
-
|
|
22
|
-
Scenario: User can remove tokens manually
|
|
23
|
-
Given the ComboBox has tokens
|
|
24
|
-
When token's remove button is clicked
|
|
25
|
-
Then the token is removed
|
|
26
|
-
|
|
27
|
-
Scenario: User can remove tokens with backspace
|
|
28
|
-
Given the ComboBox has tokens
|
|
29
|
-
When backspace is hit
|
|
30
|
-
Then the token is removed
|
|
31
|
-
|
|
32
|
-
Scenario: User can navigate tokens via keyboard
|
|
33
|
-
Given the ComboBox has tokens
|
|
34
|
-
And the Combobox Input has focus
|
|
35
|
-
When the tokens are navigated using arrow keys
|
|
36
|
-
Then the activeToken callback fires
|
|
37
|
-
|
|
38
|
-
Scenario: Component handles token addition
|
|
39
|
-
When a new token is added
|
|
40
|
-
Then the onAddToken callback fires
|
|
41
|
-
|
|
42
|
-
Scenario: Component handles token removal
|
|
43
|
-
Given the ComboBox is rendered with tokens
|
|
44
|
-
When a token is removed
|
|
45
|
-
Then the onRemoveToken callback fires
|
|
46
|
-
|
|
47
|
-
Scenario: Component handles token click
|
|
48
|
-
When a token is clicked
|
|
49
|
-
Then the onClickToken callback fires
|
|
50
|
-
|
|
51
|
-
Scenario: Component handles input change
|
|
52
|
-
When the input value changes
|
|
53
|
-
Then the onChange callback fires
|
|
54
|
-
|
|
55
|
-
Rule: API
|
|
56
|
-
Background:
|
|
57
|
-
Given an engineer is developing using the component
|
|
58
|
-
|
|
59
|
-
Scenario: TokenInput extends Combobox
|
|
60
|
-
When Combobox features are used in TokenInput
|
|
61
|
-
Then the TokenInput responds as a Combobox would
|
|
62
|
-
|
|
63
|
-
Scenario: ComboBox accepts tokens
|
|
64
|
-
When a token component is passed to the Combobox
|
|
65
|
-
And an item is selected
|
|
66
|
-
Then the token is rendered in the input
|
|
67
|
-
|
|
68
|
-
Scenario: ComboBox has a maximum token length
|
|
69
|
-
When 'tokenMaxLength' is set to <value>
|
|
70
|
-
Then the token string size is limited to <value>
|
|
71
|
-
|
|
72
|
-
Scenario Outline: ComboBox checks its types
|
|
73
|
-
When <propName> is passed a value
|
|
74
|
-
Then ComboBox expects it to be of type <propType>
|
|
75
|
-
|
|
76
|
-
Examples: Prop Types
|
|
77
|
-
| activeToken | string |
|
|
78
|
-
| tokens | array of { id: string, iconName?: EnumIconNames, value: string, valid?: boolean } |
|
|
79
|
-
| onChangeTokens | function |
|
|
80
|
-
| onAddToken | function |
|
|
81
|
-
| onRemoveToken | function |
|
|
82
|
-
| onClickToken | function |
|
|
83
|
-
| maxLength | number |
|
|
84
|
-
| tokenMaxLength | number |
|