Skip to content

Commit

Permalink
Merge pull request #1249 from core-ds/feat/select-filter-group-label
Browse files Browse the repository at this point in the history
feat(select): added filter by option group
  • Loading branch information
hextion committed Jun 21, 2024
2 parents 5f4705c + 8b7f9de commit 2a45bee
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 86 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-colts-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alfalab/core-components-select': minor
---

Добавлен поиск по группам значений
31 changes: 28 additions & 3 deletions packages/select/src/components/base-select/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ import type {
OptionsListProps,
SearchProps,
} from '../../typings';
import { defaultAccessor, defaultFilterFn, processOptions } from '../../utils';
import {
defaultAccessor,
defaultFilterFn,
defaultGroupAccessor,
isGroup,
processOptions,
} from '../../utils';
import { NativeSelect } from '../native-select';

import styles from './index.module.css';
Expand Down Expand Up @@ -162,10 +168,29 @@ export const BaseSelect = forwardRef(

const accessor = searchProps.accessor || defaultAccessor;
const filterFn = searchProps.filterFn || defaultFilterFn;
const filterGroup = searchProps.filterGroup ?? false;
const groupAccessor = searchProps.groupAccessor ?? defaultGroupAccessor;

const { filteredOptions, flatOptions, selectedOptions } = useMemo(
() => processOptions(options, selected, (option) => filterFn(accessor(option), search)),
[filterFn, accessor, options, search, selected],
() =>
processOptions(
options,
selected,
(option) => {
if (isGroup(option)) {
const groupAccessorValue = groupAccessor(option);

return (
typeof groupAccessorValue === 'string' &&
filterFn(groupAccessorValue, search)
);
}

return filterFn(accessor(option), search);
},
filterGroup,
),
[options, selected, filterFn, accessor, search, filterGroup, groupAccessor],
);

const scrollIntoView = (node: HTMLElement) => {
Expand Down
198 changes: 127 additions & 71 deletions packages/select/src/docs/description.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,86 @@ render(() => {
});
```

## Поиск среди элементов списка
## Группировка

Пункты списка можно группировать.

```jsx live mobileHeight={640}
const OPTIONS = [
{
label: 'nonmetals',
options: [
{ key: '1', content: 'Hydrogen' },
{ key: '2', content: 'Helium' },
],
},
{
label: 'metals',
options: [
{ key: '3', content: 'Aurum' },
{ key: '4', content: 'Platinum' },
],
},
];
render(() => {
return (
<div style={{ width: document.body.clientWidth < 450 ? '100%' : 320 }}>
<SelectDesktop
allowUnselect={true}
size={56}
options={OPTIONS}
placeholder='Выберите элемент'
label='Сгруппированный список'
Option={BaseOption}
block={true}
/>
</div>
);
});
//MOBILE
const OPTIONS = [
{
label: 'nonmetals',
options: [
{ key: '1', content: 'Hydrogen' },
{ key: '2', content: 'Helium' },
],
},
{
label: 'metals',
options: [
{ key: '3', content: 'Aurum' },
{ key: '4', content: 'Platinum' },
],
},
];
render(() => {
const [selected, setSelected] = React.useState();

Если список содержит большое количество пунктов, используя `showSearch` можно добавить в шапку дропдауна поиск, с помощью которого пользователь сможет отфильтровать элементы списка.
const handleChange = ({ selected }) => {
setSelected(selected);
};

По умолчанию компонент ищет строковое значение среди полей `content`, `value` и `key` и фильтрует по нему. Переопределить это поведение можно с помощью `searchProps.accessor`.
return (
<div style={{ width: document.body.clientWidth < 450 ? '100%' : 320 }}>
<SelectMobile
allowUnselect={true}
options={OPTIONS}
placeholder='Выберите элемент'
label='Сгруппированный список'
Option={BaseOption}
selected={selected}
onChange={handleChange}
block={true}
/>
</div>
);
});
```

## Поиск среди элементов списка

Если список содержит большое количество пунктов, можно добавить в шапку дропдауна поиск `showSearch`, с помощью которого пользователь сможет отфильтровать элементы списка. Можно искать по названиям групп пунктов c помощью `searchProps.filterGroup`.

```jsx live mobileHeight={640}
const OPTIONS = [
Expand All @@ -185,6 +260,23 @@ const OPTIONS = [
{ key: '8', content: 'Fermium' },
];

const GROUP_OPTIONS = [
{
label: 'nonmetals',
options: [
{ key: '1', content: 'Argon' },
{ key: '2', content: 'Helium' },
],
},
{
label: 'metals',
options: [
{ key: '3', content: 'Aurum' },
{ key: '4', content: 'Platinum' },
],
},
];

render(() => {
const [selectedFirst, setSelectedFirst] = React.useState([]);
const [selectedSecond, setSelectedSecond] = React.useState([]);
Expand Down Expand Up @@ -222,6 +314,18 @@ render(() => {
},
})}
/>
<Gap size='m' />
<SelectDesktop
allowUnselect={true}
size={56}
options={GROUP_OPTIONS}
placeholder='Выберите элемент'
label='Сгруппированный список'
Option={BaseOption}
block={true}
showSearch={true}
searchProps={{ filterGroup: true }}
/>
</div>
);
});
Expand All @@ -237,40 +341,11 @@ const OPTIONS = [
{ key: '8', content: 'Fermium' },
];

