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,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`] = `

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 => {
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];
} }

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",