diff --git a/packages/listbox/test-suites/ListboxMixin.suite.js b/packages/listbox/test-suites/ListboxMixin.suite.js index 2daadf1d7..8a230ffd7 100644 --- a/packages/listbox/test-suites/ListboxMixin.suite.js +++ b/packages/listbox/test-suites/ListboxMixin.suite.js @@ -1,5 +1,5 @@ import { Required } from '@lion/form-core'; -import { expect, html, fixture, unsafeStatic } from '@open-wc/testing'; +import { expect, html, fixture as _fixture, unsafeStatic } from '@open-wc/testing'; import { LionOptions } from '@lion/listbox'; import '@lion/listbox/lion-option.js'; import '@lion/listbox/lion-options.js'; @@ -7,12 +7,33 @@ import '../lion-listbox.js'; import '@lion/core/src/differentKeyEventNamesShimIE.js'; /** - * @typedef {import('@lion/combobox/src/LionCombobox').LionCombobox} LionCombobox * @typedef {import('../src/LionListbox').LionListbox} LionListbox + * @typedef {import('../src/LionOption').LionOption} LionOption + * @typedef {import('@lion/select-rich').LionSelectInvoker} LionSelectInvoker + * @typedef {import('lit-html').TemplateResult} TemplateResult */ +const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); + /** - * @param { {tagString:string, optionTagString:string} } [customConfig] + * @param {LionListbox} lionListboxEl + */ +function getProtectedMembers(lionListboxEl) { + // @ts-ignore protected members allowed in test + const { + _inputNode: input, + _activeDescendantOwnerNode: activeDescendantOwner, + _listboxNode: listbox, + } = lionListboxEl; + return { + input, + activeDescendantOwner, + listbox, + }; +} + +/** + * @param { {tagString?:string, optionTagString?:string} } [customConfig] */ export function runListboxMixinSuite(customConfig = {}) { const cfg = { @@ -157,12 +178,12 @@ export function runListboxMixinSuite(customConfig = {}) { await el.updateComplete; expect(el.showsFeedbackFor.includes('error')).to.be.true; - el._listboxNode.children[1].checked = true; + el.formElements[1].checked = true; await el.updateComplete; expect(el.hasFeedbackFor.includes('error')).to.be.false; expect(el.showsFeedbackFor.includes('error')).to.be.false; - el._listboxNode.children[0].checked = true; + el.formElements[0].checked = true; await el.updateComplete; expect(el.hasFeedbackFor.includes('error')).to.be.true; expect(el.showsFeedbackFor.includes('error')).to.be.true; @@ -170,19 +191,6 @@ export function runListboxMixinSuite(customConfig = {}) { }); describe('Selection', () => { - it('supports having no default selection initially', async () => { - const el = await fixture(html` - <${tag} id="color" name="color" label="Favorite color" has-no-default-selected> - <${optionTag} .choiceValue=${'red'}>Red - <${optionTag} .choiceValue=${'hotpink'}>Hotpink - <${optionTag} .choiceValue=${'teal'}>Teal - - `); - - expect(el.selectedElement).to.be.undefined; - expect(el.modelValue).to.equal(''); - }); - it('supports changing the selection through serializedValue setter', async () => { const el = await fixture(html` <${tag} id="color" name="color" label="Favorite color" has-no-default-selected> @@ -203,30 +211,30 @@ export function runListboxMixinSuite(customConfig = {}) { }); describe('Accessibility', () => { - it('[axe]: is accessible when closed', async () => { - const el = await fixture(html` - <${tag} label="age"> - <${optionTag} .choiceValue=${10}>Item 1 - <${optionTag} .choiceValue=${20}>Item 2 - - `); - await expect(el).to.be.accessible(); - }); - it('[axe]: is accessible when opened', async () => { const el = await fixture(html` - <${tag} label="age"> + <${tag} label="age" opened> <${optionTag} .choiceValue=${10}>Item 1 <${optionTag} .choiceValue=${20}>Item 2 `); - el.opened = true; await el.updateComplete; await el.updateComplete; // need 2 awaits as overlay.show is an async function await expect(el).to.be.accessible(); }); + // NB: regular listbox is always 'opened', but needed for combobox and select-rich + it('[axe]: is accessible when closed', async () => { + const el = await fixture(html` + <${tag} label="age"> + <${optionTag} .choiceValue=${10}>Item 1 + <${optionTag} .choiceValue=${20}>Item 2 + + `); + await expect(el).to.be.accessible(); + }); + it('does not have a tabindex', async () => { const el = await fixture(html`<${tag}>`); expect(el.hasAttribute('tabindex')).to.be.false; @@ -252,7 +260,9 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'20'} checked id="second">Item 2 `); - expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.be.null; + const { activeDescendantOwner } = getProtectedMembers(el); + + expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.be.null; await el.updateComplete; // Normalize @@ -262,16 +272,10 @@ export function runListboxMixinSuite(customConfig = {}) { // new KeyboardEvent('keydown', { key: 'ArrowDown' }), // ); await el.updateComplete; - expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal( - 'first', - ); - el._activeDescendantOwnerNode.dispatchEvent( - new KeyboardEvent('keydown', { key: 'ArrowDown' }), - ); + expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('first'); + activeDescendantOwner.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); await el.updateComplete; - expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal( - 'second', - ); + expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('second'); }); it('puts "aria-setsize" on all options to indicate the total amount of options', async () => { @@ -288,15 +292,14 @@ export function runListboxMixinSuite(customConfig = {}) { }); it('puts "aria-posinset" on all options to indicate their position in the listbox', async () => { - const el = /** @type {LionListbox} */ (await fixture(html` + const el = await fixture(html` <${tag} autocomplete="none"> <${optionTag} .choiceValue=${10}>Item 1 <${optionTag} .choiceValue=${20}>Item 2 <${optionTag} .choiceValue=${30}>Item 3 - `)); - const optionEls = [].slice.call(el.querySelectorAll('lion-option')); - optionEls.forEach((oEl, i) => { + `); + el.formElements.forEach((oEl, i) => { expect(oEl.getAttribute('aria-posinset')).to.equal(`${i + 1}`); }); }); @@ -326,6 +329,7 @@ export function runListboxMixinSuite(customConfig = {}) { `); + // @ts-ignore feature detect LionCombobox if (el._comboboxNode) { // note that the modelValue can only be supplied as string if we have a textbox // (parsers not supported atm) @@ -383,10 +387,11 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - expect(el.activeIndex).to.equal(-1); + const options = el.formElements; - el.querySelectorAll('lion-option')[1].active = true; - expect(el.querySelectorAll('lion-option')[0].active).to.be.false; + expect(el.activeIndex).to.equal(-1); + options[1].active = true; + expect(options[0].active).to.be.false; expect(el.activeIndex).to.equal(1); }); @@ -397,8 +402,10 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'20'}>Item 2 `); + const options = el.formElements; + expect(el.activeIndex).to.equal(-1); - el.querySelectorAll('lion-option')[1].active = true; + options[1].active = true; expect(el.activeIndex).to.equal(1); el.reset(); expect(el.activeIndex).to.equal(-1); @@ -416,15 +423,17 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue="${'Chard'}">Chard `)); + const { listbox } = getProtectedMembers(el); + // Normalize el.activeIndex = 0; const options = el.formElements; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); expect(options[0].active).to.be.true; expect(options[1].active).to.be.false; expect(options[2].active).to.be.false; el.activeIndex = 2; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(options[0].active).to.be.false; expect(options[1].active).to.be.false; expect(options[2].active).to.be.true; @@ -466,12 +475,14 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue="${'Chard'}">Chard `)); + const { listbox } = getProtectedMembers(el); + // Normalize suite el.activeIndex = 0; const options = el.formElements; el.checkedIndex = 0; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); expect(options[1].checked).to.be.true; }); }); @@ -487,17 +498,20 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue="${'Chard'}">Chard `)); + const { listbox } = getProtectedMembers(el); + // Normalize suite el.activeIndex = 0; const options = el.formElements; el.checkedIndex = 0; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); expect(options[1].checked).to.be.true; el.checkedIndex = 0; + // @ts-ignore allow protected member access in test el._listboxReceivesNoFocus = true; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); expect(options[1].checked).to.be.false; }); }); @@ -511,9 +525,9 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'c'}>C `); - expect(el.choiceValue).to.equal('a'); + expect(el.modelValue).to.equal('a'); el.dispatchEvent(new KeyboardEvent('keydown', { key: 'C' })); - expect(el.choiceValue).to.equal('c'); + expect(el.modelValue).to.equal('c'); }); it.skip('selects a value with multiple [character] keys', async () => { const el = await fixture(html` @@ -524,9 +538,9 @@ export function runListboxMixinSuite(customConfig = {}) { `); el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F' })); - expect(el.choiceValue).to.equal('far'); + expect(el.modelValue).to.equal('far'); el.dispatchEvent(new KeyboardEvent('keydown', { key: 'O' })); - expect(el.choiceValue).to.equal('foo'); + expect(el.modelValue).to.equal('foo'); }); it('navigates to first and last option with [Home] and [End] keys', async () => { const el = await fixture(html` @@ -537,15 +551,17 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'40'}>Item 4 `); + const { listbox } = getProtectedMembers(el); + // @ts-ignore allow protected members in tests if (el._listboxReceivesNoFocus) { return; } el.activeIndex = 2; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' })); expect(el.activeIndex).to.equal(0); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' })); expect(el.activeIndex).to.equal(3); }); it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => { @@ -556,6 +572,8 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'Item 3'}>Item 3 `)); + const { listbox } = getProtectedMembers(el); + // Normalize across listbox/select-rich/combobox el.activeIndex = 0; // selectionFollowsFocus will be true by default on combobox (running this suite), @@ -563,10 +581,10 @@ export function runListboxMixinSuite(customConfig = {}) { el.selectionFollowsFocus = false; expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(-1); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(-1); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(-1); }); @@ -580,6 +598,8 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue="${'Chard'}">Chard `)); + const { listbox } = getProtectedMembers(el); + expect(el.orientation).to.equal('vertical'); const options = el.formElements; // Normalize for suite tests @@ -589,21 +609,21 @@ export function runListboxMixinSuite(customConfig = {}) { expect(options[0].active).to.be.true; expect(options[1].active).to.be.false; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(options[0].active).to.be.false; expect(options[1].active).to.be.true; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); expect(options[0].active).to.be.true; expect(options[1].active).to.be.false; // No response to horizontal arrows... - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(options[0].active).to.be.true; expect(options[1].active).to.be.false; el.activeIndex = 1; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(options[0].active).to.be.false; expect(options[1].active).to.be.true; }); @@ -615,6 +635,8 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue="${'Chard'}">Chard `)); + const { listbox } = getProtectedMembers(el); + expect(el.orientation).to.equal('horizontal'); // Normalize for suite tests @@ -622,44 +644,45 @@ export function runListboxMixinSuite(customConfig = {}) { await el.updateComplete; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(el.activeIndex).to.equal(1); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(el.activeIndex).to.equal(0); // No response to vertical arrows... - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(0); el.activeIndex = 1; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(1); }); describe('Accessibility', () => { it('adds aria-orientation attribute to listbox node', async () => { - const el = /** @type {Listbox} */ (await fixture(html` + const el = await fixture(html` <${tag} name="foo" orientation="horizontal"> <${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke <${optionTag} .choiceValue="${'Chard'}">Chard - `)); - expect(el._listboxNode.getAttribute('aria-orientation')).to.equal('horizontal'); + `); + const { listbox } = getProtectedMembers(el); + expect(listbox.getAttribute('aria-orientation')).to.equal('horizontal'); }); }); }); describe('Multiple Choice', () => { it('does not uncheck siblings', async () => { - const el = /** @type {Listbox} */ (await fixture(html` + const el = await fixture(html` <${tag} name="foo" multiple-choice> <${optionTag} .choiceValue="${'Artichoke'}">Artichoke <${optionTag} .choiceValue="${'Chard'}">Chard <${optionTag} .choiceValue="${'Chicory'}">Chicory <${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum - `)); + `); const options = el.formElements; options[0].checked = true; options[1].checked = true; @@ -668,17 +691,18 @@ export function runListboxMixinSuite(customConfig = {}) { }); it('works via different interaction mechanisms (click, enter, spaces)', async () => { - const el = /** @type {Listbox} */ (await fixture(html` + const el = await fixture(html` <${tag} name="foo" multiple-choice> <${optionTag} .choiceValue="${'Artichoke'}">Artichoke <${optionTag} .choiceValue="${'Chard'}">Chard <${optionTag} .choiceValue="${'Chicory'}">Chicory <${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum - `)); + `); + const { listbox } = getProtectedMembers(el); const options = el.formElements; - // feature detection select-rich + // @ts-ignore feature detection select-rich if (el.navigateWithinInvoker !== undefined) { // Note we don't have multipleChoice in the select-rich yet. // TODO: implement in future when requested @@ -692,59 +716,69 @@ export function runListboxMixinSuite(customConfig = {}) { expect(el.modelValue).to.eql(['Artichoke', 'Chard']); // Reset + // @ts-ignore allow protected members in tests el._uncheckChildren(); // Enter el.activeIndex = 0; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); el.activeIndex = 1; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); expect(options[0].checked).to.equal(true); expect(el.modelValue).to.eql(['Artichoke', 'Chard']); + // @ts-ignore allow protected if (el._listboxReceivesNoFocus) { return; // if suite is run for combobox, we don't respond to [Space] } // Reset + // @ts-ignore allow protected members in tests el._uncheckChildren(); // Space el.activeIndex = 0; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); el.activeIndex = 1; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); expect(options[0].checked).to.equal(true); expect(el.modelValue).to.eql(['Artichoke', 'Chard']); }); describe('Accessibility', () => { it('adds aria-multiselectable="true" to listbox node', async () => { - const el = /** @type {Listbox} */ (await fixture(html` + const el = await fixture(html` <${tag} name="foo" multiple-choice> <${optionTag} .choiceValue="${'Artichoke'}">Artichoke <${optionTag} .choiceValue="${'Chard'}">Chard - `)); - expect(el._listboxNode.getAttribute('aria-multiselectable')).to.equal('true'); + `); + const { listbox } = getProtectedMembers(el); + expect(listbox.getAttribute('aria-multiselectable')).to.equal('true'); }); it('does not allow "selectionFollowsFocus"', async () => { - const el = /** @type {Listbox} */ (await fixture(html` + const el = await fixture(html` <${tag} name="foo" multiple-choice> <${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke <${optionTag} .choiceValue="${'Chard'}">Chard - `)); - el._inputNode.focus(); - el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); - expect(el._listboxNode.getAttribute('aria-multiselectable')).to.equal('true'); + `); + const { listbox, input } = getProtectedMembers(el); + + input.focus(); + listbox.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); + expect(listbox.getAttribute('aria-multiselectable')).to.equal('true'); }); }); }); describe('Selection Follows Focus', () => { it('navigates through list with [ArrowDown] [ArrowUp] keys: activates and checks the option', async () => { + /** + * @param {LionOption[]} options + * @param {number} selectedIndex + */ function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) { options.forEach((option, i) => { if (i === selectedIndex) { @@ -761,18 +795,20 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${30}>Item 3 `)); - const options = Array.from(el.querySelectorAll('lion-option')); + + const { listbox } = getProtectedMembers(el); + const options = el.formElements; // Normalize start values between listbox, slect and combobox and test interaction below el.activeIndex = 0; el.checkedIndex = 0; expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(1); expectOnlyGivenOneOptionToBeChecked(options, 1); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); @@ -786,15 +822,17 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${'40'}>Item 4 `); + const { listbox } = getProtectedMembers(el); + // @ts-ignore allow protected if (el._listboxReceivesNoFocus) { return; } expect(el.modelValue).to.equal('30'); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' })); expect(el.modelValue).to.equal('10'); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' })); expect(el.modelValue).to.equal('40'); }); }); @@ -807,9 +845,11 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} checked .choiceValue=${'20'}>Item 2 `); + const { listbox } = getProtectedMembers(el); + await el.updateComplete; const { checkedIndex } = el; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(el.checkedIndex).to.equal(checkedIndex); }); @@ -820,7 +860,7 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - const options = [...el.querySelectorAll('lion-option')]; + const options = el.formElements; el.disabled = true; await el.updateComplete; expect(options[0].disabled).to.be.true; @@ -839,7 +879,7 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - const options = [...el.querySelectorAll('lion-option')]; + const options = el.formElements; expect(options[0].disabled).to.be.true; expect(options[1].disabled).to.be.true; @@ -860,14 +900,16 @@ export function runListboxMixinSuite(customConfig = {}) { `); + const { listbox } = getProtectedMembers(el); + // Normalize activeIndex across multiple implementers of ListboxMixinSuite el.activeIndex = 0; - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(0); - el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); // Checked index stays where it was expect(el.checkedIndex).to.equal(0); }); @@ -881,11 +923,11 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20} id="myId">Item 2 `); - const opt = el.querySelectorAll('lion-option')[1]; + const { activeDescendantOwner } = getProtectedMembers(el); + + const opt = el.formElements[1]; opt.active = true; - expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal( - 'myId', - ); + expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('myId'); }); it('can set checked state', async () => { @@ -895,7 +937,7 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - const option = el.querySelectorAll('lion-option')[1]; + const option = el.formElements[1]; option.checked = true; expect(el.modelValue).to.equal(20); }); @@ -923,7 +965,7 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - const options = el.querySelectorAll('lion-option'); + const options = el.formElements; options[0].checked = true; expect(options[0].checked).to.be.true; expect(options[1].checked).to.be.false; @@ -939,7 +981,7 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); - const options = el.querySelectorAll('lion-option'); + const options = el.formElements; expect(options[0].active).to.be.true; options[1].active = true; expect(options[0].active).to.be.false; @@ -990,12 +1032,16 @@ export function runListboxMixinSuite(customConfig = {}) { `); expect(el.hasFeedbackFor).to.include('error'); + // @ts-expect-error no types for 'have.a.property' expect(el.validationStates).to.have.a.property('error'); + // @ts-expect-error no types for 'have.a.property' expect(el.validationStates.error).to.have.a.property('Required'); el.modelValue = 20; expect(el.hasFeedbackFor).not.to.include('error'); + // @ts-expect-error no types for 'have.a.property' expect(el.validationStates).to.have.a.property('error'); + // @ts-expect-error no types for 'have.a.property' expect(el.validationStates.error).not.to.have.a.property('Required'); }); }); @@ -1062,14 +1108,15 @@ export function runListboxMixinSuite(customConfig = {}) { <${optionTag} .choiceValue=${20}>Item 2 `); + const { listbox } = getProtectedMembers(el); - expect(el._listboxNode).to.exist; - expect(el._listboxNode).to.be.instanceOf(LionOptions); - expect(el.querySelector('[role=listbox]')).to.equal(el._listboxNode); + expect(listbox).to.exist; + expect(listbox).to.be.instanceOf(LionOptions); + expect(el.querySelector('[role=listbox]')).to.equal(listbox); expect(el.formElements.length).to.equal(2); - expect(el._listboxNode.children.length).to.equal(2); - expect(el._listboxNode.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase()); + expect(listbox.children.length).to.equal(2); + expect(listbox.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase()); }); }); }); diff --git a/packages/listbox/types/ListboxMixinTypes.d.ts b/packages/listbox/types/ListboxMixinTypes.d.ts index dccc85336..0fdc4f741 100644 --- a/packages/listbox/types/ListboxMixinTypes.d.ts +++ b/packages/listbox/types/ListboxMixinTypes.d.ts @@ -41,10 +41,17 @@ export declare class ListboxHost { public setCheckedIndex(index: number): void; + /** Reset interaction states and modelValue */ + public reset(): void; + protected _scrollTargetNode: LionOptions; protected _listboxNode: LionOptions; + protected _listboxReceivesNoFocus: boolean; + + protected _uncheckChildren(): void; + private __setupListboxNode(): void; protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number; @@ -64,6 +71,8 @@ export declare class ListboxHost { protected _setupListboxInteractions(): void; protected _onChildActiveChanged(ev: Event): void; + + protected _activeDescendantOwnerNode: HTMLElement; } export declare function ListboxImplementation>( diff --git a/packages/select-rich/src/LionSelectInvoker.js b/packages/select-rich/src/LionSelectInvoker.js index 0356008bf..89e2180d7 100644 --- a/packages/select-rich/src/LionSelectInvoker.js +++ b/packages/select-rich/src/LionSelectInvoker.js @@ -8,6 +8,7 @@ import { css, html } from '@lion/core'; /** * LionSelectInvoker: invoker button consuming a selected element */ +// @ts-expect-error export class LionSelectInvoker extends LionButton { static get styles() { return [ diff --git a/packages/select-rich/src/LionSelectRich.js b/packages/select-rich/src/LionSelectRich.js index 5335a0347..1776e4231 100644 --- a/packages/select-rich/src/LionSelectRich.js +++ b/packages/select-rich/src/LionSelectRich.js @@ -6,10 +6,11 @@ import { LionSelectInvoker } from './LionSelectInvoker.js'; /** * @typedef {import('@lion/listbox').LionOptions} LionOptions - */ - -/** + * @typedef {import('@lion/listbox').LionOption} LionOption * @typedef {import('@open-wc/scoped-elements/src/types').ScopedElementsHost} ScopedElementsHost + * @typedef {import('@lion/form-core/types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost + * @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost + * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap */ function detectInteractionMode() { @@ -22,6 +23,7 @@ function detectInteractionMode() { /** * LionSelectRich: wraps the element */ +// @ts-expect-error export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(LionListbox))) { static get scopedElements() { return { @@ -48,6 +50,23 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L }; } + /** + * @enhance FormControlMixin + */ + // eslint-disable-next-line class-methods-use-this + _inputGroupInputTemplate() { + return html` +
+ +
+ + +
+
+ `; + } + + // @ts-ignore get slots() { return { ...super.slots, @@ -55,23 +74,25 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L }; } - /** @type {LionSelectInvoker} */ + /** + * @protected + * @type {LionSelectInvoker} + */ get _invokerNode() { return /** @type {LionSelectInvoker} */ (Array.from(this.children).find( child => child.slot === 'invoker', )); } + /** + * @configure ListboxMixin + * @protected + */ + // @ts-ignore get _scrollTargetNode() { - return this._listboxNode._scrollTargetNode || this._listboxNode; - } - - get checkedIndex() { - return /** @type {number} */ (super.checkedIndex); - } - - set checkedIndex(i) { - super.checkedIndex = i; + // TODO: should this be defined here or in extension layer? + // @ts-expect-error we allow the _overlayContentNode to define its own _scrollTargetNode + return this._overlayContentNode._scrollTargetNode || this._overlayContentNode; } constructor() { @@ -103,7 +124,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L connectedCallback() { super.connectedCallback(); - this._invokerNode.selectedElement = this.formElements[this.checkedIndex]; + this._invokerNode.selectedElement = this.formElements[ + /** @type {number} */ (this.checkedIndex) + ]; this.__setupInvokerNode(); this.__toggleInvokerDisabled(); this.addEventListener('keyup', this.__onKeyUp); @@ -135,57 +158,6 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L } } - /** - * Overrides FormRegistrar adding to make sure children have specific default states when added - * - * @override - * @param {LionOption} child - * @param {Number} indexToInsertAt - */ - addFormElement(child, indexToInsertAt) { - super.addFormElement(child, indexToInsertAt); - // the first elements checked by default - if ( - !this.hasNoDefaultSelected && - !this.__hasInitialSelectedFormElement && - (!child.disabled || this.disabled) - ) { - /* eslint-disable no-param-reassign */ - child.active = true; - child.checked = true; - /* eslint-enable no-param-reassign */ - this.__hasInitialSelectedFormElement = true; - } - this._onFormElementsChanged(); - } - - /** - * In the select disabled options are still going to a possible value for example - * when prefilling or programmatically setting it. - * - * @override - */ - _getCheckedElements() { - return this.formElements.filter(el => el.checked); - } - - __initInteractionStates() { - this.initInteractionState(); - } - - /** - * @param {FormRegisteringHost} child the child element (field) - */ - removeFormElement(child) { - super.removeFormElement(child); - this._onFormElementsChanged(); - } - - _onFormElementsChanged() { - this.singleOption = this.formElements.length === 1; - this._invokerNode.singleOption = this.singleOption; - } - /** * @param {import('lit-element').PropertyValues } changedProperties */ @@ -226,25 +198,58 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L } } - /** @deprecated. use _overlayCtrl.toggle */ + /** + * @enhance FprmRegistrarMixin make sure children have specific default states when added + * @param {LionOption & FormControlHost} child + * @param {Number} indexToInsertAt + */ + addFormElement(child, indexToInsertAt) { + super.addFormElement(child, indexToInsertAt); + // the first elements checked by default + if ( + !this.hasNoDefaultSelected && + !this.__hasInitialSelectedFormElement && + (!child.disabled || this.disabled) + ) { + /* eslint-disable no-param-reassign */ + child.active = true; + child.checked = true; + /* eslint-enable no-param-reassign */ + this.__hasInitialSelectedFormElement = true; + } + this._onFormElementsChanged(); + } + + /** + * @enhance FprmRegistrarMixin + * @param {FormRegisteringHost} child the child element (field) + */ + removeFormElement(child) { + super.removeFormElement(child); + this._onFormElementsChanged(); + } + + // TODO: move to overlayMixin and offer open and close toggle() { this.opened = !this.opened; } /** - * @override + * In the select disabled options are still going to a possible value for example + * when prefilling or programmatically setting it. + * @override ChoiceGroupMixin */ - // eslint-disable-next-line class-methods-use-this - _inputGroupInputTemplate() { - return html` -
- -
- - -
-
- `; + _getCheckedElements() { + return this.formElements.filter(el => el.checked); + } + + _onFormElementsChanged() { + this.singleOption = this.formElements.length === 1; + this._invokerNode.singleOption = this.singleOption; + } + + __initInteractionStates() { + this.initInteractionState(); } __toggleInvokerDisabled() { @@ -257,7 +262,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L __syncInvokerElement() { // sync to invoker if (this._invokerNode) { - this._invokerNode.selectedElement = this.formElements[this.checkedIndex]; + this._invokerNode.selectedElement = this.formElements[ + /** @type {number} */ (this.checkedIndex) + ]; } } @@ -290,7 +297,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L } /** - * @override OverlayMixin + * @configure OverlayMixin */ // eslint-disable-next-line class-methods-use-this _defineOverlayConfig() { @@ -323,7 +330,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L __overlayOnShow() { if (this.checkedIndex != null) { - this.activeIndex = this.checkedIndex; + this.activeIndex = /** @type {number} */ (this.checkedIndex); } this._listboxNode.focus(); } @@ -332,6 +339,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L this._invokerNode.focus(); } + /** + * @enhance OverlayMixin + */ _setupOverlayCtrl() { super._setupOverlayCtrl(); this._initialInheritsReferenceWidth = this._overlayCtrl.inheritsReferenceWidth; @@ -342,6 +352,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L this._overlayCtrl.addEventListener('hide', this.__overlayOnHide); } + /** + * @enhance OverlayMixin + */ _teardownOverlayCtrl() { super._teardownOverlayCtrl(); this._overlayCtrl.removeEventListener('show', this.__overlayOnShow); @@ -357,14 +370,14 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L } /** - * @override Configures OverlayMixin + * @configure OverlayMixin */ get _overlayInvokerNode() { return this._invokerNode; } /** - * @override Configures OverlayMixin + * @configure OverlayMixin */ get _overlayContentNode() { return this._listboxNode; @@ -388,7 +401,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L ev.preventDefault(); if (this.navigateWithinInvoker) { - this.setCheckedIndex(this._getPreviousEnabledOption(this.checkedIndex)); + this.setCheckedIndex( + this._getPreviousEnabledOption(/** @type {number} */ (this.checkedIndex)), + ); } else { this.opened = true; } @@ -396,7 +411,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L case 'ArrowDown': ev.preventDefault(); if (this.navigateWithinInvoker) { - this.setCheckedIndex(this._getNextEnabledOption(this.checkedIndex)); + this.setCheckedIndex( + this._getNextEnabledOption(/** @type {number} */ (this.checkedIndex)), + ); } else { this.opened = true; } diff --git a/packages/select-rich/test/demos.screenshots-test.js b/packages/select-rich/test/demos.screenshots-test.js index e04ffcdf5..df3d06e9a 100644 --- a/packages/select-rich/test/demos.screenshots-test.js +++ b/packages/select-rich/test/demos.screenshots-test.js @@ -5,16 +5,21 @@ const selector = 'lion-select-rich'; describe('forms-select-rich', () => { it('main', async () => { const id = `forms-select-rich--main`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('main-opened', async () => { const id = `forms-select-rich--main`; + // @ts-expect-error const page = await getStoryPage(id); await page.evaluate(() => { const el = document.querySelector('lion-select-rich'); + // @ts-expect-error el.opened = true; }); + // @ts-expect-error await capture({ selector, id: `${id}-opened`, @@ -24,16 +29,21 @@ describe('forms-select-rich', () => { }); it('options-with-html', async () => { const id = `forms-select-rich--options-with-html`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('options-with-html-opened', async () => { const id = `forms-select-rich--options-with-html`; + // @ts-expect-error const page = await getStoryPage(id); await page.evaluate(() => { const el = document.querySelector('lion-select-rich'); + // @ts-expect-error el.opened = true; }); + // @ts-expect-error await capture({ selector, id: `${id}-opened`, @@ -43,11 +53,14 @@ describe('forms-select-rich', () => { }); it('many-options-with-scrolling-opened', async () => { const id = `forms-select-rich--many-options-with-scrolling`; + // @ts-expect-error const page = await getStoryPage(id); await page.evaluate(() => { const el = document.querySelector('lion-select-rich'); + // @ts-expect-error el.opened = true; }); + // @ts-expect-error await capture({ selector, id: `${id}-opened`, @@ -57,21 +70,28 @@ describe('forms-select-rich', () => { }); it('read-only-prefilled', async () => { const id = `forms-select-rich--read-only-prefilled`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('disabled-select', async () => { const id = `forms-select-rich--disabled-select`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('disabled-option-opened', async () => { const id = `forms-select-rich--disabled-option`; + // @ts-expect-error const page = await getStoryPage(id); await page.evaluate(() => { const el = document.querySelector('lion-select-rich'); + // @ts-expect-error el.opened = true; }); + // @ts-expect-error await capture({ selector, id: `${id}-opened`, @@ -81,26 +101,35 @@ describe('forms-select-rich', () => { }); it('validation', async () => { const id = `forms-select-rich--validation`; + // @ts-expect-error const page = await getStoryPage(id); await page.evaluate(() => { const el = document.querySelector('lion-select-rich'); + // @ts-expect-error el.updateComplete.then(() => { + // @ts-expect-error el.touched = true; + // @ts-expect-error el.dirty = true; }); }); + // @ts-expect-error await capture({ selector, id, page }); }); it('render-options', async () => { const id = `forms-select-rich--render-options`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('interaction-mode-mac', async () => { const id = `forms-select-rich--interaction-mode`; + // @ts-expect-error const page = await getStoryPage(id); await page.click('lion-select-rich'); await page.keyboard.press('ArrowDown'); + // @ts-expect-error await capture({ selector, id: `${id}-mac`, @@ -109,9 +138,11 @@ describe('forms-select-rich', () => { }); it('interaction-mode-windows-linux', async () => { const id = `forms-select-rich--interaction-mode`; + // @ts-expect-error const page = await getStoryPage(id); await page.click('lion-select-rich:last-of-type'); await page.keyboard.press('ArrowDown'); + // @ts-expect-error await capture({ selector: 'lion-select-rich:last-of-type', id: `${id}-windows-linux`, @@ -120,12 +151,16 @@ describe('forms-select-rich', () => { }); it('no-default-selection', async () => { const id = `forms-select-rich--no-default-selection`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); it('single-option', async () => { const id = `forms-select-rich--single-option`; + // @ts-expect-error const page = await getStoryPage(id); + // @ts-expect-error await capture({ selector, id, page }); }); }); diff --git a/packages/select-rich/test/lion-select-invoker.test.js b/packages/select-rich/test/lion-select-invoker.test.js index 6ced4afc8..aef18d508 100644 --- a/packages/select-rich/test/lion-select-invoker.test.js +++ b/packages/select-rich/test/lion-select-invoker.test.js @@ -10,10 +10,12 @@ describe('lion-select-invoker', () => { }); it('renders invoker info based on selectedElement child elements', async () => { - const el = await fixture(html``); - el.selectedElement = await fixture( + const el = /** @type {LionSelectInvoker} */ (await fixture( + html``, + )); + el.selectedElement = /** @type {HTMLElement} */ (await fixture( `
Textnode

I am

2 lines

`, - ); + )); await el.updateComplete; expect(el._contentWrapperNode).lightDom.to.equal( @@ -29,31 +31,40 @@ describe('lion-select-invoker', () => { }); it('renders invoker info based on selectedElement textContent', async () => { - const el = await fixture(html``); - el.selectedElement = await fixture(`
just textContent
`); + const el = /** @type {LionSelectInvoker} */ (await fixture( + html``, + )); + el.selectedElement = /** @type {HTMLElement} */ (await fixture( + `
just textContent
`, + )); await el.updateComplete; expect(el._contentWrapperNode).lightDom.to.equal('just textContent'); }); it('has tabindex="0"', async () => { - const el = await fixture(html``); + const el = /** @type {LionSelectInvoker} */ (await fixture( + html``, + )); expect(el.tabIndex).to.equal(0); expect(el.getAttribute('tabindex')).to.equal('0'); }); it('should not render after slot when singleOption is true', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectInvoker} */ (await fixture(html` - `); + `)); - expect(el.shadowRoot.querySelector('slot[name="after"]')).to.not.exist; + expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('slot[name="after"]')).to.not + .exist; }); it('should render after slot when singleOption is not true', async () => { - const el = await fixture(html``); + const el = /** @type {LionSelectInvoker} */ (await fixture( + html``, + )); - expect(el.shadowRoot.querySelector('slot[name="after"]')).to.exist; + expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('slot[name="after"]')).to.exist; }); describe('Subclassers', () => { @@ -68,13 +79,17 @@ describe('lion-select-invoker', () => { } }, ); - const el = await fixture(`<${myTag}>`); + const el = /** @type {LionSelectInvoker} */ (await fixture(`<${myTag}>`)); - el.selectedElement = await fixture(`
cat
`); + el.selectedElement = /** @type {HTMLElement} */ (await fixture( + `
cat
`, + )); await el.updateComplete; expect(el._contentWrapperNode).lightDom.to.equal('cat selected'); - el.selectedElement = await fixture(`
dog
`); + el.selectedElement = /** @type {HTMLElement} */ (await fixture( + `
dog
`, + )); await el.updateComplete; expect(el._contentWrapperNode).lightDom.to.equal('no valid selection'); }); diff --git a/packages/select-rich/test/lion-select-rich-dialog-integration.test.js b/packages/select-rich/test/lion-select-rich-dialog-integration.test.js index 0d7675802..9dd64b6a1 100644 --- a/packages/select-rich/test/lion-select-rich-dialog-integration.test.js +++ b/packages/select-rich/test/lion-select-rich-dialog-integration.test.js @@ -5,6 +5,10 @@ import '@lion/listbox/lion-option.js'; import '@lion/listbox/lion-options.js'; import '../lion-select-rich.js'; +/** + * @typedef {import('../src/LionSelectRich').LionSelectRich} LionSelectRich + */ + const tagString = defineCE( class extends OverlayMixin(LitElement) { render() { @@ -24,14 +28,14 @@ describe('Select Rich Integration tests', () => { let properlyInstantiated = false; try { - const nestedEl = await fixture(html` + const nestedEl = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); await nestedEl.registrationComplete; await fixture(html` diff --git a/packages/select-rich/test/lion-select-rich-interaction.test.js b/packages/select-rich/test/lion-select-rich-interaction.test.js index a6d25fc1a..87697a2f0 100644 --- a/packages/select-rich/test/lion-select-rich-interaction.test.js +++ b/packages/select-rich/test/lion-select-rich-interaction.test.js @@ -1,57 +1,82 @@ import { Required } from '@lion/form-core'; import { expect, html, triggerBlurFor, triggerFocusFor, fixture } from '@open-wc/testing'; import { browserDetection } from '@lion/core'; - import '@lion/core/src/differentKeyEventNamesShimIE.js'; import '@lion/listbox/lion-option.js'; import '@lion/listbox/lion-options.js'; import '../lion-select-rich.js'; +/** + * @typedef {import('../src/LionSelectRich').LionSelectRich} LionSelectRich + * @typedef {import('@lion/listbox').LionOption} LionOption + */ + +/** + * @param {LionSelectRich} lionSelectEl + */ +function getNodes(lionSelectEl) { + // @ts-ignore protected members allowed in test + const { + _invokerNode: invoker, + _feedbackNode: feedback, + _labelNode: label, + _helpTextNode: helpText, + _listboxNode: listbox, + } = lionSelectEl; + return { + invoker, + feedback, + label, + helpText, + listbox, + }; +} + describe('lion-select-rich interactions', () => { describe('Interaction mode', () => { it('autodetects interactionMode if not defined', async () => { const originalIsMac = browserDetection.isMac; browserDetection.isMac = true; - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el.interactionMode).to.equal('mac'); - const el2 = await fixture(html` + const el2 = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el2.interactionMode).to.equal('windows/linux'); browserDetection.isMac = false; - const el3 = await fixture(html` + const el3 = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el3.interactionMode).to.equal('windows/linux'); - const el4 = await fixture(html` + const el4 = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el4.interactionMode).to.equal('mac'); browserDetection.isMac = originalIsMac; }); it('derives selectionFollowsFocus and navigateWithinInvoker from interactionMode', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el.selectionFollowsFocus).to.be.true; expect(el.navigateWithinInvoker).to.be.true; - const el2 = await fixture(html` + const el2 = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el2.selectionFollowsFocus).to.be.false; expect(el2.navigateWithinInvoker).to.be.false; }); @@ -59,7 +84,15 @@ describe('lion-select-rich interactions', () => { describe('Invoker Keyboard navigation Windows', () => { it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => { + /** + * @param {LionOption[]} options + * @param {number} selectedIndex + */ function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) { + /** + * @param {{ checked: any; }} option + * @param {any} i + */ options.forEach((option, i) => { if (i === selectedIndex) { expect(option.checked).to.be.true; @@ -69,7 +102,7 @@ describe('lion-select-rich interactions', () => { }); } - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 @@ -77,9 +110,9 @@ describe('lion-select-rich interactions', () => { Item 3 - `); + `)); - const options = Array.from(el.querySelectorAll('lion-option')); + const options = el.formElements; expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); @@ -95,79 +128,83 @@ describe('lion-select-rich interactions', () => { describe('Disabled', () => { it('invoker cannot be focused if disabled', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); - expect(el._invokerNode.tabIndex).to.equal(-1); + `)); + const { invoker } = getNodes(el); + expect(invoker.tabIndex).to.equal(-1); }); it('cannot be opened via click if disabled', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); - el._invokerNode.click(); + `)); + const { invoker } = getNodes(el); + invoker.click(); expect(el.opened).to.be.false; }); it('reflects disabled attribute to invoker', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); - expect(el._invokerNode.hasAttribute('disabled')).to.be.true; + `)); + const { invoker } = getNodes(el); + expect(invoker.hasAttribute('disabled')).to.be.true; el.removeAttribute('disabled'); await el.updateComplete; - expect(el._invokerNode.hasAttribute('disabled')).to.be.false; + expect(invoker.hasAttribute('disabled')).to.be.false; }); }); describe('Interaction states', () => { it('becomes touched if blurred once', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); + const { invoker } = getNodes(el); expect(el.touched).to.be.false; - await triggerFocusFor(el._invokerNode); - await triggerBlurFor(el._invokerNode); + await triggerFocusFor(invoker); + await triggerBlurFor(invoker); expect(el.touched).to.be.true; }); }); describe('Accessibility', () => { it('sets [aria-invalid="true"] to "._invokerNode" when there is an error', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Please select a value Item 1 - `); - const invokerNode = el._invokerNode; - const options = el.querySelectorAll('lion-option'); + `)); + const { invoker } = getNodes(el); + const options = el.formElements; await el.feedbackComplete; await el.updateComplete; - expect(invokerNode.getAttribute('aria-invalid')).to.equal('false'); + expect(invoker.getAttribute('aria-invalid')).to.equal('false'); options[0].checked = true; await el.feedbackComplete; await el.updateComplete; - expect(invokerNode.getAttribute('aria-invalid')).to.equal('true'); + expect(invoker.getAttribute('aria-invalid')).to.equal('true'); options[1].checked = true; await el.feedbackComplete; await el.updateComplete; - expect(invokerNode.getAttribute('aria-invalid')).to.equal('false'); + expect(invoker.getAttribute('aria-invalid')).to.equal('false'); }); }); }); diff --git a/packages/select-rich/test/lion-select-rich.test.js b/packages/select-rich/test/lion-select-rich.test.js index 157d123ba..1f6689ff1 100644 --- a/packages/select-rich/test/lion-select-rich.test.js +++ b/packages/select-rich/test/lion-select-rich.test.js @@ -16,219 +16,297 @@ import '@lion/listbox/lion-option.js'; import '@lion/listbox/lion-options.js'; import '../lion-select-rich.js'; +/** + * @typedef {import('@lion/listbox').LionOption} LionOption + */ + +/** + * @param {LionSelectRich} lionSelectEl + */ +function getProtectedMembers(lionSelectEl) { + // @ts-ignore protected members allowed in test + const { + _invokerNode: invoker, + _feedbackNode: feedback, + _labelNode: label, + _helpTextNode: helpText, + _listboxNode: listbox, + } = lionSelectEl; + return { + invoker, + feedback, + label, + helpText, + listbox, + }; +} + describe('lion-select-rich', () => { it('clicking the label should focus the invoker', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); expect(document.activeElement === document.body).to.be.true; el._labelNode.click(); + + // @ts-ignore allow protected access in tests expect(document.activeElement === el._invokerNode).to.be.true; }); it('checks the first enabled option', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); + `)); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); }); it('still has a checked value while disabled', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Red Hotpink Blue - `); + `)); expect(el.modelValue).to.equal('Red'); }); + it('supports having no default selection initially', async () => { + const el = /** @type {LionSelectRich} */ (await fixture(html` + + Red + Hotpink + Teal + + `)); + const { invoker } = getProtectedMembers(el); + expect(invoker.selectedElement).to.be.undefined; + expect(el.modelValue).to.equal(''); + }); + describe('Invoker', () => { it('generates an lion-select-invoker if no invoker is provided', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests expect(el._invokerNode).to.exist; + // @ts-ignore allow protected access in tests expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER'); }); it('sets the first option as the selectedElement if no option is checked', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); - const options = Array.from(el.querySelectorAll('lion-option')); + `)); + const options = el.formElements; + // @ts-ignore allow protected access in tests expect(el._invokerNode.selectedElement).dom.to.equal(options[0]); }); it('syncs the selected element to the invoker', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); const options = el.querySelectorAll('lion-option'); + // @ts-ignore allow protected access in tests expect(el._invokerNode.selectedElement).dom.to.equal(options[1]); el.checkedIndex = 0; await el.updateComplete; + // @ts-ignore allow protected access in tests expect(el._invokerNode.selectedElement).dom.to.equal(options[0]); }); it('delegates readonly to the invoker', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); expect(el.hasAttribute('readonly')).to.be.true; + // @ts-ignore allow protected access in tests expect(el._invokerNode.hasAttribute('readonly')).to.be.true; }); it('delegates singleOption to the invoker', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el.singleOption).to.be.true; + // @ts-ignore allow protected access in tests expect(el._invokerNode.hasAttribute('single-option')).to.be.true; }); }); describe('overlay', () => { it('should be closed by default', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); expect(el.opened).to.be.false; }); it('shows/hides the listbox via opened attribute', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); el.opened = true; await el.updateComplete; + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.isShown).to.be.true; el.opened = false; await el.updateComplete; + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.isShown).to.be.false; }); it('syncs opened state with overlay shown', async () => { - const el = await fixture(html` `); - const outerEl = await fixture(''); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + const outerEl = /** @type {HTMLButtonElement} */ (await fixture( + '', + )); expect(el.opened).to.be.true; // a click on the button will trigger hide on outside click // which we then need to sync back to "opened" outerEl.click(); - await aTimeout(); + await aTimeout(0); expect(el.opened).to.be.false; }); it('will focus the listbox on open and invoker on close', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests await el._overlayCtrl.show(); await el.updateComplete; + // @ts-ignore allow protected access in tests expect(document.activeElement === el._listboxNode).to.be.true; + // @ts-ignore allow protected access in tests expect(document.activeElement === el._invokerNode).to.be.false; el.opened = false; await el.updateComplete; + // @ts-ignore allow protected access in tests expect(document.activeElement === el._listboxNode).to.be.false; + // @ts-ignore allow protected access in tests expect(document.activeElement === el._invokerNode).to.be.true; }); it('opens the listbox with checked option as active', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); + // @ts-ignore allow protected access in tests await el._overlayCtrl.show(); await el.updateComplete; - const options = Array.from(el.querySelectorAll('lion-option')); + const options = el.formElements; expect(options[1].active).to.be.true; expect(options[1].checked).to.be.true; }); it('stays closed on click if it is disabled or readonly or has a single option', async () => { - const elReadOnly = await fixture(html` + const elReadOnly = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); - const elDisabled = await fixture(html` + const elDisabled = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); - const elSingleoption = await fixture(html` + const elSingleoption = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); + // @ts-ignore allow protected access in tests elReadOnly._invokerNode.click(); await elReadOnly.updateComplete; expect(elReadOnly.opened).to.be.false; + // @ts-ignore allow protected access in tests elDisabled._invokerNode.click(); await elDisabled.updateComplete; expect(elDisabled.opened).to.be.false; + // @ts-ignore allow protected access in tests elSingleoption._invokerNode.click(); await elSingleoption.updateComplete; expect(elSingleoption.opened).to.be.false; }); it('sets inheritsReferenceWidth to min by default', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Red Hotpink Teal - `); + `)); + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min'); el.opened = true; await el.updateComplete; + + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min'); }); it('should override the inheritsWidth prop when no default selected feature is used', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Red Hotpink Teal - `); + `)); // The default is min, so we override that behavior here + // @ts-ignore allow protected access in tests el._overlayCtrl.updateConfig({ inheritsReferenceWidth: 'full' }); el._initialInheritsReferenceWidth = 'full'; + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full'); el.opened = true; await el.updateComplete; // Opens while hasNoDefaultSelected = true, so we expect an override + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min'); // Emulate selecting hotpink, it closing, and opening it again @@ -239,69 +317,86 @@ describe('lion-select-rich', () => { await el.updateComplete; // noDefaultSelected will now flip the override back to what was the initial reference width + // @ts-ignore allow protected access in tests expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full'); }); it('should have singleOption only if there is exactly one option', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); expect(el.singleOption).to.be.false; + // @ts-ignore allow protected access in tests expect(el._invokerNode.singleOption).to.be.false; - const optionELm = el.querySelectorAll('lion-option')[0]; + const optionELm = el.formElements[0]; + // @ts-ignore allow protected access in tests optionELm.parentNode.removeChild(optionELm); el.requestUpdate(); await el.updateComplete; expect(el.singleOption).to.be.true; + // @ts-ignore allow protected access in tests expect(el._invokerNode.singleOption).to.be.true; - const newOption = document.createElement('lion-option'); + const newOption = /** @type {LionOption} */ (document.createElement('lion-option')); newOption.choiceValue = 30; el._inputNode.appendChild(newOption); el.requestUpdate(); await el.updateComplete; expect(el.singleOption).to.be.false; + // @ts-ignore allow protected access in tests expect(el._invokerNode.singleOption).to.be.false; }); }); describe('interaction-mode', () => { it('allows to specify an interaction-mode which determines other behaviors', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); + `)); expect(el.interactionMode).to.equal('mac'); }); }); describe('Keyboard navigation', () => { it('opens the listbox with [Enter] key via click handler', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests el._invokerNode.click(); - await aTimeout(); + await aTimeout(0); expect(el.opened).to.be.true; }); it('opens the listbox with [ ](Space) key via click handler', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests el._invokerNode.click(); - await aTimeout(); + await aTimeout(0); expect(el.opened).to.be.true; }); it('closes the listbox with [Escape] key once opened', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); expect(el.opened).to.be.false; }); it('closes the listbox with [Tab] key once opened', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); // tab can only be caught via keydown + // @ts-ignore allow protected access in tests el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); expect(el.opened).to.be.false; }); @@ -309,28 +404,34 @@ describe('lion-select-rich', () => { describe('Mouse navigation', () => { it('opens the listbox via click on invoker', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); expect(el.opened).to.be.false; + // @ts-ignore allow protected access in tests el._invokerNode.click(); await nextFrame(); // reflection of click takes some time expect(el.opened).to.be.true; }); it('closes the listbox when an option gets clicked', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 - `); + `)); expect(el.opened).to.be.true; - el.querySelector('lion-option').click(); + el.formElements[0].click(); expect(el.opened).to.be.false; }); }); describe('Keyboard navigation Windows', () => { it('closes the listbox with [Enter] key once opened', async () => { - const el = await fixture(html` `); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + // @ts-ignore allow protected access in tests el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); expect(el.opened).to.be.false; }); @@ -338,35 +439,35 @@ describe('lion-select-rich', () => { describe('Keyboard navigation Mac', () => { it('checks active item and closes the listbox with [Enter] key via click handler once opened', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); + `)); // changes active but not checked el.activeIndex = 1; expect(el.checkedIndex).to.equal(0); - + // @ts-ignore allow protected access in tests el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); expect(el.opened).to.be.false; expect(el.checkedIndex).to.equal(1); }); it('opens the listbox with [ArrowUp] key', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); + `)); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); await el.updateComplete; expect(el.opened).to.be.true; }); it('opens the listbox with [ArrowDown] key', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` - `); + `)); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); await el.updateComplete; expect(el.opened).to.be.true; @@ -375,34 +476,41 @@ describe('lion-select-rich', () => { describe('Accessibility', () => { it('has the right references to its inner elements', async () => { - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` Item 1 Item 2 - `); - expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._labelNode.id); - expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._invokerNode.id); - expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._helpTextNode.id); - expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._feedbackNode.id); - expect(el._invokerNode.getAttribute('aria-haspopup')).to.equal('listbox'); + `)); + const { invoker, feedback, label, helpText } = getProtectedMembers(el); + + expect(invoker.getAttribute('aria-labelledby')).to.contain(label.id); + expect(invoker.getAttribute('aria-labelledby')).to.contain(invoker.id); + expect(invoker.getAttribute('aria-describedby')).to.contain(helpText.id); + expect(invoker.getAttribute('aria-describedby')).to.contain(feedback.id); + expect(invoker.getAttribute('aria-haspopup')).to.equal('listbox'); }); it('notifies when the listbox is expanded or not', async () => { // smoke test for overlay functionality - const el = await fixture(html` `); - expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('false'); + const el = /** @type {LionSelectRich} */ (await fixture( + html` `, + )); + const { invoker } = getProtectedMembers(el); + + expect(invoker.getAttribute('aria-expanded')).to.equal('false'); el.opened = true; await el.updateComplete; await el.updateComplete; // need 2 awaits as overlay.show is an async function - expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('true'); + expect(invoker.getAttribute('aria-expanded')).to.equal('true'); }); }); describe('Use cases', () => { it('keeps showing the selected item after a new item has been added in the selectedIndex position', async () => { const mySelectContainerTagString = defineCE( + // @ts-expect-error class extends LitElement { static get properties() { return { @@ -447,26 +555,29 @@ describe('lion-select-rich', () => { }, ); const mySelectContainerTag = unsafeStatic(mySelectContainerTagString); - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` <${mySelectContainerTag}> - `); + `)); - const selectRich = el.shadowRoot.querySelector('lion-select-rich'); - const invoker = selectRich._invokerNode; + const selectRich = /** @type {LionSelectRich} */ ( + /** @type {ShadowRoot} */ (el.shadowRoot).querySelector('lion-select-rich') + ); + + const { invoker, listbox } = getProtectedMembers(selectRich); expect(selectRich.checkedIndex).to.equal(1); expect(selectRich.modelValue).to.equal('hotpink'); - expect(invoker.selectedElement.value).to.equal('hotpink'); + expect(/** @type {LionOption} */ (invoker.selectedElement).value).to.equal('hotpink'); - const newOption = document.createElement('lion-option'); + const newOption = /** @type {LionOption} */ (document.createElement('lion-option')); newOption.modelValue = { checked: false, value: 'blue' }; newOption.textContent = 'Blue'; - const hotpinkEl = selectRich._listboxNode.children[1]; + const hotpinkEl = listbox.children[1]; hotpinkEl.insertAdjacentElement('beforebegin', newOption); expect(selectRich.checkedIndex).to.equal(2); expect(selectRich.modelValue).to.equal('hotpink'); - expect(invoker.selectedElement.value).to.equal('hotpink'); + expect(/** @type {LionOption} */ (invoker.selectedElement).value).to.equal('hotpink'); }); }); @@ -474,6 +585,7 @@ describe('lion-select-rich', () => { it('allows to override the type of overlay', async () => { const mySelectTagString = defineCE( class MySelect extends LionSelectRich { + // @ts-expect-error _defineOverlay({ invokerNode, contentNode, contentWrapperNode }) { const ctrl = new OverlayController({ placementMode: 'global', @@ -491,7 +603,7 @@ describe('lion-select-rich', () => { const mySelectTag = unsafeStatic(mySelectTagString); - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` <${mySelectTag} label="Favorite color" name="color"> ${Array(2).map( @@ -501,10 +613,12 @@ describe('lion-select-rich', () => { )} - `); + `)); await el.updateComplete; + // @ts-ignore allow protected member access in tests expect(el._overlayCtrl.placementMode).to.equal('global'); el.dispatchEvent(new Event('switch')); + // @ts-ignore allow protected member access in tests expect(el._overlayCtrl.placementMode).to.equal('local'); }); @@ -530,7 +644,7 @@ describe('lion-select-rich', () => { ); const selectTag = unsafeStatic(selectTagName); - const el = await fixture(html` + const el = /** @type {LionSelectRich} */ (await fixture(html` <${selectTag} id="color" name="color" label="Favorite color" has-no-default-selected> Red @@ -538,11 +652,12 @@ describe('lion-select-rich', () => { Teal - `); + `)); + const { invoker } = getProtectedMembers(el); - expect(el._invokerNode.shadowRoot.getElementById('content-wrapper')).dom.to.equal( - `
Please select an option..
`, - ); + expect( + /** @type {ShadowRoot} */ (invoker.shadowRoot).getElementById('content-wrapper'), + ).dom.to.equal(`
Please select an option..
`); expect(el.modelValue).to.equal(''); }); }); diff --git a/tsconfig.json b/tsconfig.json index b636def47..bf5b2ebb7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "packages/accordion/**/*.js", "packages/button/src/**/*.js", "packages/calendar/**/*.js", + "packages/button/index.js", "packages/checkbox-group/**/*.js", "packages/collapsible/**/*.js", "packages/core/**/*.js", @@ -33,13 +34,14 @@ "packages/input-iban/**/*.js", "packages/input-range/**/*.js", "packages/input-stepper/**/*.js", - "packages/listbox/src/*.js", + "packages/listbox/**/*.js", "packages/localize/**/*.js", "packages/overlays/**/*.js", "packages/pagination/**/*.js", "packages/progress-indicator/**/*.js", "packages/radio-group/**/*.js", "packages/select/**/*.js", + "packages/select-rich/**/*.js", "packages/singleton-manager/**/*.js", "packages/steps/**/*.js", "packages/switch/**/*.js",