Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: column resizable
Browse files Browse the repository at this point in the history
linxianxi committed Apr 25, 2024
1 parent b43c2f2 commit c50f2da
Showing 17 changed files with 646 additions and 118 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -129,9 +129,11 @@ React.render(<Table columns={columns} data={data} />, mountNode);
| title | React Node | | title of this column |
| dataIndex | String | | display field of the data record |
| width | String \| Number | | width of the specific proportion calculation according to the width of the columns |
| minWidth | Number | | min width of the specific proportion calculation according to the width of the columns |
| fixed | String \| Boolean | | this column will be fixed when table scroll horizontally: true or 'left' or 'right' |
| align | String | | specify how cell content is aligned |
| ellipsis | Boolean | | specify whether cell content be ellipsized |
| resizable | Boolean | | column resize |
| rowScope | 'row' \| 'rowgroup' | | Set scope attribute for all cells in this column |
| onCell | Function(record, index) | | Set custom props per each cell. |
| onHeaderCell | Function(record) | | Set custom props per each header cell. |
30 changes: 30 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
@@ -63,9 +63,39 @@
}
}

&-column-resizing {
cursor: col-resize;
}

&-cell {
background: #f4f4f4;

&-resize-handle {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
cursor: col-resize;
z-index: 1;
background: red;
}

&-resize-line {
position: absolute;
width: 4px;
background: red;
height: 100%;
top: 0;
transform: translateX(-50%);
z-index: 2;
}


&-fix-right &-resize-handle {
left: 0;
}

&-fix-left,
&-fix-right {
z-index: 2;
184 changes: 98 additions & 86 deletions docs/examples/column-resize.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,107 @@
import React from 'react';
import { Resizable } from 'react-resizable';
import Table from 'rc-table';
import React, { useState } from 'react';
import Table, { INTERNAL_HOOKS } from 'rc-table';
import type { ColumnType } from 'rc-table';
import '../../assets/index.less';
import 'react-resizable/css/styles.css';
import type { ColumnType } from '@/interface';

const ResizableTitle = props => {
const { onResize, width, ...restProps } = props;
const data = [
{ a: '123', b: 'xxxxxxxx xxxxxxxx', d: 3, key: '1' },
{ a: 'cdd', b: 'edd12221 edd12221', d: 3, key: '2' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '3' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '4' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '5' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '6' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '7' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '8' },
{ a: '133', c: 'edd12221 edd12221', d: 2, key: '9' },
];

if (!width) {
return <th {...restProps} />;
}
const Demo = () => {
const [widthMap, setWidthMap] = useState<Map<React.Key, number>>(new Map());

const columns1 = [
{ title: 'title1', dataIndex: 'aaa', key: 'aaa', width: 100 },
{ title: 'title2', dataIndex: 'bbb', key: 'bbb', width: 100 },
].map(i => ({
...i,
resizable: true,
width: widthMap.get(i.key ?? i.dataIndex) ?? i.width,
})) as ColumnType<any>[];

const columns2 = [
{ title: 'title1', dataIndex: 'a', key: 'a', fixed: 'left' },
{ title: 'title2', dataIndex: 'b', key: 'b', fixed: 'left' },
{ title: 'title3', dataIndex: 'c', key: 'c' },
{ title: 'title4', dataIndex: 'b', key: 'd' },
{ title: 'title5', dataIndex: 'b', key: 'e' },
{ title: 'title6', dataIndex: 'b', key: 'f' },
{ title: 'title7', dataIndex: 'b', key: 'g' },
{ title: 'title8', dataIndex: 'b', key: 'h' },
{ title: 'title9', dataIndex: 'b', key: 'i' },
{ title: 'title10', dataIndex: 'b', key: 'j' },
{ title: 'title11', dataIndex: 'b', key: 'k', fixed: 'right' },
{ title: 'title12', dataIndex: 'b', key: 'l', fixed: 'right' },
].map(i => ({
...i,
resizable: true,
width: widthMap.get(i.key ?? i.dataIndex) ?? 150,
})) as ColumnType<any>[];

return (
<Resizable width={width} height={0} onResize={onResize}>
<th {...restProps} />
</Resizable>
<div>
table width: 800px {'columns=[{width: 100, width: 100}]'} 情况
<Table
style={{ width: 800 }}
scroll={{ y: 300, x: columns1.reduce((t, c) => t + (c.width as number), 0) }}
columns={columns1}
data={data}
onColumnResizeComplete={({ columnWidths }) => {
setWidthMap(prev => {
const result = new Map(prev);
columnWidths.forEach(i => {
result.set(i.columnKey, i.width);
});
return result;
});
}}
internalHooks={INTERNAL_HOOKS}
getContainerWidth={(ele, width) => {
// Minus border
const borderWidth = getComputedStyle(
ele.querySelector('.rc-table-body'),
).borderInlineStartWidth;
const mergedWidth = width - parseInt(borderWidth, 10);
return mergedWidth;
}}
/>
<br />
大多数情况
<Table
style={{ width: 800 }}
scroll={{ y: 300, x: columns2.reduce((t, c) => t + (c.width as number), 0) }}
columns={columns2}
data={data}
onColumnResizeComplete={({ columnWidths }) => {
setWidthMap(prev => {
const result = new Map(prev);
columnWidths.forEach(i => {
result.set(i.columnKey, i.width);
});
return result;
});
}}
internalHooks={INTERNAL_HOOKS}
getContainerWidth={(ele, width) => {
// Minus border
const borderWidth = getComputedStyle(
ele.querySelector('.rc-table-body'),
).borderInlineStartWidth;
const mergedWidth = width - parseInt(borderWidth, 10);
return mergedWidth;
}}
/>
{/* <Table resizable style={{ width: 800 }} columns={columns} data={data} /> */}
</div>
);
};

interface RecordType {
a: string;
b?: string;
c?: string;
d?: number;
key: string;
}

interface DemoState {
columns: ColumnType<RecordType>[];
}

class Demo extends React.Component<{}, DemoState> {
state: DemoState = {
columns: [
{ title: 'title1', dataIndex: 'a', key: 'a', width: 100 },
{ title: 'title2', dataIndex: 'b', key: 'b', width: 100 },
{ title: 'title3', dataIndex: 'c', key: 'c', width: 200 },
{
title: 'Operations',
dataIndex: '',
key: 'd',
render() {
return <a href="#">Operations</a>;
},
},
],
};

components = {
header: {
cell: ResizableTitle,
},
};

data = [
{ a: '123', key: '1' },
{ a: 'cdd', b: 'edd', key: '2' },
{ a: '1333', c: 'eee', d: 2, key: '3' },
];

handleResize =
index =>
(e, { size }) => {
this.setState(({ columns }) => {
const nextColumns = [...columns];
nextColumns[index] = {
...nextColumns[index],
width: size.width,
};
return { columns: nextColumns };
});
};

render() {
const columns = this.state.columns.map((col, index) => ({
...col,
onHeaderCell: (column: ColumnType<RecordType>) =>
({
width: column.width,
onResize: this.handleResize(index),
}) as any,
}));

return (
<div>
<h2>Integrate with react-resizable</h2>
<Table components={this.components} columns={columns} data={this.data} />
</div>
);
}
}

export default Demo;
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -98,7 +98,6 @@
"react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4",
"react-dom": "^16.0.0",
"react-resizable": "^3.0.5",
"react-virtualized": "^9.12.0",
"react-window": "^1.8.5",
"regenerator-runtime": "^0.14.0",
6 changes: 3 additions & 3 deletions src/Body/MeasureCell.tsx
Original file line number Diff line number Diff line change
@@ -3,15 +3,15 @@ import ResizeObserver from 'rc-resize-observer';

export interface MeasureCellProps {
columnKey: React.Key;
onColumnResize: (key: React.Key, width: number) => void;
onColumnWidthChange: (key: React.Key, width: number) => void;
}

export default function MeasureCell({ columnKey, onColumnResize }: MeasureCellProps) {
export default function MeasureCell({ columnKey, onColumnWidthChange }: MeasureCellProps) {
const cellRef = React.useRef<HTMLTableCellElement>();

React.useEffect(() => {
if (cellRef.current) {
onColumnResize(columnKey, cellRef.current.offsetWidth);
onColumnWidthChange(columnKey, cellRef.current.offsetWidth);
}
}, []);

16 changes: 12 additions & 4 deletions src/Body/MeasureRow.tsx
Original file line number Diff line number Diff line change
@@ -4,11 +4,15 @@ import MeasureCell from './MeasureCell';

export interface MeasureCellProps {
prefixCls: string;
onColumnResize: (key: React.Key, width: number) => void;
onColumnWidthChange: (key: React.Key, width: number) => void;
columnsKey: React.Key[];
}

export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: MeasureCellProps) {
export default function MeasureRow({
prefixCls,
columnsKey,
onColumnWidthChange,
}: MeasureCellProps) {
return (
<tr
aria-hidden="true"
@@ -18,12 +22,16 @@ export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: Me
<ResizeObserver.Collection
onBatchResize={infoList => {
infoList.forEach(({ data: columnKey, size }) => {
onColumnResize(columnKey, size.offsetWidth);
onColumnWidthChange(columnKey, size.offsetWidth);
});
}}
>
{columnsKey.map(columnKey => (
<MeasureCell key={columnKey} columnKey={columnKey} onColumnResize={onColumnResize} />
<MeasureCell
key={columnKey}
columnKey={columnKey}
onColumnWidthChange={onColumnWidthChange}
/>
))}
</ResizeObserver.Collection>
</tr>
6 changes: 3 additions & 3 deletions src/Body/index.tsx
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
const {
prefixCls,
getComponent,
onColumnResize,
onColumnWidthChange,
flattenColumns,
getRowKey,
expandedKeys,
@@ -34,7 +34,7 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
} = useContext(TableContext, [
'prefixCls',
'getComponent',
'onColumnResize',
'onColumnWidthChange',
'flattenColumns',
'getRowKey',
'expandedKeys',
@@ -104,7 +104,7 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
<MeasureRow
prefixCls={prefixCls}
columnsKey={columnsKey}
onColumnResize={onColumnResize}
onColumnWidthChange={onColumnWidthChange}
/>
)}

1 change: 0 additions & 1 deletion src/Cell/index.tsx
Original file line number Diff line number Diff line change
@@ -116,7 +116,6 @@ function Cell<RecordType>(props: CellProps<RecordType>) {
additionalProps = {},
isSticky,
} = props;

const cellPrefixCls = `${prefixCls}-cell`;
const { supportSticky, allColumnsFixedLeft, rowHoverable } = useContext(TableContext, [
'supportSticky',
31 changes: 31 additions & 0 deletions src/Header/HeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import type { CellProps } from '../Cell';
import Cell from '../Cell';
import useCellResize from './useCellResize';
import { useContext } from '@rc-component/context';
import TableContext from '../context/TableContext';

interface HeaderCellProps<RecordType> extends CellProps<RecordType> {
columnKey?: React.Key;
resizable?: boolean;
minWidth?: number;
}

function HeaderCell<RecordType>({
columnKey,
resizable,
minWidth,
...cellProps
}: HeaderCellProps<RecordType>) {
const { supportSticky } = useContext(TableContext, ['supportSticky']);

const { fixRight, prefixCls } = cellProps;
const isFixRight = typeof fixRight === 'number' && supportSticky;
const cellPrefixCls = `${prefixCls}-cell`;

const resizeHandleNode = useCellResize(columnKey, isFixRight, cellPrefixCls, resizable, minWidth);

return <Cell {...cellProps} appendNode={resizeHandleNode} />;
}

export default HeaderCell;
7 changes: 5 additions & 2 deletions src/Header/HeaderRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from 'react';
import Cell from '../Cell';
import TableContext from '../context/TableContext';
import { useContext } from '@rc-component/context';
import type {
@@ -11,6 +10,7 @@ import type {
} from '../interface';
import { getCellFixedInfo } from '../utils/fixUtil';
import { getColumnsKey } from '../utils/valueUtil';
import HeaderCell from './HeaderCell';

export interface RowProps<RecordType> {
cells: readonly CellType<RecordType>[];
@@ -61,7 +61,7 @@ const HeaderRow = <RecordType extends any>(props: RowProps<RecordType>) => {
}

return (
<Cell
<HeaderCell
{...cell}
scope={column.title ? (cell.colSpan > 1 ? 'colgroup' : 'col') : null}
ellipsis={column.ellipsis}
@@ -72,6 +72,9 @@ const HeaderRow = <RecordType extends any>(props: RowProps<RecordType>) => {
{...fixedInfo}
additionalProps={additionalProps}
rowType="header"
columnKey={columnsKey[cellIndex]}
resizable={(column as ColumnType<RecordType>).resizable}
minWidth={(column as ColumnType<RecordType>).minWidth}
/>
);
})}
132 changes: 132 additions & 0 deletions src/Header/useCellResize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import TableContext from '../context/TableContext';
import { useContext } from '@rc-component/context';
import { useEvent } from 'rc-util';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

export default function useCelResize(
columnKey: React.Key,
isFixRight: boolean,
cellPrefixCls: string,
resizable?: boolean,
minWidth: number = 0,
) {
const {
colsWidths,
colsKeys,
colWidths,
componentWidth,
fullTableRef,
onColumnResizeComplete,
onResizingChange,
} = useContext(TableContext, [
'colWidths',
'colsKeys',
'colsWidths',
'componentWidth',
'fullTableRef',
'onColumnResizeComplete',
'onResizingChange',
]);
const [isResizing, setIsResizing] = useState(false);
const [lineLeft, setLineLeft] = useState(0);
const lineStartLeftRef = useRef(0);
const startRealWidth = useRef(0);
const startPageX = useRef(0);
const mouseMoveRef = useRef<(event: MouseEvent) => void>(null);
const mouseUpRef = useRef<(event: MouseEvent) => void>(null);

const removeResizeListener = () => {
document.body.removeEventListener('mousemove', mouseMoveRef.current);
document.body.removeEventListener('mouseup', mouseUpRef.current);
};

useEffect(() => removeResizeListener, []);

const onResize = useEvent((event: MouseEvent, isResizeEnd?: boolean) => {
const offset = event.pageX - startPageX.current;
const oldWidth = colsWidths.get(columnKey);
let newWidth = startRealWidth.current + (isFixRight ? -offset : offset);

if (newWidth < minWidth) {
newWidth = minWidth;
}
setLineLeft(
lineStartLeftRef.current +
(isFixRight ? startRealWidth.current - newWidth : newWidth - startRealWidth.current),
);

if (isResizeEnd) {
const totalWidth = colWidths.reduce((total, width) => total + width, 0);
const smallThanWidth = componentWidth - (totalWidth - oldWidth + newWidth);
// If it is less than the width of the table, the remaining width will be allocated to the column on the right.
// If there is no column on the right, it will be allocated to the column on the left.
let addWidthColumnKey: React.Key;
const isDecreasingWidth = oldWidth - newWidth > 0;
if (smallThanWidth > 0 && isDecreasingWidth) {
const index = colsKeys.findIndex(key => key === columnKey);
addWidthColumnKey = colsKeys[index + 1] ?? colsKeys[index - 1];
}

const columnWidthsMap = new Map(colsWidths);
columnWidthsMap.set(columnKey, newWidth);
if (addWidthColumnKey) {
const addWidthColumnNewWidth = colsWidths.get(addWidthColumnKey) + smallThanWidth;
columnWidthsMap.set(addWidthColumnKey, addWidthColumnNewWidth);
}
const columnWidths = Array.from(columnWidthsMap).map(([key, width]) => ({
columnKey: key,
width,
}));
onColumnResizeComplete?.({ columnKey, width: newWidth, columnWidths });
}
});

const onResizeEnd = (event: MouseEvent) => {
setIsResizing(false);
onResizingChange(false);
removeResizeListener();
onResize(event, true);
};

const onResizeStart = (event: React.MouseEvent) => {
// Block selected text
event.preventDefault();
const left =
(event.target as HTMLElement).parentElement.getBoundingClientRect()[
isFixRight ? 'left' : 'right'
] - fullTableRef.current.getBoundingClientRect().left;
setLineLeft(left);
lineStartLeftRef.current = left;
startRealWidth.current = colsWidths.get(columnKey);
startPageX.current = event.pageX;
document.body.addEventListener('mousemove', onResize);
document.body.addEventListener('mouseup', onResizeEnd);
mouseMoveRef.current = onResize;
mouseUpRef.current = onResizeEnd;
onResizingChange(true);
setIsResizing(true);
};

const resizeHandleNode = resizable && (
<>
<div
className={`${cellPrefixCls}-resize-handle`}
style={{ display: isResizing ? 'none' : undefined }}
onMouseDown={onResizeStart}
/>
{isResizing &&
createPortal(
<div
className={`${cellPrefixCls}-resize-line`}
style={{
left: lineLeft,
}}
/>,
fullTableRef.current,
)}
</>
);

return resizeHandleNode;
}
29 changes: 25 additions & 4 deletions src/Table.tsx
Original file line number Diff line number Diff line change
@@ -125,6 +125,11 @@ export interface TableProps<RecordType = any>

// Events
onScroll?: React.UIEventHandler<HTMLDivElement>;
onColumnResizeComplete?: (info: {
columnKey: React.Key;
width: number;
columnWidths: { columnKey: React.Key; width: number }[];
}) => void;

// =================================== Internal ===================================
/**
@@ -211,6 +216,7 @@ function Table<RecordType extends DefaultRecordType>(

// Events
onScroll,
onColumnResizeComplete,

// Internal
internalHooks,
@@ -352,9 +358,11 @@ function Table<RecordType extends DefaultRecordType>(
const [pingedLeft, setPingedLeft] = React.useState(false);
const [pingedRight, setPingedRight] = React.useState(false);
const [colsWidths, updateColsWidths] = useLayoutState(new Map<React.Key, number>());
const [isResizing, setIsResizing] = React.useState(false);

// Convert map to number width
const colsKeys = getColumnsKey(flattenColumns);
const pureColsKeys = getColumnsKey(flattenColumns);
const colsKeys = React.useMemo(() => pureColsKeys, [pureColsKeys.join('_')]);
const pureColWidths = colsKeys.map(columnKey => colsWidths.get(columnKey));
const colWidths = React.useMemo(() => pureColWidths, [pureColWidths.join('_')]);
const stickyOffsets = useStickyOffsets(colWidths, flattenColumns, direction);
@@ -404,7 +412,7 @@ function Table<RecordType extends DefaultRecordType>(
};
}

const onColumnResize = React.useCallback((columnKey: React.Key, width: number) => {
const onColumnWidthChange = React.useCallback((columnKey: React.Key, width: number) => {
if (isVisible(fullTableRef.current)) {
updateColsWidths(widths => {
if (widths.get(columnKey) !== width) {
@@ -770,6 +778,7 @@ function Table<RecordType extends DefaultRecordType>(
[`${prefixCls}-has-fix-right`]:
flattenColumns[flattenColumns.length - 1] &&
flattenColumns[flattenColumns.length - 1].fixed === 'right',
[`${prefixCls}-column-resizing`]: isResizing,
})}
style={style}
id={id}
@@ -826,7 +835,7 @@ function Table<RecordType extends DefaultRecordType>(
// Column
columns,
flattenColumns,
onColumnResize,
onColumnWidthChange,

// Row
hoverStartRow: startRow,
@@ -840,6 +849,12 @@ function Table<RecordType extends DefaultRecordType>(
childrenColumnName: mergedChildrenColumnName,

rowHoverable,
fullTableRef,
colsWidths,
colsKeys,
colWidths,
onColumnResizeComplete,
onResizingChange: setIsResizing,
}),
[
// Scroll
@@ -853,6 +868,7 @@ function Table<RecordType extends DefaultRecordType>(
fixedInfoList,
isSticky,
supportSticky,
fullTableRef,

componentWidth,
fixHeader,
@@ -875,7 +891,12 @@ function Table<RecordType extends DefaultRecordType>(
// Column
columns,
flattenColumns,
onColumnResize,
onColumnWidthChange,
colsWidths,
colsKeys,
colWidths,
onColumnResizeComplete,
setIsResizing,

// Row
startRow,
6 changes: 3 additions & 3 deletions src/VirtualTable/BodyGrid.tsx
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {

const {
flattenColumns,
onColumnResize,
onColumnWidthChange,
getRowKey,
expandedKeys,
prefixCls,
@@ -33,7 +33,7 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
scrollX,
} = useContext(TableContext, [
'flattenColumns',
'onColumnResize',
'onColumnWidthChange',
'getRowKey',
'prefixCls',
'expandedKeys',
@@ -71,7 +71,7 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {

React.useEffect(() => {
columnsWidth.forEach(([key, width]) => {
onColumnResize(key, width);
onColumnWidthChange(key, width);
});
}, [columnsWidth]);

12 changes: 11 additions & 1 deletion src/context/TableContext.tsx
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ export interface TableContextProps<RecordType = any> {
// Column
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
onColumnResize: (columnKey: React.Key, width: number) => void;
onColumnWidthChange: (columnKey: React.Key, width: number) => void;

// Row
hoverStartRow: number;
@@ -68,6 +68,16 @@ export interface TableContextProps<RecordType = any> {
childrenColumnName: string;

rowHoverable?: boolean;
fullTableRef: React.MutableRefObject<HTMLDivElement>;
colsWidths: Map<React.Key, number>;
colWidths: number[];
colsKeys: React.Key[];
onColumnResizeComplete?: (info: {
columnKey: React.Key;
width: number;
columnWidths: { columnKey: React.Key; width: number }[];
}) => void;
onResizingChange: (value: boolean) => void;
}

const TableContext = createContext<TableContextProps>();
17 changes: 8 additions & 9 deletions src/hooks/useColumns/index.tsx
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import type {
} from '../../interface';
import { INTERNAL_COL_DEFINE } from '../../utils/legacyUtil';
import useWidthColumns from './useWidthColumns';
import { getColumnsKey } from '../../utils/valueUtil';

export function convertChildrenToColumns<RecordType>(
children: React.ReactNode,
@@ -55,23 +56,19 @@ function filterHiddenColumns<RecordType>(
});
}

function flatColumns<RecordType>(
columns: ColumnsType<RecordType>,
parentKey = 'key',
): ColumnType<RecordType>[] {
return columns
function flatColumns<RecordType>(columns: ColumnsType<RecordType>): ColumnType<RecordType>[] {
const flattenColumns = columns
.filter(column => column && typeof column === 'object')
.reduce((list, column, index) => {
.reduce((list, column) => {
const { fixed } = column;
// Convert `fixed='true'` to `fixed='left'` instead
const parsedFixed = fixed === true ? 'left' : fixed;
const mergedKey = `${parentKey}-${index}`;

const subColumns = (column as ColumnGroupType<RecordType>).children;
if (subColumns && subColumns.length > 0) {
return [
...list,
...flatColumns(subColumns, mergedKey).map(subColum => ({
...flatColumns(subColumns).map(subColum => ({
fixed: parsedFixed,
...subColum,
})),
@@ -80,12 +77,14 @@ function flatColumns<RecordType>(
return [
...list,
{
key: mergedKey,
...column,
fixed: parsedFixed,
},
];
}, []);

const colKeys = getColumnsKey(flattenColumns);
return flattenColumns.map((col, index) => ({ key: colKeys[index], ...col }));
}

function revertForRtl<RecordType>(columns: ColumnsType<RecordType>): ColumnsType<RecordType> {
8 changes: 7 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
@@ -70,7 +70,11 @@ export type Direction = 'ltr' | 'rtl';
// SpecialString will be removed in antd@6
export type SpecialString<T> = T | (string & {});

export type DataIndex<T = any> = DeepNamePath<T> | SpecialString<T> | number | (SpecialString<T> | number)[];
export type DataIndex<T = any> =
| DeepNamePath<T>
| SpecialString<T>
| number
| (SpecialString<T> | number)[];

export type CellEllipsisType = { showTitle?: boolean } | boolean;

@@ -109,6 +113,8 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
shouldCellUpdate?: (record: RecordType, prevRecord: RecordType) => boolean;
rowSpan?: number;
width?: number | string;
minWidth?: number;
resizable?: boolean;
onCell?: GetComponentProps<RecordType>;
/** @deprecated Please use `onCell` instead */
onCellClick?: (record: RecordType, e: React.MouseEvent<HTMLElement>) => void;
276 changes: 276 additions & 0 deletions tests/Resizable.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { mount } from 'enzyme';
import Table from '../src';
import React from 'react';
import { act } from 'react-dom/test-utils';
import RcResizeObserver, { _rs } from 'rc-resize-observer';
import { safeAct } from './utils';
import { spyElementPrototype } from 'rc-util/lib/test/domHook';

describe('Table.resizable', () => {
let domSpy;
let containerSpy;

beforeAll(() => {
domSpy = spyElementPrototype(HTMLElement, 'offsetParent', {
get: () => ({}),
});
containerSpy = spyElementPrototype(HTMLDivElement, 'offsetWidth', {
get: () => 800,
});
});

beforeEach(() => {
vi.useFakeTimers();
});

afterAll(() => {
domSpy.mockRestore();
containerSpy.mockRestore();
});

it('change width in onColumnResizeComplete', async () => {
const onColumnResizeComplete = vi.fn();

const App = () => {
const [widthMap, setWidthMap] = React.useState(new Map());

const columns = [
{ key: 'a', dataIndex: 'a', width: 400, resizable: true },
{ key: 'b', dataIndex: 'b', width: 400, resizable: true },
].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width }));

return (
<Table
data={[{ a: '123', b: '123', key: '1' }]}
columns={columns}
scroll={{ x: columns.reduce((t, c) => t + c.width, 0) }}
onColumnResizeComplete={info => {
setWidthMap(prev => {
const result = new Map(prev);
info.columnWidths.forEach(i => {
result.set(i.columnKey, i.width);
});
return result;
});
onColumnResizeComplete(info);
}}
/>
);
};
const wrapper = mount(<App />);

async function triggerResize(resizeList) {
wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList);
await safeAct(wrapper);
wrapper.update();
}

await triggerResize([
{
data: wrapper.find('ResizeObserver').at(1).props().data,
size: { width: 400, offsetWidth: 400 },
},
{
data: wrapper.find('ResizeObserver').at(2).props().data,
size: { width: 400, offsetWidth: 400 },
},
]);

wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 });

const mousemoveEvent = new Event('mousemove');
mousemoveEvent.pageX = 100;

await act(async () => {
document.body.dispatchEvent(mousemoveEvent);
await Promise.resolve();
wrapper.update();
});

const mouseupEvent = new Event('mouseup');
mouseupEvent.pageX = 100;

await act(async () => {
document.body.dispatchEvent(mouseupEvent);
await Promise.resolve();
wrapper.update();
});

expect(onColumnResizeComplete).toHaveBeenCalledWith({
columnKey: 'a',
width: 500,
columnWidths: [
{ columnKey: 'a', width: 500 },
{ columnKey: 'b', width: 400 },
],
});

expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(500);
expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(400);
});

it('columns total width < componentWidth', async () => {
const onColumnResizeComplete = vi.fn();

const App = () => {
const [widthMap, setWidthMap] = React.useState(new Map());

const columns = [
{ key: 'a', dataIndex: 'a', width: 100, resizable: true },
{ key: 'b', dataIndex: 'b', width: 100, resizable: true },
].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width || 100 }));

return (
<Table
style={{ width: 800 }}
data={[{ a: '123', b: 'xxxxxxxx', key: '1' }]}
columns={columns}
scroll={{ x: columns.reduce((t, c) => t + c.width, 0) }}
onColumnResizeComplete={info => {
setWidthMap(prev => {
const result = new Map(prev);
info.columnWidths.forEach(i => {
result.set(i.columnKey, i.width);
});
return result;
});
onColumnResizeComplete(info);
}}
/>
);
};
const wrapper = mount(<App />);

async function triggerResize(resizeList) {
wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList);
await safeAct(wrapper);
wrapper.update();
}

wrapper.find(RcResizeObserver).first().props().onResize({ width: 800 });

await triggerResize([
{
data: wrapper.find('ResizeObserver').at(1).props().data,
size: { width: 400, offsetWidth: 400 },
},
{
data: wrapper.find('ResizeObserver').at(2).props().data,
size: { width: 400, offsetWidth: 400 },
},
]);

wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 });

const mousemoveEvent = new Event('mousemove');
mousemoveEvent.pageX = -100;

await act(async () => {
document.body.dispatchEvent(mousemoveEvent);
await Promise.resolve();
wrapper.update();
});

const mouseupEvent = new Event('mouseup');
mouseupEvent.pageX = -100;

await act(async () => {
document.body.dispatchEvent(mouseupEvent);
await Promise.resolve();
wrapper.update();
});

expect(onColumnResizeComplete).toHaveBeenCalledWith({
columnKey: 'a',
width: 300,
columnWidths: [
{ columnKey: 'a', width: 300 },
{ columnKey: 'b', width: 500 },
],
});

expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(300);
expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(500);
});

