mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-23 18:50:06 +08:00
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:
parent
a43e21e6bf
commit
9c17f94cab
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
@ -22,6 +22,8 @@ const localeValues: Locale = {
|
||||
filterConfirm: '确定',
|
||||
filterReset: '重置',
|
||||
filterEmptyText: '无筛选项',
|
||||
filterCheckall: '全选',
|
||||
filterSearchPlaceholder: '在筛选项中搜索',
|
||||
selectAll: '全选当页',
|
||||
selectInvert: '反选当页',
|
||||
selectNone: '清空所有',
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
|
122
components/table/demo/filter-in-tree.md
Normal file
122
components/table/demo/filter-in-tree.md
Normal 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);
|
||||
```
|
@ -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}
|
||||
|
39
components/table/hooks/useFilter/FilterSearch.tsx
Normal file
39
components/table/hooks/useFilter/FilterSearch.tsx
Normal 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;
|
@ -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);
|
||||
|
@ -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 | - | |
|
||||
|
@ -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 | - | |
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -12,3 +12,5 @@ import '../../dropdown/style';
|
||||
import '../../spin/style';
|
||||
import '../../pagination/style';
|
||||
import '../../tooltip/style';
|
||||
import '../../input/style';
|
||||
import '../../tree/style';
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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 />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user