Skip to content

Commit 3a5c65f

Browse files
committed
fix(infinite-scroll): awaiting for DOM writes before firing infinite scroll event
1 parent 76aa3fb commit 3a5c65f

File tree

3 files changed

+49
-40
lines changed

3 files changed

+49
-40
lines changed

core/src/components/infinite-scroll/infinite-scroll.tsx

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,18 @@ export class InfiniteScroll implements ComponentInterface {
143143

144144
if (distanceFromInfinite < 0) {
145145
if (!this.didFire) {
146+
this.isLoading = true;
147+
this.didFire = true;
148+
146149
if (this.preserveRerenderScrollPosition) {
147150
// Lock the min height of the siblings of the infinite scroll
148151
// if we are preserving the rerender scroll position
149-
this.lockSiblingMinHeight(true);
152+
this.lockSiblingMinHeight(true).then(() => {
153+
this.ionInfinite.emit();
154+
});
155+
} else {
156+
this.ionInfinite.emit();
150157
}
151-
152-
this.isLoading = true;
153-
this.didFire = true;
154-
this.ionInfinite.emit();
155158
return 3;
156159
}
157160
}
@@ -168,32 +171,44 @@ export class InfiniteScroll implements ComponentInterface {
168171
* We preserve existing min-height values, if they're set, so we don't erase what
169172
* has been previously set by the user when we restore after complete is called.
170173
*/
171-
private lockSiblingMinHeight(lock: boolean) {
172-
const siblings = this.el.parentElement?.children || [];
173-
for (const sibling of siblings) {
174-
// Loop through all the siblings of the infinite scroll, but ignore ourself
175-
if (sibling !== this.el && sibling instanceof HTMLElement) {
176-
if (lock) {
177-
const elementHeight = sibling.getBoundingClientRect().height;
178-
if (this.minHeightLocked) {
179-
// The previous min height is from us locking it before, so we can disregard it
180-
// We still need to lock the min height if we're already locked, though, because
181-
// the user could have triggered a new load before we've finished the previous one.
182-
const previousMinHeight = sibling.style.minHeight;
183-
if (previousMinHeight) {
184-
sibling.style.setProperty('--ion-previous-min-height', previousMinHeight);
185-
}
174+
private lockSiblingMinHeight(lock: boolean): Promise<void> {
175+
return new Promise((resolve) => {
176+
const siblings = this.el.parentElement?.children || [];
177+
const writes: (() => void)[] = [];
178+
179+
for (const sibling of siblings) {
180+
// Loop through all the siblings of the infinite scroll, but ignore ourself
181+
if (sibling !== this.el && sibling instanceof HTMLElement) {
182+
if (lock) {
183+
const elementHeight = sibling.getBoundingClientRect().height;
184+
writes.push(() => {
185+
if (this.minHeightLocked) {
186+
// The previous min height is from us locking it before, so we can disregard it
187+
// We still need to lock the min height if we're already locked, though, because
188+
// the user could have triggered a new load before we've finished the previous one.
189+
const previousMinHeight = sibling.style.minHeight;
190+
if (previousMinHeight) {
191+
sibling.style.setProperty('--ion-previous-min-height', previousMinHeight);
192+
}
193+
}
194+
sibling.style.minHeight = `${elementHeight}px`;
195+
});
196+
} else {
197+
writes.push(() => {
198+
const previousMinHeight = sibling.style.getPropertyValue('--ion-previous-min-height');
199+
sibling.style.minHeight = previousMinHeight || 'auto';
200+
sibling.style.removeProperty('--ion-previous-min-height');
201+
});
186202
}
187-
sibling.style.minHeight = `${elementHeight}px`;
188-
} else {
189-
const previousMinHeight = sibling.style.getPropertyValue('--ion-previous-min-height');
190-
sibling.style.minHeight = previousMinHeight || 'auto';
191-
sibling.style.removeProperty('--ion-previous-min-height');
192203
}
193204
}
194-
}
195205

196-
this.minHeightLocked = lock;
206+
writeTask(() => {
207+
writes.forEach((w) => w());
208+
this.minHeightLocked = lock;
209+
resolve();
210+
});
211+
});
197212
}
198213

199214
/**
@@ -264,8 +279,8 @@ export class InfiniteScroll implements ComponentInterface {
264279
// Unlock the min height of the siblings of the infinite scroll
265280
// if we are preserving the rerender scroll position
266281
if (this.preserveRerenderScrollPosition) {
267-
setTimeout(() => {
268-
this.lockSiblingMinHeight(false);
282+
setTimeout(async () => {
283+
await this.lockSiblingMinHeight(false);
269284
}, 100);
270285
}
271286
}
@@ -298,3 +313,4 @@ export class InfiniteScroll implements ComponentInterface {
298313
);
299314
}
300315
}
316+

core/src/components/infinite-scroll/test/preserve-rerender-scroll-position/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
}
6868
loading = true;
6969

70-
await wait(300);
70+
await wait(0);
7171
replaceAllItems();
7272
infiniteScroll.complete();
7373

@@ -99,7 +99,7 @@
9999

100100
// Add new items with new "keys" (different content/identifiers)
101101
// Start with more items to ensure scrollable content
102-
const totalItems = generation === 1 ? 30 : 30 + generation * 20;
102+
const totalItems = generation === 1 ? 50 : 30 + generation * 20;
103103
itemCount = 0;
104104

105105
for (let i = 0; i < totalItems; i++) {

core/src/components/infinite-scroll/test/preserve-rerender-scroll-position/infinite-scroll.e2e.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
1212
const content = page.locator('ion-content');
1313
const items = page.locator('ion-item');
1414
const innerScroll = page.locator('.inner-scroll');
15-
expect(await items.count()).toBe(30);
15+
expect(await items.count()).toBe(50);
1616

1717
let previousScrollTop = 0;
18-
for (let i = 0; i < 20; i++) {
18+
for (let i = 0; i < 30; i++) {
1919
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
2020
const currentScrollTop = await innerScroll.evaluate((el: HTMLIonContentElement) => el.scrollTop);
2121
expect(currentScrollTop).toBeGreaterThan(previousScrollTop);
@@ -26,13 +26,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
2626
previousScrollTop
2727
);
2828
previousScrollTop = currentScrollTop;
29-
30-
// Timeout to allow the browser to catch up.
31-
// For some reason, without this, the scroll top gets reset to 0. Adding this
32-
// prevents that, which implies it's an issue with Playwright, not the feature.
33-
// For some reason, this delay needs to be longer than the time required to
34-
// reset the minimum height in infinite scroll.
35-
await new Promise((resolve) => setTimeout(resolve, 1001));
3629
}
3730
});
3831
});

0 commit comments

Comments
 (0)