diff --git a/docs/examples/virtual.tsx b/docs/examples/virtual.tsx index bcb1a59f..8d5ebf09 100644 --- a/docs/examples/virtual.tsx +++ b/docs/examples/virtual.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import '../../assets/index.less'; import { VirtualTable } from '../../src'; import type { ColumnsType, Reference } from '../../src/interface'; @@ -11,7 +11,7 @@ interface RecordType { indexKey: string; } -const columns: ColumnsType = [ +const defaultColumns: ColumnsType = [ // { title: 'title1', dataIndex: 'a', key: 'a', width: 100,}, // { title: 'title1', dataIndex: 'a', key: 'a', width: 100, }, { title: 'title1', dataIndex: 'a', key: 'a', width: 100, fixed: 'left' }, @@ -162,15 +162,6 @@ const columns: ColumnsType = [ }, ]; -export function cleanOnCell(cols: any = []) { - cols.forEach(col => { - delete (col as any).onCell; - - cleanOnCell((col as any).children); - }); -} -cleanOnCell(columns); - const data: RecordType[] = new Array(4 * 10000).fill(null).map((_, index) => ({ a: `a${index}`, b: `b${index}`, @@ -190,9 +181,21 @@ const data: RecordType[] = new Array(4 * 10000).fill(null).map((_, index) => ({ const Demo = () => { const tblRef = React.useRef(); + const [enableColRowSpan, setEnableColRowSpan] = useState(false); + + function cleanOnCell(cols: ColumnsType) { + return cols?.map(({ onCell, ...col }: any) => { + return { ...col, children: cleanOnCell(col.children) }; + }); + } + + const columns = enableColRowSpan ? defaultColumns : cleanOnCell(defaultColumns); return (
+ b || c} scroll={{ x: 1300, y: 200 }} diff --git a/package.json b/package.json index 5a9a17a5..77de437c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", "rc-util": "^5.37.0", - "rc-virtual-list": "^3.11.1" + "rc-virtual-list": "^3.13.0" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.2", diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index dc9d3cd6..973e8579 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -109,7 +109,7 @@ const Grid = React.forwardRef((props, ref) => { }; const extraRender: ListProps['extraRender'] = info => { - const { start, end, getSize, offsetY } = info; + const { start, end, getSize, offsetX, offsetY } = info; // Do nothing if no data if (end < 0) { @@ -189,6 +189,7 @@ const Grid = React.forwardRef((props, ref) => { style={{ top: -offsetY + sizeInfo.top, }} + offsetX={offsetX} extra getHeight={getHeight} /> diff --git a/src/VirtualTable/BodyLine.tsx b/src/VirtualTable/BodyLine.tsx index 4be63445..1a8aa457 100644 --- a/src/VirtualTable/BodyLine.tsx +++ b/src/VirtualTable/BodyLine.tsx @@ -7,6 +7,8 @@ import type { FlattenData } from '../hooks/useFlattenRecords'; import useRowInfo from '../hooks/useRowInfo'; import VirtualCell from './VirtualCell'; import { StaticContext } from './context'; +import { getCellProps } from '../Body/BodyRow'; +import VirtualRow from './VirtualRow'; export interface BodyLineProps { data: FlattenData; @@ -14,6 +16,7 @@ export interface BodyLineProps { className?: string; style?: React.CSSProperties; rowKey: React.Key; + offsetX: number; /** Render cell only when it has `rowSpan > 1` */ extra?: boolean; @@ -21,17 +24,24 @@ export interface BodyLineProps { } const BodyLine = React.forwardRef((props, ref) => { - const { data, index, className, rowKey, style, extra, getHeight, ...restProps } = props; + const { data, index, className, rowKey, style, extra, getHeight, offsetX, ...restProps } = props; const { record, indent, index: renderIndex } = data; const { scrollX, flattenColumns, prefixCls, fixColumn, componentWidth } = useContext( TableContext, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX'], ); - const { getComponent } = useContext(StaticContext, ['getComponent']); + const { getComponent, horizontalVirtual } = useContext(StaticContext, [ + 'getComponent', + 'horizontalVirtual', + ]); const rowInfo = useRowInfo(record, rowKey, index, indent); + const cellPropsCollections = flattenColumns.map((column, colIndex) => + getCellProps(rowInfo, column, colIndex, indent, index), + ); + const RowComponent = getComponent(['body', 'row'], 'div'); const cellComponent = getComponent(['body', 'cell'], 'div'); @@ -87,6 +97,16 @@ const BodyLine = React.forwardRef((props, ref) => rowStyle.pointerEvents = 'none'; } + const shareCellProps = { + index, + renderIndex, + inverse: extra, + record, + rowInfo, + component: cellComponent, + getHeight, + }; + const rowNode = ( ((props, ref) => })} style={{ ...rowStyle, ...rowProps?.style }} > - {flattenColumns.map((column, colIndex) => { - return ( + {horizontalVirtual ? ( + + ) : ( + flattenColumns.map((column, colIndex) => ( - ); - })} + )) + )} ); diff --git a/src/VirtualTable/VirtualCell.tsx b/src/VirtualTable/VirtualCell.tsx index 9b1b3ebe..91452176 100644 --- a/src/VirtualTable/VirtualCell.tsx +++ b/src/VirtualTable/VirtualCell.tsx @@ -1,7 +1,7 @@ import { useContext } from '@rc-component/context'; import classNames from 'classnames'; import * as React from 'react'; -import { getCellProps } from '../Body/BodyRow'; +import type { getCellProps } from '../Body/BodyRow'; import Cell from '../Cell'; import type useRowInfo from '../hooks/useRowInfo'; import type { ColumnType, CustomizeComponent } from '../interface'; @@ -11,12 +11,12 @@ export interface VirtualCellProps { rowInfo: ReturnType>; column: ColumnType; colIndex: number; - indent: number; index: number; component?: CustomizeComponent; /** Used for `column.render` */ renderIndex: number; record: RecordType; + cellProps: ReturnType; // Follow props is used for RowSpanVirtualCell only style?: React.CSSProperties; @@ -41,7 +41,6 @@ function VirtualCell(props: VirtualCellProps) { rowInfo, column, colIndex, - indent, index, component, renderIndex, @@ -49,6 +48,7 @@ function VirtualCell(props: VirtualCellProps) { style, className, inverse, + cellProps, getHeight, } = props; @@ -56,13 +56,7 @@ function VirtualCell(props: VirtualCellProps) { const { columnsOffset } = useContext(GridContext, ['columnsOffset']); - const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps( - rowInfo, - column, - colIndex, - indent, - index, - ); + const { key, fixedInfo, appendCellNode, additionalCellProps } = cellProps; const { style: cellStyle, colSpan = 1, rowSpan = 1 } = additionalCellProps; diff --git a/src/VirtualTable/VirtualRow.tsx b/src/VirtualTable/VirtualRow.tsx new file mode 100644 index 00000000..b6a4b834 --- /dev/null +++ b/src/VirtualTable/VirtualRow.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { useContext } from '@rc-component/context'; +import TableContext from '../context/TableContext'; +import useHorizontalVirtual from './useHorizontalVirtual'; +import type { getCellProps } from '../Body/BodyRow'; +import VirtualCell from './VirtualCell'; +import type useRowInfo from '../hooks/useRowInfo'; +import type { CustomizeComponent } from '../interface'; + +export interface VirtualRowProps { + cellPropsCollections: ReturnType[]; + offsetX: number; + index: number; + renderIndex: number; + inverse: boolean; + record: any; + rowInfo: ReturnType; + component: CustomizeComponent; + getHeight: (rowSpan: number) => number; +} + +export default function VirtualRow({ + cellPropsCollections, + offsetX, + ...restProps +}: VirtualRowProps) { + const { flattenColumns } = useContext(TableContext, ['flattenColumns']); + + const [startIndex, virtualOffset, showColumnIndexes] = useHorizontalVirtual( + cellPropsCollections, + offsetX, + ); + + return showColumnIndexes.map(colIndex => ( + + )); +} diff --git a/src/VirtualTable/context.ts b/src/VirtualTable/context.ts index d2f7c1f5..8790ad6a 100644 --- a/src/VirtualTable/context.ts +++ b/src/VirtualTable/context.ts @@ -5,6 +5,7 @@ export interface StaticContextProps { scrollY: number; listItemHeight: number; sticky: boolean | TableSticky; + horizontalVirtual: boolean; getComponent: GetComponent; onScroll?: React.UIEventHandler; } diff --git a/src/VirtualTable/index.tsx b/src/VirtualTable/index.tsx index b14fc6aa..1e19a84d 100644 --- a/src/VirtualTable/index.tsx +++ b/src/VirtualTable/index.tsx @@ -21,6 +21,7 @@ export interface VirtualTableProps extends Omit(props: VirtualTableProps, ref: Rea className, listItemHeight, components, + virtual, onScroll, } = props; @@ -63,10 +65,19 @@ function VirtualTable(props: VirtualTableProps, ref: Rea // Memo this const onInternalScroll = useEvent(onScroll); + const horizontalVirtual = !!virtual?.x; + // ========================= Context ========================== const context = React.useMemo( - () => ({ sticky, scrollY, listItemHeight, getComponent, onScroll: onInternalScroll }), - [sticky, scrollY, listItemHeight, getComponent, onInternalScroll], + () => ({ + sticky, + scrollY, + listItemHeight, + horizontalVirtual, + getComponent, + onScroll: onInternalScroll, + }), + [sticky, scrollY, listItemHeight, horizontalVirtual, getComponent, onInternalScroll], ); // ========================== Render ========================== @@ -93,7 +104,7 @@ function VirtualTable(props: VirtualTableProps, ref: Rea } export type ForwardGenericVirtualTable = (( - props: TableProps & React.RefAttributes, + props: VirtualTableProps & React.RefAttributes, ) => React.ReactElement) & { displayName?: string }; const RefVirtualTable = React.forwardRef(VirtualTable) as ForwardGenericVirtualTable; diff --git a/src/VirtualTable/useHorizontalVirtual.ts b/src/VirtualTable/useHorizontalVirtual.ts new file mode 100644 index 00000000..9c7629a7 --- /dev/null +++ b/src/VirtualTable/useHorizontalVirtual.ts @@ -0,0 +1,100 @@ +import type { getCellProps } from '../Body/BodyRow'; +import TableContext from '../context/TableContext'; +import { useContext } from '@rc-component/context'; +import { useMemo } from 'react'; + +function generateRange(start: number, end: number) { + return Array.from({ length: end - start + 1 }, (_, index) => index + start); +} + +export default function useHorizontalVirtual( + cellPropsCollections: ReturnType[], + offsetX: number, +) { + const { flattenColumns, componentWidth } = useContext(TableContext, [ + 'flattenColumns', + 'componentWidth', + ]); + + const pureCellColSpanCollections = cellPropsCollections.map( + cell => cell.additionalCellProps.colSpan ?? 1, + ); + const cellColSpanCollections = useMemo( + () => pureCellColSpanCollections, + [pureCellColSpanCollections.join('_')], + ); + + const startIndex = useMemo(() => { + let virtualStartIndex = flattenColumns.findIndex(col => col.fixed !== 'left'); + let tempOffsetX = offsetX; + for (let i = virtualStartIndex; i < flattenColumns.length; i++) { + const colSpan = cellColSpanCollections[i]; + const width = flattenColumns + .slice(i, i + colSpan) + .reduce((total, col) => total + (col.width as number), 0); + tempOffsetX = tempOffsetX - width; + virtualStartIndex = i; + if (tempOffsetX < 0) { + break; + } + } + return virtualStartIndex; + }, [flattenColumns, offsetX, cellColSpanCollections]); + + const virtualOffset = useMemo( + () => + flattenColumns + .slice(0, startIndex) + .reduce( + (total, cur, colIndex) => + flattenColumns[colIndex].fixed === 'left' ? total : total + (cur.width as number), + 0, + ), + [flattenColumns, startIndex], + ); + + const endIndex = useMemo(() => { + let virtualEndIndex = startIndex; + + let availableWidth = flattenColumns.reduce((total, col) => { + if (!!col.fixed) { + return total - (col.width as number); + } + return total; + }, componentWidth); + + // 计算当前这个右边部分的距离 + const firstRightWidth = + (flattenColumns[startIndex].width as number) - (offsetX - virtualOffset); + + for (let i = startIndex; i < flattenColumns.length; i++) { + if (i === startIndex) { + availableWidth = availableWidth - firstRightWidth; + } else { + availableWidth = availableWidth - (flattenColumns[i].width as number); + } + virtualEndIndex = i; + if (availableWidth <= 0) { + break; + } + } + return virtualEndIndex; + }, [componentWidth, flattenColumns, offsetX, virtualOffset, startIndex]); + + const showColumnIndexes = useMemo(() => { + const fixedLeftIndexes = []; + const fixedRightIndexes = []; + flattenColumns.forEach((col, index) => { + if (col.fixed === 'left') { + fixedLeftIndexes.push(index); + } + if (col.fixed === 'right') { + fixedRightIndexes.push(index); + } + }); + + return [...fixedLeftIndexes, ...generateRange(startIndex, endIndex), ...fixedRightIndexes]; + }, [endIndex, flattenColumns, startIndex]); + + return [startIndex, virtualOffset, showColumnIndexes] as const; +} diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index 4cd1ec23..dc9f63bf 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -443,4 +443,110 @@ describe('Table.Virtual', () => { fireEvent.scroll(container.querySelector('.rc-table-tbody-virtual-holder')!); expect(onScroll).toHaveBeenCalled(); }); + + it('horizontal virtual', async () => { + const { container } = getTable({ + virtual: { x: true }, + columns: [ + { + width: 100, + className: 'a', + }, + { + width: 50, + className: 'b', + }, + { + width: 100, + className: 'c', + }, + { + width: 50, + className: 'd', + }, + ], + scroll: { + x: 300, + y: 10, + }, + getContainerWidth: () => 200, + data: [{}], + }); + + resize(container.querySelector('.rc-table')); + + await waitFakeTimer(); + + let rowNodeChildren = container.querySelector('.rc-table-row').children; + + expect(rowNodeChildren.length).toBe(3); + expect(rowNodeChildren[0].classList).toContain('a'); + expect(rowNodeChildren[1].classList).toContain('b'); + expect(rowNodeChildren[2].classList).toContain('c'); + + fireEvent.wheel(container.querySelector('.rc-table-tbody-virtual-holder')!, { + deltaX: 100, + }); + + rowNodeChildren = container.querySelector('.rc-table-row').children; + + expect(rowNodeChildren.length).toBe(3); + expect(rowNodeChildren[0].classList).toContain('b'); + expect(rowNodeChildren[1].classList).toContain('c'); + expect(rowNodeChildren[2].classList).toContain('d'); + }); + + it('fixed column in horizontal virtual', async () => { + const { container } = getTable({ + virtual: { x: true }, + columns: [ + { + width: 100, + className: 'a', + fixed: 'left', + }, + { + width: 100, + className: 'b', + }, + { + width: 100, + className: 'c', + }, + { + width: 100, + className: 'd', + fixed: 'right', + }, + ], + scroll: { + x: 300, + y: 10, + }, + getContainerWidth: () => 200, + data: [{}], + }); + + resize(container.querySelector('.rc-table')); + + await waitFakeTimer(); + + let rowNodeChildren = container.querySelector('.rc-table-row').children; + + expect(rowNodeChildren.length).toBe(3); + expect(rowNodeChildren[0].classList).toContain('a'); + expect(rowNodeChildren[1].classList).toContain('b'); + expect(rowNodeChildren[2].classList).toContain('d'); + + fireEvent.wheel(container.querySelector('.rc-table-tbody-virtual-holder')!, { + deltaX: 100, + }); + + rowNodeChildren = container.querySelector('.rc-table-row').children; + + expect(rowNodeChildren.length).toBe(3); + expect(rowNodeChildren[0].classList).toContain('a'); + expect(rowNodeChildren[1].classList).toContain('c'); + expect(rowNodeChildren[2].classList).toContain('d'); + }); });