Skip to content

Commit f3806cc

Browse files
Add ListFilterMulti component (#843)
* Add ListFilterMulti component that supports setting array values * Fixes to ListFilter tests to use userEvent instead of fireEvent * Fixes to ListFilter so that it uses the mapper to generate the Select value, instead of iterating through the mapper array
1 parent 767fde7 commit f3806cc

File tree

6 files changed

+171
-21
lines changed

6 files changed

+171
-21
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
3+
import { userEvent, render, screen } from '@/test-utils/rtl';
4+
5+
import ListFilterMulti from '../list-filter-multi';
6+
7+
const MOCK_LIST_FILTER_LABELS = {
8+
opt1: 'Option 1',
9+
opt2: 'Option 2',
10+
opt3: 'Option 3',
11+
};
12+
13+
type MockListFilterOption = keyof typeof MOCK_LIST_FILTER_LABELS;
14+
15+
describe(ListFilterMulti.name, () => {
16+
it('renders without errors', () => {
17+
setup({});
18+
expect(screen.getByRole('combobox')).toBeInTheDocument();
19+
expect(screen.getByText('Mock label')).toBeInTheDocument();
20+
expect(screen.getByText('Mock placeholder')).toBeInTheDocument();
21+
});
22+
23+
it('displays all the options in the select component', async () => {
24+
const { user } = setup({});
25+
26+
const selectFilter = screen.getByRole('combobox');
27+
await user.click(selectFilter);
28+
29+
Object.entries(MOCK_LIST_FILTER_LABELS).forEach(([_, value]) =>
30+
expect(screen.getByText(value)).toBeInTheDocument()
31+
);
32+
});
33+
34+
it('calls the setQueryParams function when an option is selected', async () => {
35+
const { user, mockOnChangeValues } = setup({});
36+
37+
const selectFilter = screen.getByRole('combobox');
38+
await user.click(selectFilter);
39+
40+
const option1 = screen.getByText('Option 1');
41+
await user.click(option1);
42+
43+
expect(mockOnChangeValues).toHaveBeenCalledWith(['opt1']);
44+
45+
await user.click(selectFilter);
46+
47+
const option2 = screen.getByText('Option 2');
48+
await user.click(option2);
49+
50+
expect(mockOnChangeValues).toHaveBeenCalledWith(['opt2']);
51+
});
52+
53+
it('calls the setQueryParams function when the filter is cleared', async () => {
54+
const { user, mockOnChangeValues } = setup({
55+
override: ['opt2'],
56+
});
57+
58+
const clearButton = screen.getByLabelText('Clear all');
59+
await user.click(clearButton);
60+
61+
expect(mockOnChangeValues).toHaveBeenCalledWith(undefined);
62+
});
63+
});
64+
65+
function setup({ override }: { override?: Array<MockListFilterOption> }) {
66+
const mockOnChangeValues = jest.fn();
67+
const user = userEvent.setup();
68+
render(
69+
<ListFilterMulti
70+
label="Mock label"
71+
placeholder="Mock placeholder"
72+
values={override ?? undefined}
73+
onChangeValues={mockOnChangeValues}
74+
labelMap={MOCK_LIST_FILTER_LABELS}
75+
/>
76+
);
77+
78+
return { user, mockOnChangeValues };
79+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Theme } from 'baseui';
2+
import type { FormControlOverrides } from 'baseui/form-control/types';
3+
import { type StyleObject } from 'styletron-react';
4+
5+
export const overrides = {
6+
selectFormControl: {
7+
Label: {
8+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
9+
...$theme.typography.LabelXSmall,
10+
}),
11+
},
12+
ControlContainer: {
13+
style: (): StyleObject => ({
14+
margin: '0px',
15+
}),
16+
},
17+
} satisfies FormControlOverrides,
18+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
import React from 'react';
3+
4+
import { FormControl } from 'baseui/form-control';
5+
import { Select, SIZE } from 'baseui/select';
6+
7+
import getOptionsFromLabelMap from '../list-filter/helpers/get-options-from-label-map';
8+
9+
import { overrides } from './list-filter-multi.styles';
10+
import { type Props } from './list-filter-multi.types';
11+
12+
export default function ListFilterMulti<T extends string>({
13+
values,
14+
onChangeValues,
15+
labelMap,
16+
label,
17+
placeholder,
18+
}: Props<T>) {
19+
const options = getOptionsFromLabelMap(labelMap);
20+
const optionsValues = values?.map((value) => ({
21+
id: value,
22+
label: labelMap[value],
23+
}));
24+
25+
return (
26+
<FormControl label={label} overrides={overrides.selectFormControl}>
27+
<Select
28+
multi
29+
size={SIZE.compact}
30+
value={optionsValues}
31+
options={options}
32+
onChange={(params) =>
33+
onChangeValues(
34+
params.value.length > 0
35+
? params.value.map((v) => v.id as T)
36+
: undefined
37+
)
38+
}
39+
placeholder={placeholder}
40+
/>
41+
</FormControl>
42+
);
43+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type Props<T extends string> = {
2+
values: Array<T> | undefined;
3+
onChangeValues: (values: Array<T> | undefined) => void;
4+
labelMap: Record<T, string>;
5+
label: string;
6+
placeholder: string;
7+
};
Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { render, screen, fireEvent, act } from '@/test-utils/rtl';
3+
import { render, screen, userEvent } from '@/test-utils/rtl';
44

55
import ListFilter from '../list-filter';
66

@@ -20,44 +20,45 @@ describe(ListFilter.name, () => {
2020
expect(screen.getByText('Mock placeholder')).toBeInTheDocument();
2121
});
2222

23-
it('displays all the options in the select component', () => {
24-
setup({});
23+
it('displays all the options in the select component', async () => {
24+
const { user } = setup({});
25+
2526
const selectFilter = screen.getByRole('combobox');
26-
act(() => {
27-
fireEvent.click(selectFilter);
28-
});
27+
await user.click(selectFilter);
28+
2929
Object.entries(MOCK_LIST_FILTER_LABELS).forEach(([_, value]) =>
3030
expect(screen.getByText(value)).toBeInTheDocument()
3131
);
3232
});
3333

34-
it('calls the setQueryParams function when an option is selected', () => {
35-
const { mockOnChangeValue } = setup({});
34+
it('calls the setQueryParams function when an option is selected', async () => {
35+
const { user, mockOnChangeValue } = setup({});
36+
3637
const selectFilter = screen.getByRole('combobox');
37-
act(() => {
38-
fireEvent.click(selectFilter);
39-
});
38+
await user.click(selectFilter);
39+
4040
const option = screen.getByText('Option 1');
41-
act(() => {
42-
fireEvent.click(option);
43-
});
41+
await user.click(option);
42+
4443
expect(mockOnChangeValue).toHaveBeenCalledWith('opt1');
4544
});
4645

47-
it('calls the setQueryParams function when the filter is cleared', () => {
48-
const { mockOnChangeValue } = setup({
46+
it('calls the setQueryParams function when the filter is cleared', async () => {
47+
const { user, mockOnChangeValue } = setup({
4948
override: 'opt2',
5049
});
50+
5151
const clearButton = screen.getByLabelText('Clear value');
52-
act(() => {
53-
fireEvent.click(clearButton);
54-
});
52+
await user.click(clearButton);
53+
5554
expect(mockOnChangeValue).toHaveBeenCalledWith(undefined);
5655
});
5756
});
5857

5958
function setup({ override }: { override?: MockListFilterOption }) {
6059
const mockOnChangeValue = jest.fn();
60+
const user = userEvent.setup();
61+
6162
render(
6263
<ListFilter
6364
label="Mock label"
@@ -68,5 +69,5 @@ function setup({ override }: { override?: MockListFilterOption }) {
6869
/>
6970
);
7071

71-
return { mockOnChangeValue };
72+
return { user, mockOnChangeValue };
7273
}

src/components/list-filter/list-filter.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export default function ListFilter<T extends string>({
1616
placeholder,
1717
}: Props<T>) {
1818
const options = getOptionsFromLabelMap(labelMap);
19-
const optionValue = options.filter((option) => option.id === value);
19+
const optionValue = value
20+
? [{ id: value, label: labelMap[value] }]
21+
: undefined;
2022

2123
return (
2224
<FormControl label={label} overrides={overrides.selectFormControl}>

0 commit comments

Comments
 (0)