feat: table row check strictly (#24931)

* feat: add checkStrictly on Table.rowSelection

* fix: LGTM warnings

* test: table rowSelection.checkStrictly

* test: add cov [wip]

* refactor: tree.rowSelection.checkStrictly [wip]

* test: table.rowSelection.checkStrictly basic case

* feat: support rowKey on checkStrictly table

* feat: Table checkStrictly support getCheckboxProps

* docs: Table checkStrictly

* chore: typo

* chore: remove useless comment

* chore: update snapshot

* chore: update snapshot

* fix: fire selectAll on selection dropdown menu & changeRows incorrect in selectAll callback

* docs: typo

* chore

* chore

* fix: expand buttons of leaf rows in tree data are not hidden

* feat: Table warning about rowKey index parameter

* perf: only generate keyEntities when not checkStrictly

* refactor: remove useless parseCheckedKeys

* refactor: get derived selected & half selected keys from selectedRowKeys

* chore: remove env condition stmt

* chore: revert index usage & code formatting

* chore: rerun ci

* docs: table tree-data checkstrictly

* test: update snapshots

* refactor: use useMergedState hook

* chore: rerun ci

* chore: rerun ci 2

* chore: revert selection select all behavior

* refactor: refactor code based on feature

* chore: revert table code format

* chore: revert table code format

* fix: useMemo deps

* fix: useMemo deps

* fix: useMemo deps
This commit is contained in:
07akioni 2020-06-23 22:19:33 +08:00 committed by GitHub
parent 3dbb9be339
commit 50ae190b57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 707 additions and 290 deletions

View File

@ -124,6 +124,12 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
showSorterTooltip = true, showSorterTooltip = true,
} = props; } = props;
devWarning(
!(typeof rowKey === 'function' && rowKey.length > 1),
'Table',
'`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
);
const screens = useBreakpoint(); const screens = useBreakpoint();
const mergedColumns = React.useMemo(() => { const mergedColumns = React.useMemo(() => {
const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m])); const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m]));

View File

