Skip to content

Commit 74fed35

Browse files
authored
fix: vscroll event mem leak causing unexpected behavior after filtering (#261)
- there was an event leak for the Virtual-Scroll which had the side effect of duplicating the scroll event everytime we were filtering, this indirectly caused weird behavior with the scroll and also items being skipped. I found the leak via the browser by investigating the events attached to the ul list element
1 parent 559b161 commit 74fed35

File tree

4 files changed

+56
-48
lines changed

4 files changed

+56
-48
lines changed

packages/demo/src/options/options36.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export default class Example {
77
mount() {
88
const data1 = [];
99
const data2 = [];
10-
for (let i = 0; i < 25; i++) {
10+
for (let i = 1; i <= 25; i++) {
1111
data1.push({ text: `Title ${i}`, value: i });
1212
}
13-
for (let i = 0; i < 2000; i++) {
13+
for (let i = 1; i <= 2000; i++) {
1414
data2.push({ text: `<i class="fa fa-star"></i> Task ${i}`, value: i });
1515
}
1616

packages/multiple-select-vanilla/src/MultipleSelectInstance.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,7 @@ export class MultipleSelectInstance {
460460
offset = -1;
461461
}
462462

463-
if (this.options.virtualScroll && rows.length > Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS) {
464-
this.virtualScroll?.destroy();
465-
463+
if (rows.length > Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS) {
466464
const dropVisible = this.dropElm.style.display !== 'none';
467465
if (!dropVisible) {
468466
this.dropElm.style.left = '-10000';
@@ -480,8 +478,9 @@ export class MultipleSelectInstance {
480478
this.updateDataStart = 0;
481479
this._currentHighlightIndex = 0;
482480
}
483-
if (this.updateDataEnd > this.getDataLength()) {
484-
this.updateDataEnd = this.getDataLength();
481+
const dataLn = this.getDataLength();
482+
if (this.updateDataEnd > dataLn) {
483+
this.updateDataEnd = dataLn;
485484
}
486485

487486
if (this.ulElm) {
@@ -495,18 +494,21 @@ export class MultipleSelectInstance {
495494
};
496495

497496
if (this.ulElm) {
498-
this.virtualScroll = new VirtualScroll({
499-
rows,
500-
scrollEl: this.ulElm,
501-
contentEl: this.ulElm,
502-
sanitizer: this.options.sanitizer,
503-
callback: () => {
504-
updateDataOffset();
505-
this.events();
506-
},
507-
});
497+
if (!this.virtualScroll) {
498+
this.virtualScroll = new VirtualScroll({
499+
rows,
500+
scrollEl: this.ulElm,
501+
contentEl: this.ulElm,
502+
sanitizer: this.options.sanitizer,
503+
callback: () => {
504+
updateDataOffset();
505+
this.events();
506+
},
507+
});
508+
} else {
509+
this.virtualScroll.reset(rows);
510+
}
508511
}
509-
510512
updateDataOffset();
511513

512514
if (!dropVisible) {
@@ -521,7 +523,6 @@ export class MultipleSelectInstance {
521523
}
522524
this.updateDataStart = 0;
523525
this.updateDataEnd = this.updateData.length;
524-
this.virtualScroll = null;
525526
}
526527

527528
this.events();

packages/multiple-select-vanilla/src/services/virtual-scroll.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import type { HtmlStruct, VirtualCache, VirtualScrollOption } from '../interface
33
import { convertItemRowToHtml, emptyElement } from '../utils';
44

55
export class VirtualScroll {
6-
cache: VirtualCache;
7-
clusterRows?: number;
6+
protected clusterRows?: number;
7+
protected cache: VirtualCache;
8+
protected scrollEl: HTMLElement;
9+
protected blockHeight?: number;
10+
protected clusterHeight?: number;
11+
protected contentEl: HTMLElement;
12+
protected parentEl: HTMLElement | null;
13+
protected itemHeight?: number;
14+
protected lastCluster: number;
15+
protected scrollTop: number;
816
dataStart!: number;
917
dataEnd!: number;
1018
rows: HtmlStruct[];
11-
scrollEl: HTMLElement;
12-
blockHeight?: number;
13-
clusterHeight?: number;
14-
contentEl: HTMLElement;
15-
parentEl: HTMLElement | null;
16-
itemHeight?: number;
17-
lastCluster: number;
18-
scrollTop: number;
1919
destroy: () => void;
2020
callback: () => void;
2121
sanitizer?: (dirtyHtml: string) => string;
@@ -49,7 +49,14 @@ export class VirtualScroll {
4949
};
5050
}
5151

52-
initDOM(rows: HtmlStruct[]) {
52+
reset(rows: HtmlStruct[]) {
53+
this.lastCluster = 0;
54+
this.cache = {} as any;
55+
emptyElement(this.contentEl);
56+
this.initDOM(rows);
57+
}
58+
59+
protected initDOM(rows: HtmlStruct[]) {
5360
if (typeof this.clusterHeight === 'undefined') {
5461
this.cache.scrollTop = this.scrollEl.scrollTop;
5562
const firstRowElm = convertItemRowToHtml(rows[0]);
@@ -82,7 +89,7 @@ export class VirtualScroll {
8289
}
8390
}
8491

85-
getRowsHeight() {
92+
protected getRowsHeight() {
8693
if (typeof this.itemHeight === 'undefined') {
8794
// make sure parent is not hidden before reading item list height
8895
const prevParentDisplay = this.parentEl?.style.display || '';
@@ -101,7 +108,7 @@ export class VirtualScroll {
101108
this.clusterHeight = this.blockHeight * Constants.CLUSTER_BLOCKS;
102109
}
103110

104-
getNum() {
111+
protected getNum() {
105112
this.scrollTop = this.scrollEl.scrollTop;
106113
const blockSize = (this.clusterHeight || 0) - (this.blockHeight || 0);
107114
if (blockSize) {
@@ -110,7 +117,7 @@ export class VirtualScroll {
110117
return 0;
111118
}
112119

113-
initData(rows: HtmlStruct[], num: number) {
120+
protected initData(rows: HtmlStruct[], num: number) {
114121
if (rows.length < Constants.BLOCK_ROWS) {
115122
return {
116123
topOffset: 0,
@@ -143,13 +150,13 @@ export class VirtualScroll {
143150
};
144151
}
145152

146-
checkChanges<T extends keyof VirtualCache>(type: T, value: VirtualCache[T]) {
153+
protected checkChanges<T extends keyof VirtualCache>(type: T, value: VirtualCache[T]) {
147154
const changed = value !== this.cache[type];
148155
this.cache[type] = value;
149156
return changed;
150157
}
151158

152-
getExtra(className: string, height: number) {
159+
protected getExtra(className: string, height: number) {
153160
const tag = document.createElement('li');
154161
tag.className = `virtual-scroll-${className}`;
155162
if (height) {

playwright/e2e/options36.spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,49 @@ test.describe('Options 36 - Infinite Scroll', () => {
99

1010
const ulElm1 = await page.locator('[data-test="select1"] .ms-drop ul');
1111
const liElms1 = await page.locator('[data-test="select1"] .ms-drop ul li');
12-
await expect(liElms1.nth(0)).toContainText('Title 0');
12+
await expect(liElms1.nth(0)).toContainText('Title 1');
1313
await liElms1.nth(0).click();
14-
await expect(page.locator('[data-test=select1].ms-parent .ms-choice span')).toHaveText('Title 0');
14+
await expect(page.locator('[data-test=select1].ms-parent .ms-choice span')).toHaveText('Title 1');
1515

1616
// scroll near the end of the list
1717
await page.locator('[data-test="select1"].ms-parent').click();
1818
await ulElm1.evaluate(e => (e.scrollTop = e.scrollHeight - 10));
19-
await page.locator('[data-test="select1"] .ms-drop label').filter({ hasText: 'Title 24' }).click();
19+
await page.locator('[data-test="select1"] .ms-drop label').filter({ hasText: 'Title 25' }).click();
2020

2121
// scroll completely to the end of the list & expect scrolling back to top
2222
await page.locator('[data-test="select1"].ms-parent').click();
2323
await ulElm1.evaluate(e => (e.scrollTop = e.scrollHeight));
2424
const firstTitleLoc = await page.locator('div[data-test=select1] .ms-drop li:nth-of-type(1)');
25-
await expect(firstTitleLoc).toContainText('Title 0');
25+
await expect(firstTitleLoc).toContainText('Title 1');
2626
await expect(firstTitleLoc).toHaveClass('hide-radio highlighted');
2727
await page.keyboard.press('Enter');
2828

2929
// -- 2nd Select
3030
await page.locator('[data-test=select2].ms-parent').click();
3131
const ulElm2 = await page.locator('[data-test="select2"] .ms-drop ul');
3232
const liElms2 = await page.locator('[data-test="select2"] .ms-drop ul li');
33-
await expect(await liElms2.nth(4).locator('span').innerHTML()).toBe('<i class="fa fa-star"></i> Task 4');
33+
await expect(await liElms2.nth(4).locator('span').innerHTML()).toBe('<i class="fa fa-star"></i> Task 5');
3434
await liElms2.nth(4).click();
35-
await expect(await liElms2.nth(5).locator('span').innerHTML()).toBe('<i class="fa fa-star"></i> Task 5');
35+
await expect(await liElms2.nth(5).locator('span').innerHTML()).toBe('<i class="fa fa-star"></i> Task 6');
3636
await liElms2.nth(5).click();
37-
await page.getByRole('button', { name: '4, 5' }).click();
37+
await page.getByRole('button', { name: '5, 6' }).click();
3838

3939
// scroll to the middle and click 1003
4040
await page.locator('[data-test="select2"].ms-parent').click();
4141
await ulElm2.evaluate(e => (e.scrollTop = e.scrollHeight / 2));
4242
await page.locator('[data-test="select2"] .ms-drop label').filter({ hasText: '1003' }).click();
43-
await page.getByRole('button', { name: '4, 5, 1003' });
43+
await page.getByRole('button', { name: '5, 6, 1003' });
4444

4545
// scroll to near the end and select last 2 labels
4646
await ulElm2.evaluate(e => (e.scrollTop = e.scrollHeight - 300));
4747
await expect(await page.locator('[data-test="select2"] .ms-drop li[data-key=option_1995] label span').innerHTML()).toBe(
48-
'<i class="fa fa-star"></i> Task 1995',
48+
'<i class="fa fa-star"></i> Task 1996',
4949
);
5050
await expect(await page.locator('[data-test="select2"] .ms-drop li[data-key=option_1996] label span').innerHTML()).toBe(
51-
'<i class="fa fa-star"></i> Task 1996',
51+
'<i class="fa fa-star"></i> Task 1997',
5252
);
53-
await page.locator('[data-test="select2"] .ms-drop label').filter({ hasText: '1995' }).click();
5453
await page.locator('[data-test="select2"] .ms-drop label').filter({ hasText: '1996' }).click();
54+
await page.locator('[data-test="select2"] .ms-drop label').filter({ hasText: '1997' }).click();
5555
await page.getByRole('button', { name: '5 of 2000 selected' });
5656

5757
// pressing arrow down until we reach the end will scroll back to top of the list
@@ -60,10 +60,10 @@ test.describe('Options 36 - Infinite Scroll', () => {
6060
page.keyboard.press('ArrowDown');
6161
await expect(await page.locator('[data-test="select2"] .ms-drop li[data-key=option_1999]')).toHaveClass('highlighted');
6262

63-
page.keyboard.press('ArrowDown'); // Task 0 (scrolled back to top)
63+
page.keyboard.press('ArrowDown'); // Task 1 (scrolled back to top)
6464

6565
const firstTaskLoc = await page.locator('div[data-test=select2] .ms-drop li:nth-of-type(1)');
66-
await expect(firstTaskLoc).toContainText('Task 0');
66+
await expect(firstTaskLoc).toContainText('Task 1');
6767
// await expect(await page.locator('[data-test="select2"] .ms-drop li[data-key=option_0]')).toHaveClass('highlighted');
6868
});
6969
});

0 commit comments

Comments
 (0)