diff --git a/lib/src/MultipleSelectInstance.ts b/lib/src/MultipleSelectInstance.ts index a84ac752..3dd2409f 100644 --- a/lib/src/MultipleSelectInstance.ts +++ b/lib/src/MultipleSelectInstance.ts @@ -34,6 +34,7 @@ export class MultipleSelectInstance { protected dropElm!: HTMLDivElement; protected okButtonElm?: HTMLButtonElement; protected filterParentElm?: HTMLDivElement | null; + protected lastFocusedItemKey = ''; protected ulElm?: HTMLUListElement | null; protected parentElm!: HTMLDivElement; protected labelElm?: HTMLLabelElement | null; @@ -42,13 +43,12 @@ export class MultipleSelectInstance { protected searchInputElm?: HTMLInputElement | null; protected selectGroupElms?: NodeListOf; protected selectItemElms?: NodeListOf; - protected disableItemElms?: NodeListOf; protected noResultsElm?: HTMLDivElement | null; protected options: MultipleSelectOption; protected selectAllName = ''; protected selectGroupName = ''; protected selectItemName = ''; - protected tabIndex?: string | null; + protected tabIndex?: number; protected updateDataStart?: number; protected updateDataEnd?: number; protected virtualScroll?: VirtualScroll | null; @@ -186,17 +186,17 @@ export class MultipleSelectInstance { // add placeholder to choice button this.options.placeholder = this.options.placeholder || this.elm.getAttribute('placeholder') || ''; - this.tabIndex = this.elm.getAttribute('tabindex'); - let tabIndex = ''; + this.tabIndex = this.elm.tabIndex; + let tabIndex: number | undefined; if (this.tabIndex !== null) { this.elm.tabIndex = -1; - tabIndex = this.tabIndex && `tabindex="${this.tabIndex}"`; + tabIndex = this.tabIndex; } this.choiceElm = createDomElement('button', { className: 'ms-choice', type: 'button' }, this.parentElm); - if (isNaN(tabIndex as any)) { - this.choiceElm.tabIndex = +tabIndex; + if (tabIndex !== undefined) { + this.choiceElm.tabIndex = tabIndex; } this.choiceElm.appendChild(createDomElement('span', { className: 'ms-placeholder', textContent: this.options.placeholder })); @@ -240,19 +240,25 @@ export class MultipleSelectInstance { this.selectItemName = `selectItem${name}`; if (!this.options.keepOpen) { - this._bindEventService.unbind(document.body, 'click'); - this._bindEventService.bind(document.body, 'click', ((e: MouseEvent & { target: HTMLElement }) => { - if (e.target === this.choiceElm || findParent(e.target, '.ms-choice') === this.choiceElm) { - return; - } + this._bindEventService.unbindAll('body-click'); + this._bindEventService.bind( + document.body, + 'click', + ((e: MouseEvent & { target: HTMLElement }) => { + if (e.target === this.choiceElm || findParent(e.target, '.ms-choice') === this.choiceElm) { + return; + } - if ( - (e.target === this.dropElm || (findParent(e.target, '.ms-drop') !== this.dropElm && e.target !== this.elm)) && - this.options.isOpen - ) { - this.close(); - } - }) as EventListener); + if ( + (e.target === this.dropElm || (findParent(e.target, '.ms-drop') !== this.dropElm && e.target !== this.elm)) && + this.options.isOpen + ) { + this.close(); + } + }) as EventListener, + undefined, + 'body-click' + ); } } @@ -394,7 +400,7 @@ export class MultipleSelectInstance { if (this.options.selectAll && !this.options.single) { const selectName = this.elm.getAttribute('name') || this.options.name || ''; - this.selectAllParentElm = createDomElement('div', { className: 'ms-select-all' }); + this.selectAllParentElm = createDomElement('div', { className: 'ms-select-all', tabIndex: 0 }); const saLabelElm = document.createElement('label'); createDomElement( 'input', @@ -403,6 +409,7 @@ export class MultipleSelectInstance { ariaChecked: String(this.allSelected), checked: this.allSelected, dataset: { name: `selectAll${selectName}` }, + tabIndex: -1, }, saLabelElm ); @@ -491,7 +498,7 @@ export class MultipleSelectInstance { const rows: HtmlStruct[] = []; this.updateData = []; this.data?.forEach((row) => rows.push(...this.initListItem(row))); - rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound() } }); + rows.push({ tagName: 'li', props: { className: 'ms-no-results', textContent: this.formatNoMatchesFound(), tabIndex: 0 } }); return rows; } @@ -520,7 +527,7 @@ export class MultipleSelectInstance { // - group option row - const htmlBlocks: HtmlStruct[] = []; - const groupBlock: HtmlStruct = + const itemOrGroupBlock: HtmlStruct = this.options.hideOptgroupCheckboxes || this.options.single ? { tagName: 'span', props: { dataset: { name: this.selectGroupName, key: row._key } } } : { @@ -531,6 +538,7 @@ export class MultipleSelectInstance { ariaChecked: String(row.selected || false), checked: Boolean(row.selected), disabled: row.disabled, + tabIndex: -1, }, }; @@ -543,12 +551,15 @@ export class MultipleSelectInstance { const liBlock: HtmlStruct = { tagName: 'li', - props: { className: `group ${classes}`.trim() }, + props: { + className: `group ${classes}`.trim(), + tabIndex: classes.includes('hide-radio') || row.disabled ? -1 : 0, + }, children: [ { tagName: 'label', props: { className: `optgroup${this.options.single || row.disabled ? ' disabled' : ''}` }, - children: [groupBlock, spanLabelBlock], + children: [itemOrGroupBlock, spanLabelBlock], }, ], }; @@ -593,6 +604,7 @@ export class MultipleSelectInstance { ariaChecked: String(row.selected || false), checked: Boolean(row.selected), disabled: Boolean(row.disabled), + tabIndex: -1, }, }; @@ -602,7 +614,7 @@ export class MultipleSelectInstance { const liBlock: HtmlStruct = { tagName: 'li', - props: { className: liClasses, title }, + props: { className: liClasses, title, tabIndex: row.disabled ? -1 : 0, dataset: { key: row._key } }, children: [{ tagName: 'label', props: { className: labelClasses }, children: [inputBlock, spanLabelBlock] }], }; @@ -673,13 +685,13 @@ export class MultipleSelectInstance { } protected events() { - this._bindEventService.unbind(this.okButtonElm); - this._bindEventService.unbind(this.searchInputElm); - this._bindEventService.unbind(this.selectAllElm); - this._bindEventService.unbind(this.selectGroupElms); - this._bindEventService.unbind(this.selectItemElms); - this._bindEventService.unbind(this.disableItemElms); - this._bindEventService.unbind(this.noResultsElm); + this._bindEventService.unbindAll([ + 'ok-button', + 'search-input', + 'select-all-checkbox', + 'input-checkbox-list', + 'group-checkbox-list', + ]); this.closeSearchElm = this.filterParentElm?.querySelector('.icon-close'); this.searchInputElm = this.dropElm.querySelector('.ms-search input'); @@ -688,7 +700,6 @@ export class MultipleSelectInstance { `input[data-name="${this.selectGroupName}"],span[data-name="${this.selectGroupName}"]` ); this.selectItemElms = this.dropElm.querySelectorAll(`input[data-name="${this.selectItemName}"]:enabled`); - this.disableItemElms = this.dropElm.querySelectorAll(`input[data-name="${this.selectItemName}"]:disabled`); this.noResultsElm = this.dropElm.querySelector('.ms-no-results'); const toggleOpen = (e: MouseEvent & { target: HTMLElement }) => { @@ -750,106 +761,211 @@ export class MultipleSelectInstance { } if (this.searchInputElm) { - this._bindEventService.bind(this.searchInputElm, 'keydown', ((e: KeyboardEvent) => { - // Ensure shift-tab causes lost focus from filter as with clicking away - if (e.code === 'Tab' && e.shiftKey) { - this.close(); - } - }) as EventListener); + this._bindEventService.bind( + this.searchInputElm, + 'keydown', + ((e: KeyboardEvent) => { + // Ensure shift-tab causes lost focus from filter as with clicking away + if (e.code === 'Tab' && e.shiftKey) { + this.close(); + } + }) as EventListener, + undefined, + 'search-input' + ); - this._bindEventService.bind(this.searchInputElm, 'keyup', ((e: KeyboardEvent) => { - // enter or space - // Avoid selecting/deselecting if no choices made - if (this.options.filterAcceptOnEnter && ['Enter', 'Space'].includes(e.code) && this.searchInputElm?.value) { - if (this.options.single) { - const visibleLiElms: HTMLInputElement[] = []; - this.selectItemElms?.forEach((selectedElm) => { - if (selectedElm.closest('li')?.style.display !== 'none') { - visibleLiElms.push(selectedElm); + this._bindEventService.bind( + this.searchInputElm, + 'keyup', + ((e: KeyboardEvent) => { + // enter or space + // Avoid selecting/deselecting if no choices made + if (this.options.filterAcceptOnEnter && ['Enter', 'Space'].includes(e.code) && this.searchInputElm?.value) { + if (this.options.single) { + const visibleLiElms: HTMLInputElement[] = []; + this.selectItemElms?.forEach((selectedElm) => { + if (selectedElm.closest('li')?.style.display !== 'none') { + visibleLiElms.push(selectedElm); + } + }); + if (visibleLiElms.length && visibleLiElms[0].hasAttribute('data-name')) { + this.setSelects([visibleLiElms[0].value]); } - }); - if (visibleLiElms.length && visibleLiElms[0].hasAttribute('data-name')) { - this.setSelects([visibleLiElms[0].value]); + } else { + this.selectAllElm?.click(); } - } else { - this.selectAllElm?.click(); + this.close(); + this.focus(); + return; } - this.close(); - this.focus(); - return; - } - this.filter(); - }) as EventListener); + this.filter(); + }) as EventListener, + undefined, + 'search-input' + ); } if (this.selectAllElm) { - this._bindEventService.unbind(this.selectAllElm, 'click'); - this._bindEventService.bind(this.selectAllElm, 'click', ((e: MouseEvent & { currentTarget: HTMLInputElement }) => { - this._checkAll(e.currentTarget?.checked); - }) as EventListener); + this._bindEventService.bind( + this.selectAllElm, + 'click', + ((e: MouseEvent & { currentTarget: HTMLInputElement }) => this._checkAll(e.currentTarget?.checked)) as EventListener, + undefined, + 'select-all-checkbox' + ); } if (this.okButtonElm) { - this._bindEventService.unbind(this.okButtonElm, 'click'); - this._bindEventService.bind(this.okButtonElm, 'click', ((e: MouseEvent & { target: HTMLElement }) => { - toggleOpen(e); - e.stopPropagation(); // Causes lost focus otherwise - }) as EventListener); + this._bindEventService.bind( + this.okButtonElm, + 'click', + ((e: MouseEvent & { target: HTMLElement }) => { + toggleOpen(e); + e.stopPropagation(); // Causes lost focus otherwise + }) as EventListener, + undefined, + 'ok-button' + ); } - this._bindEventService.bind(this.selectGroupElms, 'click', ((e: MouseEvent & { currentTarget: HTMLInputElement }) => { - const selectElm = e.currentTarget; - const checked = selectElm.checked; - const group = findByParam(this.data, '_key', selectElm.dataset.key); - - this._checkGroup(group, checked); - this.options.onOptgroupClick( - removeUndefined({ - label: group.label, - selected: group.selected, - data: group._data, - children: group.children.map((child: any) => { - if (child) { - return removeUndefined({ - text: child.text, - value: child.value, - selected: child.selected, - disabled: child.disabled, - data: child._data, - }); - } - }), - }) - ); - }) as EventListener); + this._bindEventService.bind( + this.selectGroupElms, + 'click', + ((e: MouseEvent & { currentTarget: HTMLInputElement }) => { + const selectElm = e.currentTarget; + const checked = selectElm.checked; + const group = findByParam(this.data, '_key', selectElm.dataset.key); + + this._checkGroup(group, checked); + this.options.onOptgroupClick( + removeUndefined({ + label: group.label, + selected: group.selected, + data: group._data, + children: group.children.map((child: any) => { + if (child) { + return removeUndefined({ + text: child.text, + value: child.value, + selected: child.selected, + disabled: child.disabled, + data: child._data, + }); + } + }), + }) + ); + }) as EventListener, + undefined, + 'group-checkbox-list' + ); + + this._bindEventService.bind( + this.selectItemElms, + 'click', + ((e: MouseEvent & { currentTarget: HTMLInputElement }) => { + const selectElm = e.currentTarget; + const checked = selectElm.checked; + const option = findByParam(this.data, '_key', selectElm.dataset.key); + const close = () => { + if (this.options.single && this.options.isOpen && !this.options.keepOpen) { + this.close(); + } + }; - this._bindEventService.bind(this.selectItemElms, 'click', ((e: MouseEvent & { currentTarget: HTMLInputElement }) => { - const selectElm = e.currentTarget; - const checked = selectElm.checked; - const option = findByParam(this.data, '_key', selectElm.dataset.key); - const close = () => { - if (this.options.single && this.options.isOpen && !this.options.keepOpen) { - this.close(); + if (this.options.onBeforeClick(option) === false) { + close(); + return; } - }; - if (this.options.onBeforeClick(option) === false) { + this._check(option, checked); + this.options.onClick( + removeUndefined({ + text: option.text, + value: option.value, + selected: option.selected, + data: option._data, + }) + ); + close(); - return; - } + }) as EventListener, + undefined, + 'input-checkbox-list' + ); - this._check(option, checked); - this.options.onClick( - removeUndefined({ - text: option.text, - value: option.value, - selected: option.selected, - data: option._data, - }) - ); + // if we previously had an item focused and the VirtualScroll recreates the list, we need to refocus on last item by its input data-key + if (this.lastFocusedItemKey) { + const input = this.dropElm.querySelector(`li[data-key=${this.lastFocusedItemKey}]`); + input?.focus(); + } + + // add keydown event listeners to watch for up/down arrows and focus on previous/next item + // we will ignore divider and if key pressed is the Enter/Space key then we'll instead select/deselect input checkbox + // we will also remove any previous bindings that might exist which happen when we use VirtualScroll + const nodes = Array.from(this.dropElm.querySelectorAll('div.ms-select-all, li')); + this._bindEventService.unbindAll('tabindex-arrow'); + this._bindEventService.bind( + this.dropElm, + 'keydown', + ((e: KeyboardEvent & { target: HTMLDivElement | HTMLLIElement }) => { + const liElm = e.target.closest('.ms-select-all') || e.target.closest('li'); + if (this.dropElm.contains(liElm)) { + let idx = 0; + const nodeLn = nodes.length; + for (idx = 0; idx < nodeLn; idx++) { + if (nodes[idx].isEqualNode(liElm)) { + break; + } + } + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + if (idx > 0) { + this.lastFocusedItemKey = this.focusOnUpDownItem(nodes, idx, e.key)?.dataset.key || ''; + } + break; + case 'ArrowDown': + e.preventDefault(); + if (idx < nodes.length - 1) { + this.lastFocusedItemKey = this.focusOnUpDownItem(nodes, idx, e.key)?.dataset.key || ''; + } + break; + case 'Enter': + case ' ': + e.preventDefault(); + liElm!.querySelector('input')?.click(); + if (this.options.single) { + this.choiceElm.focus(); + this.lastFocusedItemKey = this.choiceElm?.dataset.key || ''; + } + break; + default: + // ignore + } + } + }) as EventListener, + undefined, + 'tabindex-arrow' + ); + } - close(); - }) as EventListener); + /** focus on next up/down item depending on arrow key pressed, we will ignore divider and focus on next item */ + protected focusOnUpDownItem(items: HTMLElement[], itemIdx: number, direction: 'ArrowUp' | 'ArrowDown') { + let currentIdx = itemIdx; + let dirElm: HTMLElement | null; + while ((dirElm = items[direction === 'ArrowUp' ? currentIdx - 1 : currentIdx + 1])) { + if (dirElm.classList.contains('option-divider')) { + direction === 'ArrowUp' ? currentIdx-- : currentIdx++; + continue; + } + break; + } + if (dirElm) { + dirElm.focus(); + return dirElm; + } + return null; } /** @@ -1326,7 +1442,7 @@ export class MultipleSelectInstance { const rowLabel = `${(row as OptGroupRowData)?.label ?? ''}`; if (row !== undefined && row !== null) { const visible = this.options.customFilter({ - label: removeDiacritics(rowLabel.toLowerCase()), + label: removeDiacritics(rowLabel.toString().toLowerCase()), search: removeDiacritics(search), originalLabel: rowLabel, originalSearch, @@ -1345,7 +1461,7 @@ export class MultipleSelectInstance { if (child !== undefined && child !== null) { const childText = `${(child as OptionRowData)?.text ?? ''}`; child.visible = this.options.customFilter({ - text: removeDiacritics(childText.toLowerCase()), + text: removeDiacritics(childText.toString().toLowerCase()), search: removeDiacritics(search), originalText: childText, originalSearch, @@ -1359,7 +1475,7 @@ export class MultipleSelectInstance { } else { const rowText = `${(row as OptionRowData)?.text ?? ''}`; row.visible = this.options.customFilter({ - text: removeDiacritics(rowText.toLowerCase()), + text: removeDiacritics(rowText.toString().toLowerCase()), search: removeDiacritics(search), originalText: rowText, originalSearch, diff --git a/lib/src/services/binding-event.service.ts b/lib/src/services/binding-event.service.ts index 7125469e..f2595726 100644 --- a/lib/src/services/binding-event.service.ts +++ b/lib/src/services/binding-event.service.ts @@ -1,7 +1,8 @@ export interface ElementEventListener { element: Element; - eventName: string; - listener: EventListenerOrEventListenerObject; + eventName: keyof HTMLElementEventMap; + listener: EventListener; + groupName?: string; } export class BindingEventService { @@ -22,41 +23,50 @@ export class BindingEventService { } /** Bind an event listener to any element */ - bind( - elementOrElements: Element | NodeListOf, - eventNameOrNames: string | string[], - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions + bind( + elementOrElements: H | NodeListOf | Window, + eventNameOrNames: keyof HTMLElementEventMap | Array, + listener: EventListener, + listenerOptions?: boolean | AddEventListenerOptions, + groupName = '' ) { + // convert to array for looping in next task const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]; - if ((elementOrElements as NodeListOf)?.forEach) { - (elementOrElements as NodeListOf)?.forEach((element) => { + if (typeof (elementOrElements as NodeListOf)?.forEach === 'function') { + // multiple elements to bind to + (elementOrElements as NodeListOf).forEach((element) => { for (const eventName of eventNames) { if (!this._distinctEvent || (this._distinctEvent && !this.hasBinding(element, eventName))) { - element.addEventListener(eventName, listener, options); - this._boundedEvents.push({ element, eventName, listener }); + element.addEventListener(eventName, listener as EventListener, listenerOptions); + this._boundedEvents.push({ element, eventName, listener: listener as EventListener, groupName }); } } }); } else { + // single elements to bind to for (const eventName of eventNames) { - if (!this._distinctEvent || (this._distinctEvent && !this.hasBinding(elementOrElements as Element, eventName))) { - (elementOrElements as Element).addEventListener(eventName, listener, options); - this._boundedEvents.push({ element: elementOrElements as Element, eventName, listener }); + if (!this._distinctEvent || (this._distinctEvent && !this.hasBinding(elementOrElements as H, eventName))) { + (elementOrElements as H).addEventListener(eventName, listener as EventListener, listenerOptions); + this._boundedEvents.push({ + element: elementOrElements as H, + eventName, + listener: listener as EventListener, + groupName, + }); } } } } - hasBinding(elm: Element, eventNameOrNames?: string | string[]): boolean { + hasBinding(elm: Element, eventNameOrNames?: keyof HTMLElementEventMap | Array): boolean { return this._boundedEvents.some((f) => f.element === elm && (!eventNameOrNames || f.eventName === eventNameOrNames)); } - /** Unbind all will remove every every event handlers that were bounded earlier */ + /** Unbind a specific listener that was bounded earlier */ unbind( elementOrElements?: Element | NodeListOf | null, - eventNameOrNames?: string | string[], + eventNameOrNames?: keyof HTMLElementEventMap | Array, listener?: EventListenerOrEventListenerObject | null ) { if (elementOrElements) { @@ -80,12 +90,29 @@ export class BindingEventService { } } - /** Unbind all will remove every every event handlers that were bounded earlier */ - unbindAll() { - while (this._boundedEvents.length > 0) { - const boundedEvent = this._boundedEvents.pop() as ElementEventListener; - const { element, eventName, listener } = boundedEvent; - this.unbind(element, eventName, listener); + /** + * Unbind all event listeners that were bounded, optionally provide a group name to unbind all listeners assigned to that specific group only. + */ + unbindAll(groupName?: string | string[]) { + if (groupName) { + const groupNames = Array.isArray(groupName) ? groupName : [groupName]; + // unbind only the bounded event with a specific group + // Note: we need to loop in reverse order to avoid array reindexing (causing index offset) after a splice is called + for (let i = this._boundedEvents.length - 1; i >= 0; --i) { + const boundedEvent = this._boundedEvents[i]; + if (groupNames.some((g) => g === boundedEvent.groupName)) { + const { element, eventName, listener } = boundedEvent; + this.unbind(element, eventName, listener); + this._boundedEvents.splice(i, 1); + } + } + } else { + // unbind everything + while (this._boundedEvents.length > 0) { + const boundedEvent = this._boundedEvents.pop() as ElementEventListener; + const { element, eventName, listener } = boundedEvent; + this.unbind(element, eventName, listener); + } } } } diff --git a/lib/src/utils/domUtils.ts b/lib/src/utils/domUtils.ts index 8c80bcba..c6d02d6d 100644 --- a/lib/src/utils/domUtils.ts +++ b/lib/src/utils/domUtils.ts @@ -99,7 +99,7 @@ export function createDomStructure(item: HtmlStruct, appendToElm?: HTMLElement, delete item.props.innerHTML; } - const elm = createDomElement(item.tagName, objectRemoveEmptyProps(item.props, ['class', 'title', 'style']), appendToElm); + const elm = createDomElement(item.tagName, objectRemoveEmptyProps(item.props, ['className', 'title', 'style']), appendToElm); let parent: HTMLElement | null | undefined = parentElm; if (!parent) { parent = elm; diff --git a/playwright/e2e/example02.spec.ts b/playwright/e2e/example02.spec.ts index 66a41a69..8bedc537 100644 --- a/playwright/e2e/example02.spec.ts +++ b/playwright/e2e/example02.spec.ts @@ -8,8 +8,25 @@ test.describe('Example 02 - Multiple Select', () => { await page.locator('span').filter({ hasText: 'April' }).click(); await page.locator('span').filter({ hasText: 'May' }).click(); const parent1Span = await page.locator('div[data-test=select1] .ms-choice span'); - await page.getByRole('button', { name: 'February, April, May' }).click(); + await expect(parent1Span).toHaveText('February, April, May'); + await page.keyboard.press('ArrowDown'); + const juneLoc = await page.locator('div[data-test=select1] .ms-drop li:nth-of-type(6)'); + await expect(juneLoc).toBeFocused(); + await expect(await juneLoc.locator('label')).toHaveText('June'); + await page.keyboard.press('Enter'); + await expect(parent1Span).toHaveText('4 of 12 selected'); + + // go up until we reach "Select All" and use Space to press the option + page.keyboard.press('ArrowUp'); + page.keyboard.press('ArrowUp'); + page.keyboard.press('ArrowUp'); + page.keyboard.press('ArrowUp'); + page.keyboard.press('ArrowUp'); + page.keyboard.press('ArrowUp'); + page.keyboard.press('Space'); + await expect(parent1Span).toHaveText('All selected'); + await page.getByRole('button', { name: 'All selected' }).click(); }); test('second select with multiple selection with optgroup and expect entire group to be selected', async ({ page }) => { diff --git a/playwright/e2e/example03.spec.ts b/playwright/e2e/example03.spec.ts index bd4d61f0..ead826a8 100644 --- a/playwright/e2e/example03.spec.ts +++ b/playwright/e2e/example03.spec.ts @@ -6,12 +6,20 @@ test.describe('Example 03 - Multiple Width', () => { await page.locator('div[data-test=select1].ms-parent').click(); await page.getByRole('listitem').filter({ hasText: '30' }).locator('label').click(); await page.getByRole('checkbox', { name: '15' }).check(); - const elm16 = await page.locator('label').filter({ hasText: '16' }); + let elm16 = await page.locator('label').filter({ hasText: '16' }); await elm16.click(); expect((await elm16!.boundingBox())!.width).toBe(44); + + elm16 = await page.locator('div[data-test=select1] .ms-drop li:nth-of-type(16)'); + await elm16.focus(); + await expect(elm16).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Space'); // unselect 15 + let parent1Span = await page.locator('div[data-test=select1] .ms-choice span'); + await expect(parent1Span).toHaveText('16, 30'); + await page.keyboard.press('Enter'); // reselect 15 + parent1Span = await page.locator('div[data-test=select1] .ms-choice span'); await page.getByRole('button', { name: '15, 16, 30' }).click(); - const parent1Span = await page.locator('div[data-test=select1] .ms-choice span'); - await expect(parent1Span).toHaveText('15, 16, 30'); }); test('second select and expect optgroup selection to select the entire group when optgroup is selected', async ({ page }) => { @@ -34,6 +42,18 @@ test.describe('Example 03 - Multiple Width', () => { await page.getByRole('checkbox', { name: '3', exact: true }).check(); const selectAll2 = await page.locator('[data-test=select2] .ms-select-all input[data-name=selectAll]'); await expect(selectAll2).toBeChecked(); - await page.getByRole('button', { name: 'All selected' }).click(); + await page.getByRole('button', { name: 'All selected' }); + const selectAllLoc = await page.locator('div[data-test=select2] .ms-drop .ms-select-all'); + await selectAllLoc.focus(); + await page.keyboard.press('ArrowDown'); // unselect Group 1 + await page.keyboard.press('Space'); + let parent2Span = await page.locator('div[data-test=select2] .ms-choice span'); + await expect(parent2Span).toHaveText('10 of 15 selected'); + await page.keyboard.press('ArrowDown'); // unselect Group 1 -> 1st item + await page.keyboard.press('Enter'); + parent2Span = await page.locator('div[data-test=select2] .ms-choice span'); + await expect(parent2Span).toHaveText('11 of 15 selected'); + const group1item1Loc = await page.locator('[data-test=select2] .ms-drop li:nth-of-type(2) input'); + await expect(group1item1Loc).toBeChecked(); }); });