diff --git a/packages/demo/src/examples/example10.html b/packages/demo/src/examples/example10.html index 30f5bae5..dd74de0c 100644 --- a/packages/demo/src/examples/example10.html +++ b/packages/demo/src/examples/example10.html @@ -19,17 +19,24 @@

Virtual scroll will be used with a large set of data
-
-
-
+ +
+ + +
+ +
+
+ \ No newline at end of file diff --git a/packages/demo/src/examples/example10.ts b/packages/demo/src/examples/example10.ts index ae622eb7..b9a91a47 100644 --- a/packages/demo/src/examples/example10.ts +++ b/packages/demo/src/examples/example10.ts @@ -2,23 +2,37 @@ import { multipleSelect, MultipleSelectInstance } from 'multiple-select-vanilla' export default class Example { ms1?: MultipleSelectInstance; + ms2?: MultipleSelectInstance; mount() { - const data = []; + const data1 = []; + const data2 = []; for (let i = 0; i < 10000; i++) { - data.push(i); + data1.push(i); } + for (let i = 0; i < 10000; i++) { + data2.push({ text: ` Task ${i}`, value: i }); + } + + this.ms1 = multipleSelect('#select1', { + filter: true, + data: data1, + showSearchClear: true, + }) as MultipleSelectInstance; - this.ms1 = multipleSelect('#select', { + this.ms2 = multipleSelect('#select2', { filter: true, - data, + data: data2, showSearchClear: true, + useSelectOptionLabelToHtml: true, }) as MultipleSelectInstance; } unmount() { // destroy ms instance(s) to avoid DOM leaks this.ms1?.destroy(); + this.ms2?.destroy(); this.ms1 = undefined; + this.ms2 = undefined; } } diff --git a/packages/demo/src/main.html b/packages/demo/src/main.html index 685f721c..60d7fa4d 100644 --- a/packages/demo/src/main.html +++ b/packages/demo/src/main.html @@ -23,7 +23,6 @@ diff --git a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts index caae32cf..80592204 100644 --- a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts +++ b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts @@ -503,47 +503,47 @@ export class MultipleSelectInstance { protected getListRows(): HtmlStruct[] { const rows: HtmlStruct[] = []; this.updateData = []; - this.data?.forEach((row) => rows.push(...this.initListItem(row))); + this.data?.forEach((dataRow) => rows.push(...this.initListItem(dataRow))); rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound(), tabIndex: 0 } }); return rows; } - protected initListItem(row: OptionRowData | OptGroupRowData, level = 0): HtmlStruct[] { - const title = row?.title || ''; + protected initListItem(dataRow: OptionRowData | OptGroupRowData, level = 0): HtmlStruct[] { + const title = dataRow?.title || ''; const multiple = this.options.multiple ? 'multiple' : ''; const type = this.options.single ? 'radio' : 'checkbox'; let classes = ''; - if (!row?.visible) { + if (!dataRow?.visible) { return []; } - this.updateData.push(row); + this.updateData.push(dataRow); if (this.options.single && !this.options.singleRadio) { classes = 'hide-radio '; } - if (row.selected) { + if (dataRow.selected) { classes += 'selected '; } - if (row.type === 'optgroup') { + if (dataRow.type === 'optgroup') { // - group option row - const htmlBlocks: HtmlStruct[] = []; const itemOrGroupBlock: HtmlStruct = this.options.hideOptgroupCheckboxes || this.options.single - ? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: row._key } } } + ? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: dataRow._key } } } : { tagName: 'input', props: { type: 'checkbox', - dataset: { name: this.selectGroupName, key: row._key }, - ariaChecked: String(row.selected || false), - checked: !!row.selected, - disabled: row.disabled, + dataset: { name: this.selectGroupName, key: dataRow._key }, + ariaChecked: String(dataRow.selected || false), + checked: !!dataRow.selected, + disabled: dataRow.disabled, tabIndex: -1, }, }; @@ -553,25 +553,24 @@ export class MultipleSelectInstance { } const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} }; - this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptGroupRowData).label); - + this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptGroupRowData).label); const liBlock: HtmlStruct = { tagName: 'li', props: { className: `group ${classes}`.trim(), - tabIndex: classes.includes('hide-radio') || row.disabled ? -1 : 0, + tabIndex: classes.includes('hide-radio') || dataRow.disabled ? -1 : 0, }, children: [ { tagName: 'label', - props: { className: `optgroup${this.options.single || row.disabled ? ' disabled' : ''}` }, + props: { className: `optgroup${this.options.single || dataRow.disabled ? ' disabled' : ''}` }, children: [itemOrGroupBlock, spanLabelBlock], }, ], }; - const customStyleRules = this.options.cssStyler(row); - const customStylerStr = String(this.options.styler(row) || ''); // deprecated + const customStyleRules = this.options.cssStyler(dataRow); + const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated if (customStylerStr) { liBlock.props.style = convertStringStyleToElementStyle(customStylerStr); } @@ -580,52 +579,51 @@ export class MultipleSelectInstance { } htmlBlocks.push(liBlock); - (row as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1))); + (dataRow as OptGroupRowData).children.forEach((child) => htmlBlocks.push(...this.initListItem(child, 1))); return htmlBlocks; } // - regular row - - classes += row.classes || ''; + classes += dataRow.classes || ''; if (level && this.options.single) { classes += `option-level-${level} `; } - if (row.divider) { + if (dataRow.divider) { return [{ tagName: 'li', props: { className: 'option-divider' } } as HtmlStruct]; } const liClasses = multiple || classes ? (multiple + classes).trim() : ''; - const labelClasses = `${row.disabled ? 'disabled' : ''}`; + const labelClasses = `${dataRow.disabled ? 'disabled' : ''}`; const spanLabelBlock: HtmlStruct = { tagName: 'span', props: {} }; - this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (row as OptionRowData).text); - + this.applyAsTextOrHtmlWhenEnabled(spanLabelBlock.props, (dataRow as OptionRowData).text); const inputBlock: HtmlStruct = { tagName: 'input', props: { type, - value: encodeURI(row.value as string), - dataset: { key: row._key, name: this.selectItemName }, - ariaChecked: String(row.selected || false), - checked: !!row.selected, - disabled: !!row.disabled, + value: encodeURI(dataRow.value as string), + dataset: { key: dataRow._key, name: this.selectItemName }, + ariaChecked: String(dataRow.selected || false), + checked: !!dataRow.selected, + disabled: !!dataRow.disabled, tabIndex: -1, }, }; - if (row.selected) { + if (dataRow.selected) { inputBlock.attrs = { checked: 'checked' }; } const liBlock: HtmlStruct = { tagName: 'li', - props: { className: liClasses, title, tabIndex: row.disabled ? -1 : 0, dataset: { key: row._key } }, + props: { className: liClasses, title, tabIndex: dataRow.disabled ? -1 : 0, dataset: { key: dataRow._key } }, children: [{ tagName: 'label', props: { className: labelClasses }, children: [inputBlock, spanLabelBlock] }], }; - const customStyleRules = this.options.cssStyler(row); - const customStylerStr = String(this.options.styler(row) || ''); // deprecated + const customStyleRules = this.options.cssStyler(dataRow); + const customStylerStr = String(this.options.styler(dataRow) || ''); // deprecated if (customStylerStr) { liBlock.props.style = convertStringStyleToElementStyle(customStylerStr); } diff --git a/packages/multiple-select-vanilla/src/utils/domUtils.ts b/packages/multiple-select-vanilla/src/utils/domUtils.ts index c6d02d6d..9e21bafb 100644 --- a/packages/multiple-select-vanilla/src/utils/domUtils.ts +++ b/packages/multiple-select-vanilla/src/utils/domUtils.ts @@ -92,21 +92,21 @@ export function createDomElement { test('select should use virtual scroll', async ({ page }) => { await page.goto('#/example10'); - await page.locator('[data-test="select10"].ms-parent').click(); - - const ulElm = await page.locator('.ms-drop ul'); - const liElms = await page.locator('.ms-drop ul li'); - await expect(liElms.nth(0)).toContainText('0'); - await liElms.nth(0).click(); - await expect(liElms.nth(1)).toContainText('1'); - await liElms.nth(1).click(); + + // -- 1st Select + await page.locator('[data-test="select10-1"].ms-parent').click(); + + const ulElm1 = await page.locator('[data-test="select10-1"] .ms-drop ul'); + const liElms1 = await page.locator('[data-test="select10-1"] .ms-drop ul li'); + await expect(liElms1.nth(0)).toContainText('0'); + await liElms1.nth(0).click(); + await expect(liElms1.nth(1)).toContainText('1'); + await liElms1.nth(1).click(); await page.getByRole('button', { name: '0, 1' }).click(); // scroll to the middle and click 5001 - await page.locator('[data-test="select10"].ms-parent').click(); - await ulElm.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); - await page.locator('label').filter({ hasText: '5001' }).click(); + await page.locator('[data-test="select10-1"].ms-parent').click(); + await ulElm1.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); + await page.locator('[data-test="select10-1"] .ms-drop label').filter({ hasText: '5001' }).click(); await page.getByRole('button', { name: '0, 1, 5001' }); // scroll to the end and select last 2 labels - await ulElm.evaluate((e) => (e.scrollTop = e.scrollHeight)); + await ulElm1.evaluate((e) => (e.scrollTop = e.scrollHeight)); await page.locator('label').filter({ hasText: '9998' }).click(); await page.locator('label').filter({ hasText: '9999' }).click(); await page.getByRole('button', { name: '5 of 10000 selected' }); // filter with text 999 and expect 9998 & 9999 to show up - await page.getByPlaceholder('🔎︎').click(); - await page.getByPlaceholder('🔎︎').fill('999'); + await page.getByRole('textbox', { name: '🔎︎' }).fill('999'); await page.locator('label').filter({ hasText: '9998' }).click(); await page.locator('label').filter({ hasText: '9999' }).click(); await page.getByRole('button', { name: '0, 1, 5001' }).click(); // clear filter, scroll back to top and expect 0,1 to still be checked - await page.locator('[data-test="select10"].ms-parent').click(); - await page.locator('[data-test="select10"] .ms-search .icon-close').click(); - await ulElm.evaluate((e) => (e.scrollTop = 0)); - await expect(liElms.nth(0)).toContainText('0'); - await expect(liElms.nth(1)).toContainText('1'); - expect(await liElms.nth(0).locator('input[type=checkbox][data-key=option_0]').isChecked()).toBeTruthy(); - expect(await liElms.nth(1).locator('input[type=checkbox][data-key=option_1]').isChecked()).toBeTruthy(); - expect(await liElms.nth(2).locator('input[type=checkbox][data-key=option_2]').isChecked()).toBeFalsy(); + await page.locator('[data-test="select10-1"].ms-parent').click(); + await page.locator('[data-test="select10-1"] .ms-search .icon-close').click(); + await ulElm1.evaluate((e) => (e.scrollTop = 0)); + await expect(liElms1.nth(0)).toContainText('0'); + await expect(liElms1.nth(1)).toContainText('1'); + expect(await liElms1.nth(0).locator('input[type=checkbox][data-key=option_0]').isChecked()).toBeTruthy(); + expect(await liElms1.nth(1).locator('input[type=checkbox][data-key=option_1]').isChecked()).toBeTruthy(); + expect(await liElms1.nth(2).locator('input[type=checkbox][data-key=option_2]').isChecked()).toBeFalsy(); // scroll back to middle and expect 5001 to still be checked - await ulElm.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); + await ulElm1.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); expect(await page.locator('label').filter({ hasText: '5001' })).toBeVisible(); - expect(await liElms.locator('input[type=checkbox][data-key=option_5001]').isChecked()).toBeTruthy(); + expect(await liElms1.locator('input[type=checkbox][data-key=option_5001]').isChecked()).toBeTruthy(); + await page.locator('[data-test=select10-1].ms-parent').click(); // close drop + + // -- 2nd Select + await page.locator('[data-test=select10-2].ms-parent').click(); + const ulElm2 = await page.locator('[data-test="select10-2"] .ms-drop ul'); + const liElms2 = await page.locator('[data-test="select10-2"] .ms-drop ul li'); + await expect(await liElms2.nth(4).locator('span').innerHTML()).toBe(' Task 4'); + await liElms2.nth(4).click(); + await expect(await liElms2.nth(5).locator('span').innerHTML()).toBe(' Task 5'); + await liElms2.nth(5).click(); + await page.getByRole('button', { name: '4, 5' }).click(); + + // scroll to the middle and click 5003 + await page.locator('[data-test="select10-2"].ms-parent').click(); + await ulElm2.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); + await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '5003' }).click(); + await page.getByRole('button', { name: '4, 5, 5003' }); + + // scroll to the end and select last 2 labels + await ulElm2.evaluate((e) => (e.scrollTop = e.scrollHeight)); + await expect(await page.locator('[data-test="select10-2"] .ms-drop li[data-key=option_9995] label span').innerHTML()).toBe( + ' Task 9995' + ); + await expect(await page.locator('[data-test="select10-2"] .ms-drop li[data-key=option_9996] label span').innerHTML()).toBe( + ' Task 9996' + ); + await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '9995' }).click(); + await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '9996' }).click(); + await page.getByRole('button', { name: '5 of 10000 selected' }); + + // filter with text 999 and expect 9995 & 9996 to show up + await page.getByRole('textbox', { name: '🔎︎' }).fill('999'); + await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '9995' }).click(); + await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '9996' }).click(); + await page.getByRole('button', { name: '4, 5, 5003' }).click(); + + // clear filter, scroll back to top and expect 0,1 to still be checked + await page.locator('[data-test="select10-2"].ms-parent').click(); + await page.locator('[data-test="select10-2"] .ms-search .icon-close').click(); + await ulElm2.evaluate((e) => (e.scrollTop = 0)); + await expect(await liElms2.nth(4).locator('span').innerHTML()).toBe(' Task 4'); + await expect(await liElms2.nth(5).locator('span').innerHTML()).toBe(' Task 5'); + expect(await liElms2.nth(4).locator('input[type=checkbox][data-key=option_4]').isChecked()).toBeTruthy(); + expect(await liElms2.nth(5).locator('input[type=checkbox][data-key=option_5]').isChecked()).toBeTruthy(); + expect(await liElms2.nth(6).locator('input[type=checkbox][data-key=option_6]').isChecked()).toBeFalsy(); + + // scroll back to middle and expect 5003 to still be checked + await ulElm2.evaluate((e) => (e.scrollTop = e.scrollHeight / 2)); + expect(await page.locator('[data-test="select10-2"] .ms-drop label').filter({ hasText: '5003' })).toBeVisible(); + expect(await liElms2.locator('input[type=checkbox][data-key=option_5003]').isChecked()).toBeTruthy(); + await page.locator('[data-test=select10-2].ms-parent').click(); // close drop }); });