mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 11:10:01 +08:00
add custom selection in Table (#4962)
* add custom selection in Table * update snapshots * update snapshots again * optimize selection in Table * update test * update snapshots * improve test * fix test
This commit is contained in:
parent
36fd55c441
commit
6d903a6ba5
@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import Checkbox from '../checkbox';
|
||||
import { Store } from './createStore';
|
||||
import Dropdown from '../dropdown';
|
||||
import Menu from '../menu';
|
||||
import Icon from '../icon';
|
||||
|
||||
export interface SelectionDecorator {
|
||||
key: string;
|
||||
text: React.ReactNode;
|
||||
onSelect: (changeableRowKeys: string[]) => void;
|
||||
}
|
||||
|
||||
export interface SelectionCheckboxAllProps {
|
||||
store: Store;
|
||||
@ -8,9 +17,21 @@ export interface SelectionCheckboxAllProps {
|
||||
getCheckboxPropsByItem: (item, index) => any;
|
||||
getRecordKey: (record, index?) => string;
|
||||
data: any[];
|
||||
onChange: (e) => void;
|
||||
prefixCls: string | undefined;
|
||||
onSelect: (key: string, index: number, selectFunc: any) => void;
|
||||
selections: SelectionDecorator[];
|
||||
}
|
||||
|
||||
const defaultSelections: SelectionDecorator[] = [{
|
||||
key: 'all',
|
||||
text: '全选',
|
||||
onSelect: () => {},
|
||||
}, {
|
||||
key: 'invert',
|
||||
text: '反选',
|
||||
onSelect: () => {},
|
||||
}];
|
||||
|
||||
export default class SelectionCheckboxAll extends React.Component<SelectionCheckboxAllProps, any> {
|
||||
unsubscribe: () => void;
|
||||
|
||||
@ -106,17 +127,61 @@ export default class SelectionCheckboxAll extends React.Component<SelectionCheck
|
||||
return indeterminate;
|
||||
}
|
||||
|
||||
handleSelectAllChagne = (e) => {
|
||||
let checked = e.target.checked;
|
||||
this.props.onSelect(checked ? 'all' : 'removeAll', 0, null);
|
||||
}
|
||||
|
||||
renderMenus(selections: SelectionDecorator[]) {
|
||||
return selections.map((selection, index) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={selection.key || index}
|
||||
>
|
||||
<div
|
||||
onClick={() => {this.props.onSelect(selection.key, index, selection.onSelect);}}
|
||||
>
|
||||
{selection.text}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, onChange } = this.props;
|
||||
const { disabled, prefixCls } = this.props;
|
||||
const { checked, indeterminate } = this.state;
|
||||
|
||||
let selectionPrefixCls = `${prefixCls}-selection`;
|
||||
|
||||
let selections = defaultSelections.concat(this.props.selections || []);
|
||||
|
||||
let menu = (
|
||||
<Menu
|
||||
className={`${selectionPrefixCls}-menu`}
|
||||
>
|
||||
{this.renderMenus(selections)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
indeterminate={indeterminate}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className={selectionPrefixCls}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
indeterminate={indeterminate}
|
||||
disabled={disabled}
|
||||
onChange={this.handleSelectAllChagne}
|
||||
/>
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
>
|
||||
<div className={`${selectionPrefixCls}-down`}>
|
||||
<span>
|
||||
<Icon type="down" />
|
||||
</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import assign from 'object-assign';
|
||||
import warning from '../_util/warning';
|
||||
import createStore, { Store } from './createStore';
|
||||
import SelectionBox from './SelectionBox';
|
||||
import SelectionCheckboxAll from './SelectionCheckboxAll';
|
||||
import SelectionCheckboxAll, { SelectionDecorator } from './SelectionCheckboxAll';
|
||||
import Column, { ColumnProps } from './Column';
|
||||
import ColumnGroup from './ColumnGroup';
|
||||
import { SpinProps } from '../spin';
|
||||
@ -46,6 +46,8 @@ export interface TableRowSelection<T> {
|
||||
getCheckboxProps?: (record: T) => Object;
|
||||
onSelect?: (record: T, selected: boolean, selectedRows: Object[]) => any;
|
||||
onSelectAll?: (selected: boolean, selectedRows: Object[], changeRows: Object[]) => any;
|
||||
onSelectInvert?: (selectedRows: Object[]) => any;
|
||||
selections?: SelectionDecorator[];
|
||||
}
|
||||
|
||||
export interface TableProps<T> {
|
||||
@ -270,6 +272,8 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
|
||||
(row, i) => changeRowKeys.indexOf(this.getRecordKey(row, i)) >= 0
|
||||
);
|
||||
rowSelection.onSelectAll(checked, selectedRows, changeRows);
|
||||
} else if (selectWay === 'onSelectInvert' && rowSelection.onSelectInvert) {
|
||||
rowSelection.onSelectInvert(selectedRowKeys);
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,37 +472,63 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectAllRow = (e) => {
|
||||
const checked = e.target.checked;
|
||||
handleSelectRow = (selectionKey, index, onSelectFunc) => {
|
||||
const data = this.getFlatCurrentPageData();
|
||||
const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
|
||||
const selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
|
||||
const changableRowKeys = data
|
||||
const changeableRowKeys = data
|
||||
.filter((item, i) => !this.getCheckboxPropsByItem(item, i).disabled)
|
||||
.map((item, i) => this.getRecordKey(item, i));
|
||||
|
||||
// 记录变化的列
|
||||
const changeRowKeys: string[] = [];
|
||||
if (checked) {
|
||||
changableRowKeys.forEach(key => {
|
||||
if (selectedRowKeys.indexOf(key) < 0) {
|
||||
selectedRowKeys.push(key);
|
||||
let changeRowKeys: string[] = [];
|
||||
let selectWay = '';
|
||||
let checked;
|
||||
// handle default selection
|
||||
switch (selectionKey) {
|
||||
case 'all':
|
||||
changeableRowKeys.forEach(key => {
|
||||
if (selectedRowKeys.indexOf(key) < 0) {
|
||||
selectedRowKeys.push(key);
|
||||
changeRowKeys.push(key);
|
||||
}
|
||||
});
|
||||
selectWay = 'onSelectAll';
|
||||
checked = true;
|
||||
break;
|
||||
case 'removeAll':
|
||||
changeableRowKeys.forEach(key => {
|
||||
if (selectedRowKeys.indexOf(key) >= 0) {
|
||||
selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
|
||||
changeRowKeys.push(key);
|
||||
}
|
||||
});
|
||||
selectWay = 'onSelectAll';
|
||||
checked = false;
|
||||
break;
|
||||
case 'invert':
|
||||
changeableRowKeys.forEach(key => {
|
||||
if (selectedRowKeys.indexOf(key) < 0) {
|
||||
selectedRowKeys.push(key);
|
||||
}else {
|
||||
selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
|
||||
}
|
||||
changeRowKeys.push(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
changableRowKeys.forEach(key => {
|
||||
if (selectedRowKeys.indexOf(key) >= 0) {
|
||||
selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
|
||||
changeRowKeys.push(key);
|
||||
}
|
||||
});
|
||||
selectWay = 'onSelectInvert';
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.store.setState({
|
||||
selectionDirty: true,
|
||||
});
|
||||
// when select custom selection, callback selections[n].onSelect
|
||||
if (index > 1 && typeof onSelectFunc === 'function') {
|
||||
return onSelectFunc(changeableRowKeys);
|
||||
}
|
||||
this.setSelectedRowKeys(selectedRowKeys, {
|
||||
selectWay: 'onSelectAll',
|
||||
selectWay: selectWay,
|
||||
checked,
|
||||
changeRowKeys,
|
||||
});
|
||||
@ -597,7 +627,9 @@ export default class Table<T> extends React.Component<TableProps<T>, any> {
|
||||
getCheckboxPropsByItem={this.getCheckboxPropsByItem}
|
||||
getRecordKey={this.getRecordKey}
|
||||
disabled={checkboxAllDisabled}
|
||||
onChange={this.handleSelectAllRow}
|
||||
prefixCls={prefixCls}
|
||||
onSelect={this.handleSelectRow}
|
||||
selections={rowSelection.selections || []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { mount, render } from 'enzyme';
|
||||
import { renderToJson } from 'enzyme-to-json';
|
||||
import Table from '..';
|
||||
|
||||
describe('Table.rowSelection', () => {
|
||||
@ -166,6 +167,68 @@ describe('Table.rowSelection', () => {
|
||||
|
||||
wrapper.find('input').first().simulate('change', { target: { checked: true } });
|
||||
expect(handleSelectAll).toBeCalledWith(true, data, data);
|
||||
|
||||
wrapper.find('input').first().simulate('change', { target: { checked: false } });
|
||||
expect(handleSelectAll).toBeCalledWith(false, [], data);
|
||||
});
|
||||
|
||||
it('render selection correctly', () => {
|
||||
const wrapper = mount(createTable());
|
||||
const dropdownWrapper = render(wrapper.find('Trigger').node.getComponent());
|
||||
expect(renderToJson(dropdownWrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('click select all selection', () => {
|
||||
const handleSelectAll = jest.fn();
|
||||
const rowSelection = {
|
||||
onSelectAll: handleSelectAll,
|
||||
};
|
||||
const wrapper = mount(createTable({ rowSelection }));
|
||||
|
||||
const dropdownWrapper = mount(wrapper.find('Trigger').node.getComponent());
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item > div').first().simulate('click');
|
||||
|
||||
expect(handleSelectAll).toBeCalledWith(true, data, data);
|
||||
});
|
||||
|
||||
it('fires selectInvert event', () => {
|
||||
const handleSelectInvert = jest.fn();
|
||||
const rowSelection = {
|
||||
onSelectInvert: handleSelectInvert,
|
||||
};
|
||||
const wrapper = mount(createTable({ rowSelection }));
|
||||
const checkboxes = wrapper.find('input');
|
||||
|
||||
checkboxes.at(1).simulate('change', { target: { checked: true } });
|
||||
const dropdownWrapper = mount(wrapper.find('Trigger').node.getComponent());
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item > div').last().simulate('click');
|
||||
|
||||
expect(handleSelectInvert).toBeCalledWith([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('fires selection event', () => {
|
||||
const handleSelectOdd = jest.fn();
|
||||
const handleSelectEven = jest.fn();
|
||||
const rowSelection = {
|
||||
selections: [{
|
||||
key: 'odd',
|
||||
text: '奇数项',
|
||||
onSelect: handleSelectOdd,
|
||||
}, {
|
||||
key: 'even',
|
||||
text: '偶数项',
|
||||
onSelect: handleSelectEven,
|
||||
}],
|
||||
};
|
||||
const wrapper = mount(createTable({ rowSelection }));
|
||||
|
||||
const dropdownWrapper = mount(wrapper.find('Trigger').node.getComponent());
|
||||
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item > div').at(2).simulate('click');
|
||||
expect(handleSelectOdd).toBeCalledWith([0, 1, 2, 3]);
|
||||
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item > div').at(3).simulate('click');
|
||||
expect(handleSelectEven).toBeCalledWith([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/4245
|
||||
|
@ -0,0 +1,29 @@
|
||||
exports[`Table.rowSelection render selection correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ant-dropdown ant-dropdown-placement-bottomLeft ant-dropdown-hidden">
|
||||
<ul
|
||||
aria-activedescendant=""
|
||||
class="ant-dropdown-menu ant-dropdown-menu-vertical ant-table-selection-menu ant-dropdown-menu-light ant-dropdown-menu-root"
|
||||
role="menu"
|
||||
tabindex="0">
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="ant-dropdown-menu-item"
|
||||
role="menuitem">
|
||||
<div>
|
||||
全选
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="ant-dropdown-menu-item"
|
||||
role="menuitem">
|
||||
<div>
|
||||
反选
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,25 +1,28 @@
|
||||
exports[`Table.sorter renders sorter icon correctly 1`] = `
|
||||
<tr>
|
||||
<th
|
||||
class="">
|
||||
<span>
|
||||
Name
|
||||
<div
|
||||
class="ant-table-column-sorter">
|
||||
<span
|
||||
class="ant-table-column-sorter-up off"
|
||||
title="↑">
|
||||
<i
|
||||
class="anticon anticon-caret-up" />
|
||||
</span>
|
||||
<span
|
||||
class="ant-table-column-sorter-down off"
|
||||
title="↓">
|
||||
<i
|
||||
class="anticon anticon-caret-down" />
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
<thead
|
||||
class="ant-table-thead">
|
||||
<tr>
|
||||
<th
|
||||
class="">
|
||||
<span>
|
||||
Name
|
||||
<div
|
||||
class="ant-table-column-sorter">
|
||||
<span
|
||||
class="ant-table-column-sorter-up off"
|
||||
title="↑">
|
||||
<i
|
||||
class="anticon anticon-caret-up" />
|
||||
</span>
|
||||
<span
|
||||
class="ant-table-column-sorter-down off"
|
||||
title="↓">
|
||||
<i
|
||||
class="anticon anticon-caret-down" />
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
`;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -38,14 +38,12 @@ for (let i = 0; i < 46; i++) {
|
||||
});
|
||||
}
|
||||
|
||||
const App = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedRowKeys: [], // Check here to configure the default column
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
start() {
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
selectedRowKeys: [], // Check here to configure the default column
|
||||
loading: false,
|
||||
};
|
||||
start = () => {
|
||||
this.setState({ loading: true });
|
||||
// ajax request after empty completing
|
||||
setTimeout(() => {
|
||||
@ -54,11 +52,11 @@ const App = React.createClass({
|
||||
loading: false,
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
onSelectChange(selectedRowKeys) {
|
||||
}
|
||||
onSelectChange = (selectedRowKeys) => {
|
||||
console.log('selectedRowKeys changed: ', selectedRowKeys);
|
||||
this.setState({ selectedRowKeys });
|
||||
},
|
||||
}
|
||||
render() {
|
||||
const { loading, selectedRowKeys } = this.state;
|
||||
const rowSelection = {
|
||||
@ -77,8 +75,8 @@ const App = React.createClass({
|
||||
<Table rowSelection={rowSelection} columns={columns} dataSource={data} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
````
|
||||
|
90
components/table/demo/row-selection-custom.md
Normal file
90
components/table/demo/row-selection-custom.md
Normal file
@ -0,0 +1,90 @@
|
||||
---
|
||||
order: 4
|
||||
title:
|
||||
en-US: Custom selection
|
||||
zh-CN: 自定义选择项
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
默认有全选和反选,通过 `rowSelection.selections` 自定义选择项。
|
||||
|
||||
## en-US
|
||||
|
||||
Default selection is select all and select invert, Use `rowSelection.selections` custom selections.
|
||||
|
||||
|
||||
````jsx
|
||||
import { Table } from 'antd';
|
||||
|
||||
const columns = [{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
}, {
|
||||
title: 'Age',
|
||||
dataIndex: 'age',
|
||||
}, {
|
||||
title: 'Address',
|
||||
dataIndex: 'address',
|
||||
}];
|
||||
|
||||
const data = [];
|
||||
for (let i = 0; i < 46; i++) {
|
||||
data.push({
|
||||
key: i,
|
||||
name: `Edward King ${i}`,
|
||||
age: 32,
|
||||
address: `London, Park Lane no. ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
selectedRowKeys: [], // Check here to configure the default column
|
||||
};
|
||||
onSelectChange = (selectedRowKeys) => {
|
||||
console.log('selectedRowKeys changed: ', selectedRowKeys);
|
||||
this.setState({ selectedRowKeys });
|
||||
}
|
||||
render() {
|
||||
const { selectedRowKeys } = this.state;
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: this.onSelectChange,
|
||||
selections: [{
|
||||
key: 'odd',
|
||||
text: '奇数项',
|
||||
onSelect: (changableRowKeys) => {
|
||||
let newSelectedRowKeys = [];
|
||||
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
|
||||
if (index % 2 !== 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.setState({ selectedRowKeys: newSelectedRowKeys });
|
||||
},
|
||||
}, {
|
||||
key: 'even',
|
||||
text: '偶数项',
|
||||
onSelect: (changableRowKeys) => {
|
||||
let newSelectedRowKeys = [];
|
||||
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
|
||||
if (index % 2 !== 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.setState({ selectedRowKeys: newSelectedRowKeys });
|
||||
},
|
||||
}],
|
||||
onSelection: this.onSelection,
|
||||
};
|
||||
return (
|
||||
<Table rowSelection={rowSelection} columns={columns} dataSource={data} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
````
|
@ -119,6 +119,16 @@ Properties for selection.
|
||||
| getCheckboxProps | get Checkbox or Radio props | Function(record) | - |
|
||||
| onSelect | callback that is called when select/deselect one row | Function(record, selected, selectedRows) | - |
|
||||
| onSelectAll | callback that is called when select/deselect all | Function(selected, selectedRows, changeRows) | - |
|
||||
| onSelectInvert | callback that is called when select invert | Function(selectedRows) | - |
|
||||
| selections | custom selection, [config](#rowSelection) | object[] | - |
|
||||
|
||||
### selection
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|---------------|--------------------------|-----------------|--------------|
|
||||
| key | key of this selection | string | - |
|
||||
| text | display text is this selection | string\|React.ReactNode | - |
|
||||
| onSelect | callback when click this selection | Function(changeableRowKeys) | - |
|
||||
|
||||
## Using in TypeScript
|
||||
|
||||
|
@ -120,6 +120,16 @@ const columns = [{
|
||||
| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - |
|
||||
| onSelect | 用户手动选择/取消选择某列的回调 | Function(record, selected, selectedRows) | - |
|
||||
| onSelectAll | 用户手动选择/取消选择所有列的回调 | Function(selected, selectedRows, changeRows) | - |
|
||||
| onSelectInvert | 用户手动选择反选的回调 | Function(selectedRows) | - |
|
||||
| selections | 自定义选择项, [配置项](#selection) | object[] | - |
|
||||
|
||||
### selection
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|------------------|--------------------------|-----------------|---------------------|---------|
|
||||
| key | React 需要的 key,建议设置 | string | - |
|
||||
| text | 选择项显示的文字 | string\|React.ReactNode | - |
|
||||
| onSelect | 选择项点击回调 | Function(changeableRowKeys) | - |
|
||||
|
||||
## 在 TypeScript 中使用
|
||||
|
||||
|
@ -426,6 +426,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-selection {
|
||||
width: 40px;
|
||||
|
||||
.@{iconfont-css-prefix}-down {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
min-width: 96px;
|
||||
margin-top: 5px;
|
||||
margin-left: -30px;
|
||||
background: @component-background;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: @box-shadow-base;
|
||||
|
||||
.@{ant-prefix}-dropdown-menu-item {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-action-down {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
&-down {
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
&-expand-icon {
|
||||
cursor: pointer;
|
||||
|
Loading…
Reference in New Issue
Block a user