diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 37b34327e..68ee383eb 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -8,6 +8,8 @@ import type { ColumnType, CustomizeComponent } from '../interface'; import ExpandedRow from './ExpandedRow'; import { computedExpandedClassName } from '../utils/expandUtil'; import type { TableProps } from '..'; +import useStickyOffsets from '../hooks/useStickyOffsets'; +import { getCellFixedInfo } from '../utils/fixUtil'; export interface BodyRowProps { record: RecordType; @@ -43,12 +45,16 @@ export function getCellProps( index: number, rowKeys: React.Key[] = [], expandedRowOffset = 0, + rowStickyOffsets?: ReturnType, + hasColSpanZero?: boolean, + cachedCellProps?: Record, ) { const { record, prefixCls, columnsKey, fixedInfoList, + flattenColumns, expandIconColumnIndex, nestExpandable, indentSize, @@ -61,9 +67,11 @@ export function getCellProps( } = rowInfo; const key = columnsKey[colIndex]; - const fixedInfo = fixedInfoList[colIndex]; + let fixedInfo = fixedInfoList[colIndex]; - // ============= Used for nest expandable ============= + if (column.fixed && hasColSpanZero && rowStickyOffsets) { + fixedInfo = getCellFixedInfo(colIndex, colIndex, flattenColumns, rowStickyOffsets); + } let appendCellNode: React.ReactNode; if (colIndex === (expandIconColumnIndex || 0) && nestExpandable) { appendCellNode = ( @@ -83,7 +91,7 @@ export function getCellProps( ); } - const additionalCellProps = column.onCell?.(record, index) || {}; + const additionalCellProps = { ...(cachedCellProps || column.onCell?.(record, index) || {}) }; // Expandable row has offset if (expandedRowOffset) { @@ -143,6 +151,7 @@ function BodyRow( const { prefixCls, flattenColumns, + colWidths, expandedRowClassName, expandedRowRender, rowProps, @@ -152,6 +161,20 @@ function BodyRow( rowSupportExpand, } = rowInfo; + const cellPropsCache = React.useMemo(() => { + return flattenColumns.map(col => col.onCell?.(record, index) || {}); + }, [flattenColumns, record, index]); + + const hasColSpanZero = React.useMemo(() => { + return cellPropsCache.some(cellProps => (cellProps.colSpan ?? 1) === 0); + }, [cellPropsCache]); + + const rowStickyOffsets = useStickyOffsets( + colWidths, + flattenColumns, + hasColSpanZero ? { record, rowIndex: index } : undefined, + ); + // Force render expand row if expanded before const expandedRef = React.useRef(false); expandedRef.current ||= expanded; @@ -196,6 +219,9 @@ function BodyRow( index, rowKeys, expandedRowInfo?.offset, + rowStickyOffsets, + hasColSpanZero, + cellPropsCache[colIndex], ); return ( diff --git a/src/hooks/useRowInfo.tsx b/src/hooks/useRowInfo.tsx index 4e3e11fa8..f0d3f8669 100644 --- a/src/hooks/useRowInfo.tsx +++ b/src/hooks/useRowInfo.tsx @@ -15,6 +15,7 @@ export default function useRowInfo( | 'prefixCls' | 'fixedInfoList' | 'flattenColumns' + | 'colWidths' | 'expandableType' | 'expandRowByClick' | 'onTriggerExpand' @@ -41,6 +42,7 @@ export default function useRowInfo( 'prefixCls', 'fixedInfoList', 'flattenColumns', + 'colWidths', 'expandableType', 'expandRowByClick', 'onTriggerExpand', diff --git a/src/hooks/useStickyOffsets.ts b/src/hooks/useStickyOffsets.ts index 705c2e118..8cc045af3 100644 --- a/src/hooks/useStickyOffsets.ts +++ b/src/hooks/useStickyOffsets.ts @@ -3,10 +3,14 @@ import type { ColumnType, StickyOffsets } from '../interface'; /** * Get sticky column offset width + * @param colWidths - Column widths array + * @param flattenColumns - Flattened columns + * @param rowContext - Optional row context for dynamic colSpan calculation */ function useStickyOffsets( colWidths: number[], flattenColumns: readonly ColumnType[], + rowContext?: { record: RecordType; rowIndex: number }, ) { const stickyOffsets: StickyOffsets = useMemo(() => { const columnCount = flattenColumns.length; @@ -16,9 +20,17 @@ function useStickyOffsets( let total = 0; for (let i = startIndex; i !== endIndex; i += offset) { + const column = flattenColumns[i]; + offsets.push(total); - if (flattenColumns[i].fixed) { + let colSpan = 1; + if (rowContext) { + const cellProps = column.onCell?.(rowContext.record, rowContext.rowIndex) || {}; + colSpan = cellProps.colSpan ?? 1; + } + + if (column.fixed && colSpan !== 0) { total += colWidths[i] || 0; } } @@ -34,7 +46,7 @@ function useStickyOffsets( end: endOffsets, widths: colWidths, }; - }, [colWidths, flattenColumns]); + }, [colWidths, flattenColumns, rowContext]); return stickyOffsets; } diff --git a/tests/FixedColumn.spec.tsx b/tests/FixedColumn.spec.tsx index 3e7a5b493..c0d730d64 100644 --- a/tests/FixedColumn.spec.tsx +++ b/tests/FixedColumn.spec.tsx @@ -336,4 +336,139 @@ describe('Table.FixedColumn', () => { expect(container.querySelector('.rc-table')).toHaveClass('rc-table-fix-start-shadow-show'); expect(container.querySelector('.rc-table')).toHaveClass('rc-table-fix-end-shadow-show'); }); + + describe('colSpan=0 with fixed columns regression test', () => { + interface TestDataType { + key: string; + col0: string; + col1: string; + col2: string; + } + + const testColumns: ColumnsType = [ + { + title: 'Column 0', + dataIndex: 'col0', + key: 'col0', + width: 100, + fixed: 'left', + onCell: (record, index) => { + if (index === 1) { + return { colSpan: 0 }; + } + return {}; + }, + }, + { + title: 'Column 1', + dataIndex: 'col1', + key: 'col1', + width: 120, + fixed: 'left', + onCell: (record, index) => { + if (index === 1) { + return { colSpan: 2 }; + } + return {}; + }, + }, + { + title: 'Column 2', + dataIndex: 'col2', + key: 'col2', + width: 150, + }, + ]; + + const testData: TestDataType[] = [ + { key: '0', col0: 'Row0-Col0', col1: 'Row0-Col1', col2: 'Row0-Col2' }, + { key: '1', col0: 'Row1-Col0', col1: 'Row1-Merged', col2: 'Row1-Col2' }, + { key: '2', col0: 'Row2-Col0', col1: 'Row2-Col1', col2: 'Row2-Col2' }, + ]; + + it('should calculate correct sticky offsets when colSpan=0 exists', async () => { + const { container } = render( + , + ); + + await triggerResize(container.querySelector('.rc-table')); + + act(() => { + const coll = container.querySelector('.rc-table-resize-collection'); + if (coll) { + triggerResize(coll as HTMLElement); + } + }); + + await act(async () => { + vi.runAllTimers(); + await Promise.resolve(); + }); + + const rows = container.querySelectorAll('.rc-table-tbody .rc-table-row'); + expect(rows).toHaveLength(3); + + const secondRow = rows[1]; + const cells = secondRow.querySelectorAll('.rc-table-cell'); + expect(cells).toHaveLength(2); + + const mergedCell = cells[0]; + expect(mergedCell).toHaveAttribute('colSpan', '2'); + + expect(mergedCell.textContent).toContain('Row1-Merged'); + const hasFixedLeftClass = mergedCell.classList.contains('rc-table-cell-fix-left'); + + if (hasFixedLeftClass) { + const cellStyle = window.getComputedStyle(mergedCell); + expect(cellStyle.left).toBe('0px'); + } + }); + + it('should work correctly with expandable rows', async () => { + const expandableTestData = testData.map(item => ({ + ...item, + children: + item.key === '1' + ? [{ key: '1-0', col0: 'Child-Col0', col1: 'Child-Col1', col2: 'Child-Col2' }] + : undefined, + })); + + const { container } = render( +
, + ); + + await triggerResize(container.querySelector('.rc-table')); + + act(() => { + const coll = container.querySelector('.rc-table-resize-collection'); + if (coll) { + triggerResize(coll as HTMLElement); + } + }); + + await act(async () => { + vi.runAllTimers(); + await Promise.resolve(); + }); + + const allRows = container.querySelectorAll('.rc-table-tbody .rc-table-row'); + expect(allRows.length).toBeGreaterThan(3); // 包含展开的子行 + + const parentRow = allRows[1]; + const parentCells = parentRow.querySelectorAll('.rc-table-cell'); + expect(parentCells).toHaveLength(2); + + const childRow = allRows[2]; + const childCells = childRow.querySelectorAll('.rc-table-cell'); + expect(childCells).toHaveLength(3); + }); + }); });