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:
feng zhi hao 2017-02-23 19:29:47 +08:00 committed by 偏右
parent 36fd55c441
commit 6d903a6ba5
11 changed files with 1901 additions and 1057 deletions

View File

@ -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>
);
}
}

View File

@ -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 || []}
/>
);
}

View File

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

View File

@ -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>
`;

View File

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

View File

@ -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);
````

View 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);
````

View File

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

View File

@ -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 中使用

View File

@ -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;