diff --git a/src/components/Table/README.md b/src/components/Table/README.md index 3b553f3f16..a3cca23474 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` | +| 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 diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 13062ec13f..27613f21ee 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..ba8c70ee18 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, waitFor} 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(); + 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(); + }); + }); }); 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 b037a7c9ba..9398ae549d 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -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'; @@ -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 = (list: T[], startIndex: number, endIndex: number): T[] => { const result = [...list]; @@ -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 = () => {placeholder}; + return emptyRenderContainer; +}; + export type RenderControls = (params: { DefaultApplyButton: React.ComponentType; /** @@ -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) => { @@ -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); @@ -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(({id, isSelected}) => ({id, isSelected})); propsOnUpdate(newSettings); - setOpen(false); + onOpenChange(false); }; const DefaultApplyButton = () => ( @@ -344,7 +376,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { ), }); - const dndRenderItem = useDndRenderItem(sortable); + const dndRenderItem = useDndRenderItem(sortingEnabled); const renderControl: TreeSelectProps['renderControl'] = ({toggleOpen}) => { const onKeyDown = createOnKeyDownHandler(toggleOpen); @@ -361,9 +393,10 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { const onOpenChange = (open: boolean) => { setOpen(open); - if (open === false) { setItems(propsItems); + setSortingEnabled(sortable); + filterState.reset(); } }; @@ -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 ? ( + + ) : null; + + const renderContainer = + filterState.filter && !filterState.items.length ? emptyRenderContainer : dndRenderContainer; + return ( { 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} /> diff --git a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx index 1882e849ad..a9d1005a60 100644 --- a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx +++ b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx @@ -112,6 +112,7 @@ export function getActualItems( export interface WithTableSettingsOptions { width?: TreeSelectProps['popupWidth']; sortable?: boolean; + filterable?: boolean; } interface WithTableSettingsBaseProps { @@ -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'); @@ -169,7 +177,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); @@ -181,6 +189,9 @@ export function withTableSettings( renderControls, defaultSettings, showResetButton, + settingsFilterPlaceholder, + settingsFilterEmptyMessage, + filterSettings, ...restTableProps }: TableProps & WithTableSettingsProps & E) { const defaultActualItems = React.useMemo(() => { @@ -193,7 +204,6 @@ export function withTableSettings( const enhancedColumns = React.useMemo(() => { const actualItems = getActualItems(columns, settings || []); - return enhanceSystemColumn(filterColumns(columns, actualItems), (systemColumn) => { systemColumn.name = () => (
@@ -201,6 +211,10 @@ export function withTableSettings( popupWidth={settingsPopupWidth || width} popupPlacement={POPUP_PLACEMENT} sortable={sortable} + filterable={filterable} + filterPlaceholder={settingsFilterPlaceholder} + filterEmptyMessage={settingsFilterEmptyMessage} + filterSettings={filterSettings} onUpdate={updateSettings} items={actualItems} renderSwitcher={({onClick}) => (