it('minWidth should be worked', async () => {
const onColumnResizeComplete = vi.fn();

const App = () => {
const [widthMap, setWidthMap] = React.useState(new Map());

const columns = [
{ key: 'a', dataIndex: 'a', width: 800, resizable: true, minWidth: 400 },
{ key: 'b', dataIndex: 'b', width: 800, resizable: true },
].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width }));

return (
<Table
data={[{ a: '123', b: '123', key: '1' }]}
columns={columns}
scroll={{ x: columns.reduce((t, c) => t + c.width, 0) }}
onColumnResizeComplete={info => {
setWidthMap(prev => {
const result = new Map(prev);
info.columnWidths.forEach(i => {
result.set(i.columnKey, i.width);
});
return result;
});
onColumnResizeComplete(info);
}}
/>
);
};
const wrapper = mount(<App />);

async function triggerResize(resizeList) {
wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList);
await safeAct(wrapper);
wrapper.update();
}

await triggerResize([
{
data: wrapper.find('ResizeObserver').at(1).props().data,
size: { width: 800, offsetWidth: 800 },
},
{
data: wrapper.find('ResizeObserver').at(2).props().data,
size: { width: 800, offsetWidth: 800 },
},
]);

wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 });

const mousemoveEvent = new Event('mousemove');
mousemoveEvent.pageX = -1000;

await act(async () => {
document.body.dispatchEvent(mousemoveEvent);
await Promise.resolve();
wrapper.update();
});

const mouseupEvent = new Event('mouseup');
mouseupEvent.pageX = -1000;

await act(async () => {
document.body.dispatchEvent(mouseupEvent);
await Promise.resolve();
wrapper.update();
});

expect(onColumnResizeComplete).toHaveBeenCalledWith({
columnKey: 'a',
width: 400,
columnWidths: [
{ columnKey: 'a', width: 400 },
{ columnKey: 'b', width: 800 },
],
});

expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(400);
expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(800);
});
});

0 comments on commit c50f2da

Please sign in to comment.