Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlled filter search #840

Merged
merged 5 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 56 additions & 30 deletions src/organisms/Filter/Filter.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useMemo, useState } from 'react';
import { ChangeEvent, FC, useMemo, useState } from 'react';

import { DatePicker } from '@equinor/eds-core-react';
import { gear, van } from '@equinor/eds-icons';
Expand All @@ -14,37 +14,36 @@ const CAR_SIZE = [
{ value: 'size', label: 'Family van' },
];
const MANUFACTURER = [
{ value: 'toyota', label: 'トヨタ (Toyota)' },
{ value: 'mazda', label: 'マツダ (Mazda)' },
{ value: 'created-by', label: '鈴木 (Suzuki)' },
{ key: 'toyota', label: 'トヨタ (Toyota)' },
{ key: 'mazda', label: 'マツダ (Mazda)' },
{ key: 'created-by', label: '鈴木 (Suzuki)' },
];

type FilterStoryProps = FilterProps & { withIcons?: boolean };
type FilterStoryProps = FilterProps<string> & { withIcons?: boolean };

const Wrapper: FC<FilterStoryProps> = (props) => {
const [carSize, setCarSize] = useState<SelectOptionRequired | undefined>(
undefined
);
const [manufacturer, setManufacturer] = useState<
SelectOptionRequired | undefined
>(
const [manufacturer, setManufacturer] = useState(
props.values?.find((value) =>
MANUFACTURER.some((manufacturer) => manufacturer.value === value.value)
MANUFACTURER.some((manufacturer) => manufacturer.key === value.key)
)
);
const [manufacturerDate, setManufacturerDate] = useState<Date | undefined>(
undefined
);
const [search, setSearch] = useState<string | undefined>(undefined);
const [search, setSearch] = useState<string>('');
const [searchTags, setSearchTags] = useState<string[]>([]);

const values = useMemo(() => {
const all: FilterProps['values'] = [];
const all: FilterProps<string>['values'] = [];

if (carSize) {
if (props.withIcons) {
all.push({ ...carSize, icon: van });
all.push({ key: carSize.value, label: carSize.label, icon: van });
} else {
all.push(carSize);
all.push({ key: carSize.value, label: carSize.label });
}
}

Expand All @@ -58,19 +57,21 @@ const Wrapper: FC<FilterStoryProps> = (props) => {

if (manufacturerDate) {
all.push({
value: 'manufacturer-date',
key: 'manufacturer-date',
label: `Manufactured: ${formatDate(manufacturerDate, {
format: 'DD. month YYYY',
})}`,
});
}

if (search) {
all.push({ value: 'search', label: `Search: ${search}` });
if (searchTags) {
for (const [index, searchTag] of searchTags.entries()) {
all.push({ key: `search-${index}`, label: searchTag });
}
}

return all;
}, [carSize, manufacturer, manufacturerDate, props.withIcons, search]);
}, [carSize, manufacturer, manufacturerDate, props.withIcons, searchTags]);

const handleOnSelectEnvironment = (
value: SelectOptionRequired | undefined
Expand All @@ -79,41 +80,57 @@ const Wrapper: FC<FilterStoryProps> = (props) => {
};

const handleOnSelectCreatedBy = (value: SelectOptionRequired | undefined) => {
setManufacturer(value);
if (value) {
setManufacturer({ key: value.value, label: value.label });
} else {
setManufacturer(undefined);
}
};

const handleOnChangManufacturerDate = (value: Date | null) => {
setManufacturerDate(value || undefined);
};

const handleOnClearFilter = (value: string) => {
if (MANUFACTURER.some((manufacturer) => manufacturer.value === value)) {
if (MANUFACTURER.some((manufacturer) => manufacturer.key === value)) {
setManufacturer(undefined);
} else if (CAR_SIZE.some((size) => size.value === value)) {
setCarSize(undefined);
} else if (value === 'manufacturer-date') {
setManufacturerDate(undefined);
} else if (value === 'search') {
setSearch(undefined);
} else if (value.includes('search')) {
setSearchTags((prev) => {
const copy = [...prev];
const index = Number(value.split('-')[1]);
copy.splice(index, 1);
return copy;
});
}
};

const handleOnClearAllFilters = () => {
setCarSize(undefined);
setManufacturer(undefined);
setManufacturerDate(undefined);
setSearch(undefined);
setSearch('');
};

const handleOnSearch = (value: string) => {
setSearch(value);
const handleOnSearch = (event: ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};

const handleOnSearchEnter = (value: string) => {
setSearchTags((prev) => [...prev, value]);
setSearch('');
};

return (
<Filter
{...props}
values={values}
onSearch={handleOnSearch}
search={search}
onSearchEnter={handleOnSearchEnter}
onSearchChange={handleOnSearch}
onClearFilter={handleOnClearFilter}
onClearAllFilters={handleOnClearAllFilters}
>
Expand All @@ -136,10 +153,17 @@ const Wrapper: FC<FilterStoryProps> = (props) => {
items={CAR_SIZE}
/>
<SingleSelect
value={manufacturer}
value={
manufacturer
? { value: manufacturer.key, label: manufacturer.label }
: undefined
}
label="Created by"
onSelect={handleOnSelectCreatedBy}
items={MANUFACTURER}
items={MANUFACTURER.map((item) => ({
value: item.key,
label: item.label,
}))}
/>
</div>
</Filter>
Expand All @@ -162,6 +186,9 @@ const meta: Meta<FilterStoryProps> = {
description:
'Array with values ({ label: string, value: string, icon?: IconData })',
},
search: {
description: 'Search field value',
},
placeholder: {
type: 'string',
description: 'Search placeholder text, default is "Search..."',
Expand All @@ -177,10 +204,9 @@ const meta: Meta<FilterStoryProps> = {
type: 'function',
description: 'Callback when clear all filters button is clicked',
},
onSearch: {
onSearchChange: {
type: 'function',
description:
'Callback when search is entered (only when hitting {Enter})',
description: 'Callback when search is changed',
},
},
args: {
Expand Down
2 changes: 1 addition & 1 deletion src/organisms/Filter/Filter.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const StyledChip = styled(Chip)<StyledChipProps>`
${({ $tryingToRemove }) => {
if ($tryingToRemove) {
return css`
background: ${colors.ui.background__light.rgba};
background: ${colors.interactive.primary__hover_alt.rgba};
`;
}
}}
Expand Down
34 changes: 24 additions & 10 deletions src/organisms/Filter/Filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { faker } from '@faker-js/faker';
import { Filter, FilterProps } from 'src/organisms/Filter/Filter';
import { render, screen, userEvent } from 'src/tests/test-utils';

function fakeProps(): Omit<FilterProps, 'children'> {
function fakeProps(): Omit<FilterProps<string>, 'children'> {
return {
values: new Array(faker.number.int({ min: 1, max: 10 }))
.fill(null)
.map(() => ({
value: faker.string.uuid(),
key: faker.string.uuid(),
label: faker.animal.dog(),
})),
onClearAllFilters: vi.fn(),
onClearFilter: vi.fn(),
onSearch: vi.fn(),
search: '',
onSearchEnter: vi.fn(),
onSearchChange: vi.fn(),
};
}

Expand Down Expand Up @@ -104,7 +106,7 @@ test('onClearFilter is called when hitting backspace twice', async () => {

expect(props.onClearFilter).toHaveBeenCalledTimes(1);
expect(props.onClearFilter).toHaveBeenCalledWith(
props.values[props.values.length - 1].value
props.values[props.values.length - 1].key
);
});

Expand All @@ -126,7 +128,7 @@ test('onClearFilter is called when clicking X', async () => {
await user.click(closeButton);

expect(props.onClearFilter).toHaveBeenCalledTimes(1);
expect(props.onClearFilter).toHaveBeenCalledWith(randomValue.value);
expect(props.onClearFilter).toHaveBeenCalledWith(randomValue.key);
});

test('onClearAllFilters is called when clicking clear all and search is cleared', async () => {
Expand Down Expand Up @@ -170,9 +172,9 @@ test('initialOpen works as expected', async () => {
expect(screen.queryByText(childText)).not.toBeInTheDocument();
});

test('onSearch is called when hitting enter', async () => {
test('onSearch is called when hitting enter and search is not empty string', async () => {
const props = fakeProps();
render(
const { rerender } = render(
<Filter {...props}>
<p>child</p>
</Filter>
Expand All @@ -183,9 +185,21 @@ test('onSearch is called when hitting enter', async () => {

await user.click(searchBox);

await user.keyboard('{Enter}');
expect(props.onSearchEnter).not.toHaveBeenCalled();

const randomWord = faker.lorem.word();
await user.type(searchBox, `${randomWord}{Enter}`);
await user.type(searchBox, `${randomWord}`);
expect(props.onSearchChange).toHaveBeenCalled();

rerender(
<Filter {...props} search={randomWord}>
<p>child</p>
</Filter>
);

await user.keyboard('{Enter}');

expect(props.onSearch).toHaveBeenCalledTimes(1);
expect(props.onSearch).toHaveBeenCalledWith(randomWord);
expect(props.onSearchEnter).toHaveBeenCalledTimes(1);
expect(props.onSearchEnter).toHaveBeenCalledWith(randomWord);
});
Loading