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
39 changes: 18 additions & 21 deletions src/FixedHolder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function useColumnWidth(colWidths: readonly number[], columCount: number) {
export interface FixedHeaderProps<RecordType> extends HeaderProps<RecordType> {
className: string;
style?: React.CSSProperties;
noData: boolean;
maxContentScroll: boolean;
colWidths: readonly number[];
columCount: number;
Expand All @@ -40,7 +39,6 @@ export interface FixedHeaderProps<RecordType> extends HeaderProps<RecordType> {
tableLayout?: TableLayout;
onScroll: (info: { currentTarget: HTMLDivElement; scrollLeft?: number }) => void;
children: (info: HeaderProps<RecordType>) => React.ReactNode;
colGroup?: React.ReactNode;
}

const FixedHolder = React.forwardRef<HTMLDivElement, FixedHeaderProps<any>>((props, ref) => {
Expand All @@ -51,11 +49,9 @@ const FixedHolder = React.forwardRef<HTMLDivElement, FixedHeaderProps<any>>((pro
const {
className,
style,
noData,
columns,
flattenColumns,
colWidths,
colGroup,
columCount,
stickyOffsets,
direction,
Expand Down Expand Up @@ -158,14 +154,16 @@ const FixedHolder = React.forwardRef<HTMLDivElement, FixedHeaderProps<any>>((pro
}, [combinationScrollBarSize, stickyOffsets, isSticky]);

const mergedColumnWidth = useColumnWidth(colWidths, columCount);

const isColGroupEmpty = useMemo<boolean>(() => {
// use original ColGroup if no data or no calculated column width, otherwise use calculated column width
// Return original colGroup if no data, or mergedColumnWidth is empty, or all widths are falsy
const noWidth =
!mergedColumnWidth || !mergedColumnWidth.length || mergedColumnWidth.every(w => !w);
return noData || noWidth;
}, [noData, mergedColumnWidth]);
const hasMergedColumnWidth = !!mergedColumnWidth && mergedColumnWidth.some(width => width);

// Use the declared column width when the measured one is unavailable
// (e.g. there is no data to measure). Both cases always reserve the width
// of the trailing scrollbar column, so the extra header cell keeps a stable
// size and the table does not jump when data arrives.
const fallbackColWidths = React.useMemo(
() => flattenColumns.map(({ width }) => width),
[flattenColumns],
);

return (
<div
Expand All @@ -187,15 +185,14 @@ const FixedHolder = React.forwardRef<HTMLDivElement, FixedHeaderProps<any>>((pro
width: scrollX,
}}
>
{isColGroupEmpty ? (
colGroup
) : (
<ColGroup
colWidths={[...mergedColumnWidth, combinationScrollBarSize]}
columCount={columCount + 1}
columns={flattenColumnsWithScrollbar}
/>
)}
<ColGroup
colWidths={[
...(hasMergedColumnWidth ? mergedColumnWidth : fallbackColWidths),
combinationScrollBarSize,
]}
columCount={columCount + 1}
columns={flattenColumnsWithScrollbar}
/>
{children({
...restProps,
stickyOffsets: headerStickyOffsets,
Expand Down
3 changes: 0 additions & 3 deletions src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,6 @@ const Table = <RecordType extends DefaultRecordType>(

// Fixed holder share the props
const fixedHolderProps = {
noData: !mergedData.length,
maxContentScroll: horizonScroll && mergedScrollX === 'max-content',
...headerProps,
...columnContext,
Expand All @@ -763,7 +762,6 @@ const Table = <RecordType extends DefaultRecordType>(
stickyTopOffset={offsetHeader}
className={`${prefixCls}-header`}
ref={scrollHeaderRef}
colGroup={bodyColGroup}
>
{renderFixedHeaderTable}
</FixedHolder>
Expand All @@ -779,7 +777,6 @@ const Table = <RecordType extends DefaultRecordType>(
stickyBottomOffset={offsetSummary}
className={`${prefixCls}-summary`}
ref={scrollSummaryRef}
colGroup={bodyColGroup}
>
{renderFixedFooterTable}
</FixedHolder>
Expand Down
62 changes: 62 additions & 0 deletions tests/Scroll.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,66 @@ describe('Table.Scroll', () => {
});
expect(isTriggerScroll).toEqual(true);
});

describe('scrollbar placeholder colgroup', () => {
const scrollColumns = [
{ title: 'A', dataIndex: 'a', key: 'a', width: 100 },
{ title: 'B', dataIndex: 'b', key: 'b', width: 200 },
];

const renderScrollTable = data =>
render(<Table columns={scrollColumns} data={data} scroll={{ y: 200 }} tableLayout="fixed" />);

const serializeHeaderCols = container =>
[...container.querySelectorAll('.rc-table-header col')].map(col => col.getAttribute('style'));

it('keep scrollbar column width in header colgroup when data is empty', () => {
const { container } = renderScrollTable([]);

const headerTable = container.querySelector('.rc-table-header table');
const cols = headerTable.querySelectorAll('col');

// Real columns + a trailing scrollbar column
expect(cols).toHaveLength(scrollColumns.length + 1);
expect(cols[scrollColumns.length]).toHaveStyle({ width: '15px' });
expect(headerTable.querySelectorAll('th.rc-table-cell-scrollbar')).toHaveLength(1);
});

it('use measured widths and keep header colgroup stable between empty and filled data', () => {
// jsdom does not perform layout, so offsetWidth is always 0 and the measured
// width branch (hasMergedColumnWidth) is never exercised. Give the measure-row
// cells real widths to simulate the browser measurement. The values (120/240)
// intentionally differ from the declared widths (100/200) so the test proves
// the measured widths are used instead of the declared fallback.
const domSpy = spyElementPrototypes(HTMLTableCellElement, {
offsetWidth: {
get(originDescriptor) {
if (this.parentElement?.classList.contains('rc-table-measure-row')) {
return [120, 240][this.cellIndex] ?? 0;
}
return originDescriptor.get();
},
},
});

try {
const emptyRender = renderScrollTable([]);
const filledRender = renderScrollTable([{ key: 1, a: 'x', b: 'y' }]);

// Measured widths + the trailing scrollbar column are reserved in both cases.
// Loaded data should not change the header colgroup, otherwise the columns
// visually jump when the table gets its first rows.
expect(serializeHeaderCols(emptyRender.container)).toEqual([
'width: 120px;',
'width: 240px;',
'width: 15px;',
]);
expect(serializeHeaderCols(filledRender.container)).toEqual(
serializeHeaderCols(emptyRender.container),
);
} finally {
domSpy.mockRestore();
}
});
});
});
45 changes: 33 additions & 12 deletions tests/__snapshots__/FixedColumn.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2698,22 +2698,43 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - without data 1`] = `
>
<colgroup>
<col
style="width: 100px;"
style="width: 1000px;"
/>
<col
style="width: 100px;"
style="width: 1000px;"
/>
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col
style="width: 100px;"
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 1000px;"
/>
<col
style="width: 15px;"
/>
</colgroup>
<thead
Expand Down
8 changes: 8 additions & 0 deletions tests/__snapshots__/Table.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ exports[`Table.Basic > custom components > renders fixed column and header corre
<table
style="table-layout: fixed; min-width: 100%; width: 100px;"
>
<colgroup>
<col />
<col />
<col />
<col
style="width: 15px;"
/>
</colgroup>
<thead
class="rc-table-thead"
name="my-header-wrapper"
Expand Down