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 <scdzwyxst@gmail.com>

* fix locale link

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Peach <scdzwyxst@gmail.com>
This commit is contained in:
afc163 2021-09-01 10:49:52 +08:00 committed by GitHub
parent a43e21e6bf
commit 9c17f94cab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 7675 additions and 3001 deletions

View File

@ -53,6 +53,7 @@ export interface InputProps
suffix?: React.ReactNode;
allowClear?: boolean;
bordered?: boolean;
htmlSize?: number;
}
export function fixControlledValue<T>(value: T) {
@ -272,7 +273,14 @@ class Input extends React.Component<InputProps, InputState> {
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<InputProps, InputState> {
'size',
'inputType',
'bordered',
'htmlSize',
]);
return (
<input
@ -304,6 +313,7 @@ class Input extends React.Component<InputProps, InputState> {
},
)}
ref={this.saveInput}
size={htmlSize}
/>
);
};

View File

@ -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',

View File

@ -22,6 +22,8 @@ const localeValues: Locale = {
filterConfirm: '确定',
filterReset: '重置',
filterEmptyText: '无筛选项',
filterCheckall: '全选',
filterSearchPlaceholder: '在筛选项中搜索',
selectAll: '全选当页',
selectInvert: '反选当页',
selectNone: '清空所有',

View File

@ -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(<App />);
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 }) => (
<Table
@ -1726,4 +1744,312 @@ describe('Table.filter', () => {
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: <span>123</span>,
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']);
});
});

View File

