Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions packages/components/table/EnhancedTable.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { RefAttributes, forwardRef, useImperativeHandle, useRef } from 'react';
import { get } from 'lodash-es';
import PrimaryTable from './PrimaryTable';
import { PrimaryTableCol, TableRowData, DragSortContext, TdPrimaryTableProps } from './type';

import useConfig from '../hooks/useConfig';
import useTreeData from './hooks/useTreeData';
import useTreeSelect from './hooks/useTreeSelect';
import { EnhancedTableProps, EnhancedTableRef, PrimaryTableProps } from './interface';
import useConfig from '../hooks/useConfig';
import PrimaryTable, { type InternalPrimaryTableProps } from './PrimaryTable';

import { StyledProps } from '../common';
import type { StyledProps } from '../common';
import type { EnhancedTableProps, EnhancedTableRef } from './interface';
import type { DragSortContext, PrimaryTableCol, TableRowData, TdPrimaryTableProps } from './type';

export interface TEnhancedTableProps extends EnhancedTableProps, StyledProps {}

Expand All @@ -19,10 +20,11 @@ const EnhancedTable = forwardRef<EnhancedTableRef, TEnhancedTableProps>((props,
// treeInstanceFunctions 属于对外暴露的 Ref 方法
const { store, dataSource, formatTreeColumn, swapData, onExpandFoldIconClick, ...treeInstanceFunctions } =
useTreeData(props);

const treeDataMap = store?.treeDataMap;

const { tIndeterminateSelectedRowKeys, onInnerSelectChange } = useTreeSelect(props, treeDataMap);
const { innerIndeterminateSelectedRowKeys, innerSelectedRowKeys, onInnerSelectChange } = useTreeSelect(props, {
treeDataMap,
});

// 影响列和单元格内容的因素有:树形节点需要添加操作符 [+] [-]
const getColumns = (columns: PrimaryTableCol<TableRowData>[]) => {
Expand Down Expand Up @@ -78,14 +80,17 @@ const EnhancedTable = forwardRef<EnhancedTableRef, TEnhancedTableProps>((props,
props.onDragSort?.(params);
};

const primaryTableProps: PrimaryTableProps = {
const isTreeData = Boolean(tree && Object.keys(tree).length);
const primaryTableProps: InternalPrimaryTableProps = {
...props,
data: dataSource,
columns: tColumns,
// 半选状态节点
indeterminateSelectedRowKeys: tIndeterminateSelectedRowKeys,
selectedRowKeys: isTreeData ? innerSelectedRowKeys : props.selectedRowKeys || [],
indeterminateSelectedRowKeys: innerIndeterminateSelectedRowKeys,
// 树形结构不允许本地数据分页
disableDataPage: Boolean(tree && Object.keys(tree).length),
disableDataPage: isTreeData,
reserveSelectedRowOnPaginate: isTreeData ? true : props.reserveSelectedRowOnPaginate,
treeDataMap: isTreeData ? treeDataMap : undefined,
onSelectChange: onInnerSelectChange,
onDragSort: onDragSortChange,
rowClassName: ({ row }) => {
Expand Down
45 changes: 26 additions & 19 deletions packages/components/table/PrimaryTable.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import React, { useRef, forwardRef, useImperativeHandle, ReactNode, RefAttributes } from 'react';
import { get } from 'lodash-es';
import React, { forwardRef, ReactNode, RefAttributes, useImperativeHandle, useRef } from 'react';
import classNames from 'classnames';
import { get } from 'lodash-es';

import type { TableTreeDataMap } from '@tdesign/common-js/table/tree-store';

import useDefaultProps from '../hooks/useDefaultProps';
import BaseTable from './BaseTable';
import { primaryTableDefaultProps } from './defaultProps';
import EditableCell, { type EditableCellProps } from './EditableCell';
import useAsyncLoading from './hooks/useAsyncLoading';
import useClassName from './hooks/useClassName';
import useColumnController from './hooks/useColumnController';
import useDragSort from './hooks/useDragSort';
import { useEditableRow } from './hooks/useEditableRow';
import useFilter from './hooks/useFilter';
import useRowExpand from './hooks/useRowExpand';
import useTableHeader, { renderTitle } from './hooks/useTableHeader';
import useRowSelect from './hooks/useRowSelect';
import { TdPrimaryTableProps, PrimaryTableCol, TableRowData, PrimaryTableCellParams } from './type';
import useSorter from './hooks/useSorter';
import useFilter from './hooks/useFilter';
import useDragSort from './hooks/useDragSort';
import useAsyncLoading from './hooks/useAsyncLoading';
import { PageInfo, PaginationProps } from '../pagination';
import useClassName from './hooks/useClassName';
import useStyle from './hooks/useStyle';
import { BaseTableProps, PrimaryTableProps, PrimaryTableRef } from './interface';
import EditableCell, { EditableCellProps } from './EditableCell';
import { StyledProps } from '../common';
import { useEditableRow } from './hooks/useEditableRow';
import { primaryTableDefaultProps } from './defaultProps';
import { CheckboxGroupValue } from '../checkbox';
import useDefaultProps from '../hooks/useDefaultProps';
import useTableHeader, { renderTitle } from './hooks/useTableHeader';

import type { CheckboxGroupValue } from '../checkbox';
import type { StyledProps } from '../common';
import type { PageInfo, PaginationProps } from '../pagination';
import type { BaseTableProps, PrimaryTableProps, PrimaryTableRef } from './interface';
import type { PrimaryTableCellParams, PrimaryTableCol, TableRowData, TdPrimaryTableProps } from './type';

export { BASE_TABLE_ALL_EVENTS } from './BaseTable';

export interface TPrimaryTableProps extends PrimaryTableProps, StyledProps {}
export interface InternalPrimaryTableProps extends PrimaryTableProps, StyledProps {
treeDataMap?: TableTreeDataMap;
}

const PrimaryTable = forwardRef<PrimaryTableRef, TPrimaryTableProps>((originalProps, ref) => {
const props = useDefaultProps<TPrimaryTableProps>(originalProps, primaryTableDefaultProps);
const PrimaryTable = forwardRef<PrimaryTableRef, InternalPrimaryTableProps>((originalProps, ref) => {
const props = useDefaultProps<InternalPrimaryTableProps>(originalProps, primaryTableDefaultProps);
const { columns, columnController, editableRowKeys, style, className } = props;

const primaryTableRef = useRef(null);
const innerPagination = useRef<PaginationProps>(props.pagination);
const { classPrefix, tableDraggableClasses, tableBaseClass, tableSelectedClasses, tableSortClasses } = useClassName();
Expand Down
253 changes: 253 additions & 0 deletions packages/components/table/__tests__/row-select.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import React from 'react';
import { vi } from '@test/utils';
import { fireEvent, render } from '@testing-library/react';
import { EnhancedTable, Table } from '..';

const columns = [
{
colKey: 'row-select',
type: 'multiple' as const,
disabled: ({ row }) => row.disabled === true,
},
{ colKey: 'key', title: 'Key' },
];

describe('Table', () => {
function getTableData({ withDisabled = true } = {}) {
const data = [];
for (let i = 0; i <= 5; i++) {
data.push({
key: `${i}`,
...(withDisabled && i === 2 ? { disabled: true } : {}),
});
}
return data;
}

const TestPrimaryTable = ({ defaultSelectedRowKeys = [], onSelectChange = vi.fn(), data = getTableData() }) => (
<Table
rowKey="key"
columns={columns}
data={data}
defaultSelectedRowKeys={defaultSelectedRowKeys}
onSelectChange={onSelectChange}
/>
);

it('全选(不存在 disabled 行)', async () => {
const onSelectChange = vi.fn();

const { container } = render(
<TestPrimaryTable data={getTableData({ withDisabled: false })} onSelectChange={onSelectChange} />,
);

const selectAllCheckbox = container.querySelector('th[data-colkey="row-select"] .t-checkbox');
expect(selectAllCheckbox).not.toHaveClass('t-is-checked');
expect(selectAllCheckbox).not.toHaveClass('t-is-indeterminate');

// 全选
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).toHaveClass('t-is-checked');
expect(onSelectChange).toHaveBeenCalledWith(['0', '1', '2', '3', '4', '5'], expect.any(Object));

// 取消全选
onSelectChange.mockClear();
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).not.toHaveClass('t-is-checked');
expect(onSelectChange).toHaveBeenCalledWith([], expect.any(Object));
});

it('全选(存在 disabled 行)', async () => {
const onSelectChange = vi.fn();

const { container } = render(<TestPrimaryTable onSelectChange={onSelectChange} />);

const selectAllCheckbox = container.querySelector('th[data-colkey="row-select"] .t-checkbox');
expect(selectAllCheckbox).not.toHaveClass('t-is-checked');
expect(selectAllCheckbox).not.toHaveClass('t-is-indeterminate');

// 全选
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');
expect(onSelectChange).toHaveBeenCalledWith(expect.arrayContaining(['0', '1', '3', '4', '5']), expect.any(Object));
expect(onSelectChange.mock.calls[0][0]).not.toContain('2'); // 禁用行不应该被选中

// 取消全选
onSelectChange.mockClear();
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).not.toHaveClass('t-is-indeterminate');
expect(onSelectChange).toHaveBeenCalledWith([], expect.any(Object));
});

it('全选(初始化选中 disabled 行)', async () => {
const onSelectChange = vi.fn();

const { container } = render(<TestPrimaryTable defaultSelectedRowKeys={['2']} onSelectChange={onSelectChange} />);

const selectAllCheckbox = container.querySelector('th[data-colkey="row-select"] .t-checkbox');
expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');

// 全选
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).toHaveClass('t-is-checked');
expect(onSelectChange).toHaveBeenCalledWith(
expect.arrayContaining(['0', '1', '2', '3', '4', '5']),
expect.any(Object),
);

// 取消全选
onSelectChange.mockClear();
await fireEvent.click(selectAllCheckbox);
expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');
expect(onSelectChange).toHaveBeenCalledWith(['2'], expect.any(Object)); // 禁用行保留
});
});

describe('EnhancedTable', () => {
function getTreeData() {
const data = [];

for (let i = 0; i <= 2; i++) {
const parentItem = {
key: `${i}`,
children: [],
};

for (let j = 0; j <= 2; j++) {
const childItem = {
key: `${i}-${j}`,
children: [],
};

for (let k = 0; k <= 2; k++) {
childItem.children.push({
key: `${i}-${j}-${k}`,
});
}

parentItem.children.push(childItem);
}

data.push(parentItem);
}

return data;
}

function getTreeDataWithDisabled() {
const data = getTreeData();
data[1].children[1].children[0].disabled = true;
return data;
}

const TestEnhancedTable = ({ checkStrictly = false, defaultSelectedRowKeys = [], onSelectChange = vi.fn() }) => (
<EnhancedTable
rowKey="key"
data={getTreeDataWithDisabled()}
columns={columns}
defaultSelectedRowKeys={defaultSelectedRowKeys}
onSelectChange={onSelectChange}
tree={{
checkStrictly,
}}
/>
);

it('全选(存在 disabled 行)', async () => {
const onSelectChange = vi.fn();

const { container } = render(<TestEnhancedTable onSelectChange={onSelectChange} />);

const selectAllCheckbox = container.querySelector('th[data-colkey="row-select"] .t-checkbox');

// 初始状态
expect(selectAllCheckbox).not.toHaveClass('t-is-checked');
expect(selectAllCheckbox).not.toHaveClass('t-is-indeterminate');

await fireEvent.click(selectAllCheckbox);
const selectedKeys = onSelectChange.mock.calls[0][0];

expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');
expect(selectedKeys).toHaveLength(38); // 39 - 1
expect(selectedKeys).not.toContain('1-1-0'); // 不包含禁用项
});

it('全选(初始化选中 disabled 行)', async () => {
const onSelectChange = vi.fn();

const { container } = render(
<TestEnhancedTable defaultSelectedRowKeys={['1-1-0']} onSelectChange={onSelectChange} />,
);

const selectAllCheckbox = container.querySelector('th[data-colkey="row-select"] .t-checkbox');

expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');

// 全选
await fireEvent.click(selectAllCheckbox);

const selectedKeys = onSelectChange.mock.calls[0][0];

// 全选 + 禁用行存在 → checked
expect(selectAllCheckbox).toHaveClass('t-is-checked');

expect(selectedKeys).toHaveLength(39);
expect(selectedKeys).toContain('1-1-0');

// 取消全选
onSelectChange.mockClear();
await fireEvent.click(selectAllCheckbox);

const afterUncheck = onSelectChange.mock.calls[0][0];

// 只剩禁用行
expect(afterUncheck).toEqual(['1-1-0']);
expect(selectAllCheckbox).toHaveClass('t-is-indeterminate');
});

it('父节点选择 (checkStrictly=false)', async () => {
const onSelectChange = vi.fn();
const { container } = render(
<TestEnhancedTable defaultSelectedRowKeys={['1-1-0']} onSelectChange={onSelectChange} />,
);

const rows = container.querySelectorAll('tr.t-table-tr--level-0');
const parentCheckbox = rows[1].querySelector('.t-checkbox');

expect(parentCheckbox).toHaveClass('t-is-indeterminate');

await fireEvent.click(parentCheckbox);

const selectedKeys = onSelectChange.mock.calls[0][0];

expect(parentCheckbox).toHaveClass('t-is-checked');
expect(selectedKeys).toContain('1');
expect(selectedKeys).toContain('1-1-0');
expect(selectedKeys).toHaveLength(13);

onSelectChange.mockClear();
await fireEvent.click(parentCheckbox);

expect(onSelectChange).toHaveBeenCalledWith(['1-1-0'], expect.any(Object));
});

it('父节点选择 (checkStrictly=true)', async () => {
const onSelectChange = vi.fn();
const { container } = render(
<TestEnhancedTable defaultSelectedRowKeys={['1-1-0']} onSelectChange={onSelectChange} checkStrictly />,
);
const rows = container.querySelectorAll('tr.t-table-tr--level-0');

const parentCheckbox = rows[1].querySelector('.t-checkbox');
expect(parentCheckbox).not.toHaveClass('t-is-indeterminate');
expect(parentCheckbox).not.toHaveClass('t-is-checked');

await fireEvent.click(parentCheckbox);
expect(onSelectChange).toHaveBeenCalledWith(expect.arrayContaining(['1-1-0', '1']), expect.any(Object));

const childCheckbox = rows[2].querySelector('.t-checkbox');
onSelectChange.mockClear();
await fireEvent.click(childCheckbox);
expect(onSelectChange).toHaveBeenCalledWith(expect.arrayContaining(['1-1-0', '1', '2']), expect.any(Object));
});
});
Loading
Loading