@ -43,7 +43,21 @@ describe('Table.rowSelection', () => {
.find('BodyRow') .find('BodyRow')
.map(row => { .map(row => {
const { key } = row.props().record; const { key } = row.props().record;
if (!row.find('input').props().checked) { if (!row.find('input').at(0).props().checked) {
return null;
}
return key;
})
.filter(key => key !== null);
}
function getIndeterminateSelection(wrapper) {
return wrapper
.find('BodyRow')
.map(row => {
const { key } = row.props().record;
if (!row.find('Checkbox').at(0).props().indeterminate) {
return null; return null;
} }
@ -229,6 +243,19 @@ describe('Table.rowSelection', () => {
expect(handleSelectAll).toHaveBeenCalledWith(false, [], data); expect(handleSelectAll).toHaveBeenCalledWith(false, [], data);
}); });
it('works with selectAll option inside selection menu', () => {
const handleChange = jest.fn();
const rowSelection = {
onChange: handleChange,
selections: true,
};
const wrapper = mount(createTable({ rowSelection }));
const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
dropdownWrapper.find('.ant-dropdown-menu-item').first().simulate('click');
expect(handleChange.mock.calls[0][0]).toEqual([0, 1, 2, 3]);
});
it('render with default selection correctly', () => { it('render with default selection correctly', () => {
const rowSelection = { const rowSelection = {
selections: true, selections: true,
@ -787,6 +814,246 @@ describe('Table.rowSelection', () => {
expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]); expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]);
}); });
describe('supports children', () => {
const dataWithChildren = [
{ key: 0, name: 'Jack' },
{ key: 1, name: 'Lucy' },
{ key: 2, name: 'Tom' },
{
key: 3,
name: 'Jerry',
children: [
{
key: 4,
name: 'Jerry Jack',
},
{
key: 5,
name: 'Jerry Lucy',
},
{
key: 6,
name: 'Jerry Tom',
children: [
{
key: 7,
name: 'Jerry Tom Jack',
},
{
key: 8,
name: 'Jerry Tom Lucy',
},
{
key: 9,
name: 'Jerry Tom Tom',
},
],
},
],
},
];
describe('supports checkStrictly', () => {
it('use data entity key', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
},
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[0][0]).toEqual([3, 4, 5, 6, 7, 8, 9]);
checkboxes.at(7).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([4, 5]);
expect(getIndeterminateSelection(wrapper)).toEqual([3]);
expect(onChange.mock.calls[1][0]).toEqual([4, 5]);
});
it('use function rowkey', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
},
rowKey: entity => entity.name,
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[0][0]).toEqual([
'Jerry',
'Jerry Jack',
'Jerry Lucy',
'Jerry Tom',
'Jerry Tom Jack',
'Jerry Tom Lucy',
'Jerry Tom Tom',
]);
checkboxes.at(7).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([4, 5]);
expect(getIndeterminateSelection(wrapper)).toEqual([3]);
expect(onChange.mock.calls[1][0]).toEqual(['Jerry Jack', 'Jerry Lucy']);
});
it('use string rowkey', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
},
rowKey: 'name',
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([3, 4, 5, 6, 7, 8, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[0][0]).toEqual([
'Jerry',
'Jerry Jack',
'Jerry Lucy',
'Jerry Tom',
'Jerry Tom Jack',
'Jerry Tom Lucy',
'Jerry Tom Tom',
]);
checkboxes.at(7).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper)).toEqual([4, 5]);
expect(getIndeterminateSelection(wrapper)).toEqual([3]);
expect(onChange.mock.calls[1][0]).toEqual(['Jerry Jack', 'Jerry Lucy']);
});
it('initialized correctly', () => {
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
selectedRowKeys: [7, 8, 9],
},
rowKey: 'key',
});
const wrapper = mount(table);
expect(getSelections(wrapper)).toEqual([6, 7, 8, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([3]);
});
it('works with disabled checkbox', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
getCheckboxProps(record) {
return {
disabled: record.name === 'Jerry Tom',
};
},
},
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(10).simulate('change', { target: { checked: true } });
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([3, 4, 5, 9]);
checkboxes.at(4).simulate('change', { target: { checked: false } });
expect(getSelections(wrapper)).toEqual([9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[2][0]).toEqual([9]);
});
it('works with disabled checkbox and function rowkey', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
getCheckboxProps(record) {
return {
disabled: record.name === 'Jerry Tom',
};
},
},
rowKey: entity => entity.name,
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(10).simulate('change', { target: { checked: true } });
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([
'Jerry',
'Jerry Jack',
'Jerry Lucy',
'Jerry Tom Tom',
]);
checkboxes.at(4).simulate('change', { target: { checked: false } });
expect(getSelections(wrapper)).toEqual([9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[2][0]).toEqual(['Jerry Tom Tom']);
});
it('works with disabled checkbox and string rowkey', () => {
const onChange = jest.fn();
const table = createTable({
dataSource: dataWithChildren,
defaultExpandAllRows: true,
rowSelection: {
checkStrictly: false,
onChange,
getCheckboxProps(record) {
return {
disabled: record.name === 'Jerry Tom',
};
},
},
rowKey: 'name',
});
const wrapper = mount(table);
const checkboxes = wrapper.find('input');
checkboxes.at(10).simulate('change', { target: { checked: true } });
checkboxes.at(4).simulate('change', { target: { checked: true } });
expect(getSelections(wrapper).sort()).toEqual([3, 4, 5, 9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(Array.from(onChange.mock.calls[1][0]).sort()).toEqual([
'Jerry',
'Jerry Jack',
'Jerry Lucy',
'Jerry Tom Tom',
]);
checkboxes.at(4).simulate('change', { target: { checked: false } });
expect(getSelections(wrapper)).toEqual([9]);
expect(getIndeterminateSelection(wrapper)).toEqual([]);
expect(onChange.mock.calls[2][0]).toEqual(['Jerry Tom Tom']);
});
});
});
describe('cache with selected keys', () => { describe('cache with selected keys', () => {
it('default not cache', () => { it('default not cache', () => {
const onChange = jest.fn(); const onChange = jest.fn();

View File

@ -221,4 +221,31 @@ describe('Table', () => {
expect(td.getDOMNode().attributes.getNamedItem('title')).toBeFalsy(); expect(td.getDOMNode().attributes.getNamedItem('title')).toBeFalsy();
}); });
}); });
it('warn about rowKey when using index parameter', () => {
warnSpy.mockReset();
const columns = [
{
title: 'Name',
key: 'name',
dataIndex: 'name',
},
];
mount(<Table columns={columns} rowKey={(record, index) => record + index} />);
expect(warnSpy).toBeCalledWith(
'Warning: [antd: Table] `index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
);
});
it('not warn about rowKey', () => {
warnSpy.mockReset();
const columns = [
{
title: 'Name',
key: 'name',
dataIndex: 'name',
},
];
mount(<Table columns={columns} rowKey={record => record.key} />);
expect(warnSpy).not.toBeCalled();
});
}); });