@ -5585,6 +5585,351 @@ exports[`renders ./components/table/demo/expand.md correctly 1`] = `
</div>
`;
exports[`renders ./components/table/demo/filter-in-tree.md correctly 1`] = `
<div
class="ant-table-wrapper"
>
<div
class="ant-spin-nested-loading"
>
<div
class="ant-spin-container"
>
<div
class="ant-table"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
>
<table
style="table-layout:auto"
>
<colgroup>
<col
style="width:30%"
/>
<col />
<col
style="width:40%"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell"
>
<div
class="ant-table-filter-column"
>
<span
class="ant-table-column-title"
>
Name
</span>
<span
class="ant-dropdown-trigger ant-table-filter-trigger"
role="button"
tabindex="-1"
>
<span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span>
</div>
</th>
<th
class="ant-table-cell ant-table-column-has-sorters"
>
<div
class="ant-table-column-sorters"
>
<span
class="ant-table-column-title"
>
Age
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
>
<span
class="ant-table-column-sorter-inner"
>
<span
aria-label="caret-up"
class="anticon anticon-caret-up ant-table-column-sorter-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</span>
<span
aria-label="caret-down"
class="anticon anticon-caret-down ant-table-column-sorter-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</span>
</div>
</th>
<th
class="ant-table-cell"
>
<div
class="ant-table-filter-column"
>
<span
class="ant-table-column-title"
>
Address
</span>
<span
class="ant-dropdown-trigger ant-table-filter-trigger"
role="button"
tabindex="-1"
>
<span
aria-label="filter"
class="anticon anticon-filter"
role="img"
>
<svg
aria-hidden="true"
data-icon="filter"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z"
/>
</svg>
</span>
</span>
</div>
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="1"
>
<td
class="ant-table-cell"
>
John Brown
</td>
<td
class="ant-table-cell"
>
32
</td>
<td
class="ant-table-cell"
>
New York No. 1 Lake Park
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="2"
>
<td
class="ant-table-cell"
>
Jim Green
</td>
<td
class="ant-table-cell"
>
42
</td>
<td
class="ant-table-cell"
>
London No. 1 Lake Park
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="3"
>
<td
class="ant-table-cell"
>
Joe Black
</td>
<td
class="ant-table-cell"
>
32
</td>
<td
class="ant-table-cell"
>
Sidney No. 1 Lake Park
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="4"
>
<td
class="ant-table-cell"
>
Jim Red
</td>
<td
class="ant-table-cell"
>
32
</td>
<td
class="ant-table-cell"
>
London No. 2 Lake Park
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ul
class="ant-pagination ant-table-pagination ant-table-pagination-right"
unselectable="unselectable"
>
<li
aria-disabled="true"
class="ant-pagination-prev ant-pagination-disabled"
title="Previous Page"
>
<button
class="ant-pagination-item-link"
disabled=""
tabindex="-1"
type="button"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</button>
</li>
<li
class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"
tabindex="0"
title="1"
>
<a
rel="nofollow"
>
1
</a>
</li>
<li
aria-disabled="true"
class="ant-pagination-next ant-pagination-disabled"
title="Next Page"
>
<button
class="ant-pagination-item-link"
disabled=""
tabindex="-1"
type="button"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
`;
exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `
<div
class="ant-table-wrapper"

View File

@ -0,0 +1,122 @@
---
order: 6.1
title:
en-US: Filter in Tree
zh-CN: 树型筛选菜单
---
## zh-CN
可以使用 `filterMode` 来修改筛选菜单的 UI可选值有 `menu`(默认)和 `tree`
> `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(<Table columns={columns} dataSource={data} onChange={onChange} />, mountNode);
```

View File

@ -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 <div /> to avoid react warning
// https://github.com/ant-design/ant-design/issues/25979
return (
<MenuItem key="empty">
<div
style={{
margin: '16px 0',
}}
>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={locale.filterEmptyText}
imageStyle={{
height: 24,
}}
/>
</div>
</MenuItem>
);
}
return filters.map((filter, index) => {
const key = String(filter.value);
if (filter.children) {
return (
<SubMenu
<Menu.SubMenu
key={key || index}
title={filter.text}
popupClassName={`${prefixCls}-dropdown-submenu`}
@ -69,20 +57,24 @@ function renderFilterItems({
prefixCls,
filteredKeys,
filterMultiple,
locale,
searchValue,
})}
</SubMenu>
</Menu.SubMenu>
);
}
const Component = filterMultiple ? Checkbox : Radio;
return (
<MenuItem key={filter.value !== undefined ? key : index}>
const item = (
<Menu.Item key={filter.value !== undefined ? key : index}>
<Component checked={filteredKeys.includes(key)} />
<span>{filter.text}</span>
</MenuItem>
</Menu.Item>
);
if (searchValue.trim()) {
return searchValueMatched(searchValue, filter.text) ? item : undefined;
}
return item;
});
}
@ -93,6 +85,8 @@ export interface FilterDropdownProps<RecordType> {
column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>;
filterMultiple: boolean;
filterMode?: 'menu' | 'tree';
filterSearch?: boolean;
columnKey: Key;
children: React.ReactNode;
triggerFilter: (filterState: FilterState<RecordType>) => void;
@ -108,6 +102,8 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
dropdownPrefixCls,
columnKey,
filterMultiple,
filterMode = 'menu',
filterSearch = false,
filterState,
triggerFilter,
locale,
@ -134,11 +130,22 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
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<RecordType>(props: FilterDropdownProps<RecordType>) {
[],
);
// search in tree mode column filter
const [searchValue, setSearchValue] = React.useState('');
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
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<RecordType>(props: FilterDropdownProps<RecordType>) {
};
const onReset = () => {
setSearchValue('');
setFilteredKeysSync([]);
triggerVisible(false);
internalTriggerFilter([]);
};
const doFilter = ({ closeDropdown } = { closeDropdown: true }) => {
@ -215,8 +234,29 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
[`${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<RecordType>(props: FilterDropdownProps<RecordType>) {
dropdownContent = column.filterDropdown;
} else {
const selectedKeys = (getFilteredKeysSync() || []) as any;
const getFilterComponent = () => {
if ((column.filters || []).length === 0) {
return (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={locale.filterEmptyText}
imageStyle={{
height: 24,
}}
style={{
margin: 0,
padding: '16px 0',
}}
/>
);
}
if (filterMode === 'tree') {
return (
<>
<FilterSearch
filterSearch={filterSearch}
value={searchValue}
onChange={onSearch}
tablePrefixCls={tablePrefixCls}
locale={locale}
/>
<div className={`${tablePrefixCls}-filter-dropdown-tree`}>
{filterMultiple ? (
<Checkbox
className={`${tablePrefixCls}-filter-dropdown-checkall`}
onChange={onCheckAll}
>
{locale.filterCheckall}
</Checkbox>
) : null}
<Tree
checkable
selectable={false}
blockNode
multiple={filterMultiple}
checkStrictly={!filterMultiple}
className={`${dropdownPrefixCls}-menu`}
onCheck={onCheck}
checkedKeys={selectedKeys}
selectedKeys={selectedKeys}
showIcon={false}
treeData={getTreeData({ filters: column.filters })}
autoExpandParent
defaultExpandAll
filterTreeNode={
searchValue.trim()
? node => searchValueMatched(searchValue, node.title)
: undefined
}
/>
</div>
</>
);
}
return (
<>
<FilterSearch
filterSearch={filterSearch}
value={searchValue}
onChange={onSearch}
tablePrefixCls={tablePrefixCls}
locale={locale}
/>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
className={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={selectedKeys}
getPopupContainer={getPopupContainer}
openKeys={openKeys}
onOpenChange={onOpenChange}
>
{renderFilterItems({
filters: column.filters || [],
prefixCls,
filteredKeys: getFilteredKeysSync(),
filterMultiple,
searchValue,
})}
</Menu>
</>
);
};
dropdownContent = (
<>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
className={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={selectedKeys}
getPopupContainer={getPopupContainer}
openKeys={openKeys}
onOpenChange={onOpenChange}
>
{renderFilterItems({
filters: column.filters || [],
prefixCls,
filteredKeys: getFilteredKeysSync(),
filterMultiple,
locale,
})}
</Menu>
{getFilterComponent()}
<div className={`${prefixCls}-dropdown-btns`}>
<Button type="link" size="small" disabled={selectedKeys.length === 0} onClick={onReset}>
{locale.filterReset}

View File

@ -0,0 +1,39 @@
import * as React from 'react';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import Input from '../../../input';
import { TableLocale } from '../../interface';
interface FilterSearchProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
filterSearch: Boolean;
tablePrefixCls: string;
locale: TableLocale;
}
const FilterSearch: React.FC<FilterSearchProps> = ({
value,
onChange,
filterSearch,
tablePrefixCls,
locale,
}) => {
if (!filterSearch) {
return null;
}
return (
<div className={`${tablePrefixCls}-filter-dropdown-search`}>
<Input
prefix={<SearchOutlined />}
placeholder={locale.filterSearchPlaceholder}
onChange={onChange}
value={value}
// for skip min-width of input
htmlSize={1}
className={`${tablePrefixCls}-filter-dropdown-search-input`}
/>
</div>
);
};
export default FilterSearch;

View File

@ -78,7 +78,7 @@ function injectFilter<RecordType>(
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
const { filterMultiple = true } = column as ColumnType<RecordType>;
const { filterMultiple = true, filterMode, filterSearch } = column as ColumnType<RecordType>;
let newColumn: ColumnsType<RecordType>[number] = column;
@ -97,6 +97,8 @@ function injectFilter<RecordType>(
columnKey={columnKey}
filterState={filterState}
filterMultiple={filterMultiple}
filterMode={filterMode}
filterSearch={filterSearch}
triggerFilter={triggerFilter}
locale={locale}
getPopupContainer={getPopupContainer}
@ -127,7 +129,7 @@ function injectFilter<RecordType>(
});
}
function flattenKeys(filters?: ColumnFilterItem[]) {
export function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: FilterValue = [];
(filters || []).forEach(({ value, children }) => {
keys.push(value);

View File

@ -68,7 +68,7 @@ const columns = [
| footer | Table footer renderer | function(currentPageData) | - | |
| getPopupContainer | The render container of dropdowns in table | (triggerNode) => HTMLElement | () => TableHtmlElement | |
| loading | Loading status of table | boolean \| [Spin Props](/components/spin/#API) | false | |
| locale | The i18n text including filter, sort, empty text, etc | object | filterConfirm: `Ok` <br> filterReset: `Reset` <br> emptyText: `No Data` <br> [Default](https://github.com/ant-design/ant-design/blob/4ad1ccac277782d7ed14f7e5d02d6346aae0db67/components/locale/default.tsx#L19) | |
| locale | The i18n text including filter, sort, empty text, etc | object | [Default Value](https://github.com/ant-design/ant-design/blob/6dae4a7e18ad1ba193aedd5ab6867e1d823e2aa4/components/locale/default.tsx#L19-L37) | |
| pagination | Config of pagination. You can ref table pagination [config](#pagination) or full [`pagination`](/components/pagination/) document, hide it by setting it to `false` | object | - | |
| rowClassName | Row's className | function(record, index): string | - | |
| rowKey | Row's unique key, could be a string or function that returns a string | string \| function(record): string | `key` | |
@ -129,6 +129,8 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| filteredValue | Controlled filtered value, filter icon will highlight | string\[] | - | |
| filterIcon | Customized filter icon | ReactNode \| (filtered: boolean) => ReactNode | - | |
| filterMultiple | Whether multiple filters can be selected | boolean | true | |
| filterMode | To specify the filter interface | 'menu' \| 'tree' | 'menu' | 4.17.0 |
| filterSearch | Whether to be searchable for filter menu | Boolean | false | 4.17.0 |
| filters | Filter menu config | object\[] | - | |
| fixed | (IE not support) Set column to be fixed: `true`(same as left) `'left'` `'right'` | boolean \| string | false | |
| key | Unique key of this column, you can ignore this prop if you've set a unique `dataIndex` | string | - | |

View File

@ -75,7 +75,7 @@ const columns = [
| footer | 表格尾部 | function(currentPageData) | - | |
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | () => TableHtmlElement | |
| loading | 页面是否加载中 | boolean \| [Spin Props](/components/spin/#API) | false | |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: `确定` <br> filterReset: `重置` <br> emptyText: `暂无数据` <br> [默认值](https://github.com/ant-design/ant-design/blob/4ad1ccac277782d7ed14f7e5d02d6346aae0db67/components/locale/default.tsx#L19) | |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | [默认值](https://github.com/ant-design/ant-design/blob/6dae4a7e18ad1ba193aedd5ab6867e1d823e2aa4/components/locale/zh_CN.tsx#L20-L37) | |
| pagination | 分页器,参考[配置项](#pagination)或 [pagination](/components/pagination/) 文档,设为 false 时不展示和进行分页 | object | - | |
| rowClassName | 表格行的类名 | function(record, index): string | - | |
| rowKey | 表格行 key 的取值,可以是字符串或一个函数 | string \| function(record): string | `key` | |
@ -136,6 +136,8 @@ const columns = [
| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | string\[] | - | |
| filterIcon | 自定义 filter 图标。 | ReactNode \| (filtered: boolean) => ReactNode | false | |
| filterMultiple | 是否多选 | boolean | true | |
| filterMode | 指定筛选菜单的用户界面 | 'menu' \| 'tree' | 'menu' | 4.17.0 |
| filterSearch | 筛选菜单项是否可搜索 | Boolean | false | 4.17.0 |
| filters | 表头的筛选菜单项 | object\[] | - | |
| fixed | IE 下无效)列是否固定,可选 true (等效于 left) `left` `right` | boolean \| string | false | |
| key | React 需要的 key如果已经设置了唯一的 `dataIndex`,可以忽略这个属性 | string | - | |

View File

@ -28,6 +28,8 @@ export interface TableLocale {
filterConfirm?: React.ReactNode;
filterReset?: React.ReactNode;
filterEmptyText?: React.ReactNode;
filterCheckall?: React.ReactNode;
filterSearchPlaceholder?: string;
emptyText?: React.ReactNode | (() => React.ReactNode);
selectAll?: React.ReactNode;
selectNone?: React.ReactNode;
@ -108,6 +110,8 @@ export interface ColumnType<RecordType> extends RcColumnType<RecordType> {
filteredValue?: FilterValue | null;
defaultFilteredValue?: FilterValue | null;
filterIcon?: React.ReactNode | ((filtered: boolean) => React.ReactNode);
filterMode?: 'menu' | 'tree';
filterSearch?: boolean;
onFilter?: (value: string | number | boolean, record: RecordType) => boolean;
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;

View File

@ -4,12 +4,14 @@
@import './bordered';
@table-prefix-cls: ~'@{ant-prefix}-table';
@tree-prefix-cls: ~'@{ant-prefix}-tree';
@dropdown-prefix-cls: ~'@{ant-prefix}-dropdown';
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
@table-header-icon-color: #bfbfbf;
@table-header-icon-color-hover: darken(@table-header-icon-color, 10%);
@table-sticky-zindex: calc(@zindex-table-fixed + 1);
@table-sticky-scroll-bar-active-bg: fade(@table-sticky-scroll-bar-bg, 80%);
@table-filter-dropdown-max-height: 264px;
.@{table-prefix-cls}-wrapper {
clear: both;
@ -330,21 +332,64 @@
&-filter-dropdown {
.reset-component();
min-width: 120px;
background-color: @table-filter-dropdown-bg;
border-radius: @border-radius-base;
box-shadow: @box-shadow-base;
// Reset menu
.@{dropdown-prefix-cls}-menu {
// https://github.com/ant-design/ant-design/issues/4916
// https://github.com/ant-design/ant-design/issues/19542
max-height: 264px;
max-height: @table-filter-dropdown-max-height;
overflow-x: hidden;
border: 0;
box-shadow: none;
&:empty::after {
display: block;
padding: 8px 0;
color: @disabled-color;
font-size: @font-size-sm;
text-align: center;
content: 'Not Found';
}
}
min-width: 120px;
background-color: @table-filter-dropdown-bg;
&-tree {
padding: 8px 8px 0 8px;
border-radius: @border-radius-base;
box-shadow: @box-shadow-base;
.@{tree-prefix-cls}-treenode .@{tree-prefix-cls}-node-content-wrapper:hover {
background-color: @tree-node-hover-bg;
}
.@{tree-prefix-cls}-treenode-checkbox-checked .@{tree-prefix-cls}-node-content-wrapper {
&,
&:hover {
background-color: @tree-node-selected-bg;
}
}
}
&-search {
padding: 8px;
border-bottom: @border-width-base @border-color-split @border-style-base;
&-input {
input {
min-width: 140px;
}
.@{iconfont-css-prefix} {
color: @disabled-color;
}
}
}
&-checkall {
width: 100%;
margin-bottom: 4px;
margin-left: 4px;
}
&-submenu > ul {
max-height: calc(100vh - 130px);
@ -364,7 +409,7 @@
&-btns {
display: flex;
justify-content: space-between;
padding: 7px 8px 7px 3px;
padding: 7px 8px;
overflow: hidden;
background-color: @table-filter-btns-bg;
border-top: @border-width-base @border-style-base @table-border-color;

View File

@ -12,3 +12,5 @@ import '../../dropdown/style';
import '../../spin/style';
import '../../pagination/style';
import '../../tooltip/style';
import '../../input/style';
import '../../tree/style';

View File

@ -60,33 +60,61 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
<div
class="ant-transfer-list-body-search-wrapper"
>
<input
class="ant-input ant-transfer-list-search"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-transfer-list-search-action"
class="ant-input-affix-wrapper ant-transfer-list-search"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
class="ant-input-prefix"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</div>
@ -271,33 +299,61 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
<div
class="ant-transfer-list-body-search-wrapper"
>
<input
class="ant-input ant-transfer-list-search"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-transfer-list-search-action"
class="ant-input-affix-wrapper ant-transfer-list-search"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
class="ant-input-prefix"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</div>
@ -2802,33 +2858,61 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
<div
class="ant-transfer-list-body-search-wrapper"
>
<input
class="ant-input ant-transfer-list-search"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-transfer-list-search-action"
class="ant-input-affix-wrapper ant-transfer-list-search"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
class="ant-input-prefix"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</div>
@ -2993,33 +3077,61 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
<div
class="ant-transfer-list-body-search-wrapper"
>
<input
class="ant-input ant-transfer-list-search"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-transfer-list-search-action"
class="ant-input-affix-wrapper ant-transfer-list-search"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
class="ant-input-prefix"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Search here"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</div>

View File

@ -1,15 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transfer.Search should show cross icon when input value exists 1`] = `
Array [
<input
class="ant-input"
placeholder=""
type="text"
value=""
/>,
<span
class="ant-input-affix-wrapper"
>
<span
class="undefined-action"
class="ant-input-prefix"
>
<span
aria-label="search"
@ -30,25 +26,21 @@ Array [
/>
</svg>
</span>
</span>,
]
`;
exports[`Transfer.Search should show cross icon when input value exists 2`] = `
Array [
</span>
<input
class="ant-input"
placeholder=""
type="text"
value="a"
/>,
<a
class="undefined-action"
value=""
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
@ -64,6 +56,66 @@ Array [
/>
</svg>
</span>
</a>,
]
</span>
</span>
`;
exports[`Transfer.Search should show cross icon when input value exists 2`] = `
<span
class="ant-input-affix-wrapper"
>
<span
class="ant-input-prefix"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder=""
type="text"
value="a"
/>
<span
class="ant-input-suffix"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
`;

View File

@ -59,7 +59,7 @@ describe('Transfer.Search', () => {
.simulate('change', { target: { value: 'a' } });
expect(onSearch).toHaveBeenCalledWith('left', 'a');
onSearch.mockReset();
wrapper.find('.ant-transfer-list-search-action').at(0).simulate('click');
wrapper.find('.ant-input-clear-icon').at(0).simulate('click');
expect(onSearch).toHaveBeenCalledWith('left', '');
jest.useRealTimers();
});

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import Input from '../input';
@ -8,7 +7,7 @@ export interface TransferSearchProps {
prefixCls?: string;
placeholder?: string;
onChange?: (e: React.FormEvent<HTMLElement>) => void;
handleClear?: (e: React.MouseEvent<HTMLElement>) => void;
handleClear?: () => void;
value?: string;
disabled?: boolean;
}
@ -19,35 +18,22 @@ export default function Search(props: TransferSearchProps) {
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
if (e.target.value === '') {
handleClear?.();
}
},
[onChange],
);
const handleClearFn = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (!disabled && handleClear) {
handleClear(e);
}
};
return (
<>
<Input
placeholder={placeholder}
className={prefixCls}
value={value}
onChange={handleChange}
disabled={disabled}
/>
{value && value.length > 0 ? (
<a className={`${prefixCls}-action`} onClick={handleClearFn}>
<CloseCircleFilled />
</a>
) : (
<span className={`${prefixCls}-action`}>
<SearchOutlined />
</span>
)}
</>
<Input
placeholder={placeholder}
className={prefixCls}
value={value}
onChange={handleChange}
disabled={disabled}
allowClear
prefix={<SearchOutlined />}
/>
);
}

View File

@ -36,28 +36,8 @@
}
&-search {
padding-right: 24px;
padding-left: @control-padding-horizontal-sm;
&-action {
position: absolute;
top: @transfer-list-search-icon-top;
right: 12px;
bottom: 12px;
width: 28px;
.anticon-search {
color: @disabled-color;
line-height: @input-height-base;
text-align: center;
.@{iconfont-css-prefix} {
color: @disabled-color;
transition: all 0.3s;
&:hover {
color: @text-color-secondary;
}
}
span& {
pointer-events: none;
}
}
}