From 3abcceabf5874f2c6404f7699382abe877dc978c Mon Sep 17 00:00:00 2001 From: Olga Polikashina Date: Thu, 23 May 2024 09:58:31 +0300 Subject: [PATCH] feat: search in table colum settings --- src/components/Table/README.md | 24 ++++--- .../Table/__stories__/Table.stories.tsx | 19 +++++ src/components/Table/__stories__/utils.tsx | 4 ++ .../Table.withTableSettings.test.tsx | 46 ++++++++++++- .../TableColumnSetup/TableColumnSetup.scss | 11 +++ .../TableColumnSetup/TableColumnSetup.tsx | 69 +++++++++++++++++-- .../withTableSettings/withTableSettings.tsx | 20 +++++- 7 files changed, 174 insertions(+), 19 deletions(-) diff --git a/src/components/Table/README.md b/src/components/Table/README.md index 3b553f3f16..7542657316 100644 --- a/src/components/Table/README.md +++ b/src/components/Table/README.md @@ -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 @@ -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` | -| 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` | +| renderControls | Allows to render custom actions | `RenderControls` | +| filterPlaceholder | Text that appears in the control when no search value is set | `string` | +| filterEmptyPlaceholder | Text that appears when no one item is found | `string` | +| filterItems | Function for filtering items | `(item: TableColumnSetupItem, value: string) => boolean` | ### TableSettingsData diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 13062ec13f..397050e429 100644 --- a/src/components/Table/__stories__/Table.stories.tsx +++ b/src/components/Table/__stories__/Table.stories.tsx @@ -16,6 +16,7 @@ import {WithTableSettingsCustomActionsShowcase} from './WithTableSettingsCustomA import { TableWithAction, TableWithCopy, + TableWithFilterableSettings, TableWithSelection, TableWithSettings, TableWithSettingsFactory, @@ -233,6 +234,24 @@ HOCWithTableSettings.args = { columns: columnsWithSettings, }; +const WithFilterableSettingsTemplate: StoryFn> = (args) => { + const [settings, setSettings] = React.useState(DEFAULT_SETTINGS); + return ( + + ); +}; + +export const HOCWithFilterableTableSettings = WithFilterableSettingsTemplate.bind({}); +HOCWithFilterableTableSettings.parameters = { + disableStrictMode: true, +}; + export const HOCWithTableSettingsFactory = WithTableSettingsTemplate.bind({}); HOCWithTableSettingsFactory.parameters = { isFactory: true, diff --git a/src/components/Table/__stories__/utils.tsx b/src/components/Table/__stories__/utils.tsx index 77ccc5840e..c16087eb96 100644 --- a/src/components/Table/__stories__/utils.tsx +++ b/src/components/Table/__stories__/utils.tsx @@ -97,5 +97,9 @@ export const TableWithAction = withTableActions(Table); export const TableWithCopy = withTableCopy(Table); export const TableWithSelection = withTableSelection(Table); export const TableWithSettings = withTableSettings(Table); +export const TableWithFilterableSettings = withTableSettings({ + filterable: true, + width: 200, +})(Table); export const TableWithSettingsFactory = withTableSettings({sortable: false})(Table); export const TableWithSorting = withTableSorting(Table); diff --git a/src/components/Table/__tests__/Table.withTableSettings.test.tsx b/src/components/Table/__tests__/Table.withTableSettings.test.tsx index 9b84eea2b4..c60e8cf042 100644 --- a/src/components/Table/__tests__/Table.withTableSettings.test.tsx +++ b/src/components/Table/__tests__/Table.withTableSettings.test.tsx @@ -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} from '../../../../test-utils/utils'; import {Button} from '../../Button'; import {Table} from '../Table'; import type {TableColumnConfig, TableProps} from '../Table'; @@ -331,4 +331,48 @@ describe('withTableSettings', () => { expect(customControl).toBeVisible(); }); }); + + describe('filterableSettings', () => { + const TableWithSettings = withTableSettings({sortable: true, filterable: true})( + Table, + ); + const settings = columns.map((column) => ({id: column.id, isSelected: true})); + const updateSettings = jest.fn(); + const placeholder = 'Filter list'; + + it('should filter columns', async () => { + render( + , + ); + + 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(); + 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(); + }); + }); }); diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss index aba68b4df8..450de43090 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss @@ -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); + } } diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 4f01d1ffa2..2afb420c40 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -14,12 +14,14 @@ 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'; @@ -33,6 +35,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 = (list: T[], startIndex: number, endIndex: number): T[] => { const result = [...list]; @@ -242,6 +246,17 @@ const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps => }; }; +const defaultFilterItemsFn = (item: TableColumnSetupItem, value: string) => { + return typeof item.title === 'string' + ? item.title.toLowerCase().includes(value.toLowerCase()) + : true; +}; + +const useEmptyRenderContainer = (placeholder?: string): TreeSelectRenderContainer<{}> => { + const emptyRenderContainer = () => {placeholder}; + return emptyRenderContainer; +}; + export type RenderControls = (params: { DefaultApplyButton: React.ComponentType; /** @@ -269,6 +284,11 @@ export interface TableColumnSetupProps { defaultItems?: TableColumnSetupItem[]; showResetButton?: boolean | ((currentItems: TableColumnSetupItem[]) => boolean); + + filterable?: boolean; + filterPlaceholder?: string; + filterEmptyPlaceholder?: string; + filterItems?: (item: TableColumnSetupItem, value: string) => boolean; } export const TableColumnSetup = (props: TableColumnSetupProps) => { @@ -283,9 +303,22 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { className, defaultItems = propsItems, showResetButton: propsShowResetButton, + filterable, + filterPlaceholder, + filterEmptyPlaceholder, + filterItems = defaultFilterItemsFn, } = props; const [open, setOpen] = React.useState(false); + const [filter, setFilter] = React.useState(''); + const [filteredItems, setFilteredItems] = React.useState([]); + + 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); @@ -298,7 +331,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { const onApply = () => { const newSettings = items.map(({id, isSelected}) => ({id, isSelected})); propsOnUpdate(newSettings); - setOpen(false); + onOpenChange(false); }; const DefaultApplyButton = () => ( @@ -342,7 +375,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { ), }); - const dndRenderItem = useDndRenderItem(sortable); + const dndRenderItem = useDndRenderItem(sortingEnabled); const renderControl: TreeSelectProps['renderControl'] = ({toggleOpen}) => { const onKeyDown = createOnKeyDownHandler(toggleOpen); @@ -359,9 +392,11 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { const onOpenChange = (open: boolean) => { setOpen(open); - if (open === false) { setItems(propsItems); + setFilter(''); + setFilteredItems([]); + setSortingEnabled(sortable); } }; @@ -376,6 +411,29 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { const value = React.useMemo(() => prepareValue(items), [items]); + const emptyRenderContainer = useEmptyRenderContainer(filterEmptyPlaceholder); + + const onFilterValueUpdate = (value: string) => { + setFilter(value); + setFilteredItems(items.filter((item) => filterItems(item, value))); + setSortingEnabled(!value.length); + }; + + const slotBeforeListBody = filterable ? ( + + ) : null; + + const renderContainer = + filter && !filteredItems.length ? emptyRenderContainer : dndRenderContainer; + return ( { size="l" open={open} value={value} - items={items} + items={filter ? filteredItems : items} onUpdate={onUpdate} popupWidth={popupWidth} onOpenChange={onOpenChange} placement={popupPlacement} - renderContainer={dndRenderContainer} + slotBeforeListBody={slotBeforeListBody} + renderContainer={renderContainer} renderControl={renderControl} renderItem={dndRenderItem} /> diff --git a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx index 68871c0adf..f51186f33f 100644 --- a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx +++ b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx @@ -110,6 +110,7 @@ export function getActualItems( export interface WithTableSettingsOptions { width?: TreeSelectProps['popupWidth']; sortable?: boolean; + filterable?: boolean; } interface WithTableSettingsBaseProps { @@ -143,8 +144,15 @@ interface WithoutDefaultSettings { showResetButton?: boolean; } +interface WithFilter { + filterPlaceholder?: string; + filterEmptyPlaceholder?: string; + filterItems?: (item: TableColumnSetupItem, value: string) => boolean; +} + export type WithTableSettingsProps = WithTableSettingsBaseProps & - (WithDefaultSettings | WithoutDefaultSettings); + (WithDefaultSettings | WithoutDefaultSettings) & + WithFilter; const b = block('table'); @@ -167,7 +175,7 @@ export function withTableSettings( ) => React.ComponentType & WithTableSettingsProps & E>) { function tableWithSettingsFactory( TableComponent: React.ComponentType & E>, - {width, sortable}: WithTableSettingsOptions = {}, + {width, sortable, filterable}: WithTableSettingsOptions = {}, ) { const componentName = getComponentName(TableComponent); @@ -179,6 +187,9 @@ export function withTableSettings( renderControls, defaultSettings, showResetButton, + filterPlaceholder, + filterEmptyPlaceholder, + filterItems, ...restTableProps }: TableProps & WithTableSettingsProps & E) { const defaultActualItems = React.useMemo(() => { @@ -191,7 +202,6 @@ export function withTableSettings( const enhancedColumns = React.useMemo(() => { const actualItems = getActualItems(columns, settings || []); - return enhanceSystemColumn(filterColumns(columns, actualItems), (systemColumn) => { systemColumn.name = () => (
@@ -199,6 +209,10 @@ export function withTableSettings( popupWidth={settingsPopupWidth || width} popupPlacement={POPUP_PLACEMENT} sortable={sortable} + filterable={filterable} + filterPlaceholder={filterPlaceholder} + filterEmptyPlaceholder={filterEmptyPlaceholder} + filterItems={filterItems} onUpdate={updateSettings} items={actualItems} renderSwitcher={({onClick}) => (