Skip to content

Commit

Permalink
feat(Table): add filter to colum settings (#1627)
Browse files Browse the repository at this point in the history
  • Loading branch information
polikashina authored Jun 13, 2024
1 parent 5cdc675 commit 6eca546
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 20 deletions.
24 changes: 14 additions & 10 deletions src/components/Table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,11 @@ const MyTable1 = withTableSettings({sortable: false})(Table);

### Options

| Name | Description | Type | Default |
| :------- | :------------------------------------------------ | :------------: | :-----: |
| width | Settings' popup width | `number` `fit` | |
| sortable | Whether or not add ability to sort settings items | `boolean` | `true` |
| Name | Description | Type | Default |
| :--------- | :-------------------------------------------------- | :--------------: | :-----: |
| width | Settings' popup width | `number` `"fit"` | |
| sortable | Whether or not add ability to sort settings items | `boolean` | `true` |
| filterable | Whether or not add ability to filter settings items | `boolean` | `false` |

### ColumnMeta

Expand All @@ -251,12 +252,15 @@ const MyTable1 = withTableSettings({sortable: false})(Table);

### Properties

| Name | Description | Type |
| :----------------- | :------------------------------ | :------------------------------------------: |
| settingsPopupWidth | TableColumnSetup pop-up width | `number` `fit` |
| settings | Current settings | `TableSettingsData` |
| updateSettings | Settings update handle | `(data: TableSettingsData) => Promise<void>` |
| renderControls | Allows to render custom actions | `RenderControls` |
| Name | Description | Type |
| :------------------------- | :----------------------------------------------------------- | :------------------------------------------------------: |
| settingsPopupWidth | TableColumnSetup pop-up width | `number` `"fit"` |
| settings | Current settings | `TableSettingsData` |
| updateSettings | Settings update handle | `(data: TableSettingsData) => Promise<void>` |
| renderControls | Allows to render custom actions | `RenderControls` |
| settingsFilterPlaceholder | Text that appears in the control when no search value is set | `string` |
| settingsFilterEmptyMessage | Text that appears when no one item is found | `string` |
| filterSettings | Function for filtering items | `(value: string, item: TableColumnSetupItem) => boolean` |

### TableSettingsData

Expand Down
19 changes: 19 additions & 0 deletions src/components/Table/__stories__/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {WithTableSettingsCustomActionsShowcase} from './WithTableSettingsCustomA
import {
TableWithAction,
TableWithCopy,
TableWithFilterableSettings,
TableWithSelection,
TableWithSettings,
TableWithSettingsFactory,
Expand Down Expand Up @@ -233,6 +234,24 @@ HOCWithTableSettings.args = {
columns: columnsWithSettings,
};

const WithFilterableSettingsTemplate: StoryFn<TableProps<DataItem>> = (args) => {
const [settings, setSettings] = React.useState<TableSettingsData>(DEFAULT_SETTINGS);
return (
<TableWithFilterableSettings
{...args}
settings={settings}
updateSettings={setSettings}
settingsFilterPlaceholder="Filter list"
settingsFilterEmptyMessage="No results"
/>
);
};

export const HOCWithFilterableTableSettings = WithFilterableSettingsTemplate.bind({});
HOCWithFilterableTableSettings.parameters = {
disableStrictMode: true,
};

export const HOCWithTableSettingsFactory = WithTableSettingsTemplate.bind({});
HOCWithTableSettingsFactory.parameters = {
isFactory: true,
Expand Down
4 changes: 4 additions & 0 deletions src/components/Table/__stories__/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,9 @@ export const TableWithAction = withTableActions<DataItem>(Table);
export const TableWithCopy = withTableCopy<DataItem>(Table);
export const TableWithSelection = withTableSelection<DataItem>(Table);
export const TableWithSettings = withTableSettings<DataItem>(Table);
export const TableWithFilterableSettings = withTableSettings<DataItem>({
filterable: true,
width: 200,
})(Table);
export const TableWithSettingsFactory = withTableSettings<DataItem>({sortable: false})(Table);
export const TableWithSorting = withTableSorting<DataItem>(Table);
46 changes: 45 additions & 1 deletion src/components/Table/__tests__/Table.withTableSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import userEvent from '@testing-library/user-event';

import {render, screen} from '../../../../test-utils/utils';
import {fireEvent, render, screen, waitFor} from '../../../../test-utils/utils';
import {Button} from '../../Button';
import {Table} from '../Table';
import type {TableColumnConfig, TableProps} from '../Table';
Expand Down Expand Up @@ -331,4 +331,48 @@ describe('withTableSettings', () => {
expect(customControl).toBeVisible();
});
});

describe('filterableSettings', () => {
const TableWithSettings = withTableSettings<SomeItem>({sortable: true, filterable: true})(
Table,
);
const settings = columns.map<TableSetting>((column) => ({id: column.id, isSelected: true}));
const updateSettings = jest.fn();
const placeholder = 'Filter list';

it('should filter columns', async () => {
render(
<TableWithSettings
columns={columns}
data={data}
settings={settings}
settingsFilterPlaceholder={placeholder}
updateSettings={updateSettings}
/>,
);

await userEvent.click(screen.getByRole('button', {name: 'Table settings'}));
const textInput = screen.getByRole('textbox') as HTMLInputElement;
expect(textInput).toBeVisible();
expect(textInput.placeholder).toBe(placeholder);

const column = screen.getByRole('button', {name: 'description'});
expect(column.hasAttribute('draggable')).toBeTruthy();

fireEvent.change(textInput, {target: {value: 'na'}});
const filteredOption = screen.getByRole('option', {name: 'name'});
expect(filteredOption).toBeInTheDocument();
expect(filteredOption.hasAttribute('draggable')).toBeFalsy();
await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(1));

fireEvent.change(textInput, {target: {value: ''}});
expect(screen.getByRole('button', {name: 'id'}).hasAttribute('draggable')).toBeTruthy();
expect(
screen.getByRole('button', {name: 'name'}).hasAttribute('draggable'),
).toBeTruthy();
expect(
screen.getByRole('button', {name: 'description'}).hasAttribute('draggable'),
).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,15 @@ $block: '.#{variables.$ns}inner-table-column-setup';
&__controls {
margin: var(--g-spacing-1) var(--g-spacing-1) 0;
}

&__filter-input {
box-sizing: border-box;
padding: 0 var(--g-spacing-2) var(--g-spacing-1);

border-block-end: 1px solid var(--g-color-line-generic);
}

&__empty-placeholder {
padding: var(--g-spacing-2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import type {PopperPlacement} from '../../../../../hooks/private';
import {createOnKeyDownHandler} from '../../../../../hooks/useActionHandlers/useActionHandlers';
import {Button} from '../../../../Button';
import {Icon} from '../../../../Icon';
import {Text} from '../../../../Text';
import {TreeSelect} from '../../../../TreeSelect/TreeSelect';
import type {
TreeSelectProps,
TreeSelectRenderContainer,
TreeSelectRenderItem,
} from '../../../../TreeSelect/types';
import {TextInput} from '../../../../controls/TextInput';
import {Flex} from '../../../../layout/Flex/Flex';
import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList';
import {ListContainerView, ListItemView} from '../../../../useList';
import {ListContainerView, ListItemView, useListFilter} from '../../../../useList';
import {block} from '../../../../utils/cn';
import type {TableColumnConfig} from '../../../Table';
import type {TableSetting} from '../withTableSettings';
Expand All @@ -35,6 +37,8 @@ import './TableColumnSetup.scss';

const b = block('inner-table-column-setup');
const controlsCn = b('controls');
const filterInputCn = b('filter-input');
const emptyPlaceholderCn = b('empty-placeholder');

const reorderArray = <T extends unknown>(list: T[], startIndex: number, endIndex: number): T[] => {
const result = [...list];
Expand Down Expand Up @@ -244,6 +248,17 @@ const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps =>
};
};

const defaultFilterSettingsFn = (value: string, item: TableColumnSetupItem) => {
return typeof item.title === 'string'
? item.title.toLowerCase().includes(value.trim().toLowerCase())
: true;
};

const useEmptyRenderContainer = (placeholder?: string): TreeSelectRenderContainer<{}> => {
const emptyRenderContainer = () => <Text className={emptyPlaceholderCn}>{placeholder}</Text>;
return emptyRenderContainer;
};

export type RenderControls = (params: {
DefaultApplyButton: React.ComponentType;
/**
Expand Down Expand Up @@ -271,6 +286,11 @@ export interface TableColumnSetupProps {

defaultItems?: TableColumnSetupItem[];
showResetButton?: boolean | ((currentItems: TableColumnSetupItem[]) => boolean);

filterable?: boolean;
filterPlaceholder?: string;
filterEmptyMessage?: string;
filterSettings?: (value: string, item: TableColumnSetupItem) => boolean;
}

export const TableColumnSetup = (props: TableColumnSetupProps) => {
Expand All @@ -285,9 +305,19 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
className,
defaultItems = propsItems,
showResetButton: propsShowResetButton,
filterable,
filterPlaceholder,
filterEmptyMessage,
filterSettings = defaultFilterSettingsFn,
} = props;

const [open, setOpen] = React.useState(false);
const [sortingEnabled, setSortingEnabled] = React.useState(sortable);
const [prevSortingEnabled, setPrevSortingEnabled] = React.useState(sortable);
if (sortable !== prevSortingEnabled) {
setPrevSortingEnabled(sortable);
setSortingEnabled(sortable);
}

const [items, setItems] = React.useState(propsItems);
const [prevPropsItems, setPrevPropsItems] = React.useState(propsItems);
Expand All @@ -297,10 +327,12 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
setItems(propsItems);
}

const filterState = useListFilter({items, filterItem: filterSettings, debounceTimeout: 0});

const onApply = () => {
const newSettings = items.map<TableSetting>(({id, isSelected}) => ({id, isSelected}));
propsOnUpdate(newSettings);
setOpen(false);
onOpenChange(false);
};

const DefaultApplyButton = () => (
Expand Down Expand Up @@ -344,7 +376,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
),
});

const dndRenderItem = useDndRenderItem(sortable);
const dndRenderItem = useDndRenderItem(sortingEnabled);

const renderControl: TreeSelectProps<unknown>['renderControl'] = ({toggleOpen}) => {
const onKeyDown = createOnKeyDownHandler(toggleOpen);
Expand All @@ -361,9 +393,10 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {

const onOpenChange = (open: boolean) => {
setOpen(open);

if (open === false) {
setItems(propsItems);
setSortingEnabled(sortable);
filterState.reset();
}
};

Expand All @@ -378,6 +411,28 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {

const value = React.useMemo(() => prepareValue(items), [items]);

const emptyRenderContainer = useEmptyRenderContainer(filterEmptyMessage);

const onFilterValueUpdate = (value: string) => {
filterState.onFilterUpdate(value);
setSortingEnabled(!value.length);
};

const slotBeforeListBody = filterable ? (
<TextInput
size="m"
view="clear"
placeholder={filterPlaceholder}
value={filterState.filter}
className={filterInputCn}
onUpdate={onFilterValueUpdate}
hasClear
/>
) : null;

const renderContainer =
filterState.filter && !filterState.items.length ? emptyRenderContainer : dndRenderContainer;

return (
<TreeSelect
className={b(null, className)}
Expand All @@ -386,12 +441,13 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
size="l"
open={open}
value={value}
items={items}
items={filterState.filter ? filterState.items : items}
onUpdate={onUpdate}
popupWidth={popupWidth}
onOpenChange={onOpenChange}
placement={popupPlacement}
renderContainer={dndRenderContainer}
slotBeforeListBody={slotBeforeListBody}
renderContainer={renderContainer}
renderControl={renderControl}
renderItem={dndRenderItem}
/>
Expand Down
20 changes: 17 additions & 3 deletions src/components/Table/hoc/withTableSettings/withTableSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function getActualItems<I>(
export interface WithTableSettingsOptions {
width?: TreeSelectProps<any>['popupWidth'];
sortable?: boolean;
filterable?: boolean;
}

interface WithTableSettingsBaseProps {
Expand Down Expand Up @@ -145,8 +146,15 @@ interface WithoutDefaultSettings {
showResetButton?: boolean;
}

interface WithFilter {
settingsFilterPlaceholder?: string;
settingsFilterEmptyMessage?: string;
filterSettings?: (value: string, item: TableColumnSetupItem) => boolean;
}

export type WithTableSettingsProps = WithTableSettingsBaseProps &
(WithDefaultSettings | WithoutDefaultSettings);
(WithDefaultSettings | WithoutDefaultSettings) &
WithFilter;

const b = block('table');

Expand All @@ -169,7 +177,7 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(
) => React.ComponentType<TableProps<I> & WithTableSettingsProps & E>) {
function tableWithSettingsFactory(
TableComponent: React.ComponentType<TableProps<I> & E>,
{width, sortable}: WithTableSettingsOptions = {},
{width, sortable, filterable}: WithTableSettingsOptions = {},
) {
const componentName = getComponentName(TableComponent);

Expand All @@ -181,6 +189,9 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(
renderControls,
defaultSettings,
showResetButton,
settingsFilterPlaceholder,
settingsFilterEmptyMessage,
filterSettings,
...restTableProps
}: TableProps<I> & WithTableSettingsProps & E) {
const defaultActualItems = React.useMemo(() => {
Expand All @@ -193,14 +204,17 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(

const enhancedColumns = React.useMemo(() => {
const actualItems = getActualItems(columns, settings || []);

return enhanceSystemColumn(filterColumns(columns, actualItems), (systemColumn) => {
systemColumn.name = () => (
<div className={b('settings')}>
<TableColumnSetup
popupWidth={settingsPopupWidth || width}
popupPlacement={POPUP_PLACEMENT}
sortable={sortable}
filterable={filterable}
filterPlaceholder={settingsFilterPlaceholder}
filterEmptyMessage={settingsFilterEmptyMessage}
filterSettings={filterSettings}
onUpdate={updateSettings}
items={actualItems}
renderSwitcher={({onClick}) => (
Expand Down

0 comments on commit 6eca546

Please sign in to comment.