@bitrise/bitkit 10.6.0 → 10.9.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/package.json +17 -7
- package/src/Components/Dropdown/Dropdown.context.tsx +37 -0
- package/src/Components/Dropdown/Dropdown.stories.tsx +89 -0
- package/src/Components/Dropdown/Dropdown.test.tsx +431 -0
- package/src/Components/Dropdown/Dropdown.theme.ts +101 -0
- package/src/Components/Dropdown/Dropdown.tsx +306 -0
- package/src/Components/Dropdown/DropdownButton.tsx +27 -0
- package/src/Components/Dropdown/DropdownOption.tsx +83 -0
- package/src/Components/Dropdown/hooks/useAutoScroll.ts +61 -0
- package/src/Components/Dropdown/hooks/useFloatingDropdown.ts +92 -0
- package/src/Components/Dropdown/hooks/useSimpleSearch.tsx +52 -0
- package/src/Components/Dropdown/isNodeMatch.ts +39 -0
- package/src/Components/Icons/16x16/StepstatusNext.tsx +15 -0
- package/src/Components/Icons/16x16/index.ts +1 -0
- package/src/Components/Icons/24x24/StepstatusNext.tsx +15 -0
- package/src/Components/Icons/24x24/index.ts +1 -0
- package/src/Components/Input/Input.theme.ts +19 -0
- package/src/Components/Popover/PopoverArrow.tsx +7 -0
- package/src/index.ts +3 -0
- package/src/theme.ts +4 -0
- package/src/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitrise/bitkit",
|
|
3
3
|
"description": "Bitrise React component library",
|
|
4
|
-
"version": "10.
|
|
4
|
+
"version": "10.9.0",
|
|
5
5
|
"repository": "git@github.com:bitrise-io/bitkit.git",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"license": "UNLICENSED",
|
|
@@ -21,8 +21,11 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@chakra-ui/react": "^2.2.4",
|
|
24
|
+
"@chakra-ui/react-utils": "^2.0.1",
|
|
25
|
+
"@chakra-ui/utils": "^2.0.4",
|
|
24
26
|
"@emotion/react": "^11.9.3",
|
|
25
27
|
"@emotion/styled": "^11.9.3",
|
|
28
|
+
"@floating-ui/react-dom-interactions": "^0.8.1",
|
|
26
29
|
"@popperjs/core": "^2.11.5",
|
|
27
30
|
"classnames": "^2.3.1",
|
|
28
31
|
"clipboard": "^2.0.11",
|
|
@@ -58,10 +61,14 @@
|
|
|
58
61
|
"@storybook/testing-library": "^0.0.13",
|
|
59
62
|
"@storybook/theming": "^6.5.9",
|
|
60
63
|
"@svgr/core": "^6.3.0",
|
|
64
|
+
"@testing-library/dom": "^8.16.0",
|
|
65
|
+
"@testing-library/jest-dom": "^5.16.4",
|
|
66
|
+
"@testing-library/react": "^13.3.0",
|
|
67
|
+
"@testing-library/user-event": "^14.3.0",
|
|
61
68
|
"@types/cheerio": "^0.22.31",
|
|
62
69
|
"@types/clipboard": "^2.0.1",
|
|
63
70
|
"@types/enzyme": "^3.10.12",
|
|
64
|
-
"@types/jest": "^
|
|
71
|
+
"@types/jest": "^28.1.6",
|
|
65
72
|
"@types/luxon": "^2.4.0",
|
|
66
73
|
"@types/react": "18.0.15",
|
|
67
74
|
"@types/react-dom": "^18.0.6",
|
|
@@ -87,12 +94,14 @@
|
|
|
87
94
|
"glob": "^8.0.3",
|
|
88
95
|
"husky": "^7.0.4",
|
|
89
96
|
"identity-obj-proxy": "^3.0.0",
|
|
90
|
-
"jest": "^
|
|
97
|
+
"jest": "^28.1.3",
|
|
98
|
+
"jest-environment-jsdom": "^28.1.3",
|
|
91
99
|
"jsdom": "^19.0.0",
|
|
92
100
|
"prettier": "^2.7.1",
|
|
101
|
+
"react-hook-form": "^7.33.1",
|
|
93
102
|
"recast": "^0.21.1",
|
|
94
103
|
"semantic-release": "^19.0.3",
|
|
95
|
-
"ts-jest": "^
|
|
104
|
+
"ts-jest": "^28.0.7",
|
|
96
105
|
"ts-node": "^10.9.1",
|
|
97
106
|
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
|
98
107
|
"typescript": "^4.7.4",
|
|
@@ -110,12 +119,13 @@
|
|
|
110
119
|
"moduleNameMapper": {
|
|
111
120
|
"^.+\\.css$": "identity-obj-proxy"
|
|
112
121
|
},
|
|
113
|
-
"
|
|
114
|
-
"./jest.setup.
|
|
122
|
+
"setupFilesAfterEnv": [
|
|
123
|
+
"./spec/jest.setup.ts"
|
|
115
124
|
],
|
|
116
125
|
"transform": {
|
|
117
126
|
"\\.tsx?$": "ts-jest"
|
|
118
|
-
}
|
|
127
|
+
},
|
|
128
|
+
"testEnvironment": "./spec/test-env.js"
|
|
119
129
|
},
|
|
120
130
|
"resolutions": {
|
|
121
131
|
"**/ast-types": "npm:@gkz/ast-types",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { createStylesContext } from '@chakra-ui/react';
|
|
3
|
+
import { createContext } from '@chakra-ui/react-utils';
|
|
4
|
+
|
|
5
|
+
const [DropdownStylesProvider, useDropdownStyles] = createStylesContext('Dropdown');
|
|
6
|
+
|
|
7
|
+
export type DropdownEventArgs = { value: string; index: number | undefined; label: ReactNode };
|
|
8
|
+
|
|
9
|
+
type DropdownContext = {
|
|
10
|
+
formValue: string;
|
|
11
|
+
onOptionSelected: (arg: DropdownEventArgs) => void;
|
|
12
|
+
searchValue: string;
|
|
13
|
+
searchRef: React.Ref<HTMLInputElement>;
|
|
14
|
+
searchOnChange: (sv: string) => void;
|
|
15
|
+
searchOnSubmit: () => void;
|
|
16
|
+
filter?: string;
|
|
17
|
+
activeIndex?: number | null;
|
|
18
|
+
getItemProps: (x: object) => object;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const [DropdownContextProvider, useDropdownContext] = createContext<DropdownContext>();
|
|
22
|
+
|
|
23
|
+
const DropdownProvider = ({
|
|
24
|
+
styles,
|
|
25
|
+
context,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
styles: ComponentProps<typeof DropdownStylesProvider>['value'];
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
context: DropdownContext;
|
|
31
|
+
}) => (
|
|
32
|
+
<DropdownContextProvider value={context}>
|
|
33
|
+
<DropdownStylesProvider value={styles}>{children}</DropdownStylesProvider>
|
|
34
|
+
</DropdownContextProvider>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export { DropdownProvider, useDropdownContext, useDropdownStyles };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ReactNode, useState } from 'react';
|
|
2
|
+
import { ComponentStoryFn } from '@storybook/react';
|
|
3
|
+
import Notification from '../Notification/Notification';
|
|
4
|
+
import Avatar from '../Avatar/Avatar';
|
|
5
|
+
import Dropdown, {
|
|
6
|
+
DropdownGroup,
|
|
7
|
+
DropdownOption,
|
|
8
|
+
DropdownDetailedOption,
|
|
9
|
+
NoResultsFound,
|
|
10
|
+
DropdownSearch,
|
|
11
|
+
} from './Dropdown';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
title: 'Components/Dropdown',
|
|
15
|
+
component: Dropdown,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const opts = [
|
|
19
|
+
{ value: 'x1', label: 'text1' },
|
|
20
|
+
{ value: 'x2', label: 'text2' },
|
|
21
|
+
{ value: 'x3', label: 'text3' },
|
|
22
|
+
{ value: 'x4', label: 'text4' },
|
|
23
|
+
];
|
|
24
|
+
export const CustomSearch = () => {
|
|
25
|
+
const [searchValue, setSearchValue] = useState('');
|
|
26
|
+
const [options, setOptions] = useState<ReactNode[]>([]);
|
|
27
|
+
const searchOnChange = (nv: string) => {
|
|
28
|
+
setSearchValue(nv);
|
|
29
|
+
setOptions(
|
|
30
|
+
opts
|
|
31
|
+
.filter((opt) => opt.label.includes(nv))
|
|
32
|
+
.map((opt) => (
|
|
33
|
+
<DropdownOption value={opt.value} key={opt.value}>
|
|
34
|
+
{opt.label}
|
|
35
|
+
</DropdownOption>
|
|
36
|
+
)),
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
return <Dropdown search={<DropdownSearch value={searchValue} onChange={searchOnChange} />}>{options}</Dropdown>;
|
|
40
|
+
};
|
|
41
|
+
export const CustomSearchError = () => {
|
|
42
|
+
return (
|
|
43
|
+
<Dropdown search={<DropdownSearch />}>
|
|
44
|
+
<NoResultsFound>
|
|
45
|
+
<Notification status="error">Error</Notification>
|
|
46
|
+
</NoResultsFound>
|
|
47
|
+
</Dropdown>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithDetail = () => {
|
|
52
|
+
return (
|
|
53
|
+
<Dropdown>
|
|
54
|
+
<DropdownDetailedOption value="user1" icon={<Avatar name="some" />} title="hello" subtitle="sub" />
|
|
55
|
+
</Dropdown>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Default: ComponentStoryFn<typeof Dropdown> = (args) => {
|
|
60
|
+
return (
|
|
61
|
+
<form
|
|
62
|
+
onSubmit={(ev) => {
|
|
63
|
+
ev.preventDefault();
|
|
64
|
+
const data = new FormData(ev.target as HTMLFormElement);
|
|
65
|
+
console.log(data.get('dropdown'));
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<Dropdown aria-label="test" name="dropdown" w="800px" {...args}>
|
|
69
|
+
<DropdownOption>Unset</DropdownOption>
|
|
70
|
+
<DropdownOption value="x1">text1</DropdownOption>
|
|
71
|
+
<DropdownOption value="x2">text2</DropdownOption>
|
|
72
|
+
<DropdownOption value="x3">text3</DropdownOption>
|
|
73
|
+
<DropdownOption value="x4">text4</DropdownOption>
|
|
74
|
+
<DropdownGroup label="test">
|
|
75
|
+
<DropdownOption value="x5">text5</DropdownOption>
|
|
76
|
+
<DropdownOption value="x6">text6</DropdownOption>
|
|
77
|
+
<DropdownOption value="x7">text7</DropdownOption>
|
|
78
|
+
</DropdownGroup>
|
|
79
|
+
<DropdownOption value="v1">
|
|
80
|
+
<div>label</div>
|
|
81
|
+
</DropdownOption>
|
|
82
|
+
<DropdownOption value="v2">
|
|
83
|
+
<div>label</div>
|
|
84
|
+
</DropdownOption>
|
|
85
|
+
</Dropdown>
|
|
86
|
+
<button type="submit">Send</button>
|
|
87
|
+
</form>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { forwardRef, MutableRefObject, StrictMode, useState } from 'react';
|
|
2
|
+
import { setTimeout } from 'timers/promises';
|
|
3
|
+
import { render as renderRTL, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { useForm } from 'react-hook-form';
|
|
6
|
+
import Avatar from '../Avatar/Avatar';
|
|
7
|
+
import Dropdown, { DropdownGroup, DropdownOption } from './Dropdown';
|
|
8
|
+
import { CustomSearch } from './Dropdown.stories';
|
|
9
|
+
import { DropdownDetailedOption } from './DropdownOption';
|
|
10
|
+
|
|
11
|
+
const render = (ui: React.ReactElement) => {
|
|
12
|
+
return renderRTL(<StrictMode>{ui}</StrictMode>);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const TestComponent = forwardRef<HTMLFormElement, { submit: (data: unknown) => void }>(({ submit, ...props }, ref) => {
|
|
16
|
+
const { register, handleSubmit } = useForm({ defaultValues: { dropdown: 'unset' } });
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<form {...props} onSubmit={handleSubmit(submit)} ref={ref}>
|
|
20
|
+
<Dropdown aria-label="Test" {...register('dropdown')}>
|
|
21
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
22
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
23
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
24
|
+
</Dropdown>
|
|
25
|
+
<button type="submit">Send</button>
|
|
26
|
+
</form>
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Dropdown', () => {
|
|
31
|
+
it('shows option on button', async () => {
|
|
32
|
+
render(<TestComponent submit={() => {}} />);
|
|
33
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
34
|
+
await userEvent.click(button);
|
|
35
|
+
const opt = await screen.findByRole('listbox');
|
|
36
|
+
await userEvent.selectOptions(opt, 'Test Opt1');
|
|
37
|
+
expect(button).toHaveTextContent('Test Opt1');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('search field can be disabled', async () => {
|
|
41
|
+
render(
|
|
42
|
+
<Dropdown search={false}>
|
|
43
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
44
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
45
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
46
|
+
</Dropdown>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const button = await screen.findByRole('combobox');
|
|
50
|
+
await userEvent.click(button);
|
|
51
|
+
await screen.findByRole('listbox');
|
|
52
|
+
expect(screen.queryByRole('search')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('works in controlled mode (from the outside)', async () => {
|
|
56
|
+
const Test = () => {
|
|
57
|
+
const [value, setValue] = useState('x');
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<button type="button" onClick={() => setValue('y')}>
|
|
61
|
+
Test
|
|
62
|
+
</button>
|
|
63
|
+
<Dropdown value={value}>
|
|
64
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
65
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
66
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
67
|
+
</Dropdown>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
render(<Test />);
|
|
72
|
+
const button = await screen.findByRole('combobox');
|
|
73
|
+
expect(button).toHaveTextContent('Test Opt1');
|
|
74
|
+
const testBtn = await screen.findByRole('button');
|
|
75
|
+
await userEvent.click(testBtn);
|
|
76
|
+
expect(button).toHaveTextContent('Test Opt2');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('works in controlled mode (from the inside)', async () => {
|
|
80
|
+
const Test = ({ valueRef }: { valueRef: MutableRefObject<string> }) => {
|
|
81
|
+
const [value, setValue] = useState('x');
|
|
82
|
+
valueRef.current = value;
|
|
83
|
+
return (
|
|
84
|
+
<Dropdown value={value} onChange={(ev) => setValue(ev.target.value)}>
|
|
85
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
86
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
87
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
88
|
+
</Dropdown>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
const valueRef: { current: string } = { current: '' };
|
|
92
|
+
render(<Test valueRef={valueRef} />);
|
|
93
|
+
const button = await screen.findByRole('combobox');
|
|
94
|
+
expect(button).toHaveTextContent('Test Opt1');
|
|
95
|
+
await userEvent.click(button);
|
|
96
|
+
const opt = await screen.findByRole('listbox');
|
|
97
|
+
await userEvent.selectOptions(opt, 'Test Opt2');
|
|
98
|
+
expect(button).toHaveTextContent('Test Opt2');
|
|
99
|
+
expect(valueRef.current).toBe('y');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('works in uncontrolled mode', async () => {
|
|
103
|
+
const handler = jest.fn();
|
|
104
|
+
const Test = () => {
|
|
105
|
+
return (
|
|
106
|
+
<Dropdown defaultValue="y" onChange={(ev) => handler(ev.target.value)}>
|
|
107
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
108
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
109
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
110
|
+
</Dropdown>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
render(<Test />);
|
|
114
|
+
const button = await screen.findByRole('combobox');
|
|
115
|
+
expect(button).toHaveTextContent('Test Opt2');
|
|
116
|
+
await userEvent.click(button);
|
|
117
|
+
const opt = await screen.findByRole('listbox');
|
|
118
|
+
await userEvent.selectOptions(opt, 'Test Opt1');
|
|
119
|
+
expect(button).toHaveTextContent('Test Opt1');
|
|
120
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(handler).toHaveBeenCalledWith('x');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('react-hook-form support', () => {
|
|
125
|
+
it('sends data when submitted', async () => {
|
|
126
|
+
const handler = jest.fn();
|
|
127
|
+
render(<TestComponent submit={(data: unknown) => handler(data)} />);
|
|
128
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
129
|
+
const submit = await screen.findByRole('button', { name: 'Send' });
|
|
130
|
+
await userEvent.click(button);
|
|
131
|
+
const opt = await screen.findByRole('listbox');
|
|
132
|
+
await userEvent.selectOptions(opt, 'Test Opt1');
|
|
133
|
+
await userEvent.click(submit);
|
|
134
|
+
|
|
135
|
+
expect(handler).toHaveBeenCalledWith({ dropdown: 'x' });
|
|
136
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('works with click', async () => {
|
|
140
|
+
const handler = jest.fn();
|
|
141
|
+
render(<TestComponent submit={(data: unknown) => handler(data)} />);
|
|
142
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
143
|
+
const submit = await screen.findByRole('button', { name: 'Send' });
|
|
144
|
+
await userEvent.click(button);
|
|
145
|
+
const option = await screen.findByRole('option', { name: 'Test Opt1' });
|
|
146
|
+
await userEvent.click(option);
|
|
147
|
+
await userEvent.click(submit);
|
|
148
|
+
|
|
149
|
+
expect(handler).toHaveBeenCalledWith({ dropdown: 'x' });
|
|
150
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not send data at all when not submitted', async () => {
|
|
154
|
+
const handler = jest.fn();
|
|
155
|
+
render(<TestComponent submit={(data: unknown) => handler(data)} />);
|
|
156
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
157
|
+
await userEvent.click(button);
|
|
158
|
+
const opt = await screen.findByRole('listbox');
|
|
159
|
+
await userEvent.selectOptions(opt, 'Test Opt1');
|
|
160
|
+
|
|
161
|
+
expect(handler).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('readOnly', () => {
|
|
166
|
+
it('does not open when clicked', async () => {
|
|
167
|
+
render(<Dropdown readOnly />);
|
|
168
|
+
const button = await screen.findByRole('combobox');
|
|
169
|
+
await userEvent.click(button);
|
|
170
|
+
await waitFor(() => setTimeout(50));
|
|
171
|
+
const list = screen.queryByRole('listbox');
|
|
172
|
+
expect(list).not.toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('does not open when activated with keyboard', async () => {
|
|
176
|
+
render(<Dropdown readOnly />);
|
|
177
|
+
await userEvent.keyboard('{Tab}');
|
|
178
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
179
|
+
await waitFor(() => setTimeout(50));
|
|
180
|
+
const list = screen.queryByRole('listbox');
|
|
181
|
+
expect(list).not.toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('detailed option', () => {
|
|
186
|
+
it('shows up as default value', async () => {
|
|
187
|
+
const Test = () => {
|
|
188
|
+
return (
|
|
189
|
+
<Dropdown defaultValue="y">
|
|
190
|
+
<DropdownDetailedOption value="y" icon={<Avatar name="hello" />} subtitle="ok" title="test" />
|
|
191
|
+
</Dropdown>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
render(<Test />);
|
|
195
|
+
const button = await screen.findByRole('combobox');
|
|
196
|
+
expect(button).toHaveTextContent('hotestok');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('is searchable', async () => {
|
|
200
|
+
const Test = () => {
|
|
201
|
+
return (
|
|
202
|
+
<Dropdown>
|
|
203
|
+
<DropdownDetailedOption value="y" icon={<Avatar name="hello" />} subtitle="ok" title="testx" />
|
|
204
|
+
<DropdownDetailedOption value="x" icon={<Avatar name="hello" />} subtitle="ok" title="test" />
|
|
205
|
+
</Dropdown>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
render(<Test />);
|
|
209
|
+
const button = await screen.findByRole('combobox');
|
|
210
|
+
await userEvent.click(button);
|
|
211
|
+
const search = await screen.findByRole('search');
|
|
212
|
+
await userEvent.type(search, 'testx');
|
|
213
|
+
const option1 = screen.queryByRole('option', { name: 'testx' });
|
|
214
|
+
const option2 = screen.queryByRole('option', { name: 'test' });
|
|
215
|
+
expect(option1).toBeInTheDocument();
|
|
216
|
+
expect(option2).not.toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('search', () => {
|
|
221
|
+
it('supports custom search', async () => {
|
|
222
|
+
render(<CustomSearch />);
|
|
223
|
+
const button = await screen.findByRole('combobox');
|
|
224
|
+
|
|
225
|
+
await userEvent.click(button);
|
|
226
|
+
const search = await screen.findByRole('search');
|
|
227
|
+
await userEvent.type(search, 'text1');
|
|
228
|
+
let option1 = screen.queryByRole('option', { name: 'text1' });
|
|
229
|
+
let option2 = screen.queryByRole('option', { name: 'text2' });
|
|
230
|
+
let option3 = screen.queryByRole('option', { name: 'text3' });
|
|
231
|
+
let option4 = screen.queryByRole('option', { name: 'text4' });
|
|
232
|
+
expect(option1).toBeInTheDocument();
|
|
233
|
+
expect(option2).not.toBeInTheDocument();
|
|
234
|
+
expect(option3).not.toBeInTheDocument();
|
|
235
|
+
expect(option4).not.toBeInTheDocument();
|
|
236
|
+
await userEvent.clear(search);
|
|
237
|
+
option1 = screen.queryByRole('option', { name: 'text1' });
|
|
238
|
+
option2 = screen.queryByRole('option', { name: 'text2' });
|
|
239
|
+
option3 = screen.queryByRole('option', { name: 'text3' });
|
|
240
|
+
option4 = screen.queryByRole('option', { name: 'text4' });
|
|
241
|
+
expect(option1).toBeInTheDocument();
|
|
242
|
+
expect(option2).toBeInTheDocument();
|
|
243
|
+
expect(option3).toBeInTheDocument();
|
|
244
|
+
expect(option4).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('removes group when items are filtered', async () => {
|
|
248
|
+
render(
|
|
249
|
+
<Dropdown>
|
|
250
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
251
|
+
<DropdownGroup label="hey">
|
|
252
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
253
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
254
|
+
</DropdownGroup>
|
|
255
|
+
</Dropdown>,
|
|
256
|
+
);
|
|
257
|
+
const button = await screen.findByRole('combobox');
|
|
258
|
+
|
|
259
|
+
await userEvent.click(button);
|
|
260
|
+
const search = await screen.findByRole('search');
|
|
261
|
+
await userEvent.type(search, 'opt1');
|
|
262
|
+
const option1 = screen.queryByRole('option', { name: 'Test Opt1' });
|
|
263
|
+
const option2 = screen.queryByRole('option', { name: 'Test Opt2' });
|
|
264
|
+
const option3 = screen.queryByRole('option', { name: 'Test Opt3' });
|
|
265
|
+
const group = screen.queryByText('hey');
|
|
266
|
+
expect(option1).toBeInTheDocument();
|
|
267
|
+
expect(option2).not.toBeInTheDocument();
|
|
268
|
+
expect(option3).not.toBeInTheDocument();
|
|
269
|
+
expect(group).not.toBeInTheDocument();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('filters inside group', async () => {
|
|
273
|
+
render(
|
|
274
|
+
<Dropdown>
|
|
275
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
276
|
+
<DropdownGroup label="hey">
|
|
277
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
278
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
279
|
+
</DropdownGroup>
|
|
280
|
+
</Dropdown>,
|
|
281
|
+
);
|
|
282
|
+
const button = await screen.findByRole('combobox');
|
|
283
|
+
|
|
284
|
+
await userEvent.click(button);
|
|
285
|
+
const search = await screen.findByRole('search');
|
|
286
|
+
await userEvent.type(search, 'opt2');
|
|
287
|
+
const option1 = screen.queryByRole('option', { name: 'Test Opt1' });
|
|
288
|
+
const option2 = screen.queryByRole('option', { name: 'Test Opt2' });
|
|
289
|
+
const option3 = screen.queryByRole('option', { name: 'Test Opt3' });
|
|
290
|
+
const group = screen.queryByText('hey');
|
|
291
|
+
expect(option1).not.toBeInTheDocument();
|
|
292
|
+
expect(option2).toBeInTheDocument();
|
|
293
|
+
expect(option3).not.toBeInTheDocument();
|
|
294
|
+
expect(group).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('filters options', async () => {
|
|
298
|
+
render(<TestComponent submit={() => {}} />);
|
|
299
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
300
|
+
|
|
301
|
+
await userEvent.click(button);
|
|
302
|
+
const search = await screen.findByRole('search');
|
|
303
|
+
await userEvent.type(search, 'opt3');
|
|
304
|
+
const option1 = screen.queryByRole('option', { name: 'Test Opt1' });
|
|
305
|
+
const option2 = screen.queryByRole('option', { name: 'Test Opt2' });
|
|
306
|
+
const option3 = screen.queryByRole('option', { name: 'Test Opt3' });
|
|
307
|
+
expect(option1).not.toBeInTheDocument();
|
|
308
|
+
expect(option2).not.toBeInTheDocument();
|
|
309
|
+
expect(option3).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('showns no results found', async () => {
|
|
313
|
+
render(<TestComponent submit={() => {}} />);
|
|
314
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
315
|
+
|
|
316
|
+
await userEvent.click(button);
|
|
317
|
+
const search = await screen.findByRole('search');
|
|
318
|
+
await userEvent.type(search, 'x');
|
|
319
|
+
const notFoundMessage = await screen.findByText('No results found for `x`');
|
|
320
|
+
expect(notFoundMessage).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('resets after closing', async () => {
|
|
324
|
+
render(<TestComponent submit={() => {}} />);
|
|
325
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
326
|
+
|
|
327
|
+
await userEvent.click(button);
|
|
328
|
+
const search = await screen.findByRole('search');
|
|
329
|
+
const option = await screen.findByRole('option', { name: 'Test Opt1' });
|
|
330
|
+
await userEvent.type(search, 'x');
|
|
331
|
+
await waitFor(() => expect(option).not.toBeInTheDocument());
|
|
332
|
+
await userEvent.click(button);
|
|
333
|
+
await userEvent.click(button);
|
|
334
|
+
const option2 = await screen.findByRole('option', { name: 'Test Opt1' });
|
|
335
|
+
expect(option2).toBeInTheDocument();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('keyboard navigation', () => {
|
|
340
|
+
it('the initial value is the active element', async () => {
|
|
341
|
+
const handler = jest.fn();
|
|
342
|
+
const Test = () => {
|
|
343
|
+
return (
|
|
344
|
+
<Dropdown defaultValue="y" onChange={(ev) => handler(ev.target.value)}>
|
|
345
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
346
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
347
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
348
|
+
</Dropdown>
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
render(<Test />);
|
|
352
|
+
const button = await screen.findByRole('combobox');
|
|
353
|
+
await userEvent.click(button);
|
|
354
|
+
const search = await screen.findByRole('search');
|
|
355
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
356
|
+
await screen.findByRole('listbox');
|
|
357
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
358
|
+
await userEvent.keyboard('{Enter}');
|
|
359
|
+
expect(handler).toHaveBeenCalledWith('z');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('the selected value is the active element', async () => {
|
|
363
|
+
const handler = jest.fn();
|
|
364
|
+
render(<TestComponent submit={(data: unknown) => handler(data)} />);
|
|
365
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
366
|
+
const submit = await screen.findByRole('button', { name: 'Send' });
|
|
367
|
+
|
|
368
|
+
await userEvent.click(button);
|
|
369
|
+
const search = await screen.findByRole('search');
|
|
370
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
371
|
+
await screen.findByRole('listbox');
|
|
372
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
373
|
+
await userEvent.keyboard('{Enter}');
|
|
374
|
+
await userEvent.click(button);
|
|
375
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
376
|
+
await userEvent.keyboard('{Enter}');
|
|
377
|
+
await userEvent.click(submit);
|
|
378
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
379
|
+
expect(handler).toHaveBeenCalledWith({ dropdown: 'y' });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('works', async () => {
|
|
383
|
+
const handler = jest.fn();
|
|
384
|
+
render(<TestComponent submit={(data: unknown) => handler(data)} />);
|
|
385
|
+
const button = await screen.findByRole('combobox', { name: 'Test' });
|
|
386
|
+
const submit = await screen.findByRole('button', { name: 'Send' });
|
|
387
|
+
|
|
388
|
+
await userEvent.click(button);
|
|
389
|
+
const search = await screen.findByRole('search');
|
|
390
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
391
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
392
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
393
|
+
await userEvent.keyboard('{Enter}');
|
|
394
|
+
await userEvent.click(submit);
|
|
395
|
+
expect(handler).toHaveBeenCalledWith({ dropdown: 'y' });
|
|
396
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('works with groups', async () => {
|
|
400
|
+
const handler = jest.fn();
|
|
401
|
+
const Test = () => {
|
|
402
|
+
return (
|
|
403
|
+
<Dropdown onChange={(ev) => handler(ev.target.value)}>
|
|
404
|
+
<DropdownOption value="x">Test Opt1</DropdownOption>
|
|
405
|
+
<DropdownGroup label="hey">
|
|
406
|
+
<DropdownOption value="y">Test Opt2</DropdownOption>
|
|
407
|
+
<DropdownOption value="z">Test Opt3</DropdownOption>
|
|
408
|
+
</DropdownGroup>
|
|
409
|
+
</Dropdown>
|
|
410
|
+
);
|
|
411
|
+
};
|
|
412
|
+
render(<Test />);
|
|
413
|
+
const button = await screen.findByRole('combobox');
|
|
414
|
+
await userEvent.click(button);
|
|
415
|
+
let search = await screen.findByRole('search');
|
|
416
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
417
|
+
await screen.findByRole('listbox');
|
|
418
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
419
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
420
|
+
await userEvent.keyboard('{Enter}');
|
|
421
|
+
expect(handler).toHaveBeenCalledWith('y');
|
|
422
|
+
await userEvent.click(button);
|
|
423
|
+
search = await screen.findByRole('search');
|
|
424
|
+
await waitFor(() => expect(search).toHaveFocus());
|
|
425
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
426
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
427
|
+
await userEvent.keyboard('{Enter}');
|
|
428
|
+
expect(handler).toHaveBeenCalledWith('x');
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|