From 9c17f94cab4be617cb460c7b8a1e73e14efc43e2 Mon Sep 17 00:00:00 2001 From: afc163 Date: Wed, 1 Sep 2021 10:49:52 +0800 Subject: [PATCH] feat: Table filterMode and filterSearch functions (#31809) * feat: Table supports filterMode="tree-select" * add tree component * fix detail * use tree * add @table-filter-dropdown-max-height * feat: add check all to filter tree * feat: add search * feat: use filterTreeNode * fix code style * fix style * style: tree node selected bg * fix demo * feat: add filterSearch * fix: clear search value after close filter dropdown * update snapshot * code style * fix test case * chore: new FilterDropdown.tsx file * chore: searchValueMatched function * add test case * test: add test cases * feat: reset only works on dropdown state now * chore: add table locales * fix search input width * tweak style * style: update transfer search input style * perf: improve table perf * fix: filterMuiltiple={false} * test: add test for selecting * chore: fix table filter selection * fix lint * remove unused code * fix: style dependencies * test: turn off bail config for duplidated-package-plugin in feature branch * Update components/table/hooks/useFilter/FilterSearch.tsx Co-authored-by: Peach * fix locale link * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Peach --- components/input/Input.tsx | 12 +- .../__snapshots__/index.test.js.snap | 9072 ++++++++++++----- components/locale/default.tsx | 2 + components/locale/zh_CN.tsx | 2 + .../table/__tests__/Table.filter.test.js | 330 +- .../__tests__/__snapshots__/demo.test.js.snap | 345 + components/table/demo/filter-in-tree.md | 122 + .../table/hooks/useFilter/FilterDropdown.tsx | 227 +- .../table/hooks/useFilter/FilterSearch.tsx | 39 + components/table/hooks/useFilter/index.tsx | 6 +- components/table/index.en-US.md | 4 +- components/table/index.zh-CN.md | 4 +- components/table/interface.tsx | 4 + components/table/style/index.less | 57 +- components/table/style/index.tsx | 2 + .../__tests__/__snapshots__/demo.test.js.snap | 288 +- .../__snapshots__/search.test.js.snap | 96 +- components/transfer/__tests__/search.test.js | 2 +- components/transfer/search.tsx | 40 +- components/transfer/style/index.less | 22 +- 20 files changed, 7675 insertions(+), 3001 deletions(-) create mode 100644 components/table/demo/filter-in-tree.md create mode 100644 components/table/hooks/useFilter/FilterSearch.tsx diff --git a/components/input/Input.tsx b/components/input/Input.tsx index f329db6845..17e28e1ba2 100644 --- a/components/input/Input.tsx +++ b/components/input/Input.tsx @@ -53,6 +53,7 @@ export interface InputProps suffix?: React.ReactNode; allowClear?: boolean; bordered?: boolean; + htmlSize?: number; } export function fixControlledValue(value: T) { @@ -272,7 +273,14 @@ class Input extends React.Component { bordered: boolean, input: ConfigConsumerProps['input'] = {}, ) => { - const { className, addonBefore, addonAfter, size: customizeSize, disabled } = this.props; + const { + className, + addonBefore, + addonAfter, + size: customizeSize, + disabled, + htmlSize, + } = this.props; // Fix https://fb.me/react-unknown-prop const otherProps = omit(this.props as InputProps & { inputType: any }, [ 'prefixCls', @@ -288,6 +296,7 @@ class Input extends React.Component { 'size', 'inputType', 'bordered', + 'htmlSize', ]); return ( { }, )} ref={this.saveInput} + size={htmlSize} /> ); }; diff --git a/components/locale-provider/__tests__/__snapshots__/index.test.js.snap b/components/locale-provider/__tests__/__snapshots__/index.test.js.snap index aca16eb5a1..8bb277a094 100644 --- a/components/locale-provider/__tests__/__snapshots__/index.test.js.snap +++ b/components/locale-provider/__tests__/__snapshots__/index.test.js.snap @@ -5428,33 +5428,61 @@ exports[`Locale Provider should display the text as ar 1`] = `
- - + + + + + + + +
@@ -5620,33 +5648,61 @@ exports[`Locale Provider should display the text as ar 1`] = `
- - + + + + + + + +
@@ -10446,33 +10502,61 @@ exports[`Locale Provider should display the text as az 1`] = `
- - + + + + + + + +
@@ -10638,33 +10722,61 @@ exports[`Locale Provider should display the text as az 1`] = `
- - + + + + + + + +
@@ -15464,33 +15576,61 @@ exports[`Locale Provider should display the text as bg 1`] = `
- - + + + + + + + +
@@ -15656,33 +15796,61 @@ exports[`Locale Provider should display the text as bg 1`] = `
- - + + + + + + + +
@@ -20482,33 +20650,61 @@ exports[`Locale Provider should display the text as bn-bd 1`] = `
- - + + + + + + + +
@@ -20674,33 +20870,61 @@ exports[`Locale Provider should display the text as bn-bd 1`] = `
- - + + + + + + + +
@@ -25500,33 +25724,61 @@ exports[`Locale Provider should display the text as by 1`] = `
- - + + + + + + + +
@@ -25692,33 +25944,61 @@ exports[`Locale Provider should display the text as by 1`] = `
- - + + + + + + + +
@@ -30518,33 +30798,61 @@ exports[`Locale Provider should display the text as ca 1`] = `
- - + + + + + + + +
@@ -30710,33 +31018,61 @@ exports[`Locale Provider should display the text as ca 1`] = `
- - + + + + + + + +
@@ -35536,33 +35872,61 @@ exports[`Locale Provider should display the text as cs 1`] = `
- - + + + + + + + +
@@ -35728,33 +36092,61 @@ exports[`Locale Provider should display the text as cs 1`] = `
- - + + + + + + + +
@@ -40554,33 +40946,61 @@ exports[`Locale Provider should display the text as da 1`] = `
- - + + + + + + + +
@@ -40746,33 +41166,61 @@ exports[`Locale Provider should display the text as da 1`] = `
- - + + + + + + + +
@@ -45572,33 +46020,61 @@ exports[`Locale Provider should display the text as de 1`] = `
- - + + + + + + + +
@@ -45764,33 +46240,61 @@ exports[`Locale Provider should display the text as de 1`] = `
- - + + + + + + + +
@@ -50590,33 +51094,61 @@ exports[`Locale Provider should display the text as el 1`] = `
- - + + + + + + + +
@@ -50782,33 +51314,61 @@ exports[`Locale Provider should display the text as el 1`] = `
- - + + + + + + + +
@@ -55608,33 +56168,61 @@ exports[`Locale Provider should display the text as en 1`] = `
- - + + + + + + + +
@@ -55800,33 +56388,61 @@ exports[`Locale Provider should display the text as en 1`] = `
- - + + + + + + + +
@@ -60626,33 +61242,61 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
- - + + + + + + + +
@@ -60818,33 +61462,61 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
- - + + + + + + + +
@@ -65644,33 +66316,61 @@ exports[`Locale Provider should display the text as es 1`] = `
- - + + + + + + + +
@@ -65836,33 +66536,61 @@ exports[`Locale Provider should display the text as es 1`] = `
- - + + + + + + + +
@@ -70662,33 +71390,61 @@ exports[`Locale Provider should display the text as et 1`] = `
- - + + + + + + + +
@@ -70854,33 +71610,61 @@ exports[`Locale Provider should display the text as et 1`] = `
- - + + + + + + + +
@@ -75680,33 +76464,61 @@ exports[`Locale Provider should display the text as fa 1`] = `
- - + + + + + + + +
@@ -75872,33 +76684,61 @@ exports[`Locale Provider should display the text as fa 1`] = `
- - + + + + + + + +
@@ -80698,33 +81538,61 @@ exports[`Locale Provider should display the text as fi 1`] = `
- - + + + + + + + +
@@ -80890,33 +81758,61 @@ exports[`Locale Provider should display the text as fi 1`] = `
- - + + + + + + + +
@@ -85716,33 +86612,61 @@ exports[`Locale Provider should display the text as fr 1`] = `
- - + + + + + + + +
@@ -85908,33 +86832,61 @@ exports[`Locale Provider should display the text as fr 1`] = `
- - + + + + + + + +
@@ -90734,33 +91686,61 @@ exports[`Locale Provider should display the text as fr 2`] = `
- - + + + + + + + +
@@ -90926,33 +91906,61 @@ exports[`Locale Provider should display the text as fr 2`] = `
- - + + + + + + + +
@@ -95752,33 +96760,61 @@ exports[`Locale Provider should display the text as fr 3`] = `
- - + + + + + + + +
@@ -95944,33 +96980,61 @@ exports[`Locale Provider should display the text as fr 3`] = `
- - + + + + + + + +
@@ -100770,33 +101834,61 @@ exports[`Locale Provider should display the text as ga 1`] = `
- - + + + + + + + +
@@ -100962,33 +102054,61 @@ exports[`Locale Provider should display the text as ga 1`] = `
- - + + + + + + + +
@@ -105788,33 +106908,61 @@ exports[`Locale Provider should display the text as gl 1`] = `
- - + + + + + + + +
@@ -105980,33 +107128,61 @@ exports[`Locale Provider should display the text as gl 1`] = `
- - + + + + + + + +
@@ -110806,33 +111982,61 @@ exports[`Locale Provider should display the text as he 1`] = `
- - + + + + + + + +
@@ -110998,33 +112202,61 @@ exports[`Locale Provider should display the text as he 1`] = `
- - + + + + + + + +
@@ -115824,33 +117056,61 @@ exports[`Locale Provider should display the text as hi 1`] = `
- - + + + + + + + +
@@ -116016,33 +117276,61 @@ exports[`Locale Provider should display the text as hi 1`] = `
- - + + + + + + + +
@@ -120842,33 +122130,61 @@ exports[`Locale Provider should display the text as hr 1`] = `
- - + + + + + + + +
@@ -121034,33 +122350,61 @@ exports[`Locale Provider should display the text as hr 1`] = `
- - + + + + + + + +
@@ -125860,33 +127204,61 @@ exports[`Locale Provider should display the text as hu 1`] = `
- - + + + + + + + +
@@ -126052,33 +127424,61 @@ exports[`Locale Provider should display the text as hu 1`] = `
- - + + + + + + + +
@@ -130878,33 +132278,61 @@ exports[`Locale Provider should display the text as hy-am 1`] = `
- - + + + + + + + +
@@ -131070,33 +132498,61 @@ exports[`Locale Provider should display the text as hy-am 1`] = `
- - + + + + + + + +
@@ -135896,33 +137352,61 @@ exports[`Locale Provider should display the text as id 1`] = `
- - + + + + + + + +
@@ -136088,33 +137572,61 @@ exports[`Locale Provider should display the text as id 1`] = `
- - + + + + + + + +
@@ -140914,33 +142426,61 @@ exports[`Locale Provider should display the text as is 1`] = `
- - + + + + + + + +
@@ -141106,33 +142646,61 @@ exports[`Locale Provider should display the text as is 1`] = `
- - + + + + + + + +
@@ -145932,33 +147500,61 @@ exports[`Locale Provider should display the text as it 1`] = `
- - + + + + + + + +
@@ -146124,33 +147720,61 @@ exports[`Locale Provider should display the text as it 1`] = `
- - + + + + + + + +
@@ -150950,33 +152574,61 @@ exports[`Locale Provider should display the text as ja 1`] = `
- - + + + + + + + +
@@ -151142,33 +152794,61 @@ exports[`Locale Provider should display the text as ja 1`] = `
- - + + + + + + + +
@@ -155968,33 +157648,61 @@ exports[`Locale Provider should display the text as kk 1`] = `
- - + + + + + + + +
@@ -156160,33 +157868,61 @@ exports[`Locale Provider should display the text as kk 1`] = `
- - + + + + + + + +
@@ -160986,33 +162722,61 @@ exports[`Locale Provider should display the text as kn 1`] = `
- - + + + + + + + +
@@ -161178,33 +162942,61 @@ exports[`Locale Provider should display the text as kn 1`] = `
- - + + + + + + + +
@@ -166004,33 +167796,61 @@ exports[`Locale Provider should display the text as ko 1`] = `
- - + + + + + + + +
@@ -166196,33 +168016,61 @@ exports[`Locale Provider should display the text as ko 1`] = `
- - + + + + + + + +
@@ -171022,33 +172870,61 @@ exports[`Locale Provider should display the text as ku 1`] = `
- - + + + + + + + +
@@ -171214,33 +173090,61 @@ exports[`Locale Provider should display the text as ku 1`] = `
- - + + + + + + + +
@@ -176040,33 +177944,61 @@ exports[`Locale Provider should display the text as ku-iq 1`] = `
- - + + + + + + + +
@@ -176232,33 +178164,61 @@ exports[`Locale Provider should display the text as ku-iq 1`] = `
- - + + + + + + + +
@@ -181058,33 +183018,61 @@ exports[`Locale Provider should display the text as lt 1`] = `
- - + + + + + + + +
@@ -181250,33 +183238,61 @@ exports[`Locale Provider should display the text as lt 1`] = `
- - + + + + + + + +
@@ -186076,33 +188092,61 @@ exports[`Locale Provider should display the text as lv 1`] = `
- - + + + + + + + +
@@ -186268,33 +188312,61 @@ exports[`Locale Provider should display the text as lv 1`] = `
- - + + + + + + + +
@@ -191094,33 +193166,61 @@ exports[`Locale Provider should display the text as mk 1`] = `
- - + + + + + + + +
@@ -191286,33 +193386,61 @@ exports[`Locale Provider should display the text as mk 1`] = `
- - + + + + + + + +
@@ -196112,33 +198240,61 @@ exports[`Locale Provider should display the text as ml 1`] = `
- - + + + + + + + +
@@ -196304,33 +198460,61 @@ exports[`Locale Provider should display the text as ml 1`] = `
- - + + + + + + + +
@@ -201130,33 +203314,61 @@ exports[`Locale Provider should display the text as mn-mn 1`] = `
- - + + + + + + + +
@@ -201322,33 +203534,61 @@ exports[`Locale Provider should display the text as mn-mn 1`] = `
- - + + + + + + + +
@@ -206148,33 +208388,61 @@ exports[`Locale Provider should display the text as ms-my 1`] = `
- - + + + + + + + +
@@ -206340,33 +208608,61 @@ exports[`Locale Provider should display the text as ms-my 1`] = `
- - + + + + + + + +
@@ -211166,33 +213462,61 @@ exports[`Locale Provider should display the text as nb 1`] = `
- - + + + + + + + +
@@ -211358,33 +213682,61 @@ exports[`Locale Provider should display the text as nb 1`] = `
- - + + + + + + + +
@@ -216184,33 +218536,61 @@ exports[`Locale Provider should display the text as ne-np 1`] = `
- - + + + + + + + +
@@ -216376,33 +218756,61 @@ exports[`Locale Provider should display the text as ne-np 1`] = `
- - + + + + + + + +
@@ -221202,33 +223610,61 @@ exports[`Locale Provider should display the text as nl 1`] = `
- - + + + + + + + +
@@ -221394,33 +223830,61 @@ exports[`Locale Provider should display the text as nl 1`] = `
- - + + + + + + + +
@@ -226220,33 +228684,61 @@ exports[`Locale Provider should display the text as nl-be 1`] = `
- - + + + + + + + +
@@ -226412,33 +228904,61 @@ exports[`Locale Provider should display the text as nl-be 1`] = `
- - + + + + + + + +
@@ -231238,33 +233758,61 @@ exports[`Locale Provider should display the text as pl 1`] = `
- - + + + + + + + +
@@ -231430,33 +233978,61 @@ exports[`Locale Provider should display the text as pl 1`] = `
- - + + + + + + + +
@@ -236256,33 +238832,61 @@ exports[`Locale Provider should display the text as pt 1`] = `
- - + + + + + + + +
@@ -236448,33 +239052,61 @@ exports[`Locale Provider should display the text as pt 1`] = `
- - + + + + + + + +
@@ -241274,33 +243906,61 @@ exports[`Locale Provider should display the text as pt-br 1`] = `
- - + + + + + + + +
@@ -241466,33 +244126,61 @@ exports[`Locale Provider should display the text as pt-br 1`] = `
- - + + + + + + + +
@@ -246292,33 +248980,61 @@ exports[`Locale Provider should display the text as ro 1`] = `
- - + + + + + + + +
@@ -246484,33 +249200,61 @@ exports[`Locale Provider should display the text as ro 1`] = `
- - + + + + + + + +
@@ -251310,33 +254054,61 @@ exports[`Locale Provider should display the text as ru 1`] = `
- - + + + + + + + +
@@ -251502,33 +254274,61 @@ exports[`Locale Provider should display the text as ru 1`] = `
- - + + + + + + + +
@@ -256328,33 +259128,61 @@ exports[`Locale Provider should display the text as sk 1`] = `
- - + + + + + + + +
@@ -256520,33 +259348,61 @@ exports[`Locale Provider should display the text as sk 1`] = `
- - + + + + + + + +
@@ -261346,33 +264202,61 @@ exports[`Locale Provider should display the text as sl 1`] = `
- - + + + + + + + +
@@ -261538,33 +264422,61 @@ exports[`Locale Provider should display the text as sl 1`] = `
- - + + + + + + + +
@@ -266364,33 +269276,61 @@ exports[`Locale Provider should display the text as sr 1`] = `
- - + + + + + + + +
@@ -266556,33 +269496,61 @@ exports[`Locale Provider should display the text as sr 1`] = `
- - + + + + + + + +
@@ -271382,33 +274350,61 @@ exports[`Locale Provider should display the text as sv 1`] = `
- - + + + + + + + +
@@ -271574,33 +274570,61 @@ exports[`Locale Provider should display the text as sv 1`] = `
- - + + + + + + + +
@@ -276400,33 +279424,61 @@ exports[`Locale Provider should display the text as ta 1`] = `
- - + + + + + + + +
@@ -276592,33 +279644,61 @@ exports[`Locale Provider should display the text as ta 1`] = `
- - + + + + + + + +
@@ -281418,33 +284498,61 @@ exports[`Locale Provider should display the text as th 1`] = `
- - + + + + + + + +
@@ -281610,33 +284718,61 @@ exports[`Locale Provider should display the text as th 1`] = `
- - + + + + + + + +
@@ -286436,33 +289572,61 @@ exports[`Locale Provider should display the text as tr 1`] = `
- - + + + + + + + +
@@ -286628,33 +289792,61 @@ exports[`Locale Provider should display the text as tr 1`] = `
- - + + + + + + + +
@@ -291454,33 +294646,61 @@ exports[`Locale Provider should display the text as uk 1`] = `
- - + + + + + + + +
@@ -291646,33 +294866,61 @@ exports[`Locale Provider should display the text as uk 1`] = `
- - + + + + + + + +
@@ -296472,33 +299720,61 @@ exports[`Locale Provider should display the text as ur 1`] = `
- - + + + + + + + +
@@ -296664,33 +299940,61 @@ exports[`Locale Provider should display the text as ur 1`] = `
- - + + + + + + + +
@@ -301490,33 +304794,61 @@ exports[`Locale Provider should display the text as vi 1`] = `
- - + + + + + + + +
@@ -301682,33 +305014,61 @@ exports[`Locale Provider should display the text as vi 1`] = `
- - + + + + + + + +
@@ -306508,33 +309868,61 @@ exports[`Locale Provider should display the text as zh-cn 1`] = `
- - + + + + + + + +
@@ -306700,33 +310088,61 @@ exports[`Locale Provider should display the text as zh-cn 1`] = `
- - + + + + + + + +
@@ -311526,33 +314942,61 @@ exports[`Locale Provider should display the text as zh-hk 1`] = `
- - + + + + + + + +
@@ -311718,33 +315162,61 @@ exports[`Locale Provider should display the text as zh-hk 1`] = `
- - + + + + + + + +
@@ -316544,33 +320016,61 @@ exports[`Locale Provider should display the text as zh-tw 1`] = `
- - + + + + + + + +
@@ -316736,33 +320236,61 @@ exports[`Locale Provider should display the text as zh-tw 1`] = `
- - + + + + + + + +
diff --git a/components/locale/default.tsx b/components/locale/default.tsx index 06cea25102..39dbbcc57d 100644 --- a/components/locale/default.tsx +++ b/components/locale/default.tsx @@ -21,6 +21,8 @@ const localeValues: Locale = { filterConfirm: 'OK', filterReset: 'Reset', filterEmptyText: 'No filters', + filterCheckall: 'Select all items', + filterSearchPlaceholder: 'Search in filters', emptyText: 'No data', selectAll: 'Select current page', selectInvert: 'Invert current page', diff --git a/components/locale/zh_CN.tsx b/components/locale/zh_CN.tsx index 79635e5613..fc82cda35e 100644 --- a/components/locale/zh_CN.tsx +++ b/components/locale/zh_CN.tsx @@ -22,6 +22,8 @@ const localeValues: Locale = { filterConfirm: '确定', filterReset: '重置', filterEmptyText: '无筛选项', + filterCheckall: '全选', + filterSearchPlaceholder: '在筛选项中搜索', selectAll: '全选当页', selectInvert: '反选当页', selectNone: '清空所有', diff --git a/components/table/__tests__/Table.filter.test.js b/components/table/__tests__/Table.filter.test.js index 3d4c7b7bfc..f096908e70 100644 --- a/components/table/__tests__/Table.filter.test.js +++ b/components/table/__tests__/Table.filter.test.js @@ -7,7 +7,10 @@ import Input from '../../input'; import Tooltip from '../../tooltip'; import Button from '../../button'; import Select from '../../select'; +import Tree from '../../tree'; import ConfigProvider from '../../config-provider'; +import Checkbox from '../../checkbox'; +import Menu from '../../menu'; // https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73 const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } }; @@ -205,6 +208,7 @@ describe('Table.filter', () => { wrapper.find('#confirm').simulate('click'); expect(getFilterMenu().props().filterState.filteredKeys).toEqual([42]); wrapper.find('#reset').simulate('click'); + wrapper.find('#confirm').simulate('click'); expect(getFilterMenu().props().filterState.filteredKeys).toBeFalsy(); // try to use confirm btn @@ -688,15 +692,21 @@ describe('Table.filter', () => { const wrapper = mount(); wrapper.find('.ant-dropdown-trigger').first().simulate('click'); - + expect(wrapper.find('Dropdown').first().props().visible).toBe(true); wrapper.find('MenuItem').first().simulate('click'); wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click'); wrapper.update(); + expect(wrapper.find('Dropdown').first().props().visible).toBe(false); expect(renderedNames(wrapper)).toEqual(['Jack']); + wrapper.find('.ant-dropdown-trigger').first().simulate('click'); wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-link').simulate('click'); wrapper.update(); + expect(wrapper.find('Dropdown').first().props().visible).toBe(true); + expect(renderedNames(wrapper)).toEqual(['Jack']); + wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click'); expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); + expect(wrapper.find('Dropdown').first().props().visible).toBe(false); }); it('works with grouping columns in controlled mode', () => { @@ -1355,6 +1365,8 @@ describe('Table.filter', () => { { ...column, filterDropdownVisible: true, + filterSearch: true, + filterMode: 'tree', }, ], }), @@ -1366,6 +1378,12 @@ describe('Table.filter', () => { expect(wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-link').last().text()).toEqual( 'Reset', ); + expect(wrapper.find('.ant-table-filter-dropdown-checkall').first().text()).toEqual( + 'Select all items', + ); + expect(wrapper.find('.ant-input').getDOMNode().getAttribute('placeholder')).toEqual( + 'Search in filters', + ); }); it('filtered should work', () => { @@ -1456,7 +1474,7 @@ describe('Table.filter', () => { expect(checkbox.props().checked).toEqual(false); }); - it('should not trigger onChange when filter is empty', () => { + it('should not trigger onChange when filters is empty', () => { const onChange = jest.fn(); const Test = ({ filters }) => ( { wrapper.find('.ant-btn.ant-btn-primary.ant-btn-sm').simulate('click'); expect(wrapper.find('.ant-table-tbody .ant-table-cell').first().text()).toEqual(`${66}`); }); + + describe('filter tree mode', () => { + it('supports filter tree', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Tree).length).toBe(1); + expect(wrapper.find('.ant-tree-checkbox').length).toBe(5); + }); + + it('supports search input in filter tree', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + filterSearch: true, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Tree).length).toBe(1); + expect(wrapper.find(Input).length).toBe(1); + wrapper + .find(Input) + .find('input') + .simulate('change', { target: { value: '111' } }); + }); + + it('supports search input in filter menu', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterSearch: true, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Menu).length).toBe(1); + expect(wrapper.find(Input).length).toBe(1); + wrapper + .find(Input) + .find('input') + .simulate('change', { target: { value: '111' } }); + }); + + it('should skip search when filters[0].text is ReactNode', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filters: [ + { + text: '123', + value: '456', + }, + { + text: 123456, + value: '456', + }, + { + text: 123, + value: '456', + }, + ], + filterSearch: true, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Menu).length).toBe(1); + expect(wrapper.find(Input).length).toBe(1); + expect(wrapper.find('li.ant-dropdown-menu-item').length).toBe(3); + wrapper + .find(Input) + .find('input') + .simulate('change', { target: { value: '123' } }); + expect(wrapper.find('li.ant-dropdown-menu-item').length).toBe(2); + }); + + it('supports check all items', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + filterSearch: true, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Checkbox).length).toBe(1); + expect(wrapper.find(Checkbox).text()).toBe('Select all items'); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + wrapper + .find(Checkbox) + .find('input') + .simulate('change', { target: { checked: true } }); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(5); + wrapper + .find(Checkbox) + .find('input') + .simulate('change', { target: { checked: false } }); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + }); + + it('supports check item by selecting it', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + filterSearch: true, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find(Checkbox).length).toBe(1); + expect(wrapper.find(Checkbox).text()).toBe('Select all items'); + wrapper.find('.ant-tree-node-content-wrapper').at(0).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(0).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + }); + }); + + it('filterMultiple is false - check item', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + filterMultiple: false, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find('.ant-tree-checkbox').length).toBe(5); + expect(wrapper.find('.ant-table-filter-dropdown-checkall').exists()).toBe(false); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + wrapper.find('.ant-tree-checkbox').at(2).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(2).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(1); + wrapper.find('.ant-tree-checkbox').at(1).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(1).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(1); + wrapper.find('.ant-tree-checkbox').at(1).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(1).hasClass('ant-tree-checkbox-checked')).toBe( + false, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + }); + + it('filterMultiple is false - select item', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filterMode: 'tree', + filterMultiple: false, + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect(wrapper.find('.ant-tree-checkbox').length).toBe(5); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + wrapper.find('.ant-tree-node-content-wrapper').at(2).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(2).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(1); + wrapper.find('.ant-tree-node-content-wrapper').at(1).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(1).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(1); + wrapper.find('.ant-tree-node-content-wrapper').at(1).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(1).hasClass('ant-tree-checkbox-checked')).toBe( + false, + ); + expect(wrapper.find('.ant-tree-checkbox-checked').length).toBe(0); + }); + + it('should select children when select parent', () => { + jest.useFakeTimers(); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const wrapper = mount( + createTable({ + columns: [ + { + ...column, + filters: [ + { text: 'Boy', value: 'boy' }, + { text: 'Girl', value: 'girl' }, + { + text: 'Title', + value: 'title', + children: [ + { text: 'Jack', value: 'Jack' }, + { text: 'Coder', value: 'coder' }, + ], + }, + ], + filterMode: 'tree', + }, + ], + }), + ); + wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + // check parentnode + wrapper.find('.ant-tree-checkbox').at(2).simulate('click'); + expect(wrapper.find('.ant-tree-checkbox').at(2).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox').at(3).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + expect(wrapper.find('.ant-tree-checkbox').at(4).hasClass('ant-tree-checkbox-checked')).toBe( + true, + ); + wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click'); + expect(renderedNames(wrapper)).toEqual(['Jack']); + wrapper.find('.ant-tree-checkbox').at(2).simulate('click'); + wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click'); + expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); + wrapper.find('.ant-tree-node-content-wrapper').at(2).simulate('click'); + wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click'); + expect(renderedNames(wrapper)).toEqual(['Jack']); + }); }); diff --git a/components/table/__tests__/__snapshots__/demo.test.js.snap b/components/table/__tests__/__snapshots__/demo.test.js.snap index 67de1cfcc0..96fe0679c4 100644 --- a/components/table/__tests__/__snapshots__/demo.test.js.snap +++ b/components/table/__tests__/__snapshots__/demo.test.js.snap @@ -5585,6 +5585,351 @@ exports[`renders ./components/table/demo/expand.md correctly 1`] = ` `; +exports[`renders ./components/table/demo/filter-in-tree.md correctly 1`] = ` +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Name + + + + + + +
+
+
+ + Age + + + + + + + + + + + +
+
+
+ + Address + + + + + + +
+
+ John Brown + + 32 + + New York No. 1 Lake Park +
+ Jim Green + + 42 + + London No. 1 Lake Park +
+ Joe Black + + 32 + + Sidney No. 1 Lake Park +
+ Jim Red + + 32 + + London No. 2 Lake Park +
+ + + +
    +
  • + +
  • +
  • + + 1 + +
  • +
  • + +
  • +
+ + + +`; + exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `
`filterSearch` 用于开启筛选项的搜索。 + +## en-US + +You can use `filterMode` to change default filter interface, options: `menu`(default) and `tree`. + +> `filterSearch` is used for making filter dropdown items searchable. + +```jsx +import { Table } from 'antd'; + +const columns = [ + { + title: 'Name', + dataIndex: 'name', + filters: [ + { + text: 'Joe', + value: 'Joe', + }, + { + text: 'Category 1', + value: 'Category 1', + children: [ + { + text: 'Yellow', + value: 'Yellow', + }, + { + text: 'Pink', + value: 'Pink', + }, + ], + }, + { + text: 'Category 2', + value: 'Category 2', + children: [ + { + text: 'Green', + value: 'Green', + }, + { + text: 'Black', + value: 'Black', + }, + ], + }, + ], + filterMode: 'tree', + filterSearch: true, + onFilter: (value, record) => record.name.includes(value), + width: '30%', + }, + { + title: 'Age', + dataIndex: 'age', + sorter: (a, b) => a.age - b.age, + }, + { + title: 'Address', + dataIndex: 'address', + filters: [ + { + text: 'London', + value: 'London', + }, + { + text: 'New York', + value: 'New York', + }, + ], + onFilter: (value, record) => record.address.startsWith(value), + filterSearch: true, + width: '40%', + }, +]; + +const data = [ + { + key: '1', + name: 'John Brown', + age: 32, + address: 'New York No. 1 Lake Park', + }, + { + key: '2', + name: 'Jim Green', + age: 42, + address: 'London No. 1 Lake Park', + }, + { + key: '3', + name: 'Joe Black', + age: 32, + address: 'Sidney No. 1 Lake Park', + }, + { + key: '4', + name: 'Jim Red', + age: 32, + address: 'London No. 2 Lake Park', + }, +]; + +function onChange(pagination, filters, sorter, extra) { + console.log('params', pagination, filters, sorter, extra); +} + +ReactDOM.render(, mountNode); +``` diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx index 20c93f95d5..da8272d893 100644 --- a/components/table/hooks/useFilter/FilterDropdown.tsx +++ b/components/table/hooks/useFilter/FilterDropdown.tsx @@ -4,62 +4,50 @@ import isEqual from 'lodash/isEqual'; import FilterFilled from '@ant-design/icons/FilterFilled'; import Button from '../../../button'; import Menu from '../../../menu'; +import Tree from '../../../tree'; +import type { DataNode, EventDataNode } from '../../../tree'; import Checkbox from '../../../checkbox'; +import type { CheckboxChangeEvent } from '../../../checkbox'; import Radio from '../../../radio'; import Dropdown from '../../../dropdown'; import Empty from '../../../empty'; import { ColumnType, ColumnFilterItem, Key, TableLocale, GetPopupContainer } from '../../interface'; import FilterDropdownMenuWrapper from './FilterWrapper'; -import { FilterState } from '.'; +import FilterSearch from './FilterSearch'; +import { FilterState, flattenKeys } from '.'; import useSyncState from '../../../_util/hooks/useSyncState'; import { ConfigContext } from '../../../config-provider/context'; -const { SubMenu, Item: MenuItem } = Menu; - function hasSubMenu(filters: ColumnFilterItem[]) { return filters.some(({ children }) => children); } +function searchValueMatched(searchValue: string, text: React.ReactNode) { + if (typeof text === 'string' || typeof text === 'number') { + return text?.toString().toLowerCase().includes(searchValue.trim().toLowerCase()); + } + return false; +} + function renderFilterItems({ filters, prefixCls, filteredKeys, filterMultiple, - locale, + searchValue, }: { filters: ColumnFilterItem[]; prefixCls: string; filteredKeys: Key[]; filterMultiple: boolean; - locale: TableLocale; + searchValue: string; }) { - if (filters.length === 0) { - // wrapped with
to avoid react warning - // https://github.com/ant-design/ant-design/issues/25979 - return ( - -
- -
-
- ); - } return filters.map((filter, index) => { const key = String(filter.value); if (filter.children) { return ( - + ); } const Component = filterMultiple ? Checkbox : Radio; - return ( - + const item = ( + {filter.text} - + ); + if (searchValue.trim()) { + return searchValueMatched(searchValue, filter.text) ? item : undefined; + } + return item; }); } @@ -93,6 +85,8 @@ export interface FilterDropdownProps { column: ColumnType; filterState?: FilterState; filterMultiple: boolean; + filterMode?: 'menu' | 'tree'; + filterSearch?: boolean; columnKey: Key; children: React.ReactNode; triggerFilter: (filterState: FilterState) => void; @@ -108,6 +102,8 @@ function FilterDropdown(props: FilterDropdownProps) { dropdownPrefixCls, columnKey, filterMultiple, + filterMode = 'menu', + filterSearch = false, filterState, triggerFilter, locale, @@ -134,11 +130,22 @@ function FilterDropdown(props: FilterDropdownProps) { const propFilteredKeys = filterState?.filteredKeys; const [getFilteredKeysSync, setFilteredKeysSync] = useSyncState(propFilteredKeys || []); - const onSelectKeys = ({ selectedKeys }: { selectedKeys?: Key[] }) => { - setFilteredKeysSync(selectedKeys!); + const onSelectKeys = ({ selectedKeys }: { selectedKeys: Key[] }) => { + setFilteredKeysSync(selectedKeys); + }; + + const onCheck = (keys: Key[], { node, checked }: { node: EventDataNode; checked: boolean }) => { + if (!filterMultiple) { + onSelectKeys({ selectedKeys: checked && node.key ? [node.key] : [] }); + } else { + onSelectKeys({ selectedKeys: keys as Key[] }); + } }; React.useEffect(() => { + if (!visible) { + return; + } onSelectKeys({ selectedKeys: propFilteredKeys || [] }); }, [propFilteredKeys]); @@ -160,6 +167,19 @@ function FilterDropdown(props: FilterDropdownProps) { [], ); + // search in tree mode column filter + const [searchValue, setSearchValue] = React.useState(''); + const onSearch = (e: React.ChangeEvent) => { + const { value } = e.target; + setSearchValue(value); + }; + // clear search value after close filter dropdown + React.useEffect(() => { + if (!visible) { + setSearchValue(''); + } + }, [visible]); + // ======================= Submit ======================== const internalTriggerFilter = (keys: Key[] | undefined | null) => { const mergedKeys = keys && keys.length ? keys : null; @@ -184,9 +204,8 @@ function FilterDropdown(props: FilterDropdownProps) { }; const onReset = () => { + setSearchValue(''); setFilteredKeysSync([]); - triggerVisible(false); - internalTriggerFilter([]); }; const doFilter = ({ closeDropdown } = { closeDropdown: true }) => { @@ -215,8 +234,29 @@ function FilterDropdown(props: FilterDropdownProps) { [`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []), }); - let dropdownContent: React.ReactNode; + const onCheckAll = (e: CheckboxChangeEvent) => { + if (e.target.checked) { + const allFilterKeys = flattenKeys(column?.filters).map(key => String(key)); + setFilteredKeysSync(allFilterKeys); + } else { + setFilteredKeysSync([]); + } + }; + const getTreeData = ({ filters }: { filters?: ColumnFilterItem[] }) => + (filters || []).map((filter, index) => { + const key = String(filter.value); + const item: DataNode = { + title: filter.text, + key: filter.value !== undefined ? key : index, + }; + if (filter.children) { + item.children = getTreeData({ filters: filter.children }); + } + return item; + }); + + let dropdownContent: React.ReactNode; if (typeof column.filterDropdown === 'function') { dropdownContent = column.filterDropdown({ prefixCls: `${dropdownPrefixCls}-custom`, @@ -231,28 +271,101 @@ function FilterDropdown(props: FilterDropdownProps) { dropdownContent = column.filterDropdown; } else { const selectedKeys = (getFilteredKeysSync() || []) as any; + const getFilterComponent = () => { + if ((column.filters || []).length === 0) { + return ( + + ); + } + if (filterMode === 'tree') { + return ( + <> + +
+ {filterMultiple ? ( + + {locale.filterCheckall} + + ) : null} + searchValueMatched(searchValue, node.title) + : undefined + } + /> +
+ + ); + } + return ( + <> + + + {renderFilterItems({ + filters: column.filters || [], + prefixCls, + filteredKeys: getFilteredKeysSync(), + filterMultiple, + searchValue, + })} + + + ); + }; + dropdownContent = ( <> - - {renderFilterItems({ - filters: column.filters || [], - prefixCls, - filteredKeys: getFilteredKeysSync(), - filterMultiple, - locale, - })} - + {getFilterComponent()}