mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-28 13:09:40 +08:00
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:
parent
3dbb9be339
commit
50ae190b57
@ -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]));
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -5462,9 +5462,38 @@ 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 [
|
||||||
|
<div
|
||||||
|
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||||
|
style="margin-bottom:16px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-space-item"
|
||||||
|
style="margin-right:8px"
|
||||||
|
>
|
||||||
|
CheckStrictly:
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ant-space-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-checked="false"
|
||||||
|
class="ant-switch"
|
||||||
|
role="switch"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-switch-handle"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="ant-switch-inner"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
class="ant-table-wrapper"
|
class="ant-table-wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-spin-nested-loading"
|
class="ant-spin-nested-loading"
|
||||||
>
|
>
|
||||||
@ -5725,7 +5754,8 @@ exports[`renders ./components/table/demo/expand-children.md correctly 1`] = `
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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`] = `
|
||||||
|
@ -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);
|
||||||
```
|
```
|
||||||
|
@ -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 => {
|
||||||
|
if (!keySet.has(key)) {
|
||||||
keySet.add(key);
|
keySet.add(key);
|
||||||
changeKeys.push(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;
|
||||||
|
|
||||||
|
// 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, Array.from(keySet), nativeEvent);
|
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];
|
||||||
}
|
}
|
||||||
|
@ -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[] | \[] | |
|
||||||
|
@ -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 | - | |
|
||||||
|
@ -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,
|
||||||
|
@ -471,6 +471,7 @@
|
|||||||
}
|
}
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{table-prefix-cls}-row-indent + & {
|
.@{table-prefix-cls}-row-indent + & {
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user