View File

@ -5462,48 +5462,121 @@ exports[`renders ./components/table/demo/expand.md correctly 1`] = `
`; `;
exports[`renders ./components/table/demo/expand-children.md correctly 1`] = ` exports[`renders ./components/table/demo/expand-children.md correctly 1`] = `
<div Array [
class="ant-table-wrapper"
>
<div <div
class="ant-spin-nested-loading" class="ant-space ant-space-horizontal ant-space-align-center"
style="margin-bottom:16px"
> >
<div <div
class="ant-spin-container" class="ant-space-item"
style="margin-right:8px"
> >
<div CheckStrictly:
class="ant-table" </div>
<div
class="ant-space-item"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
> >
<div <div
class="ant-table-container" class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
/>
</button>
</div>
</div>,
<div
class="ant-table-wrapper"
>
<div
class="ant-spin-nested-loading"
>
<div
class="ant-spin-container"
>
<div
class="ant-table"
> >
<div <div
class="ant-table-content" class="ant-table-container"
> >
<table <div
style="table-layout:auto" class="ant-table-content"
> >
<colgroup> <table
<col style="table-layout:auto"
class="ant-table-selection-col"
/>
<col />
<col
style="width:12%;min-width:12%"
/>
<col
style="width:30%;min-width:30%"
/>
</colgroup>
<thead
class="ant-table-thead"
> >
<tr> <colgroup>
<th <col
class="ant-table-cell ant-table-selection-column" class="ant-table-selection-col"
/>
<col />
<col
style="width:12%;min-width:12%"
/>
<col
style="width:30%;min-width:30%"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell ant-table-selection-column"
>
<div
class="ant-table-selection"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</div>
</th>
<th
class="ant-table-cell"
>
Name
</th>
<th
class="ant-table-cell"
>
Age
</th>
<th
class="ant-table-cell"
>
Address
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="1"
> >
<div <td
class="ant-table-selection" class="ant-table-cell ant-table-selection-column"
> >
<label <label
class="ant-checkbox-wrapper" class="ant-checkbox-wrapper"
@ -5520,212 +5593,169 @@ exports[`renders ./components/table/demo/expand-children.md correctly 1`] = `
/> />
</span> </span>
</label> </label>
</div> </td>
</th> <td
<th class="ant-table-cell ant-table-cell-with-append"
class="ant-table-cell"
>
Name
</th>
<th
class="ant-table-cell"
>
Age
</th>
<th
class="ant-table-cell"
>
Address
</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 ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
> >
<span <span
class="ant-checkbox" class="ant-table-row-indent indent-level-0"
style="padding-left:0px"
/>
<button
aria-label="Expand row"
class="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
type="button"
/>
John Brown sr.
</td>
<td
class="ant-table-cell"
>
60
</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 ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
> >
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span <span
class="ant-checkbox-inner" class="ant-checkbox"
/> >
</span> <input
</label> class="ant-checkbox-input"
</td> type="checkbox"
<td />
class="ant-table-cell ant-table-cell-with-append" <span
> class="ant-checkbox-inner"
<span />
class="ant-table-row-indent indent-level-0" </span>
style="padding-left:0px" </label>
/> </td>
<button <td
aria-label="Expand row" class="ant-table-cell ant-table-cell-with-append"
class="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
type="button"
/>
John Brown sr.
</td>
<td
class="ant-table-cell"
>
60
</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 ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
> >
<span <span
class="ant-checkbox" class="ant-table-row-indent indent-level-0"
> style="padding-left:0px"
<input />
class="ant-checkbox-input" <button
type="checkbox" aria-label="Expand row"
/> class="ant-table-row-expand-icon ant-table-row-expand-icon-spaced"
<span type="button"
class="ant-checkbox-inner" />
/> Joe Black
</span> </td>
</label> <td
</td> class="ant-table-cell"
<td >
class="ant-table-cell ant-table-cell-with-append" 32
> </td>
<span <td
class="ant-table-row-indent indent-level-0" class="ant-table-cell"
style="padding-left:0px" >
/> Sidney No. 1 Lake Park
<button </td>
aria-label="Expand row" </tr>
class="ant-table-row-expand-icon ant-table-row-expand-icon-spaced" </tbody>
type="button" </table>
/> </div>
Joe Black
</td>
<td
class="ant-table-cell"
>
32
</td>
<td
class="ant-table-cell"
>
Sidney No. 1 Lake Park
</td>
</tr>
</tbody>
</table>
</div> </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"
class=""
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>
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"
class=""
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>
<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"
class=""
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>
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"
class=""
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> </div>,
</div> ]
`; `;
exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = ` exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `

View File

@ -11,18 +11,14 @@ title:
可以通过设置 `indentSize` 以控制每一层的缩进宽度。 可以通过设置 `indentSize` 以控制每一层的缩进宽度。
> 注:暂不支持父子数据递归关联选择。
## en-US ## en-US
Display tree structure data in Table when there is field key `children` in dataSource, try to customize `childrenColumnName` property to avoid tree table structure. Display tree structure data in Table when there is field key `children` in dataSource, try to customize `childrenColumnName` property to avoid tree table structure.
You can control the indent width by setting `indentSize`. You can control the indent width by setting `indentSize`.
> Note, no support for recursive selection of tree structure data table yet.
```jsx ```jsx
import { Table } from 'antd'; import { Table, Switch, Space } from 'antd';
const columns = [ const columns = [
{ {
@ -122,8 +118,21 @@ const rowSelection = {
}, },
}; };
ReactDOM.render( function TreeData() {
<Table columns={columns} rowSelection={rowSelection} dataSource={data} />, const [checkStrictly, setCheckStrictly] = React.useState(false);
mountNode, return (
); <>
<Space align="center" style={{ marginBottom: 16 }}>
CheckStrictly: <Switch checked={checkStrictly} onChange={setCheckStrictly} />
</Space>
<Table
columns={columns}
rowSelection={{ ...rowSelection, checkStrictly }}
dataSource={data}
/>
</>
);
}
ReactDOM.render(<TreeData />, mountNode);
``` ```

View File

@ -1,8 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined'; import DownOutlined from '@ant-design/icons/DownOutlined';
import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil';
import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
import { arrAdd, arrDel } from 'rc-tree/lib/util';
import { DataNode, GetCheckDisabled } from 'rc-tree/lib/interface';
import { INTERNAL_COL_DEFINE } from 'rc-table'; import { INTERNAL_COL_DEFINE } from 'rc-table';
import { FixedType } from 'rc-table/lib/interface'; import { FixedType } from 'rc-table/lib/interface';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Checkbox, { CheckboxProps } from '../../checkbox'; import Checkbox, { CheckboxProps } from '../../checkbox';
import Dropdown from '../../dropdown'; import Dropdown from '../../dropdown';
import Menu from '../../menu'; import Menu from '../../menu';
@ -20,8 +25,6 @@ import {
GetPopupContainer, GetPopupContainer,
} from '../interface'; } from '../interface';
const EMPTY_LIST: any[] = [];
// TODO: warning if use ajax!!! // TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const; export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const; export const SELECTION_INVERT = 'SELECT_INVERT' as const;
@ -86,6 +89,7 @@ export default function useSelection<RecordType>(
fixed, fixed,
renderCell: customizeRenderCell, renderCell: customizeRenderCell,
hideSelectAll, hideSelectAll,
checkStrictly = true,
} = rowSelection || {}; } = rowSelection || {};
const { const {
@ -105,12 +109,73 @@ export default function useSelection<RecordType>(
const preserveRecordsRef = React.useRef(new Map<Key, RecordType>()); const preserveRecordsRef = React.useRef(new Map<Key, RecordType>());
// ========================= Keys ========================= // ========================= Keys =========================
const [innerSelectedKeys, setInnerSelectedKeys] = useState<Key[]>(); const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(selectedRowKeys || [], {
const mergedSelectedKeys = selectedRowKeys || innerSelectedKeys || EMPTY_LIST; value: selectedRowKeys,
const mergedSelectedKeySet = useMemo(() => { });
const keys = selectionType === 'radio' ? mergedSelectedKeys.slice(0, 1) : mergedSelectedKeys;
const { keyEntities } = useMemo(
() =>
checkStrictly
? { keyEntities: null }
: convertDataToEntities((data as unknown) as DataNode[], undefined, getRowKey as any),
[data, getRowKey, checkStrictly],
);
// Get flatten data
const flattedData = useMemo(() => flattenData(pageData, childrenColumnName), [
pageData,
childrenColumnName,
]);
// Get all checkbox props
const checkboxPropsMap = useMemo(() => {
const map = new Map<Key, Partial<CheckboxProps>>();
flattedData.forEach((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
map.set(key, checkboxProps);
if (
process.env.NODE_ENV !== 'production' &&
('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
) {
devWarning(
false,
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
});
return map;
}, [flattedData, getRowKey, getCheckboxProps]);
const isCheckboxDisabled: GetCheckDisabled<RecordType> = useCallback(
(r: RecordType) => {
return !!checkboxPropsMap.get(getRowKey(r))?.disabled;
},
[checkboxPropsMap, getRowKey],
);
const [derivedSelectedKeys, derivedHalfSelectedKeys] = useMemo(() => {
if (checkStrictly) {
return [mergedSelectedKeys, []];
}
const { checkedKeys, halfCheckedKeys } = conductCheck(
mergedSelectedKeys,
true,
keyEntities as any,
isCheckboxDisabled as any,
);
return [checkedKeys, halfCheckedKeys];
}, [mergedSelectedKeys, checkStrictly, keyEntities, isCheckboxDisabled]);
const derivedSelectedKeySet: Set<Key> = useMemo(() => {
const keys = selectionType === 'radio' ? derivedSelectedKeys.slice(0, 1) : derivedSelectedKeys;
return new Set(keys); return new Set(keys);
}, [mergedSelectedKeys, selectionType]); }, [derivedSelectedKeys, selectionType]);
const derivedHalfSelectedKeySet = useMemo(() => {
return selectionType === 'radio' ? new Set() : new Set(derivedHalfSelectedKeys);
}, [derivedHalfSelectedKeys, selectionType]);
// Save last selected key to enable range selection // Save last selected key to enable range selection
const [lastSelectedKey, setLastSelectedKey] = useState<Key | null>(null); const [lastSelectedKey, setLastSelectedKey] = useState<Key | null>(null);
@ -118,7 +183,7 @@ export default function useSelection<RecordType>(
// Reset if rowSelection reset // Reset if rowSelection reset
React.useEffect(() => { React.useEffect(() => {
if (!rowSelection) { if (!rowSelection) {
setInnerSelectedKeys([]); setMergedSelectedKeys([]);
} }
}, [!!rowSelection]); }, [!!rowSelection]);
@ -159,13 +224,13 @@ export default function useSelection<RecordType>(
}); });
} }
setInnerSelectedKeys(availableKeys); setMergedSelectedKeys(availableKeys);
if (onSelectionChange) { if (onSelectionChange) {
onSelectionChange(availableKeys, records); onSelectionChange(availableKeys, records);
} }
}, },
[setInnerSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys], [setMergedSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys],
); );
// ====================== Selections ====================== // ====================== Selections ======================
@ -205,7 +270,7 @@ export default function useSelection<RecordType>(
key: 'invert', key: 'invert',
text: tableLocale.selectInvert, text: tableLocale.selectInvert,
onSelect() { onSelect() {
const keySet = new Set(mergedSelectedKeySet); const keySet = new Set(derivedSelectedKeySet);
pageData.forEach((record, index) => { pageData.forEach((record, index) => {
const key = getRowKey(record, index); const key = getRowKey(record, index);
@ -231,7 +296,7 @@ export default function useSelection<RecordType>(
} }
return selection as SelectionItem; return selection as SelectionItem;
}); });
}, [selections, mergedSelectedKeySet, pageData, getRowKey]); }, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
// ======================= Columns ======================== // ======================= Columns ========================
const transformColumns = useCallback( const transformColumns = useCallback(
@ -240,30 +305,8 @@ export default function useSelection<RecordType>(
return columns; return columns;
} }
// Get flatten data
const flattedData = flattenData(pageData, childrenColumnName);
// Support selection // Support selection
const keySet = new Set(mergedSelectedKeySet); const keySet = new Set(derivedSelectedKeySet);
// Get all checkbox props
const checkboxPropsMap = new Map<Key, Partial<CheckboxProps>>();
flattedData.forEach((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
checkboxPropsMap.set(key, checkboxProps);
if (
process.env.NODE_ENV !== 'production' &&
('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
) {
devWarning(
false,
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
});
// Record key only need check with enabled // Record key only need check with enabled
const recordKeys = flattedData const recordKeys = flattedData
@ -282,8 +325,10 @@ export default function useSelection<RecordType>(
}); });
} else { } else {
recordKeys.forEach(key => { recordKeys.forEach(key => {
keySet.add(key); if (!keySet.has(key)) {
changeKeys.push(key); keySet.add(key);
changeKeys.push(key);
}
}); });
} }
@ -385,6 +430,7 @@ export default function useSelection<RecordType>(
renderCell = (_, record, index) => { renderCell = (_, record, index) => {
const key = getRowKey(record, index); const key = getRowKey(record, index);
const checked = keySet.has(key); const checked = keySet.has(key);
const indeterminate = derivedHalfSelectedKeySet.has(key);
// Record checked // Record checked
return { return {
@ -392,6 +438,7 @@ export default function useSelection<RecordType>(
<Checkbox <Checkbox
{...checkboxPropsMap.get(key)} {...checkboxPropsMap.get(key)}
checked={checked} checked={checked}
indeterminate={indeterminate}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
onChange={({ nativeEvent }) => { onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent; const { shiftKey } = nativeEvent;
@ -400,7 +447,7 @@ export default function useSelection<RecordType>(
let endIndex: number = -1; let endIndex: number = -1;
// Get range of this // Get range of this
if (shiftKey) { if (shiftKey && checkStrictly) {
const pointKeys = new Set([lastSelectedKey, key]); const pointKeys = new Set([lastSelectedKey, key]);
recordKeys.some((recordKey, recordIndex) => { recordKeys.some((recordKey, recordIndex) => {
@ -417,7 +464,7 @@ export default function useSelection<RecordType>(
}); });
} }
if (endIndex !== -1 && startIndex !== endIndex) { if (endIndex !== -1 && startIndex !== endIndex && checkStrictly) {
// Batch update selections // Batch update selections
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1); const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = []; const changedKeys: Key[] = [];
@ -449,13 +496,37 @@ export default function useSelection<RecordType>(
} }
} else { } else {
// Single record selected // Single record selected
if (checked) { const originCheckedKeys = derivedSelectedKeys;
keySet.delete(key); if (checkStrictly) {
const checkedKeys = checked
? arrDel(originCheckedKeys, key)
: arrAdd(originCheckedKeys, key);
triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
} else { } else {
keySet.add(key); // Always fill first
} const result = conductCheck(
[...originCheckedKeys, key],
true,
keyEntities as any,
isCheckboxDisabled as any,
);
const { checkedKeys, halfCheckedKeys } = result;
let nextCheckedKeys = checkedKeys;
triggerSingleSelection(key, !checked, Array.from(keySet), nativeEvent); // If remove, we do it again to correction
if (checked) {
const tempKeySet = new Set(checkedKeys);
tempKeySet.delete(key);
nextCheckedKeys = conductCheck(
Array.from(tempKeySet),
{ checked: false, halfCheckedKeys },
keyEntities as any,
isCheckboxDisabled as any,
).checkedKeys;
}
triggerSingleSelection(key, !checked, nextCheckedKeys, nativeEvent);
}
} }
setLastSelectedKey(key); setLastSelectedKey(key);
@ -500,18 +571,21 @@ export default function useSelection<RecordType>(
}, },
[ [
getRowKey, getRowKey,
pageData, flattedData,
rowSelection, rowSelection,
innerSelectedKeys, derivedSelectedKeys,
mergedSelectedKeys, derivedSelectedKeySet,
derivedHalfSelectedKeySet,
selectionColWidth, selectionColWidth,
mergedSelections, mergedSelections,
expandType, expandType,
lastSelectedKey, lastSelectedKey,
checkboxPropsMap,
onSelectMultiple, onSelectMultiple,
triggerSingleSelection, triggerSingleSelection,
isCheckboxDisabled,
], ],
); );
return [transformColumns, mergedSelectedKeySet]; return [transformColumns, derivedSelectedKeySet];
} }

View File

@ -185,11 +185,12 @@ Properties for row selection.
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| columnWidth | Set the width of the selection column | string\|number | `60px` | 4.0 | | checkStrictly | Check table row precisely; parent row and children rows are not associated | boolean | true | 4.4.0 |
| columnTitle | Set the title of the selection column | string\|React.ReactNode | - | 4.0 | | columnWidth | Set the width of the selection column | string\|number | `60px` | |
| fixed | Fixed selection column on the left | boolean | - | 4.0 | | columnTitle | Set the title of the selection column | string\|ReactNode | - | |
| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | 4.0 | | fixed | Fixed selection column on the left | boolean | - | |
| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | false | 4.3 | | getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | |
| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | `false` | 4.3 |
| preserveSelectedRowKeys | Keep selection `key` even when it removed from `dataSource` | boolean | - | 4.4 | | preserveSelectedRowKeys | Keep selection `key` even when it removed from `dataSource` | boolean | - | 4.4 |
| renderCell | Renderer of the table cell. Same as `render` in column | Function(checked, record, index, originNode) {} | - | 4.1 | | renderCell | Renderer of the table cell. Same as `render` in column | Function(checked, record, index, originNode) {} | - | 4.1 |
| selectedRowKeys | Controlled selected row keys | string\[]\|number[] | \[] | | | selectedRowKeys | Controlled selected row keys | string\[]\|number[] | \[] | |

View File

@ -190,6 +190,7 @@ const columns = [
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| checkStrictly | checkable 状态下节点选择完全受控(父子数据选中状态不再关联) | boolean | true | 4.4.0 |
| columnWidth | 自定义列表选择框宽度 | string\|number | `60px` | | | columnWidth | 自定义列表选择框宽度 | string\|number | `60px` | |
| columnTitle | 自定义列表选择框标题 | string\|ReactNode | - | | | columnTitle | 自定义列表选择框标题 | string\|ReactNode | - | |
| fixed | 把选择框列固定在左边 | boolean | - | | | fixed | 把选择框列固定在左边 | boolean | - | |

View File

@ -148,6 +148,7 @@ export interface TableRowSelection<T> {
fixed?: boolean; fixed?: boolean;
columnWidth?: string | number; columnWidth?: string | number;
columnTitle?: string | React.ReactNode; columnTitle?: string | React.ReactNode;
checkStrictly?: boolean;
renderCell?: ( renderCell?: (
value: boolean, value: boolean,
record: T, record: T,

View File

@ -471,6 +471,7 @@
} }
background: transparent; background: transparent;
border: 0; border: 0;
visibility: hidden;
} }
.@{table-prefix-cls}-row-indent + & { .@{table-prefix-cls}-row-indent + & {

View File

@ -139,7 +139,7 @@
"rc-tabs": "~11.4.1", "rc-tabs": "~11.4.1",
"rc-textarea": "~0.2.2", "rc-textarea": "~0.2.2",
"rc-tooltip": "~4.2.0", "rc-tooltip": "~4.2.0",
"rc-tree": "~3.3.0", "rc-tree": "~3.5.0",
"rc-tree-select": "~4.0.0", "rc-tree-select": "~4.0.0",
"rc-trigger": "~4.3.0", "rc-trigger": "~4.3.0",
"rc-upload": "~3.2.0", "rc-upload": "~3.2.0",