diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts
index ee11fce0646..d6abade655f 100644
--- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts
+++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts
@@ -3697,7 +3697,12 @@ export abstract class IgxGridBaseDirective implements GridType,
this.throttleTime$.pipe(
take(1),
switchMap(time => timer(time, this.throttleScheduler))
- )
+ ),
+ // `trailing: true` ensures the final settle position of a fast momentum
+ // scroll is processed; otherwise the last scroll events are dropped and the
+ // rows stay frozen at an intermediate startIndex while the scrollbar is at top.
+ // `leading: true` keeps the immediate response on scroll start.
+ { leading: true, trailing: true }
),
destructor
)
diff --git a/projects/igniteui-angular/grids/grid/src/grid.component.spec.ts b/projects/igniteui-angular/grids/grid/src/grid.component.spec.ts
index 07b9e63e6c7..41d2fd7f13f 100644
--- a/projects/igniteui-angular/grids/grid/src/grid.component.spec.ts
+++ b/projects/igniteui-angular/grids/grid/src/grid.component.spec.ts
@@ -882,6 +882,73 @@ describe('IgxGrid Component Tests #grid', () => {
expect(grid.verticalScrollContainer.getScroll().scrollTop).toBe(initialScroll);
expect(grid.headerContainer.getScroll().scrollLeft).toBeGreaterThanOrEqual(2 * (initialHorScroll + 50));
}));
+
+ describe('scroll throttle trailing edge', () => {
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, IgxGridScrollThrottleComponent],
+ providers: [{ provide: SCROLL_THROTTLE_TIME_MULTIPLIER, useValue: 0 }]
+ }).compileComponents();
+ }));
+
+ // Drive scrollNotify directly (not programmatic scrollTop, whose async native events would mask a dropped-trailing regression) to exercise the throttle window deterministically.
+ it('should settle at the top row after a fast momentum scroll back to scrollTop 0', async () => {
+ const fix = TestBed.createComponent(IgxGridScrollThrottleComponent);
+ fix.detectChanges();
+ await wait(50);
+ fix.detectChanges();
+ const grid = fix.componentInstance.grid;
+ const virtDir = grid.verticalScrollContainer;
+ const scrollEl = virtDir.getScroll();
+ const hScroll = grid.headerContainer.getScroll();
+ const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
+
+ // Guard the reproduction condition: the fixture must overflow horizontally.
+ expect(hScroll.scrollWidth).toBeGreaterThan(hScroll.clientWidth);
+
+ // Move away from the top so the first rows are virtualized out of view.
+ grid.scrollNotify.next({ target: { scrollTop: maxScroll } });
+ await wait(50);
+ fix.detectChanges();
+ expect(virtDir.state.startIndex).toBeGreaterThan(0);
+
+ // Momentum scroll back to top: intermediate on the leading edge, scrollTop = 0 settle only on the trailing edge.
+ grid.scrollNotify.next({ target: { scrollTop: Math.round(maxScroll / 2) } });
+ grid.scrollNotify.next({ target: { scrollTop: 0 } });
+ await wait(50);
+ fix.detectChanges();
+
+ // Without the trailing edge the settle is dropped and startIndex stays frozen mid-list.
+ expect(virtDir.state.startIndex).toBe(0);
+ expect(grid.gridAPI.get_row_by_index(0)).toBeDefined();
+ });
+
+ it('should settle at the last row after a fast momentum scroll to the bottom', async () => {
+ const fix = TestBed.createComponent(IgxGridScrollThrottleComponent);
+ fix.detectChanges();
+ await wait(50);
+ fix.detectChanges();
+ const grid = fix.componentInstance.grid;
+ const virtDir = grid.verticalScrollContainer;
+ const scrollEl = virtDir.getScroll();
+ const hScroll = grid.headerContainer.getScroll();
+ const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
+ const lastIndex = fix.componentInstance.data.length - 1;
+
+ expect(hScroll.scrollWidth).toBeGreaterThan(hScroll.clientWidth);
+ expect(virtDir.state.startIndex).toBe(0);
+
+ // Momentum scroll top to bottom: intermediate on the leading edge, max-scroll settle only on the trailing edge.
+ grid.scrollNotify.next({ target: { scrollTop: Math.round(maxScroll / 2) } });
+ grid.scrollNotify.next({ target: { scrollTop: maxScroll } });
+ await wait(50);
+ fix.detectChanges();
+
+ // Without the trailing edge the settle is dropped and the last row is never brought into view.
+ expect((virtDir.state.startIndex ?? 0) + (virtDir.state.chunkSize ?? 0)).toBeGreaterThanOrEqual(lastIndex + 1);
+ expect(grid.gridAPI.get_row_by_index(lastIndex)).toBeDefined();
+ });
+ });
});
describe('IgxGrid - default rendering for rows and columns', () => {
@@ -4051,3 +4118,19 @@ export class IgxGridPerformanceComponent implements AfterViewInit, OnInit {
export class IgxGridNoDataComponent {
@ViewChild(IgxGridComponent, { static: true }) public grid: IgxGridComponent;
}
+
+@Component({
+ template: ``,
+ imports: [IgxGridComponent]
+})
+class IgxGridScrollThrottleComponent {
+ @ViewChild(IgxGridComponent, { static: true }) public grid: IgxGridComponent;
+ // 30 columns in a 600px-wide grid guarantee horizontal overflow, plus 200 rows for vertical virtualization.
+ public data = Array.from({ length: 200 }, (_row, rowIndex) => {
+ const record: Record = { ID: rowIndex };
+ for (let col = 0; col < 30; col++) {
+ record[`Col${col}`] = `r${rowIndex}c${col}`;
+ }
+ return record;
+ });
+}