diff --git a/src/FixedHolder/index.tsx b/src/FixedHolder/index.tsx index fdcc1132f..da858895d 100644 --- a/src/FixedHolder/index.tsx +++ b/src/FixedHolder/index.tsx @@ -27,7 +27,6 @@ function useColumnWidth(colWidths: readonly number[], columCount: number) { export interface FixedHeaderProps extends HeaderProps { className: string; style?: React.CSSProperties; - noData: boolean; maxContentScroll: boolean; colWidths: readonly number[]; columCount: number; @@ -40,7 +39,6 @@ export interface FixedHeaderProps extends HeaderProps { tableLayout?: TableLayout; onScroll: (info: { currentTarget: HTMLDivElement; scrollLeft?: number }) => void; children: (info: HeaderProps) => React.ReactNode; - colGroup?: React.ReactNode; } const FixedHolder = React.forwardRef>((props, ref) => { @@ -51,11 +49,9 @@ const FixedHolder = React.forwardRef>((pro const { className, style, - noData, columns, flattenColumns, colWidths, - colGroup, columCount, stickyOffsets, direction, @@ -158,14 +154,16 @@ const FixedHolder = React.forwardRef>((pro }, [combinationScrollBarSize, stickyOffsets, isSticky]); const mergedColumnWidth = useColumnWidth(colWidths, columCount); - - const isColGroupEmpty = useMemo(() => { - // 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 (
>((pro width: scrollX, }} > - {isColGroupEmpty ? ( - colGroup - ) : ( - - )} + {children({ ...restProps, stickyOffsets: headerStickyOffsets, diff --git a/src/Table.tsx b/src/Table.tsx index 6fdbe1e35..1875dd79c 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -743,7 +743,6 @@ const Table = ( // Fixed holder share the props const fixedHolderProps = { - noData: !mergedData.length, maxContentScroll: horizonScroll && mergedScrollX === 'max-content', ...headerProps, ...columnContext, @@ -763,7 +762,6 @@ const Table = ( stickyTopOffset={offsetHeader} className={`${prefixCls}-header`} ref={scrollHeaderRef} - colGroup={bodyColGroup} > {renderFixedHeaderTable} @@ -779,7 +777,6 @@ const Table = ( stickyBottomOffset={offsetSummary} className={`${prefixCls}-summary`} ref={scrollSummaryRef} - colGroup={bodyColGroup} > {renderFixedFooterTable} diff --git a/tests/Scroll.spec.jsx b/tests/Scroll.spec.jsx index dfe96087c..eb6f9fd6a 100644 --- a/tests/Scroll.spec.jsx +++ b/tests/Scroll.spec.jsx @@ -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(); + + 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(); + } + }); + }); }); diff --git a/tests/__snapshots__/FixedColumn.spec.tsx.snap b/tests/__snapshots__/FixedColumn.spec.tsx.snap index f56c89857..96aad62e7 100644 --- a/tests/__snapshots__/FixedColumn.spec.tsx.snap +++ b/tests/__snapshots__/FixedColumn.spec.tsx.snap @@ -2698,22 +2698,43 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - without data 1`] = ` > - - - - - - - - - + + + + + + + + + + custom components > renders fixed column and header corre
+ + + + + +