Package / Version:
@progress/kendo-angular-grid v23.2.1 (also reproduced on v23.3.0)
Angular 20.x
Description:
When using a Kendo Grid with scrollable="virtual", [rowHeight]="30", and [data] bound to a GridDataResult, if the user scrolls down (triggering (pageChange) with a non-zero skip), and then the data is replaced programmatically (e.g., by applying a filter that changes the dataset), the grid's virtual scroll container does not reset properly. The result is a large white/empty area above the actual rows — the user must scroll past this empty space to see the data (or the "No records available" message if the filtered result is empty).
Steps to Reproduce:
1. Create a grid with virtual scrolling:
<kendo-grid [data]="gridData" [kendoGridBinding]="filteredData" scrollable="virtual" [rowHeight]="30" [pageSize]="300" [skip]="currentSkip" (pageChange)="onPageChange($event)"></kendo-grid>
2.Load the grid with enough data to enable scrolling (e.g., 1000+ rows).
3.Scroll down significantly so the grid fires (pageChange) with a skip value > 0 (e.g., skip=300+).
This causes the grid's internal virtual scroller to:
Set translateY(Npx) on the grid table element to offset the visible rows
Set .k-height-container > div height to total * rowHeight
4.Now programmatically change the bound data to a much smaller or empty dataset:
this.filteredData = this.allData.filter(item => item.driverId === selectedDriverId);
this.gridData = {
data: this.filteredData.slice(0, this.pageSize),
total: this.filteredData.length
};
6.Actual: The grid retains the old translateY offset and .k-height-container height from before the filter. This produces a large empty white/blue area at the top of the grid. The actual rows (or "No records available") are pushed far below the visible viewport. The user must scroll down through the empty space to find them.
Root Cause Analysis:
The grid's ListComponent.resetVirtualScroll(newTotal) method correctly resets translateY, scrollTop, and recreates the RowHeightService. However, this method is only called from the grid's ngOnChanges when both conditions are met:
this.skip === 0
this.isVirtualScrollOutOfSync() returns true (i.e., virtualSkip > 0 or tableTransformOffset > 0)
The problem is that ngOnChanges checks this.skip === 0, but the grid's internal skip (managed by the virtual scroller) may not match the value passed through [skip] binding until after change detection. When data changes and [skip] is set to 0 simultaneously, the grid's internal skip may still hold the old value at the time ngOnChanges runs, so resetVirtualScroll is never called.
Additionally, the kendoGridBinding directive's rebind() path does call resetVirtualScroll, but it appears to have a timing issue in v23 where the internal scroller state (translateY, height container) is not fully synchronized when the data reference changes without the directive detecting a meaningful change.
This worked correctly in Kendo Angular Grid v20.x — downgrading to v20 resolves the issue entirely, confirming this is a regression in v23.
Workaround:
We manually reset the virtual scroll DOM after programmatically changing the data:
private resetGridVirtualScroll(): void {
try {
const gridEl: HTMLElement = this.myGrid.ariaRoot.nativeElement;
// Reset scroll position const content = gridEl.querySelector('.k-grid-content');
if (content) {
content.scrollTop = 0;
}
// Reset the table translateY that virtual scroll sets
const table = gridEl.querySelector('.k-grid-content table, .k-grid-content .k-grid-table-wrap');
if (table instanceofHTMLElement) {
table.style.transform = 'translateY(0px)';
}
// Reset the height container to match new total
const heightDiv = gridEl.querySelector('.k-height-container > div') asHTMLElement;
if (heightDiv) {
const total = this.filteredData?.length || 0;
heightDiv.style.height = (total * this.rowHeight) + 'px';
}
}
catch (e) { }
}
applyFilter() {
this.skip = 0;
this.filterData();
this.loadGridData();
this.resetGridVirtualScroll();
}We also had to fix loadData() to handle empty datasets — when FilteredData is empty, gridData must be set to { data: [], total: 0 }, otherwise the grid retains the old total and the virtual container keeps its large height.
Expected Fix:
The grid should automatically call resetVirtualScroll() whenever [data] changes with a new (different) total value and the virtual scroller is out of sync — regardless of the current internal skip state. The ngOnChanges guard this.skip === 0 should either be removed or should check the incoming skip binding value rather than the stale internal state.