render(() => {
const [selected, setSelected] = React.useState([]);

return (
<div style={{ width: '100%' }}>
<SelectMobile
showSearch={true}
multiple={true}
allowUnselect={true}
options={OPTIONS}
placeholder='Выберите элементы'
label='Множественный выбор'
Option={BaseOption}
block={true}
selected={selected}
onChange={({ selectedMultiple }) => {
setSelected(selectedMultiple.map((option) => option.key));
}}
/>
</div>
);
});
```

## Группировка

Пункты списка можно группировать.

```jsx live mobileHeight={640}
const OPTIONS = [
const GROUP_OPTIONS = [
{
label: 'nonmetals',
options: [
{ key: '1', content: 'Hydrogen' },
{ key: '1', content: 'Argon' },
{ key: '2', content: 'Helium' },
],
},
Expand All @@ -282,56 +357,37 @@ const OPTIONS = [
],
},
];

render(() => {
const [selected, setSelected] = React.useState([]);

return (
<div style={{ width: document.body.clientWidth < 450 ? '100%' : 320 }}>
<SelectDesktop
<div style={{ width: '100%' }}>
<SelectMobile
showSearch={true}
multiple={true}
allowUnselect={true}
size={56}
options={OPTIONS}
placeholder='Выберите элемент'
label='Сгруппированный список'
placeholder='Выберите элементы'
label='Множественный выбор'
Option={BaseOption}
block={true}
selected={selected}
onChange={({ selectedMultiple }) => {
setSelected(selectedMultiple.map((option) => option.key));
}}
/>
</div>
);
});
//MOBILE
const OPTIONS = [
{
label: 'nonmetals',
options: [
{ key: '1', content: 'Hydrogen' },
{ key: '2', content: 'Helium' },
],
},
{
label: 'metals',
options: [
{ key: '3', content: 'Aurum' },
{ key: '4', content: 'Platinum' },
],
},
];
render(() => {
const [selected, setSelected] = React.useState();

const handleChange = ({ selected }) => {
setSelected(selected);
};

return (
<div style={{ width: document.body.clientWidth < 450 ? '100%' : 320 }}>
<Gap size='m' />
<SelectMobile
allowUnselect={true}
options={OPTIONS}
size={56}
options={GROUP_OPTIONS}
placeholder='Выберите элемент'
label='Сгруппированный список'
Option={BaseOption}
selected={selected}
onChange={handleChange}
block={true}
showSearch={true}
searchProps={{ filterGroup: true }}
/>
</div>
);
Expand Down
30 changes: 26 additions & 4 deletions packages/select/src/presets/useSelectWithApply/hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import deepEqual from 'deep-equal';

import type { AnyObject, BaseSelectProps, OptionShape } from '../../typings';
import { defaultAccessor, defaultFilterFn, processOptions } from '../../utils';
import {
defaultAccessor,
defaultFilterFn,
defaultGroupAccessor,
isGroup,
processOptions,
} from '../../utils';

import { OptionsListWithApply } from './options-list-with-apply';

Expand Down Expand Up @@ -71,7 +77,7 @@ export function useSelectWithApply({
showClear = true,
showSelectAll = false,
showHeaderWithSelectAll = false,
showSearch,
showSearch = false,
searchProps = {},
}: UseSelectWithApplyProps) {
const [searchState, setSearchState] = useState('');
Expand All @@ -83,15 +89,31 @@ export function useSelectWithApply({

const accessor = searchProps.accessor || defaultAccessor;
const filterFn = searchProps.filterFn || defaultFilterFn;
const groupAccessor = searchProps.groupAccessor ?? defaultGroupAccessor;
const filterGroup = searchProps.filterGroup ?? false;

const { flatOptions, selectedOptions } = useMemo(
() =>
processOptions(
options,
selected,
showSearch ? (option) => filterFn(accessor(option), search) : undefined,
showSearch
? (option) => {
if (isGroup(option)) {
const groupAccessorValue = groupAccessor(option);

return (
typeof groupAccessorValue === 'string' &&
filterFn(groupAccessorValue, search)
);
}

return filterFn(accessor(option), search);
}
: undefined,
filterGroup,
),
[filterFn, accessor, options, search, selected, showSearch],
[options, selected, showSearch, filterGroup, filterFn, accessor, search, groupAccessor],
);

const [selectedDraft, setSelectedDraft] = useState<OptionShape[]>(selectedOptions);
Expand Down
2 changes: 2 additions & 0 deletions packages/select/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export type BaseSelectProps = {
filterFn?: (optionText: string, search: string) => boolean;
value?: string;
onChange?: (value: string) => void;
filterGroup?: boolean;
groupAccessor?: (group: GroupShape) => string | undefined;
};

/**
Expand Down
Loading

0 comments on commit 2a45bee

Please sign in to comment.