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,
} = 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 mergedColumns = React.useMemo(() => {
const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m]));

View File

@ -43,7 +43,21 @@ describe('Table.rowSelection', () => {
.find('BodyRow')
.map(row => {
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;
}
@ -229,6 +243,19 @@ describe('Table.rowSelection', () => {
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', () => {
const rowSelection = {
selections: true,
@ -787,6 +814,246 @@ describe('Table.rowSelection', () => {
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', () => {
it('default not cache', () => {
const onChange = jest.fn();

View File

@ -221,4 +221,31 @@ describe('Table', () => {
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`] = `
<div
class="ant-table-wrapper"
>
Array [
<div
class="ant-spin-nested-loading"
class="ant-space ant-space-horizontal ant-space-align-center"
style="margin-bottom:16px"
>
<div
class="ant-spin-container"
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-table"
CheckStrictly:
</div>
<div
class="ant-space-item"
>
<button
aria-checked="false"
class="ant-switch"
role="switch"
type="button"
>
<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
class="ant-table-content"
class="ant-table-container"
>
<table
style="table-layout:auto"
<div
class="ant-table-content"
>
<colgroup>
<col
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"
<table
style="table-layout:auto"
>
<tr>
<th
class="ant-table-cell ant-table-selection-column"
<colgroup>
<col
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
class="ant-table-selection"
<td
class="ant-table-cell ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
@ -5520,212 +5593,169 @@ exports[`renders ./components/table/demo/expand-children.md correctly 1`] = `
/>
</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"
>
<td
class="ant-table-cell ant-table-selection-column"
>
<label
class="ant-checkbox-wrapper"
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<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
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
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"
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
</td>
<td
class="ant-table-cell ant-table-cell-with-append"
>
<span
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-spaced"
type="button"
/>
Joe Black
</td>
<td
class="ant-table-cell"
>
32
</td>
<td
class="ant-table-cell"
>
Sidney No. 1 Lake Park
</td>
</tr>
</tbody>
</table>
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-spaced"
type="button"
/>
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>
<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>
<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>,
]
`;
exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `

View File

@ -11,18 +11,14 @@ title:
可以通过设置 `indentSize` 以控制每一层的缩进宽度。
> 注:暂不支持父子数据递归关联选择。
## 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.
You can control the indent width by setting `indentSize`.
> Note, no support for recursive selection of tree structure data table yet.
```jsx
import { Table } from 'antd';
import { Table, Switch, Space } from 'antd';
const columns = [
{
@ -122,8 +118,21 @@ const rowSelection = {
},
};
ReactDOM.render(
<Table columns={columns} rowSelection={rowSelection} dataSource={data} />,
mountNode,
);
function TreeData() {
const [checkStrictly, setCheckStrictly] = React.useState(false);
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 { useState, useCallback, useMemo } from 'react';
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 { FixedType } from 'rc-table/lib/interface';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Checkbox, { CheckboxProps } from '../../checkbox';
import Dropdown from '../../dropdown';
import Menu from '../../menu';
@ -20,8 +25,6 @@ import {
GetPopupContainer,
} from '../interface';
const EMPTY_LIST: any[] = [];
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
@ -86,6 +89,7 @@ export default function useSelection<RecordType>(
fixed,
renderCell: customizeRenderCell,
hideSelectAll,
checkStrictly = true,
} = rowSelection || {};
const {
@ -105,12 +109,73 @@ export default function useSelection<RecordType>(
const preserveRecordsRef = React.useRef(new Map<Key, RecordType>());
// ========================= Keys =========================
const [innerSelectedKeys, setInnerSelectedKeys] = useState<Key[]>();
const mergedSelectedKeys = selectedRowKeys || innerSelectedKeys || EMPTY_LIST;
const mergedSelectedKeySet = useMemo(() => {
const keys = selectionType === 'radio' ? mergedSelectedKeys.slice(0, 1) : mergedSelectedKeys;
const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(selectedRowKeys || [], {
value: selectedRowKeys,
});
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);
}, [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
const [lastSelectedKey, setLastSelectedKey] = useState<Key | null>(null);
@ -118,7 +183,7 @@ export default function useSelection<RecordType>(
// Reset if rowSelection reset
React.useEffect(() => {
if (!rowSelection) {
setInnerSelectedKeys([]);
setMergedSelectedKeys([]);
}
}, [!!rowSelection]);
@ -159,13 +224,13 @@ export default function useSelection<RecordType>(
});
}
setInnerSelectedKeys(availableKeys);
setMergedSelectedKeys(availableKeys);
if (onSelectionChange) {
onSelectionChange(availableKeys, records);
}
},
[setInnerSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys],
[setMergedSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys],
);
// ====================== Selections ======================
@ -205,7 +270,7 @@ export default function useSelection<RecordType>(
key: 'invert',
text: tableLocale.selectInvert,
onSelect() {
const keySet = new Set(mergedSelectedKeySet);
const keySet = new Set(derivedSelectedKeySet);
pageData.forEach((record, index) => {
const key = getRowKey(record, index);
@ -231,7 +296,7 @@ export default function useSelection<RecordType>(
}
return selection as SelectionItem;
});
}, [selections, mergedSelectedKeySet, pageData, getRowKey]);
}, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
// ======================= Columns ========================
const transformColumns = useCallback(
@ -240,30 +305,8 @@ export default function useSelection<RecordType>(
return columns;
}
// Get flatten data
const flattedData = flattenData(pageData, childrenColumnName);
// Support selection
const keySet = new Set(mergedSelectedKeySet);
// 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.',
);
}
});
const keySet = new Set(derivedSelectedKeySet);
// Record key only need check with enabled
const recordKeys = flattedData
@ -282,8 +325,10 @@ export default function useSelection<RecordType>(
});
} else {
recordKeys.forEach(key => {
keySet.add(key);
changeKeys.push(key);
if (!keySet.has(key)) {
keySet.add(key);
changeKeys.push(key);
}
});
}
@ -385,6 +430,7 @@ export default function useSelection<RecordType>(
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const checked = keySet.has(key);
const indeterminate = derivedHalfSelectedKeySet.has(key);
// Record checked
return {
@ -392,6 +438,7 @@ export default function useSelection<RecordType>(
<Checkbox
{...checkboxPropsMap.get(key)}
checked={checked}
indeterminate={indeterminate}
onClick={e => e.stopPropagation()}
onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent;
@ -400,7 +447,7 @@ export default function useSelection<RecordType>(
let endIndex: number = -1;
// Get range of this
if (shiftKey) {
if (shiftKey && checkStrictly) {
const pointKeys = new Set([lastSelectedKey, key]);
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
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = [];
@ -449,13 +496,37 @@ export default function useSelection<RecordType>(
}
} else {
// Single record selected
if (checked) {
keySet.delete(key);
const originCheckedKeys = derivedSelectedKeys;
if (checkStrictly) {
const checkedKeys = checked
? arrDel(originCheckedKeys, key)
: arrAdd(originCheckedKeys, key);
triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
} 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);
@ -500,18 +571,21 @@ export default function useSelection<RecordType>(
},
[
getRowKey,
pageData,
flattedData,
rowSelection,
innerSelectedKeys,
mergedSelectedKeys,
derivedSelectedKeys,
derivedSelectedKeySet,
derivedHalfSelectedKeySet,
selectionColWidth,
mergedSelections,
expandType,
lastSelectedKey,
checkboxPropsMap,
onSelectMultiple,
triggerSingleSelection,
isCheckboxDisabled,
],
);
return [transformColumns, mergedSelectedKeySet];
return [transformColumns, derivedSelectedKeySet];
}

View File

@ -185,11 +185,12 @@ Properties for row selection.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| columnWidth | Set the width of the selection column | string\|number | `60px` | 4.0 |
| columnTitle | Set the title of the selection column | string\|React.ReactNode | - | 4.0 |
| fixed | Fixed selection column on the left | boolean | - | 4.0 |
| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | 4.0 |
| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | false | 4.3 |
| checkStrictly | Check table row precisely; parent row and children rows are not associated | boolean | true | 4.4.0 |
| columnWidth | Set the width of the selection column | string\|number | `60px` | |
| columnTitle | Set the title of the selection column | string\|ReactNode | - | |
| fixed | Fixed selection column on the left | boolean | - | |
| 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 |
| 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[] | \[] | |

View File

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

View File

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

View File

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

View File

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