This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,13 @@
import * as React from 'react';
import type { OnCustomizeScroll, ScrollConfig } from '../interface';
export interface GridProps<RecordType = any> {
data: RecordType[];
onScroll: OnCustomizeScroll;
}
export interface GridRef {
scrollLeft: number;
nativeElement: HTMLDivElement;
scrollTo: (scrollConfig: ScrollConfig) => void;
}
declare const ResponseGrid: React.ForwardRefExoticComponent<GridProps<any> & React.RefAttributes<GridRef>>;
export default ResponseGrid;
@@ -0,0 +1,261 @@
import { useContext } from '@rc-component/context';
import VirtualList from '@rc-component/virtual-list';
import * as React from 'react';
import TableContext, { responseImmutable } from "../context/TableContext";
import useFlattenRecords from "../hooks/useFlattenRecords";
import BodyLine from "./BodyLine";
import { GridContext, StaticContext } from "./context";
const Grid = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
data,
onScroll
} = props;
const {
flattenColumns,
onColumnResize,
getRowKey,
expandedKeys,
prefixCls,
childrenColumnName,
scrollX,
direction
} = useContext(TableContext, ['flattenColumns', 'onColumnResize', 'getRowKey', 'prefixCls', 'expandedKeys', 'childrenColumnName', 'scrollX', 'direction']);
const {
sticky,
scrollY,
listItemHeight,
getComponent,
onScroll: onTablePropScroll
} = useContext(StaticContext);
// =========================== Ref ============================
const listRef = React.useRef(null);
// =========================== Data ===========================
const flattenData = useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey);
// ========================== Column ==========================
const columnsWidth = React.useMemo(() => {
let total = 0;
return flattenColumns.map(({
width,
minWidth,
key
}) => {
const finalWidth = Math.max(width || 0, minWidth || 0);
total += finalWidth;
return [key, finalWidth, total];
});
}, [flattenColumns]);
const columnsOffset = React.useMemo(() => columnsWidth.map(colWidth => colWidth[2]), [columnsWidth]);
React.useEffect(() => {
columnsWidth.forEach(([key, width]) => {
onColumnResize(key, width);
});
}, [columnsWidth]);
// =========================== Ref ============================
React.useImperativeHandle(ref, () => {
const obj = {
scrollTo: config => {
const {
offset,
...restConfig
} = config;
// If offset is provided, force align to 'top' for consistent behavior
if (offset) {
listRef.current?.scrollTo({
...restConfig,
offset,
align: 'top'
});
} else {
listRef.current?.scrollTo(config);
}
},
nativeElement: listRef.current?.nativeElement
};
Object.defineProperty(obj, 'scrollLeft', {
get: () => listRef.current?.getScrollInfo().x || 0,
set: value => {
listRef.current?.scrollTo({
left: value
});
}
});
// https://github.com/ant-design/ant-design/issues/54734
Object.defineProperty(obj, 'scrollTop', {
get: () => listRef.current?.getScrollInfo().y || 0,
set: value => {
listRef.current?.scrollTo({
top: value
});
}
});
return obj;
});
// ======================= Col/Row Span =======================
const getRowSpan = (column, index) => {
const record = flattenData[index]?.record;
const {
onCell
} = column;
if (onCell) {
const cellProps = onCell(record, index);
return cellProps?.rowSpan ?? 1;
}
return 1;
};
const extraRender = info => {
const {
start,
end,
getSize,
offsetY
} = info;
// Do nothing if no data
if (end < 0) {
return null;
}
// Find first rowSpan column
let firstRowSpanColumns = flattenColumns.filter(
// rowSpan is 0
column => getRowSpan(column, start) === 0);
let startIndex = start;
for (let i = start; i >= 0; i -= 1) {
firstRowSpanColumns = firstRowSpanColumns.filter(column => getRowSpan(column, i) === 0);
if (!firstRowSpanColumns.length) {
startIndex = i;
break;
}
}
// Find last rowSpan column
let lastRowSpanColumns = flattenColumns.filter(
// rowSpan is not 1
column => getRowSpan(column, end) !== 1);
let endIndex = end;
for (let i = end; i < flattenData.length; i += 1) {
lastRowSpanColumns = lastRowSpanColumns.filter(column => getRowSpan(column, i) !== 1);
if (!lastRowSpanColumns.length) {
endIndex = Math.max(i - 1, end);
break;
}
}
// Collect the line who has rowSpan
const spanLines = [];
for (let i = startIndex; i <= endIndex; i += 1) {
const item = flattenData[i];
// This code will never reach, just incase
if (!item) {
continue;
}
if (flattenColumns.some(column => getRowSpan(column, i) > 1)) {
spanLines.push(i);
}
}
// Patch extra line on the page
const nodes = spanLines.map(index => {
const item = flattenData[index];
const rowKey = getRowKey(item.record, index);
const getHeight = rowSpan => {
const endItemIndex = index + rowSpan - 1;
const endItem = flattenData[endItemIndex];
if (!endItem || !endItem.record) {
// clamp 到当前可用的最后一行,或退化为默认高度
const safeEndIndex = Math.min(endItemIndex, flattenData.length - 1);
const safeEndItem = flattenData[safeEndIndex];
const endItemKey = getRowKey(safeEndItem.record, safeEndIndex);
const sizeInfo = getSize(rowKey, endItemKey);
return sizeInfo.bottom - sizeInfo.top;
}
const endItemKey = getRowKey(endItem.record, endItemIndex);
const sizeInfo = getSize(rowKey, endItemKey);
return sizeInfo.bottom - sizeInfo.top;
};
const sizeInfo = getSize(rowKey);
return /*#__PURE__*/React.createElement(BodyLine, {
key: index,
data: item,
rowKey: rowKey,
index: index,
style: {
top: -offsetY + sizeInfo.top
},
extra: true,
getHeight: getHeight
});
});
return nodes;
};
// ========================= Context ==========================
const gridContext = React.useMemo(() => ({
columnsOffset
}), [columnsOffset]);
// ========================== Render ==========================
const tblPrefixCls = `${prefixCls}-tbody`;
// default 'div' in @rc-component/virtual-list
const wrapperComponent = getComponent(['body', 'wrapper']);
// ========================== Sticky Scroll Bar ==========================
const horizontalScrollBarStyle = {};
if (sticky) {
horizontalScrollBarStyle.position = 'sticky';
horizontalScrollBarStyle.bottom = 0;
if (typeof sticky === 'object' && sticky.offsetScroll) {
horizontalScrollBarStyle.bottom = sticky.offsetScroll;
}
}
return /*#__PURE__*/React.createElement(GridContext.Provider, {
value: gridContext
}, /*#__PURE__*/React.createElement(VirtualList, {
fullHeight: false,
ref: listRef,
prefixCls: `${tblPrefixCls}-virtual`,
styles: {
horizontalScrollBar: horizontalScrollBarStyle
},
className: tblPrefixCls,
height: scrollY,
itemHeight: listItemHeight || 24,
data: flattenData,
itemKey: item => getRowKey(item.record),
component: wrapperComponent,
scrollWidth: scrollX,
direction: direction,
onVirtualScroll: ({
x
}) => {
onScroll({
currentTarget: listRef.current?.nativeElement,
scrollLeft: x
});
},
onScroll: onTablePropScroll,
extraRender: extraRender
}, (item, index, itemProps) => {
const rowKey = getRowKey(item.record, index);
return /*#__PURE__*/React.createElement(BodyLine, {
data: item,
rowKey: rowKey,
index: index,
style: itemProps.style
});
}));
});
const ResponseGrid = responseImmutable(Grid);
if (process.env.NODE_ENV !== 'production') {
ResponseGrid.displayName = 'ResponseGrid';
}
export default ResponseGrid;
@@ -0,0 +1,14 @@
import * as React from 'react';
import type { FlattenData } from '../hooks/useFlattenRecords';
export interface BodyLineProps<RecordType = any> {
data: FlattenData<RecordType>;
index: number;
className?: string;
style?: React.CSSProperties;
rowKey: React.Key;
/** Render cell only when it has `rowSpan > 1` */
extra?: boolean;
getHeight?: (rowSpan: number) => number;
}
declare const ResponseBodyLine: React.ForwardRefExoticComponent<BodyLineProps<any> & React.RefAttributes<HTMLDivElement>>;
export default ResponseBodyLine;
@@ -0,0 +1,119 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { useContext } from '@rc-component/context';
import { clsx } from 'clsx';
import * as React from 'react';
import Cell from "../Cell";
import TableContext, { responseImmutable } from "../context/TableContext";
import useRowInfo from "../hooks/useRowInfo";
import VirtualCell from "./VirtualCell";
import { StaticContext } from "./context";
import { computedExpandedClassName } from "../utils/expandUtil";
const BodyLine = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
data,
index,
className,
rowKey,
style,
extra,
getHeight,
...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 rowInfo = useRowInfo(record, rowKey, index, indent);
const RowComponent = getComponent(['body', 'row'], 'div');
const cellComponent = getComponent(['body', 'cell'], 'div');
// ========================== Expand ==========================
const {
rowSupportExpand,
expanded,
rowProps,
expandedRowRender,
expandedRowClassName
} = rowInfo;
let expandRowNode;
if (rowSupportExpand && expanded) {
const expandContent = expandedRowRender(record, index, indent + 1, expanded);
const expandedClsName = computedExpandedClassName(expandedRowClassName, record, index, indent);
let additionalProps = {};
if (fixColumn) {
additionalProps = {
style: {
['--virtual-width']: `${componentWidth}px`
}
};
}
const rowCellCls = `${prefixCls}-expanded-row-cell`;
expandRowNode = /*#__PURE__*/React.createElement(RowComponent, {
className: clsx(`${prefixCls}-expanded-row`, `${prefixCls}-expanded-row-level-${indent + 1}`, expandedClsName)
}, /*#__PURE__*/React.createElement(Cell, {
component: cellComponent,
prefixCls: prefixCls,
className: clsx(rowCellCls, {
[`${rowCellCls}-fixed`]: fixColumn
}),
additionalProps: additionalProps
}, expandContent));
}
// ========================== Render ==========================
const rowStyle = {
...style,
width: scrollX
};
if (extra) {
rowStyle.position = 'absolute';
rowStyle.pointerEvents = 'none';
}
const rowNode = /*#__PURE__*/React.createElement(RowComponent, _extends({}, rowProps, restProps, {
"data-row-key": rowKey,
ref: rowSupportExpand ? null : ref,
className: clsx(className, `${prefixCls}-row`, rowProps?.className, {
[`${prefixCls}-row-extra`]: extra
}),
style: {
...rowStyle,
...rowProps?.style
}
}), flattenColumns.map((column, colIndex) => {
return /*#__PURE__*/React.createElement(VirtualCell, {
key: colIndex,
component: cellComponent,
rowInfo: rowInfo,
column: column,
colIndex: colIndex,
indent: indent,
index: index,
renderIndex: renderIndex,
record: record,
inverse: extra,
getHeight: getHeight
});
}));
if (rowSupportExpand) {
return /*#__PURE__*/React.createElement("div", {
ref: ref
}, rowNode, expandRowNode);
}
return rowNode;
});
const ResponseBodyLine = responseImmutable(BodyLine);
if (process.env.NODE_ENV !== 'production') {
ResponseBodyLine.displayName = 'BodyLine';
}
export default ResponseBodyLine;
@@ -0,0 +1,26 @@
import * as React from 'react';
import type useRowInfo from '../hooks/useRowInfo';
import type { ColumnType, CustomizeComponent } from '../interface';
export interface VirtualCellProps<RecordType> {
rowInfo: ReturnType<typeof useRowInfo<RecordType>>;
column: ColumnType<RecordType>;
colIndex: number;
indent: number;
index: number;
component?: CustomizeComponent;
/** Used for `column.render` */
renderIndex: number;
record: RecordType;
style?: React.CSSProperties;
className?: string;
/** Render cell only when it has `rowSpan > 1` */
inverse?: boolean;
getHeight?: (rowSpan: number) => number;
}
/**
* Return the width of the column by `colSpan`.
* When `colSpan` is `0` will be trade as `1`.
*/
export declare function getColumnWidth(colIndex: number, colSpan: number, columnsOffset: number[]): number;
declare const VirtualCell: <RecordType>(props: VirtualCellProps<RecordType>) => React.JSX.Element;
export default VirtualCell;
@@ -0,0 +1,120 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { useContext } from '@rc-component/context';
import { clsx } from 'clsx';
import * as React from 'react';
import { getCellProps } from "../Body/BodyRow";
import Cell from "../Cell";
import { GridContext } from "./context";
/**
* Return the width of the column by `colSpan`.
* When `colSpan` is `0` will be trade as `1`.
*/
export function getColumnWidth(colIndex, colSpan, columnsOffset) {
const mergedColSpan = colSpan || 1;
return columnsOffset[colIndex + mergedColSpan] - (columnsOffset[colIndex] || 0);
}
const VirtualCell = props => {
const {
rowInfo,
column,
colIndex,
indent,
index,
component,
renderIndex,
record,
style,
className,
inverse,
getHeight
} = props;
const {
render,
dataIndex,
className: columnClassName,
width: colWidth
} = column;
const {
columnsOffset
} = useContext(GridContext, ['columnsOffset']);
// TODO: support `expandableRowOffset`
const {
key,
fixedInfo,
appendCellNode,
additionalCellProps
} = getCellProps(rowInfo, column, colIndex, indent, index);
const {
style: cellStyle,
colSpan = 1,
rowSpan = 1
} = additionalCellProps;
// ========================= ColWidth =========================
// column width
const startColIndex = colIndex - 1;
const concatColWidth = getColumnWidth(startColIndex, colSpan, columnsOffset);
// margin offset
const marginOffset = colSpan > 1 ? colWidth - concatColWidth : 0;
// ========================== Style ===========================
const mergedStyle = {
...cellStyle,
...style,
flex: `0 0 ${concatColWidth}px`,
width: `${concatColWidth}px`,
marginRight: marginOffset,
pointerEvents: 'auto'
};
// When `colSpan` or `rowSpan` is `0`, should skip render.
const needHide = React.useMemo(() => {
if (inverse) {
return rowSpan <= 1;
} else {
return colSpan === 0 || rowSpan === 0 || rowSpan > 1;
}
}, [rowSpan, colSpan, inverse]);
// 0 rowSpan or colSpan should not render
if (needHide) {
mergedStyle.visibility = 'hidden';
} else if (inverse) {
mergedStyle.height = getHeight?.(rowSpan);
}
const mergedRender = needHide ? () => null : render;
// ========================== Render ==========================
const cellSpan = {};
// Virtual should reset `colSpan` & `rowSpan`
if (rowSpan === 0 || colSpan === 0) {
cellSpan.rowSpan = 1;
cellSpan.colSpan = 1;
}
return /*#__PURE__*/React.createElement(Cell, _extends({
className: clsx(columnClassName, className),
ellipsis: column.ellipsis,
align: column.align,
scope: column.rowScope,
component: component,
prefixCls: rowInfo.prefixCls,
key: key,
record: record,
index: index,
renderIndex: renderIndex,
dataIndex: dataIndex,
render: mergedRender,
shouldCellUpdate: column.shouldCellUpdate
}, fixedInfo, {
appendNode: appendCellNode,
additionalProps: {
...additionalCellProps,
style: mergedStyle,
...cellSpan
}
}));
};
export default VirtualCell;
@@ -0,0 +1,14 @@
/// <reference types="react" />
import type { GetComponent, TableSticky } from '../interface';
export interface StaticContextProps {
scrollY: number;
listItemHeight: number;
sticky: boolean | TableSticky;
getComponent: GetComponent;
onScroll?: React.UIEventHandler<HTMLDivElement>;
}
export declare const StaticContext: import("@rc-component/context").SelectorContext<StaticContextProps>;
export interface GridContextProps {
columnsOffset: number[];
}
export declare const GridContext: import("@rc-component/context").SelectorContext<GridContextProps>;
@@ -0,0 +1,3 @@
import { createContext } from '@rc-component/context';
export const StaticContext = createContext(null);
export const GridContext = createContext(null);
@@ -0,0 +1,17 @@
import type { CompareProps } from '@rc-component/context/lib/Immutable';
import * as React from 'react';
import type { Reference } from '../interface';
import { type TableProps } from '../Table';
export interface VirtualTableProps<RecordType> extends Omit<TableProps<RecordType>, 'scroll'> {
listItemHeight?: number;
scroll: {
x?: number;
y?: number;
};
}
export type ForwardGenericVirtualTable = (<RecordType>(props: TableProps<RecordType> & React.RefAttributes<Reference>) => React.ReactElement<any>) & {
displayName?: string;
};
export declare const genVirtualTable: (shouldTriggerRender?: CompareProps<ForwardGenericVirtualTable>) => ForwardGenericVirtualTable;
declare const _default: ForwardGenericVirtualTable;
export default _default;
@@ -0,0 +1,95 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import { useEvent, warning } from '@rc-component/util';
import * as React from 'react';
import { INTERNAL_HOOKS } from "../constant";
import { makeImmutable } from "../context/TableContext";
import Table, { DEFAULT_PREFIX } from "../Table";
import Grid from "./BodyGrid";
import { StaticContext } from "./context";
import getValue from "@rc-component/util/es/utils/get";
const renderBody = (rawData, props) => {
const {
ref,
onScroll
} = props;
return /*#__PURE__*/React.createElement(Grid, {
ref: ref,
data: rawData,
onScroll: onScroll
});
};
const VirtualTable = (props, ref) => {
const {
data,
columns,
scroll,
sticky,
prefixCls = DEFAULT_PREFIX,
className,
listItemHeight,
components,
onScroll
} = props;
let {
x: scrollX,
y: scrollY
} = scroll || {};
// Fill scrollX
if (typeof scrollX !== 'number') {
if (process.env.NODE_ENV !== 'production') {
warning(!scrollX, '`scroll.x` in virtual table must be number.');
}
scrollX = 1;
}
// Fill scrollY
if (typeof scrollY !== 'number') {
scrollY = 500;
if (process.env.NODE_ENV !== 'production') {
warning(false, '`scroll.y` in virtual table must be number.');
}
}
const getComponent = useEvent((path, defaultComponent) => getValue(components, path) || defaultComponent);
// Memo this
const onInternalScroll = useEvent(onScroll);
// ========================= Context ==========================
const context = React.useMemo(() => ({
sticky,
scrollY,
listItemHeight,
getComponent,
onScroll: onInternalScroll
}), [sticky, scrollY, listItemHeight, getComponent, onInternalScroll]);
// ========================== Render ==========================
return /*#__PURE__*/React.createElement(StaticContext.Provider, {
value: context
}, /*#__PURE__*/React.createElement(Table, _extends({}, props, {
className: clsx(className, `${prefixCls}-virtual`),
scroll: {
...scroll,
x: scrollX
},
components: {
...components,
// fix https://github.com/ant-design/ant-design/issues/48991
body: data?.length ? renderBody : undefined
},
columns: columns,
internalHooks: INTERNAL_HOOKS,
tailor: true,
ref: ref
})));
};
const RefVirtualTable = /*#__PURE__*/React.forwardRef(VirtualTable);
if (process.env.NODE_ENV !== 'production') {
RefVirtualTable.displayName = 'VirtualTable';
}
export const genVirtualTable = shouldTriggerRender => {
return makeImmutable(RefVirtualTable, shouldTriggerRender);
};
export default genVirtualTable();