From 55f8e6d537d5522b2d7e8fd4e0d079fdeff7652d Mon Sep 17 00:00:00 2001 From: Harpush Date: Tue, 21 Apr 2026 23:53:08 +0300 Subject: [PATCH] fix(cdk/scrolling): scrollToIndex combined with cdkVirtualScrollingEement can result in wrong scroll offset Fixes a bug when using cdkVirtualScrollingElement and there is some space between the scrolling element and the viewport the scroll to index function doesn't take into account the viewport offset from the scrolling container. Fixes #33063 --- goldens/cdk/scrolling/index.api.md | 2 +- .../scrolling/fixed-size-virtual-scroll.ts | 2 +- .../scrolling/virtual-scroll-viewport.spec.ts | 25 +++++++++++++++---- src/cdk/scrolling/virtual-scroll-viewport.ts | 25 ++++++++++++++++--- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/goldens/cdk/scrolling/index.api.md b/goldens/cdk/scrolling/index.api.md index 6329b30634cb..70e8bc021ab1 100644 --- a/goldens/cdk/scrolling/index.api.md +++ b/goldens/cdk/scrolling/index.api.md @@ -202,7 +202,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On scrollable: CdkVirtualScrollable; readonly scrolledIndexChange: Observable; scrollToIndex(index: number, behavior?: ScrollBehavior): void; - scrollToOffset(offset: number, behavior?: ScrollBehavior): void; + scrollToOffset(offset: number, behavior?: ScrollBehavior, relativeTo?: 'viewport' | 'scrollingContainer'): void; setRenderedContentOffset(offset: number, to?: 'to-start' | 'to-end'): void; setRenderedRange(range: ListRange): void; setTotalContentSize(size: number): void; diff --git a/src/cdk/scrolling/fixed-size-virtual-scroll.ts b/src/cdk/scrolling/fixed-size-virtual-scroll.ts index 906d93c4d556..e9bc3cf4b8f8 100644 --- a/src/cdk/scrolling/fixed-size-virtual-scroll.ts +++ b/src/cdk/scrolling/fixed-size-virtual-scroll.ts @@ -104,7 +104,7 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { */ scrollToIndex(index: number, behavior: ScrollBehavior): void { if (this._viewport) { - this._viewport.scrollToOffset(index * this._itemSize, behavior); + this._viewport.scrollToOffset(index * this._itemSize, behavior, 'viewport'); } } diff --git a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts index 4a22f31b652d..0f5aa166dceb 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts @@ -1076,16 +1076,27 @@ describe('CdkVirtualScrollViewport', () => { .toBe(50); }); - it('should measure scroll offset with custom scrolling element', async () => { + it('should scroll to offset relative to scrolling container', async () => { await finishInit(fixture); - await triggerScroll(viewport, 100); + await triggerScroll(viewport, 100, 'scrollingContainer'); fixture.detectChanges(); await fixture.whenStable(); expect(viewport.measureScrollOffset('top')) - .withContext('should be 50 (actual scroll offset - viewport offset)') + .withContext('should be 50 (scrolling container offset)') .toBe(50); }); + + it('should scroll to offset relative to viewport', async () => { + await finishInit(fixture); + await triggerScroll(viewport, 100, 'viewport'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(viewport.measureScrollOffset('top')) + .withContext('should be 100 (viewport offset)') + .toBe(100); + }); }); describe('with scrollable window', () => { @@ -1138,9 +1149,13 @@ async function finishInit(fixture: ComponentFixture) { } /** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */ -async function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { +async function triggerScroll( + viewport: CdkVirtualScrollViewport, + offset?: number, + relativeTo?: 'viewport' | 'scrollingContainer', +) { if (offset !== undefined) { - viewport.scrollToOffset(offset); + viewport.scrollToOffset(offset, 'auto', relativeTo); } dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll'); await new Promise(resolve => setTimeout(resolve, 50)); diff --git a/src/cdk/scrolling/virtual-scroll-viewport.ts b/src/cdk/scrolling/virtual-scroll-viewport.ts index a0a93216dbd3..850f95566307 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.ts @@ -411,13 +411,28 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On * direction, this would be the equivalent of setting a fictional `scrollRight` property. * @param offset The offset to scroll to. * @param behavior The ScrollBehavior to use when scrolling. Default is behavior is `auto`. + * @param relativeTo The start point of the offset. Default is `scrollingContainer`. */ - scrollToOffset(offset: number, behavior: ScrollBehavior = 'auto') { + scrollToOffset( + offset: number, + behavior: ScrollBehavior = 'auto', + relativeTo: 'viewport' | 'scrollingContainer' = 'scrollingContainer', + ) { const options: ExtendedScrollToOptions = {behavior}; if (this.orientation === 'horizontal') { - options.start = offset; + if (relativeTo === 'scrollingContainer') { + options.start = offset; + } else { + const viewportOffset = this.measureViewportOffset('start'); + options.start = viewportOffset + offset; + } } else { - options.top = offset; + if (relativeTo === 'scrollingContainer') { + options.top = offset; + } else { + const viewportOffset = this.measureViewportOffset('top'); + options.top = viewportOffset + offset; + } } this.scrollable.scrollTo(options); } @@ -460,6 +475,10 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On * @param from The edge to measure from. */ measureViewportOffset(from?: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end') { + if (this.scrollable === this) { + return 0; + } + let fromRect: 'left' | 'top' | 'right' | 'bottom'; const LEFT = 'left'; const RIGHT = 'right';