From 61782466ca57d8200ab5c2d8c689e0383ed89db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 12 Sep 2023 15:02:00 +0800 Subject: [PATCH] fix: VirtualTable should force align column width (#1018) * fix: scroll width fit * test: add test case * fix: ts * adjust header ts --- docs/examples/virtual.tsx | 284 ++++++++++++----------- src/FixedHolder/index.tsx | 2 +- src/Table.tsx | 20 +- src/VirtualTable/BodyGrid.tsx | 8 +- src/VirtualTable/BodyLine.tsx | 18 +- src/VirtualTable/context.ts | 1 - src/VirtualTable/index.tsx | 5 +- src/context/TableContext.tsx | 3 + src/hooks/useColumns/index.tsx | 10 +- src/hooks/useColumns/useWidthColumns.tsx | 20 +- tests/Virtual.spec.tsx | 42 +++- 11 files changed, 228 insertions(+), 185 deletions(-) diff --git a/docs/examples/virtual.tsx b/docs/examples/virtual.tsx index a161d549e..8e3380ca1 100644 --- a/docs/examples/virtual.tsx +++ b/docs/examples/virtual.tsx @@ -1,7 +1,7 @@ import React from 'react'; import '../../assets/index.less'; -import type { ColumnsType } from '../../src/interface'; import { VirtualTable } from '../../src'; +import type { ColumnsType } from '../../src/interface'; interface RecordType { a: string; @@ -12,152 +12,154 @@ interface RecordType { } const columns: ColumnsType = [ - { title: 'title1', dataIndex: 'a', key: 'a', width: 100, fixed: 'left' }, - { title: 'title2', dataIndex: 'b', key: 'b', width: 100, fixed: 'left', ellipsis: true }, - { - title: 'title3', - dataIndex: 'c', - key: 'c', - onCell: (_, index) => { - if (index % 4 === 0) { - return { - rowSpan: 3, - }; - } + { title: 'title1', dataIndex: 'a', key: 'a', width: 100 }, + { title: 'title1', dataIndex: 'a', key: 'a', width: 800 }, + { title: 'title1', dataIndex: 'a', key: 'a', width: 100 }, + // { title: 'title2', dataIndex: 'b', key: 'b', width: 100, fixed: 'left', ellipsis: true }, + // { + // title: 'title3', + // dataIndex: 'c', + // key: 'c', + // onCell: (_, index) => { + // if (index % 4 === 0) { + // return { + // rowSpan: 3, + // }; + // } - if (index % 4 === 3) { - return { - rowSpan: 1, - colSpan: 3, - }; - } + // if (index % 4 === 3) { + // return { + // rowSpan: 1, + // colSpan: 3, + // }; + // } - return { - rowSpan: 0, - }; - }, - }, - { - title: 'title4', - key: 'd', - children: [ - // Children columns - { - title: 'title4-1', - dataIndex: 'b', - onCell: (_, index) => { - if (index % 4 === 0) { - return { - colSpan: 3, - }; - } + // return { + // rowSpan: 0, + // }; + // }, + // }, + // { + // title: 'title4', + // key: 'd', + // children: [ + // // Children columns + // { + // title: 'title4-1', + // dataIndex: 'b', + // onCell: (_, index) => { + // if (index % 4 === 0) { + // return { + // colSpan: 3, + // }; + // } - if (index % 4 === 3) { - return { - colSpan: 0, - }; - } - }, - }, - { - title: 'title4-2', - dataIndex: 'b', - onCell: (_, index) => { - if (index % 4 === 0 || index % 4 === 3) { - return { - colSpan: 0, - }; - } - }, - }, - ], - }, - { - title: 'title6', - dataIndex: 'b', - key: 'f', - onCell: (_, index) => { - if (index % 4 === 0) { - return { - rowSpan: 0, - colSpan: 0, - }; - } + // if (index % 4 === 3) { + // return { + // colSpan: 0, + // }; + // } + // }, + // }, + // { + // title: 'title4-2', + // dataIndex: 'b', + // onCell: (_, index) => { + // if (index % 4 === 0 || index % 4 === 3) { + // return { + // colSpan: 0, + // }; + // } + // }, + // }, + // ], + // }, + // { + // title: 'title6', + // dataIndex: 'b', + // key: 'f', + // onCell: (_, index) => { + // if (index % 4 === 0) { + // return { + // rowSpan: 0, + // colSpan: 0, + // }; + // } - if (index % 4 === 1) { - return { - rowSpan: 3, - }; - } + // if (index % 4 === 1) { + // return { + // rowSpan: 3, + // }; + // } - return { - rowSpan: 0, - }; - }, - }, - { - title: ( -
- title7 -
-
-
- Hello world! -
- ), - dataIndex: 'bk', - key: 'g', - }, - { - title: 'title8', - dataIndex: 'b', - onCell: (_, index) => { - if (index % 2 === 0) { - return { - rowSpan: 2, - colSpan: 2, - }; - } + // return { + // rowSpan: 0, + // }; + // }, + // }, + // { + // title: ( + //
+ // title7 + //
+ //
+ //
+ // Hello world! + //
+ // ), + // dataIndex: 'bk', + // key: 'g', + // }, + // { + // title: 'title8', + // dataIndex: 'b', + // onCell: (_, index) => { + // if (index % 2 === 0) { + // return { + // rowSpan: 2, + // colSpan: 2, + // }; + // } - return { - rowSpan: 0, - }; - }, - }, - { - title: 'title9 i', - dataIndex: 'b', - key: 'i', - onCell: () => ({ - colSpan: 0, - }), - }, - { title: 'title10', dataIndex: 'b', key: 'j' }, - { - title: 'title11', - dataIndex: 'b', - key: 'k', - width: 50, - fixed: 'right', - onCell: (_, index) => { - return { - rowSpan: index % 2 === 0 ? 2 : 0, - // colSpan: 2, - }; - }, - }, - { - title: 'title12', - dataIndex: 'b', - key: 'l', - width: 100, - fixed: 'right', - onCell: () => { - return { - // colSpan: 0, - }; - }, - }, + // return { + // rowSpan: 0, + // }; + // }, + // }, + // { + // title: 'title9 i', + // dataIndex: 'b', + // key: 'i', + // onCell: () => ({ + // colSpan: 0, + // }), + // }, + // { title: 'title10', dataIndex: 'b', key: 'j' }, + // { + // title: 'title11', + // dataIndex: 'b', + // key: 'k', + // width: 50, + // fixed: 'right', + // onCell: (_, index) => { + // return { + // rowSpan: index % 2 === 0 ? 2 : 0, + // // colSpan: 2, + // }; + // }, + // }, + // { + // title: 'title12', + // dataIndex: 'b', + // key: 'l', + // width: 100, + // fixed: 'right', + // onCell: () => { + // return { + // // colSpan: 0, + // }; + // }, + // }, ]; export function cleanOnCell(cols: any = []) { diff --git a/src/FixedHolder/index.tsx b/src/FixedHolder/index.tsx index 7d60c7ea0..b7e48ce7f 100644 --- a/src/FixedHolder/index.tsx +++ b/src/FixedHolder/index.tsx @@ -96,7 +96,7 @@ const FixedHolder = React.forwardRef>( // Check if all flattenColumns has width const allFlattenColumnsWithWidth = React.useMemo( - () => flattenColumns.every(column => column.width >= 0), + () => flattenColumns.every(column => column.width), [flattenColumns], ); diff --git a/src/Table.tsx b/src/Table.tsx index 74f64ed9b..ab0b3a451 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -264,9 +264,10 @@ function Table(tableProps: TableProps(tableProps: TableProps ({ @@ -308,7 +310,7 @@ function Table(tableProps: TableProps pureColWidths, [pureColWidths.join('_')]); const stickyOffsets = useStickyOffsets(colWidths, flattenColumns.length, direction); const fixHeader = scroll && validateValue(scroll.y); - const horizonScroll = (scroll && validateValue(scroll.x)) || Boolean(expandableConfig.fixed); + const horizonScroll = (scroll && validateValue(mergedScrollX)) || Boolean(expandableConfig.fixed); const fixColumn = horizonScroll && flattenColumns.some(({ fixed }) => fixed); // Sticky @@ -345,7 +347,7 @@ function Table(tableProps: TableProps(tableProps: TableProps ellipsis)) { return 'fixed'; @@ -615,7 +617,7 @@ function Table(tableProps: TableProps(tableProps: TableProps ({ + // Scroll + scrollX: mergedScrollX, + // Table prefixCls, getComponent, @@ -775,6 +780,9 @@ function Table(tableProps: TableProps { data: RecordType[]; @@ -29,6 +29,7 @@ const Grid = React.forwardRef((props, ref) => { prefixCls, childrenColumnName, emptyNode, + scrollX, } = useContext(TableContext, [ 'flattenColumns', 'onColumnResize', @@ -37,8 +38,9 @@ const Grid = React.forwardRef((props, ref) => { 'expandedKeys', 'childrenColumnName', 'emptyNode', + 'scrollX', ]); - const { scrollY, scrollX, listItemHeight } = useContext(StaticContext); + const { scrollY, listItemHeight } = useContext(StaticContext); // =========================== Ref ============================ const listRef = React.useRef(); @@ -201,7 +203,7 @@ const Grid = React.forwardRef((props, ref) => { itemHeight={listItemHeight || 24} data={flattenData} itemKey={item => getRowKey(item.record)} - scrollWidth={scrollX} + scrollWidth={scrollX as number} onVirtualScroll={({ x }) => { onScroll({ scrollLeft: x, diff --git a/src/VirtualTable/BodyLine.tsx b/src/VirtualTable/BodyLine.tsx index 5d793059c..7655294df 100644 --- a/src/VirtualTable/BodyLine.tsx +++ b/src/VirtualTable/BodyLine.tsx @@ -1,12 +1,11 @@ import { useContext } from '@rc-component/context'; import classNames from 'classnames'; import * as React from 'react'; +import Cell from '../Cell'; import TableContext, { responseImmutable } from '../context/TableContext'; import type { FlattenData } from '../hooks/useFlattenRecords'; -import { StaticContext } from './context'; -import VirtualCell from './VirtualCell'; import useRowInfo from '../hooks/useRowInfo'; -import Cell from '../Cell'; +import VirtualCell from './VirtualCell'; export interface BodyLineProps { data: FlattenData; @@ -24,13 +23,10 @@ const BodyLine = React.forwardRef((props, ref) => const { data, index, className, rowKey, style, extra, getHeight, ...restProps } = props; const { record, indent } = data; - const { flattenColumns, prefixCls, fixColumn, componentWidth } = useContext(TableContext, [ - 'prefixCls', - 'flattenColumns', - 'fixColumn', - 'componentWidth', - ]); - const { scrollX } = useContext(StaticContext, ['scrollX']); + const { scrollX, flattenColumns, prefixCls, fixColumn, componentWidth } = useContext( + TableContext, + ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX'], + ); const rowInfo = useRowInfo(record, rowKey, index, indent); @@ -79,7 +75,7 @@ const BodyLine = React.forwardRef((props, ref) => const rowStyle: React.CSSProperties = { ...style, - width: scrollX, + width: scrollX as number, }; if (extra) { diff --git a/src/VirtualTable/context.ts b/src/VirtualTable/context.ts index d22fd0a98..5e6786737 100644 --- a/src/VirtualTable/context.ts +++ b/src/VirtualTable/context.ts @@ -1,7 +1,6 @@ import { createContext } from '@rc-component/context'; export interface StaticContextProps { - scrollX: number; scrollY: number; listItemHeight: number; } diff --git a/src/VirtualTable/index.tsx b/src/VirtualTable/index.tsx index 73064b5b8..fcb2102a8 100644 --- a/src/VirtualTable/index.tsx +++ b/src/VirtualTable/index.tsx @@ -49,10 +49,7 @@ function VirtualTable(props: VirtualTableProps) { } // ========================= Context ========================== - const context = React.useMemo( - () => ({ scrollX, scrollY, listItemHeight }), - [scrollX, scrollY, listItemHeight], - ); + const context = React.useMemo(() => ({ scrollY, listItemHeight }), [scrollY, listItemHeight]); // ========================== Render ========================== return ( diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index 625531ff9..ee5eeae2a 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -19,6 +19,9 @@ const { makeImmutable, responseImmutable, useImmutableMark } = createImmutable() export { makeImmutable, responseImmutable, useImmutableMark }; export interface TableContextProps { + // Scroll + scrollX: number | string | true; + // Table prefixCls: string; getComponent: GetComponent; diff --git a/src/hooks/useColumns/index.tsx b/src/hooks/useColumns/index.tsx index 174486e1a..baa026f32 100644 --- a/src/hooks/useColumns/index.tsx +++ b/src/hooks/useColumns/index.tsx @@ -151,7 +151,11 @@ function useColumns( scrollWidth?: number; }, transformColumns: (columns: ColumnsType) => ColumnsType, -): [ColumnsType, readonly ColumnType[]] { +): [ + columns: ColumnsType, + flattenColumns: readonly ColumnType[], + realScrollWidth: undefined | number, +] { const baseColumns = React.useMemo>( () => columns || convertChildrenToColumns(children), [columns, children], @@ -274,9 +278,9 @@ function useColumns( } // ========================= FillWidth ======================== - const filledColumns = useWidthColumns(flattenColumns, scrollWidth); + const [filledColumns, realScrollWidth] = useWidthColumns(flattenColumns, scrollWidth); - return [mergedColumns, filledColumns]; + return [mergedColumns, filledColumns, realScrollWidth]; } export default useColumns; diff --git a/src/hooks/useColumns/useWidthColumns.tsx b/src/hooks/useColumns/useWidthColumns.tsx index a85863643..34bcb6afe 100644 --- a/src/hooks/useColumns/useWidthColumns.tsx +++ b/src/hooks/useColumns/useWidthColumns.tsx @@ -15,15 +15,15 @@ function parseColWidth(totalWidth: number, width: string | number = '') { /** * Fill all column with width */ -export default function useWidthColumns(columns: ColumnsType, scrollWidth: number) { - const filledColumns = React.useMemo(() => { +export default function useWidthColumns(flattenColumns: ColumnsType, scrollWidth: number) { + return React.useMemo<[columns: ColumnsType, realScrollWidth: number]>(() => { // Fill width if needed if (scrollWidth && scrollWidth > 0) { let totalWidth = 0; let missWidthCount = 0; // collect not given width column - columns.forEach((col: any) => { + flattenColumns.forEach((col: any) => { const colWidth = parseColWidth(scrollWidth, col.width); if (colWidth) { @@ -38,7 +38,9 @@ export default function useWidthColumns(columns: ColumnsType, scrollWidth: let restCount = missWidthCount; const avgWidth = restWidth / missWidthCount; - return columns.map((col: any) => { + let realTotal = 0; + + const filledColumns = flattenColumns.map((col: any) => { const clone = { ...col, }; @@ -56,12 +58,14 @@ export default function useWidthColumns(columns: ColumnsType, scrollWidth: restCount -= 1; } + realTotal += clone.width; + return clone; }); - } - return columns; - }, [columns, scrollWidth]); + return [filledColumns, realTotal]; + } - return filledColumns; + return [flattenColumns, scrollWidth]; + }, [flattenColumns, scrollWidth]); } diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index fedd372ed..d54bf79be 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -1,10 +1,22 @@ -import { resetWarned } from 'rc-util/lib/warning'; -import React from 'react'; -import { type VirtualTableProps, VirtualTable } from '../src'; import { act, fireEvent, render } from '@testing-library/react'; -import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil'; import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import { resetWarned } from 'rc-util/lib/warning'; +import React from 'react'; +import { VirtualTable, type VirtualTableProps } from '../src'; + +vi.mock('rc-virtual-list', async () => { + const RealVirtualList = ((await vi.importActual('rc-virtual-list')) as any).default; + + const WrapperVirtualList = React.forwardRef((props: any, ref) => ( + + )); + + return { + default: WrapperVirtualList, + }; +}); describe('Table.Virtual', () => { let scrollLeftCalled = false; @@ -170,14 +182,30 @@ describe('Table.Virtual', () => { scrollLeftCalled = false; expect(scrollLeftCalled).toBeFalsy(); - console.log('!!!!!'); - // fireEvent.scroll(container.querySelector('.rc-table-header')!); fireEvent.wheel(container.querySelector('.rc-virtual-list-holder')!, { deltaX: 10, }); expect(scrollLeftCalled).toBeTruthy(); + }); + + it('should follow correct width', () => { + const { container } = getTable({ + columns: [ + { + width: 93, + }, + { + width: 510, + }, + ], + scroll: { + x: 1128, + y: 10, + }, + data: [{}], + }); - console.log(container.innerHTML); + expect(container.querySelector('.rc-virtual-list')).toHaveAttribute('data-scroll-width', '603'); }); });