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
22 changes: 6 additions & 16 deletions src/Body/MeasureCell.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import * as React from 'react';
import ResizeObserver from '@rc-component/resize-observer';
import { useLayoutEffect } from '@rc-component/util';

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

/**
* Measure cell renders the td used for width measurement and reports its
* size changes to the MeasureRow collection (#1507).
*/
const MeasureCell: React.FC<MeasureCellProps> = props => {
const { columnKey, onColumnResize, title } = props;

const cellRef = React.useRef<HTMLTableCellElement>(null);

useLayoutEffect(() => {
if (cellRef.current) {
onColumnResize(columnKey, cellRef.current.offsetWidth);
}
}, []);
const { columnKey, title } = props;

return (
<ResizeObserver data={columnKey}>
<td
ref={cellRef}
style={{ paddingTop: 0, paddingBottom: 0, borderTop: 0, borderBottom: 0, height: 0 }}
>
<td style={{ paddingTop: 0, paddingBottom: 0, borderTop: 0, borderBottom: 0, height: 0 }}>
<div style={{ height: 0, overflow: 'hidden', fontWeight: 'bold' }}>{title || '\xa0'}</div>
</td>
</ResizeObserver>
Expand Down
44 changes: 34 additions & 10 deletions src/Body/MeasureRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import ResizeObserver from '@rc-component/resize-observer';
import MeasureCell from './MeasureCell';
import { isVisible } from '@rc-component/util';
import { isVisible, useEvent, useLayoutEffect } from '@rc-component/util';
import { useContext } from '@rc-component/context';
import TableContext from '../context/TableContext';
import type { ColumnType } from '../interface';
import MeasureCell from './MeasureCell';

export interface MeasureRowProps {
prefixCls: string;
Expand All @@ -23,6 +23,37 @@ const MeasureRow: React.FC<MeasureRowProps> = ({

const { measureRowRender } = useContext(TableContext, ['measureRowRender']);

// Read every cell width in one synchronous pass. The reads have no writes
// between them, so the row costs one layout instead of one per cell, and
// every onColumnResize call batches into a single state update (#1507).
// It covers mount and column-set changes; later width changes (a column
// gaining width while the table keeps its width, auto layout reacting to
// fonts or content) are reported by the per-cell observers below, which a
// row-level observer can not see.
const measureColumns = useEvent(() => {
const row = ref.current;
if (!row || !isVisible(row)) {
return;
}
const cells = row.children;
columnsKey.forEach((columnKey, index) => {
const cell = cells[index];
if (cell) {
onColumnResize(columnKey, (cell as HTMLTableCellElement).offsetWidth);
}
});
});

// Serialize with separators: ['a_b', 'c'] and ['a', 'b_c'] are different
// column sets and must re-measure even though they join to the same string.
const columnsKeyStr = JSON.stringify(columnsKey);

useLayoutEffect(() => {
measureColumns();
// measureColumns is a stable useEvent handle; only the column set re-runs it.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnsKeyStr]);

const measureRow = (
<tr aria-hidden="true" className={`${prefixCls}-measure-row`} style={{ height: 0 }} ref={ref}>
<ResizeObserver.Collection
Expand All @@ -40,14 +71,7 @@ const MeasureRow: React.FC<MeasureRowProps> = ({
const titleForMeasure = React.isValidElement<React.RefAttributes<any>>(rawTitle)
? React.cloneElement(rawTitle, { ref: null })
: rawTitle;
return (
<MeasureCell
key={columnKey}
columnKey={columnKey}
onColumnResize={onColumnResize}
title={titleForMeasure}
/>
);
return <MeasureCell key={columnKey} columnKey={columnKey} title={titleForMeasure} />;
})}
</ResizeObserver.Collection>
</tr>
Expand Down
98 changes: 98 additions & 0 deletions tests/FixedHeader.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,104 @@ describe('Table.FixedHeader', () => {
vi.useRealTimers();
});

it('re-measures when the column set changes', async () => {
const { container, rerender } = render(
<Table
columns={[{ dataIndex: 'light' }]}
data={[{ light: 'bamboo', key: 1 }]}
scroll={{ y: 10 }}
/>,
);

await act(async () => {
vi.runAllTimers();
await Promise.resolve();
});
const headerCols = () =>
[...container.querySelectorAll('.rc-table-header table col')].map(col => col.style.width);
expect(headerCols()[0]).toEqual('100px');

// A wider column set must be measured again without waiting for a resize.
rerender(
<Table
columns={[{ dataIndex: 'light' }, { dataIndex: 'bamboo' }, { dataIndex: 'extra' }]}
data={[{ light: 'bamboo', bamboo: 'light', extra: '!', key: 1 }]}
scroll={{ y: 10 }}
/>,
);

await act(async () => {
vi.runAllTimers();
await Promise.resolve();
});

const widths = headerCols();
expect(widths).toHaveLength(4); // 3 measured columns + the scroll gutter
expect(widths[0]).toEqual('100px');
expect(widths[1]).toEqual('100px');
expect(widths[2]).toEqual('100px');

vi.useRealTimers();
});

it('re-measures when new column keys collide in a joined identifier', async () => {
const { container, rerender } = render(
<Table columns={[{ key: 'a_b' }, { key: 'c' }]} data={[{ key: 1 }]} scroll={{ y: 10 }} />,
);

await act(async () => {
vi.runAllTimers();
await Promise.resolve();
});

// ['a_b', 'c'] -> ['a', 'b_c'] keep the same joined form 'a_b_c'.
rerender(
<Table columns={[{ key: 'a' }, { key: 'b_c' }]} data={[{ key: 1 }]} scroll={{ y: 10 }} />,
);

await act(async () => {
vi.runAllTimers();
await Promise.resolve();
});

const widths = [...container.querySelectorAll('.rc-table-header table col')].map(
col => col.style.width,
);
expect(widths[0]).toEqual('100px');
expect(widths[1]).toEqual('100px');

vi.useRealTimers();
});

it('reports a cell resize when the row keeps its size', async () => {
const { container } = render(
<Table
columns={[{ dataIndex: 'light' }, { dataIndex: 'bamboo' }]}
data={[{ light: 'bamboo', bamboo: 'light', key: 1 }]}
scroll={{ y: 10 }}
/>,
);

await act(async () => {
vi.runAllTimers();
await Promise.resolve();
});
const headerCols = () =>
[...container.querySelectorAll('.rc-table-header table col')].map(col => col.style.width);
expect(headerCols()[0]).toEqual('100px');

// Width moves between columns (fixed layout, constant table width):
// only cells change size, the measure row keeps its own size.
measureWidth = 80;
const cells = container.querySelectorAll('.rc-table-measure-row td');
await triggerResize(cells[0]);

expect(headerCols()[0]).toEqual('80px');
expect(headerCols()[1]).toEqual('100px');

vi.useRealTimers();
});

it('do not mask as fixed in nested table parent cell', async () => {
const columns = [
{
Expand Down