diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts
index 5ecd791266..6cfa42c657 100644
--- a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts
+++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts
@@ -142,10 +142,7 @@ describe('ListTable checkbox record index api', () => {
{
task: { text: 'parent', checked: true },
hierarchyState: TYPES.HierarchyState.collapse,
- children: [
- { task: { text: 'child 1', checked: true } },
- { task: { text: 'child 2', checked: true } }
- ]
+ children: [{ task: { text: 'child 1', checked: true } }, { task: { text: 'child 2', checked: true } }]
}
],
enableCheckboxCascade: true
diff --git a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts
new file mode 100644
index 0000000000..88b1877e06
--- /dev/null
+++ b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts
@@ -0,0 +1,125 @@
+import * as VTable from '../../src';
+
+const CONTAINER_ID = 'vTable';
+
+const filteredChildren = Array.from({ length: 300 }, (_, index) => ({
+ name: `filtered child ${index + 1}`,
+ status: 'visible before filter',
+ desc: 'This row is hidden after filter interaction',
+ _show: true
+}));
+const visibleChildren = Array.from({ length: 40 }, (_, index) => ({
+ name: `visible child ${index + 1}`,
+ status: 'visible after filter',
+ desc: `Visible tree row ${index + 1} should fill the viewport after filtered zero-height rows`,
+ _show: true
+}));
+const records = [
+ {
+ name: 'expanded group filtered by interaction',
+ status: 'group',
+ desc: 'Children below become _show=false after simulated filter interaction',
+ _show: true,
+ hierarchyState: 'expand',
+ children: filteredChildren
+ },
+ {
+ name: 'expanded group with visible children',
+ status: 'group',
+ desc: 'These rows should be pulled into first screen after zero-height rows',
+ _show: true,
+ hierarchyState: 'expand',
+ children: visibleChildren
+ }
+];
+
+export function createTable() {
+ const container = document.getElementById(CONTAINER_ID)!;
+ document.getElementById('issue5115Toolbar')?.remove();
+ container.style.width = '720px';
+ container.style.height = '420px';
+
+ const toolbar = document.createElement('div');
+ toolbar.id = 'issue5115Toolbar';
+ toolbar.style.cssText = 'height: 48px; font-size: 12px; display: flex; gap: 12px; align-items: center;';
+ toolbar.innerHTML = `
+
+
+
+ `;
+ container.before(toolbar);
+
+ const option: VTable.ListTableConstructorOptions = {
+ container,
+ records,
+ columns: [
+ { field: 'name', title: 'Name', tree: true, width: 260 },
+ { field: 'status', title: 'Status', width: 180 },
+ { field: 'desc', title: 'Description', width: 260 }
+ ],
+ widthMode: 'standard',
+ heightMode: 'autoHeight',
+ defaultRowHeight: 40,
+ hierarchyIndent: 20,
+ hierarchyExpandLevel: 2,
+ customComputeRowHeight: ({ row, table }) => {
+ const record = table.getCellOriginRecord(0, row);
+ return record && record._show === false ? 0 : 'auto';
+ }
+ };
+ const tableInstance = new VTable.ListTable(option);
+
+ const check = () => {
+ const proxy = tableInstance.scenegraph.proxy;
+ const bodyStart = tableInstance.frozenRowCount;
+ const bodyHeight =
+ tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight() - tableInstance.getBottomFrozenRowsHeight();
+ const renderedHeight = tableInstance.getRowsHeight(bodyStart, proxy.rowEnd);
+ const firstFilteredRowHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount + 1);
+ const proxyRowsSynced =
+ proxy.totalRow >= proxy.rowEnd && proxy.totalActualBodyRowCount >= proxy.rowEnd - proxy.rowStart + 1;
+ const pass = firstFilteredRowHeight === 0 && renderedHeight >= bodyHeight && proxyRowsSynced;
+ const state = document.getElementById('issue5115State')!;
+ state.textContent =
+ `${pass ? 'PASS' : 'FAIL'} | firstFilteredRowHeight=${firstFilteredRowHeight} renderedHeight=${renderedHeight} ` +
+ `bodyHeight=${bodyHeight} rowEnd=${proxy.rowEnd} totalRow=${proxy.totalRow}`;
+ return {
+ pass,
+ firstFilteredRowHeight,
+ renderedHeight,
+ bodyHeight,
+ rowEnd: proxy.rowEnd,
+ totalRow: proxy.totalRow,
+ totalActualBodyRowCount: proxy.totalActualBodyRowCount,
+ proxyRowsSynced
+ };
+ };
+
+ const filterRows = () => {
+ filteredChildren.forEach(record => {
+ record._show = false;
+ record.status = 'hidden by _show=false';
+ });
+ tableInstance.updateOption(option, { clearRowHeightCache: true, clearColWidthCache: false });
+ };
+
+ const toggleCheck = async () => {
+ tableInstance.toggleHierarchyState(0, 1, false);
+ await new Promise(resolve => setTimeout(resolve, 60));
+ tableInstance.toggleHierarchyState(0, 1, false);
+ await new Promise(resolve => setTimeout(resolve, 120));
+ return check();
+ };
+
+ document.getElementById('issue5115Check')!.addEventListener('click', check);
+ document.getElementById('issue5115ToggleCheck')!.addEventListener('click', toggleCheck);
+
+ window.tableInstance = tableInstance;
+ (window as any).issue5115Check = check;
+ (window as any).issue5115ToggleCheck = toggleCheck;
+
+ setTimeout(() => {
+ filterRows();
+ setTimeout(check, 0);
+ }, 0);
+}
diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts
index 49090d023b..d339e67072 100644
--- a/packages/vtable/examples/menu.ts
+++ b/packages/vtable/examples/menu.ts
@@ -34,6 +34,10 @@ export const menus = [
path: 'debug',
name: 'issue-5114'
},
+ {
+ path: 'debug',
+ name: 'issue-5115-auto-height-zero-row'
+ },
{
path: 'debug',
name: 'issue-5117-auto-height-real-height'
diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts
index 4fcf6d1baa..01b31ddeb1 100644
--- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts
+++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts
@@ -6,6 +6,30 @@ import { computeRowsHeight } from '../../layout/compute-row-height';
import { createColGroup } from '../column';
import type { SceneProxy } from './proxy';
+function fillVisibleBodyRows(proxy: SceneProxy, distRow: number): number {
+ const { table } = proxy;
+ const bodyBottomRow = table.rowCount - 1 - table.bottomFrozenRowCount;
+ const visibleBodyHeight = table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
+ let targetRow = distRow;
+
+ while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) {
+ const nextRow = targetRow + 1;
+ computeRowsHeight(table, nextRow, nextRow, false);
+ targetRow = nextRow;
+ }
+
+ return targetRow;
+}
+
+function syncVisibleBodyRows(proxy: SceneProxy, targetRow: number) {
+ if (targetRow <= proxy.totalRow) {
+ return;
+ }
+
+ proxy.totalRow = targetRow;
+ proxy.totalActualBodyRowCount = Math.max(proxy.totalActualBodyRowCount, targetRow - proxy.bodyTopRow + 1);
+}
+
export function createGroupForFirstScreen(
cornerHeaderGroup: Group,
colHeaderGroup: Group,
@@ -50,6 +74,7 @@ export function createGroupForFirstScreen(
} else {
distRow = Math.min(proxy.firstScreenRowLimit - 1, table.rowCount - 1);
}
+ let bodyDistRow = Math.min(proxy.bodyBottomRow, distRow - table.bottomFrozenRowCount);
if (table.internalProps._widthResizedColMap.size === 0) {
// compute colums width in first screen
computeColsWidth(table, 0, distColForCompute ?? distCol);
@@ -64,6 +89,10 @@ export function createGroupForFirstScreen(
? table.rowCount - 1
: distRowForCompute ?? distRow
); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用
+ if (table.heightMode === 'autoHeight') {
+ bodyDistRow = fillVisibleBodyRows(proxy, bodyDistRow);
+ syncVisibleBodyRows(proxy, bodyDistRow);
+ }
}
if (distCol < table.colCount - table.rightFrozenColCount) {
@@ -128,7 +157,7 @@ export function createGroupForFirstScreen(
table.leftRowSeriesNumberCount - 1, // colEnd
table.frozenRowCount, // rowStart
// Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd
- distRow - table.bottomFrozenRowCount,
+ bodyDistRow,
'rowHeader', // isHeader
table
);
@@ -142,7 +171,7 @@ export function createGroupForFirstScreen(
Math.min(table.frozenColCount - 1, table.rowHeaderLevelCount + table.leftRowSeriesNumberCount - 1), // colEnd
table.frozenRowCount, // rowStart
// Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd
- distRow - table.bottomFrozenRowCount,
+ bodyDistRow,
'rowHeader', // isHeader
table
);
@@ -156,7 +185,7 @@ export function createGroupForFirstScreen(
table.frozenColCount - 1, // colEnd
table.frozenRowCount, // rowStart
// Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd
- distRow - table.bottomFrozenRowCount,
+ bodyDistRow,
'body',
table
);
@@ -275,7 +304,7 @@ export function createGroupForFirstScreen(
table.colCount - 1, // colEnd
table.frozenRowCount, // rowStart
// Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd
- distRow - table.bottomFrozenRowCount,
+ bodyDistRow,
table.isPivotChart() ? 'rowHeader' : 'body', // isHeader
table
);
@@ -307,7 +336,7 @@ export function createGroupForFirstScreen(
distCol - table.rightFrozenColCount,
table.frozenRowCount, // rowStart
// Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd
- distRow - table.bottomFrozenRowCount,
+ bodyDistRow,
'body', // isHeader
table
);
diff --git a/packages/vtable/src/scenegraph/layout/update-row.ts b/packages/vtable/src/scenegraph/layout/update-row.ts
index a2175ff157..610bbcefa5 100644
--- a/packages/vtable/src/scenegraph/layout/update-row.ts
+++ b/packages/vtable/src/scenegraph/layout/update-row.ts
@@ -7,6 +7,7 @@ import type { Scenegraph } from '../scenegraph';
import { getCellMergeInfo } from '../utils/get-cell-merge';
import { deduplication } from '../../tools/util';
import { checkHaveTextStick, resetTextStick } from '../stick-text';
+import { computeRowsHeight } from './compute-row-height';
/**
* add and remove rows in scenegraph
@@ -78,6 +79,11 @@ export function updateRow(
updateAfter = updateAfter ?? needUpdateAfter;
rowHeightsMap.insert(row);
});
+ const filledVisibleRowStart = fillVisibleBodyRows(scene);
+ if (isNumber(filledVisibleRowStart)) {
+ updateAfter = updateAfter ?? filledVisibleRowStart;
+ rowUpdatePos = isValid(rowUpdatePos) ? Math.min(rowUpdatePos, filledVisibleRowStart) : filledVisibleRowStart;
+ }
// reset attribute y and row number in CellGroup
// const newTotalHeight = resetRowNumberAndY(scene);
@@ -277,6 +283,40 @@ function addRow(row: number, scene: Scenegraph, skipUpdateProxy?: boolean) {
// scene.proxy.rowEnd++;
// scene.proxy.currentRow++;
}
+
+function fillVisibleBodyRows(scene: Scenegraph): number | undefined {
+ const { table, proxy } = scene;
+ if (table.heightMode !== 'autoHeight') {
+ return undefined;
+ }
+ const bodyBottomRow = table.rowCount - 1 - table.bottomFrozenRowCount;
+ const visibleBodyHeight = table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
+ let targetRow = Math.min(proxy.rowEnd, bodyBottomRow);
+
+ computeRowsHeight(table, proxy.rowStart, targetRow, false);
+ while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) {
+ const nextRow = targetRow + 1;
+ computeRowsHeight(table, nextRow, nextRow, false);
+ targetRow = nextRow;
+ }
+
+ if (targetRow <= proxy.rowEnd) {
+ return undefined;
+ }
+
+ const startRow = proxy.rowEnd + 1;
+ for (let row = startRow; row <= targetRow; row++) {
+ addRowCellGroup(row, scene);
+ }
+ proxy.rowEnd = targetRow;
+ proxy.currentRow = Math.max(proxy.currentRow, targetRow);
+ proxy.totalRow = Math.max(proxy.totalRow, targetRow);
+ proxy.totalActualBodyRowCount = Math.max(proxy.totalActualBodyRowCount, targetRow - proxy.rowStart + 1);
+ proxy.rowUpdatePos = Math.min(proxy.rowUpdatePos, startRow);
+
+ return startRow;
+}
+
function resetRowNumber(scene: Scenegraph) {
scene.bodyGroup.forEachChildren((colGroup: Group) => {
let rowIndex = scene.bodyRowStart;
diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts
index 9b7d676f07..5b4da0ea5e 100644
--- a/packages/vtable/src/state/checkbox/checkbox.ts
+++ b/packages/vtable/src/state/checkbox/checkbox.ts
@@ -6,6 +6,9 @@ import type { BaseTableAPI } from '../../ts-types/base-table';
import type { CachedDataSource } from '../../data';
import type { CheckBox } from '@src/vrender';
+type CheckboxStateValue = boolean | 'indeterminate';
+type CheckboxRecordValue = CheckboxStateValue | { checked?: CheckboxStateValue };
+
export function setCheckedState(
col: number,
row: number,
@@ -590,9 +593,12 @@ function updateParentCheckboxStateByRecordIndex(recordIndex: number | number[],
continue;
}
- const childStates = parentRecord.children.map((child: any, childIndex: number) =>
- getRecordCheckboxState(parentIndex.concat(childIndex), child, field, table)
- );
+ const childStates: CheckboxStateValue[] = parentRecord.children.map((child: any, childIndex: number) => {
+ const childRecordIndex = parentIndex.concat(childIndex);
+ const childState = getRecordCheckboxState(childRecordIndex, child, field, table);
+ setRecordCheckboxState(childRecordIndex, field, childState, table.stateManager.checkedState);
+ return childState;
+ });
const allChecked = childStates.every(state => state === true);
const allUnchecked = childStates.every(state => state !== true && state !== 'indeterminate');
const parentState = allChecked ? true : allUnchecked ? false : 'indeterminate';
@@ -606,7 +612,7 @@ function getRecordCheckboxState(
record: any,
field: FieldDef,
table: BaseTableAPI
-): boolean | 'indeterminate' {
+): CheckboxStateValue {
const fieldKey = field as string | number;
const dataIndex = normalizeRecordIndex(recordIndex).toString();
const cachedState = table.stateManager.checkedState.get(dataIndex)?.[fieldKey];
@@ -614,9 +620,9 @@ function getRecordCheckboxState(
return cachedState;
}
- const value = record?.[fieldKey];
- if (isObject(value) && isValid(value.checked)) {
- return value.checked;
+ const value = record?.[fieldKey] as CheckboxRecordValue | undefined;
+ if (isObject(value) && isValid((value as { checked?: CheckboxStateValue }).checked)) {
+ return (value as { checked?: CheckboxStateValue }).checked;
}
if (typeof value === 'boolean') {
return value;
@@ -639,7 +645,11 @@ function normalizeRecordIndexToArray(recordIndex: number | number[]): number[] {
return isArray(recordIndex) ? recordIndex : [recordIndex];
}
-function traverseRecords(records: any[], handler: (record: any, recordIndex: number | number[]) => void, parentIndex: number[] = []) {
+function traverseRecords(
+ records: any[],
+ handler: (record: any, recordIndex: number | number[]) => void,
+ parentIndex: number[] = []
+) {
if (!isArray(records)) {
return;
}