Skip to content

Commit a123bea

Browse files
authored
fix: avoid virtualizer losing scroll position on large size change (#11256)
1 parent c39961c commit a123bea

File tree

2 files changed

+46
-8
lines changed

2 files changed

+46
-8
lines changed

packages/component-base/src/virtualizer-iron-list-adapter.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,19 @@ export class IronListAdapter {
333333
return element ? this.scrollTarget.getBoundingClientRect().top - element.getBoundingClientRect().top : undefined;
334334
}
335335

336+
/**
337+
* Adjusts the scroll position to compensate for any offset change of a given index.
338+
* @param {number} index - The index whose scroll offset to restore
339+
* @param {number|undefined} offsetBefore - The scroll offset of the index before the change
340+
* @private
341+
*/
342+
__restoreScrollOffset(index, offsetBefore) {
343+
const offsetAfter = this.__getIndexScrollOffset(index);
344+
if (offsetBefore !== undefined && offsetAfter !== undefined) {
345+
this._scrollTop += offsetBefore - offsetAfter;
346+
}
347+
}
348+
336349
get size() {
337350
return this.__size;
338351
}
@@ -382,10 +395,7 @@ export class IronListAdapter {
382395
// and remove exceeding items when size is decreased.
383396
this.scrollToIndex(fvi);
384397

385-
const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
386-
if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
387-
this._scrollTop += fviOffsetBefore - fviOffsetAfter;
388-
}
398+
this.__restoreScrollOffset(fvi, fviOffsetBefore);
389399
}
390400

391401
this.__preventElementUpdates = false;
@@ -851,6 +861,9 @@ export class IronListAdapter {
851861
const threshold = OFFSET_ADJUST_MIN_THRESHOLD;
852862
const maxShift = 100;
853863

864+
const fvi = this.adjustedFirstVisibleIndex;
865+
const fviOffsetBefore = this.__getIndexScrollOffset(fvi);
866+
854867
// Near start
855868
if (this._scrollTop === 0) {
856869
this._vidxOffset = 0;
@@ -872,6 +885,8 @@ export class IronListAdapter {
872885
this._vidxOffset += Math.min(maxOffset - this._vidxOffset, maxShift);
873886
super.scrollToIndex(this.firstVisibleIndex - (this._vidxOffset - oldOffset));
874887
}
888+
889+
this.__restoreScrollOffset(fvi, fviOffsetBefore);
875890
}
876891
}
877892
}

packages/component-base/test/virtualizer-unlimited-size.test.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ describe('unlimited size', () => {
4141
return [...elementsContainer.children].reduce((max, el) => Math.max(max, el.index), 0);
4242
}
4343

44+
function getIndexScrollOffset(fvi) {
45+
const element = elementsContainer.querySelector(`#item-${fvi}`);
46+
return scrollTarget.getBoundingClientRect().top - element.getBoundingClientRect().top;
47+
}
48+
4449
it('should scroll to a large index', () => {
4550
const index = Math.floor(virtualizer.size / 2);
4651
virtualizer.scrollToIndex(index);
@@ -159,10 +164,18 @@ describe('unlimited size', () => {
159164
smallestIndex = Math.min(...Array.from(elementsContainer.children).map((el) => el.index));
160165

161166
scrollTarget.scrollTop -= (elementHeight * elementCount) / 2;
167+
168+
// Scroll offset of the index before the scroll event
169+
const scrollOffsetBefore = getIndexScrollOffset(smallestIndex);
162170
await oneEvent(scrollTarget, 'scroll');
163171

164172
const item = elementsContainer.querySelector(`#item-${smallestIndex}`);
165173
expect(item).to.be.ok;
174+
175+
// Scroll offset of the same index after the scroll event
176+
const scrollOffsetAfter = getIndexScrollOffset(smallestIndex);
177+
// Expect the scroll offset to be approximately the same, meaning the scroll position is preserved relative to the item
178+
expect(scrollOffsetAfter).to.be.closeTo(scrollOffsetBefore, 1);
166179
}
167180

168181
const item = elementsContainer.querySelector('#item-0');
@@ -267,8 +280,7 @@ describe('unlimited size', () => {
267280
expect(item.getBoundingClientRect().top).to.be.closeTo(scrollTarget.getBoundingClientRect().top - 10, 1);
268281
});
269282

270-
// FIXME: Fails due to a scroll offset reset caused by _adjustVirtualIndexOffset on scroll event.
271-
it.skip('should preserve scroll position when size decrease does not affect any rendered indexes', async () => {
283+
it('should preserve scroll position when size decrease does not affect any rendered indexes', async () => {
272284
// Scroll to an index and add an additional scroll offset.
273285
const index = virtualizer.size - 2000;
274286
virtualizer.scrollToIndex(index);
@@ -282,8 +294,7 @@ describe('unlimited size', () => {
282294
expect(item.getBoundingClientRect().top).to.be.closeTo(scrollTarget.getBoundingClientRect().top - 10, 1);
283295
});
284296

285-
// FIXME: Fails due to a scroll offset reset caused by _adjustVirtualIndexOffset on scroll event.
286-
it.skip('should preserve scroll position on size increase', async () => {
297+
it('should preserve scroll position on size increase', async () => {
287298
const index = virtualizer.size - 2000;
288299
virtualizer.scrollToIndex(index);
289300
scrollTarget.scrollTop += 10;
@@ -294,4 +305,16 @@ describe('unlimited size', () => {
294305
const item = elementsContainer.querySelector(`#item-${index}`);
295306
expect(item.getBoundingClientRect().top).to.be.closeTo(scrollTarget.getBoundingClientRect().top - 10, 1);
296307
});
308+
309+
it('should preserve scroll position on large size decrease', async () => {
310+
const index = 300000;
311+
virtualizer.scrollToIndex(index);
312+
scrollTarget.scrollTop += 10;
313+
314+
virtualizer.size = 500000;
315+
await oneEvent(scrollTarget, 'scroll');
316+
317+
const item = elementsContainer.querySelector(`#item-${index}`);
318+
expect(item.getBoundingClientRect().top).to.be.closeTo(scrollTarget.getBoundingClientRect().top - 10, 1);
319+
});
297320
});

0 commit comments

Comments
 (0)