Skip to content

Commit df6ab9a

Browse files
committed
fix: correct scroll-to-row centering for virtual DOM bottom-pad bug
Replace scrollIntoView with \_centerRow that restores vDomBottomPad before setting scrollTop via offsetTop. Tabulator's \_addBottomRow zeroes vDomBottomPad when vDomBottom reaches the last row index — even for mid-table rows — shrinking scrollHeight and preventing center alignment. Also refactor \_scrollToRow to async/await.
1 parent 449f4ea commit df6ab9a

1 file changed

Lines changed: 50 additions & 19 deletions

File tree

log-viewer/src/tabulator/module/RowNavigation.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class RowNavigation extends Module {
1313
this.registerTableFunction('goToRow', this.goToRow.bind(this));
1414
}
1515

16-
goToRow(
16+
async goToRow(
1717
row: RowComponent,
1818
opts: GoToRowOptions = { scrollIfVisible: true, focusRow: true },
1919
): Promise<void> {
@@ -58,30 +58,27 @@ export class RowNavigation extends Module {
5858
this.tableHolder.focus();
5959
}
6060

61-
return this._scrollToRow(row, opts);
61+
return new Promise<void>((resolve) => {
62+
// Need to wait for any pending redraws to finish before scrolling or it will not work
63+
setTimeout(() => {
64+
this._scrollToRow(row, opts).then(resolve);
65+
});
66+
});
6267
}
6368

64-
_scrollToRow(row: RowComponent, opts: GoToRowOptions): Promise<void> {
69+
async _scrollToRow(row: RowComponent, opts: GoToRowOptions): Promise<void> {
6570
const { scrollIfVisible, focusRow } = opts;
6671

67-
return this.table
68-
.scrollToRow(row, 'center', scrollIfVisible)
69-
.catch(() => {})
70-
.then(() => {
71-
return new Promise<void>((resolve) => {
72-
const elem = row.getElement();
73-
74-
if (scrollIfVisible || !this._isVisible(elem)) {
75-
elem.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' });
76-
}
72+
await this.table.scrollToRow(row, 'center', scrollIfVisible);
7773

78-
if (focusRow) {
79-
elem.focus();
80-
}
74+
const elem = row.getElement();
75+
if (scrollIfVisible || !this._isVisible(elem)) {
76+
this._centerRow(elem);
77+
}
8178

82-
resolve();
83-
});
84-
});
79+
if (focusRow) {
80+
elem.focus();
81+
}
8582
}
8683

8784
_isVisible(el: Element) {
@@ -92,4 +89,38 @@ export class RowNavigation extends Module {
9289
const rect = el.getBoundingClientRect();
9390
return rect.top >= holderRect.top && rect.bottom <= holderRect.bottom;
9491
}
92+
93+
// TODO: Remove once fixed upstream in tabulator-tables.
94+
//
95+
// Tabulator bug: _addBottomRow zeroes vDomBottomPad when vDomBottom reaches the last
96+
// row index — even for mid-table rows in expanded trees. This shrinks scrollHeight,
97+
// clamping scrollTop so scrollToRow places the row at the viewport bottom not center.
98+
// Fix: restore the minimum vDomBottomPad needed for centering, then set scrollTop
99+
// directly via offsetTop.
100+
_centerRow(elem: HTMLElement) {
101+
if (!this.tableHolder) return;
102+
103+
// Only near-bottom rows have vDomBottomPad forced to 0 — skip the DOM write for
104+
// all other rows where Tabulator already set it correctly.
105+
const renderer = this.table.rowManager?.renderer as Record<string, unknown> | undefined;
106+
if (renderer && this.tableHolder && ((renderer.vDomBottomPad as number) ?? 0) === 0) {
107+
const displayRows: unknown[] = this.table.rowManager?.getDisplayRows?.() ?? [];
108+
const vDomBottom = (renderer.vDomBottom as number) ?? 0;
109+
const vDomRowHeight = (renderer.vDomRowHeight as number) ?? 24;
110+
const truePad = Math.max(0, (displayRows.length - vDomBottom - 1) * vDomRowHeight);
111+
// Cap at clientHeight/2 — the maximum extra scroll range needed to center any row.
112+
// Avoids large paddingBottom values for mid-table rows. If truePad < clientHeight/2
113+
// the row is genuinely near the bottom and the browser clamps naturally — no blank space.
114+
const neededPad = Math.min(truePad, this.tableHolder.clientHeight / 2);
115+
if (neededPad > 0) {
116+
renderer.vDomBottomPad = neededPad;
117+
(renderer.tableElement as HTMLElement).style.paddingBottom = `${neededPad}px`;
118+
}
119+
}
120+
121+
// Reading elem.offsetTop forces a layout flush — paddingBottom is included in
122+
// scrollHeight before scrollTop is assigned.
123+
const holderHeight = this.tableHolder.clientHeight;
124+
this.tableHolder.scrollTop = elem.offsetTop - holderHeight / 2 + elem.offsetHeight / 2;
125+
}
95126
}

0 commit comments

Comments
 (0)