chore: types for listbox and select-rich
This commit is contained in:
parent
928a673a2f
commit
5aadf0b2f9
10 changed files with 618 additions and 336 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { Required } from '@lion/form-core';
|
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 { LionOptions } from '@lion/listbox';
|
||||||
import '@lion/listbox/lion-option.js';
|
import '@lion/listbox/lion-option.js';
|
||||||
import '@lion/listbox/lion-options.js';
|
import '@lion/listbox/lion-options.js';
|
||||||
|
|
@ -7,12 +7,33 @@ import '../lion-listbox.js';
|
||||||
import '@lion/core/src/differentKeyEventNamesShimIE.js';
|
import '@lion/core/src/differentKeyEventNamesShimIE.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/combobox/src/LionCombobox').LionCombobox} LionCombobox
|
|
||||||
* @typedef {import('../src/LionListbox').LionListbox} LionListbox
|
* @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<LionListbox>} */ (_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 = {}) {
|
export function runListboxMixinSuite(customConfig = {}) {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
@ -157,12 +178,12 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.showsFeedbackFor.includes('error')).to.be.true;
|
expect(el.showsFeedbackFor.includes('error')).to.be.true;
|
||||||
|
|
||||||
el._listboxNode.children[1].checked = true;
|
el.formElements[1].checked = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasFeedbackFor.includes('error')).to.be.false;
|
expect(el.hasFeedbackFor.includes('error')).to.be.false;
|
||||||
expect(el.showsFeedbackFor.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;
|
await el.updateComplete;
|
||||||
expect(el.hasFeedbackFor.includes('error')).to.be.true;
|
expect(el.hasFeedbackFor.includes('error')).to.be.true;
|
||||||
expect(el.showsFeedbackFor.includes('error')).to.be.true;
|
expect(el.showsFeedbackFor.includes('error')).to.be.true;
|
||||||
|
|
@ -170,19 +191,6 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Selection', () => {
|
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}>
|
|
||||||
<${optionTag} .choiceValue=${'hotpink'}>Hotpink</${optionTag}>
|
|
||||||
<${optionTag} .choiceValue=${'teal'}>Teal</${optionTag}>
|
|
||||||
</${tag}>
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(el.selectedElement).to.be.undefined;
|
|
||||||
expect(el.modelValue).to.equal('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports changing the selection through serializedValue setter', async () => {
|
it('supports changing the selection through serializedValue setter', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} id="color" name="color" label="Favorite color" has-no-default-selected>
|
<${tag} id="color" name="color" label="Favorite color" has-no-default-selected>
|
||||||
|
|
@ -203,30 +211,30 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('[axe]: is accessible when closed', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<${tag} label="age">
|
|
||||||
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
|
||||||
</${tag}>
|
|
||||||
`);
|
|
||||||
await expect(el).to.be.accessible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('[axe]: is accessible when opened', async () => {
|
it('[axe]: is accessible when opened', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} label="age">
|
<${tag} label="age" opened>
|
||||||
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
el.opened = true;
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
||||||
|
|
||||||
await expect(el).to.be.accessible();
|
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}>
|
||||||
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not have a tabindex', async () => {
|
it('does not have a tabindex', async () => {
|
||||||
const el = await fixture(html`<${tag}></${tag}>`);
|
const el = await fixture(html`<${tag}></${tag}>`);
|
||||||
expect(el.hasAttribute('tabindex')).to.be.false;
|
expect(el.hasAttribute('tabindex')).to.be.false;
|
||||||
|
|
@ -252,7 +260,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.be.null;
|
const { activeDescendantOwner } = getProtectedMembers(el);
|
||||||
|
|
||||||
|
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.be.null;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
|
|
@ -262,16 +272,10 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
// new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
// new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
||||||
// );
|
// );
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('first');
|
||||||
'first',
|
activeDescendantOwner.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
);
|
|
||||||
el._activeDescendantOwnerNode.dispatchEvent(
|
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
|
||||||
);
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('second');
|
||||||
'second',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('puts "aria-setsize" on all options to indicate the total amount of options', async () => {
|
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 () => {
|
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">
|
<${tag} autocomplete="none">
|
||||||
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
|
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
const optionEls = [].slice.call(el.querySelectorAll('lion-option'));
|
el.formElements.forEach((oEl, i) => {
|
||||||
optionEls.forEach((oEl, i) => {
|
|
||||||
expect(oEl.getAttribute('aria-posinset')).to.equal(`${i + 1}`);
|
expect(oEl.getAttribute('aria-posinset')).to.equal(`${i + 1}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -326,6 +329,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// @ts-ignore feature detect LionCombobox
|
||||||
if (el._comboboxNode) {
|
if (el._comboboxNode) {
|
||||||
// note that the modelValue can only be supplied as string if we have a textbox
|
// note that the modelValue can only be supplied as string if we have a textbox
|
||||||
// (parsers not supported atm)
|
// (parsers not supported atm)
|
||||||
|
|
@ -383,10 +387,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el.activeIndex).to.equal(-1);
|
const options = el.formElements;
|
||||||
|
|
||||||
el.querySelectorAll('lion-option')[1].active = true;
|
expect(el.activeIndex).to.equal(-1);
|
||||||
expect(el.querySelectorAll('lion-option')[0].active).to.be.false;
|
options[1].active = true;
|
||||||
|
expect(options[0].active).to.be.false;
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -397,8 +402,10 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'20'}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'20'}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
el.querySelectorAll('lion-option')[1].active = true;
|
options[1].active = true;
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
el.reset();
|
el.reset();
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
|
|
@ -416,15 +423,17 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
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[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
expect(options[2].active).to.be.false;
|
expect(options[2].active).to.be.false;
|
||||||
el.activeIndex = 2;
|
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[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
expect(options[2].active).to.be.true;
|
expect(options[2].active).to.be.true;
|
||||||
|
|
@ -466,12 +475,14 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
// Normalize suite
|
// Normalize suite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
expect(options[1].checked).to.be.true;
|
expect(options[1].checked).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -487,17 +498,20 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
// Normalize suite
|
// Normalize suite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||||
expect(options[1].checked).to.be.true;
|
expect(options[1].checked).to.be.true;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
|
// @ts-ignore allow protected member access in test
|
||||||
el._listboxReceivesNoFocus = true;
|
el._listboxReceivesNoFocus = true;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||||
expect(options[1].checked).to.be.false;
|
expect(options[1].checked).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -511,9 +525,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'c'}>C</${optionTag}>
|
<${optionTag} .choiceValue=${'c'}>C</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el.choiceValue).to.equal('a');
|
expect(el.modelValue).to.equal('a');
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'C' }));
|
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 () => {
|
it.skip('selects a value with multiple [character] keys', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
|
|
@ -524,9 +538,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F' }));
|
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' }));
|
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 () => {
|
it('navigates to first and last option with [Home] and [End] keys', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
|
|
@ -537,15 +551,17 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
|
// @ts-ignore allow protected members in tests
|
||||||
if (el._listboxReceivesNoFocus) {
|
if (el._listboxReceivesNoFocus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.activeIndex = 2;
|
el.activeIndex = 2;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
|
||||||
expect(el.activeIndex).to.equal(0);
|
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);
|
expect(el.activeIndex).to.equal(3);
|
||||||
});
|
});
|
||||||
it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
|
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</${optionTag}>
|
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
// Normalize across listbox/select-rich/combobox
|
// Normalize across listbox/select-rich/combobox
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
// selectionFollowsFocus will be true by default on combobox (running this suite),
|
// selectionFollowsFocus will be true by default on combobox (running this suite),
|
||||||
|
|
@ -563,10 +581,10 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
el.selectionFollowsFocus = false;
|
el.selectionFollowsFocus = false;
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
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.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).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.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
});
|
});
|
||||||
|
|
@ -580,6 +598,8 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
expect(el.orientation).to.equal('vertical');
|
expect(el.orientation).to.equal('vertical');
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// Normalize for suite tests
|
// Normalize for suite tests
|
||||||
|
|
@ -589,21 +609,21 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
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[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.true;
|
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[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
||||||
// No response to horizontal arrows...
|
// 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[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
||||||
el.activeIndex = 1;
|
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[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.true;
|
expect(options[1].active).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
@ -615,6 +635,8 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
expect(el.orientation).to.equal('horizontal');
|
expect(el.orientation).to.equal('horizontal');
|
||||||
|
|
||||||
// Normalize for suite tests
|
// Normalize for suite tests
|
||||||
|
|
@ -622,44 +644,45 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
|
||||||
expect(el.activeIndex).to.equal(1);
|
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);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
// No response to vertical arrows...
|
// 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);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('adds aria-orientation attribute to listbox node', async () => {
|
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">
|
<${tag} name="foo" orientation="horizontal">
|
||||||
<${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
<${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
expect(el._listboxNode.getAttribute('aria-orientation')).to.equal('horizontal');
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
expect(listbox.getAttribute('aria-orientation')).to.equal('horizontal');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Multiple Choice', () => {
|
describe('Multiple Choice', () => {
|
||||||
it('does not uncheck siblings', async () => {
|
it('does not uncheck siblings', async () => {
|
||||||
const el = /** @type {Listbox} */ (await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} name="foo" multiple-choice>
|
<${tag} name="foo" multiple-choice>
|
||||||
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chicory'}">Chicory</${optionTag}>
|
<${optionTag} .choiceValue="${'Chicory'}">Chicory</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
options[0].checked = true;
|
options[0].checked = true;
|
||||||
options[1].checked = true;
|
options[1].checked = true;
|
||||||
|
|
@ -668,17 +691,18 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works via different interaction mechanisms (click, enter, spaces)', async () => {
|
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>
|
<${tag} name="foo" multiple-choice>
|
||||||
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chicory'}">Chicory</${optionTag}>
|
<${optionTag} .choiceValue="${'Chicory'}">Chicory</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
|
|
||||||
// feature detection select-rich
|
// @ts-ignore feature detection select-rich
|
||||||
if (el.navigateWithinInvoker !== undefined) {
|
if (el.navigateWithinInvoker !== undefined) {
|
||||||
// Note we don't have multipleChoice in the select-rich yet.
|
// Note we don't have multipleChoice in the select-rich yet.
|
||||||
// TODO: implement in future when requested
|
// TODO: implement in future when requested
|
||||||
|
|
@ -692,59 +716,69 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
|
// @ts-ignore allow protected members in tests
|
||||||
el._uncheckChildren();
|
el._uncheckChildren();
|
||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
el.activeIndex = 1;
|
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(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
||||||
|
|
||||||
|
// @ts-ignore allow protected
|
||||||
if (el._listboxReceivesNoFocus) {
|
if (el._listboxReceivesNoFocus) {
|
||||||
return; // if suite is run for combobox, we don't respond to [Space]
|
return; // if suite is run for combobox, we don't respond to [Space]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
|
// @ts-ignore allow protected members in tests
|
||||||
el._uncheckChildren();
|
el._uncheckChildren();
|
||||||
|
|
||||||
// Space
|
// Space
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||||
expect(options[0].checked).to.equal(true);
|
expect(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('adds aria-multiselectable="true" to listbox node', async () => {
|
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>
|
<${tag} name="foo" multiple-choice>
|
||||||
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
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 () => {
|
it('does not allow "selectionFollowsFocus"', async () => {
|
||||||
const el = /** @type {Listbox} */ (await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} name="foo" multiple-choice>
|
<${tag} name="foo" multiple-choice>
|
||||||
<${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
<${optionTag} checked .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`);
|
||||||
el._inputNode.focus();
|
const { listbox, input } = getProtectedMembers(el);
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
|
||||||
expect(el._listboxNode.getAttribute('aria-multiselectable')).to.equal('true');
|
input.focus();
|
||||||
|
listbox.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(listbox.getAttribute('aria-multiselectable')).to.equal('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Selection Follows Focus', () => {
|
describe('Selection Follows Focus', () => {
|
||||||
it('navigates through list with [ArrowDown] [ArrowUp] keys: activates and checks the option', async () => {
|
it('navigates through list with [ArrowDown] [ArrowUp] keys: activates and checks the option', async () => {
|
||||||
|
/**
|
||||||
|
* @param {LionOption[]} options
|
||||||
|
* @param {number} selectedIndex
|
||||||
|
*/
|
||||||
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
|
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
|
||||||
options.forEach((option, i) => {
|
options.forEach((option, i) => {
|
||||||
if (i === selectedIndex) {
|
if (i === selectedIndex) {
|
||||||
|
|
@ -761,18 +795,20 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
|
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
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
|
// Normalize start values between listbox, slect and combobox and test interaction below
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 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.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 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.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
|
@ -786,15 +822,17 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
|
// @ts-ignore allow protected
|
||||||
if (el._listboxReceivesNoFocus) {
|
if (el._listboxReceivesNoFocus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('30');
|
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');
|
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');
|
expect(el.modelValue).to.equal('40');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -807,9 +845,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} checked .choiceValue=${'20'}>Item 2</${optionTag}>
|
<${optionTag} checked .choiceValue=${'20'}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const { checkedIndex } = el;
|
const { checkedIndex } = el;
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
expect(el.checkedIndex).to.equal(checkedIndex);
|
expect(el.checkedIndex).to.equal(checkedIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -820,7 +860,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const options = [...el.querySelectorAll('lion-option')];
|
const options = el.formElements;
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(options[0].disabled).to.be.true;
|
expect(options[0].disabled).to.be.true;
|
||||||
|
|
@ -839,7 +879,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const options = [...el.querySelectorAll('lion-option')];
|
const options = el.formElements;
|
||||||
expect(options[0].disabled).to.be.true;
|
expect(options[0].disabled).to.be.true;
|
||||||
expect(options[1].disabled).to.be.true;
|
expect(options[1].disabled).to.be.true;
|
||||||
|
|
||||||
|
|
@ -860,14 +900,16 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
||||||
el.activeIndex = 0;
|
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.activeIndex).to.equal(1);
|
||||||
|
|
||||||
expect(el.checkedIndex).to.equal(0);
|
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
|
// Checked index stays where it was
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
@ -881,11 +923,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20} id="myId">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20} id="myId">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const opt = el.querySelectorAll('lion-option')[1];
|
const { activeDescendantOwner } = getProtectedMembers(el);
|
||||||
|
|
||||||
|
const opt = el.formElements[1];
|
||||||
opt.active = true;
|
opt.active = true;
|
||||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('myId');
|
||||||
'myId',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set checked state', async () => {
|
it('can set checked state', async () => {
|
||||||
|
|
@ -895,7 +937,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const option = el.querySelectorAll('lion-option')[1];
|
const option = el.formElements[1];
|
||||||
option.checked = true;
|
option.checked = true;
|
||||||
expect(el.modelValue).to.equal(20);
|
expect(el.modelValue).to.equal(20);
|
||||||
});
|
});
|
||||||
|
|
@ -923,7 +965,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const options = el.querySelectorAll('lion-option');
|
const options = el.formElements;
|
||||||
options[0].checked = true;
|
options[0].checked = true;
|
||||||
expect(options[0].checked).to.be.true;
|
expect(options[0].checked).to.be.true;
|
||||||
expect(options[1].checked).to.be.false;
|
expect(options[1].checked).to.be.false;
|
||||||
|
|
@ -939,7 +981,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const options = el.querySelectorAll('lion-option');
|
const options = el.formElements;
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
options[1].active = true;
|
options[1].active = true;
|
||||||
expect(options[0].active).to.be.false;
|
expect(options[0].active).to.be.false;
|
||||||
|
|
@ -990,12 +1032,16 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(el.hasFeedbackFor).to.include('error');
|
expect(el.hasFeedbackFor).to.include('error');
|
||||||
|
// @ts-expect-error no types for 'have.a.property'
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
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');
|
expect(el.validationStates.error).to.have.a.property('Required');
|
||||||
|
|
||||||
el.modelValue = 20;
|
el.modelValue = 20;
|
||||||
expect(el.hasFeedbackFor).not.to.include('error');
|
expect(el.hasFeedbackFor).not.to.include('error');
|
||||||
|
// @ts-expect-error no types for 'have.a.property'
|
||||||
expect(el.validationStates).to.have.a.property('error');
|
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');
|
expect(el.validationStates.error).not.to.have.a.property('Required');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1062,14 +1108,15 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
const { listbox } = getProtectedMembers(el);
|
||||||
|
|
||||||
expect(el._listboxNode).to.exist;
|
expect(listbox).to.exist;
|
||||||
expect(el._listboxNode).to.be.instanceOf(LionOptions);
|
expect(listbox).to.be.instanceOf(LionOptions);
|
||||||
expect(el.querySelector('[role=listbox]')).to.equal(el._listboxNode);
|
expect(el.querySelector('[role=listbox]')).to.equal(listbox);
|
||||||
|
|
||||||
expect(el.formElements.length).to.equal(2);
|
expect(el.formElements.length).to.equal(2);
|
||||||
expect(el._listboxNode.children.length).to.equal(2);
|
expect(listbox.children.length).to.equal(2);
|
||||||
expect(el._listboxNode.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
|
expect(listbox.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,17 @@ export declare class ListboxHost {
|
||||||
|
|
||||||
public setCheckedIndex(index: number): void;
|
public setCheckedIndex(index: number): void;
|
||||||
|
|
||||||
|
/** Reset interaction states and modelValue */
|
||||||
|
public reset(): void;
|
||||||
|
|
||||||
protected _scrollTargetNode: LionOptions;
|
protected _scrollTargetNode: LionOptions;
|
||||||
|
|
||||||
protected _listboxNode: LionOptions;
|
protected _listboxNode: LionOptions;
|
||||||
|
|
||||||
|
protected _listboxReceivesNoFocus: boolean;
|
||||||
|
|
||||||
|
protected _uncheckChildren(): void;
|
||||||
|
|
||||||
private __setupListboxNode(): void;
|
private __setupListboxNode(): void;
|
||||||
|
|
||||||
protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
|
protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
|
||||||
|
|
@ -64,6 +71,8 @@ export declare class ListboxHost {
|
||||||
protected _setupListboxInteractions(): void;
|
protected _setupListboxInteractions(): void;
|
||||||
|
|
||||||
protected _onChildActiveChanged(ev: Event): void;
|
protected _onChildActiveChanged(ev: Event): void;
|
||||||
|
|
||||||
|
protected _activeDescendantOwnerNode: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ListboxImplementation<T extends Constructor<LitElement>>(
|
export declare function ListboxImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { css, html } from '@lion/core';
|
||||||
/**
|
/**
|
||||||
* LionSelectInvoker: invoker button consuming a selected element
|
* LionSelectInvoker: invoker button consuming a selected element
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
export class LionSelectInvoker extends LionButton {
|
export class LionSelectInvoker extends LionButton {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import { LionSelectInvoker } from './LionSelectInvoker.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/listbox').LionOptions} LionOptions
|
* @typedef {import('@lion/listbox').LionOptions} LionOptions
|
||||||
*/
|
* @typedef {import('@lion/listbox').LionOption} LionOption
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('@open-wc/scoped-elements/src/types').ScopedElementsHost} ScopedElementsHost
|
* @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() {
|
function detectInteractionMode() {
|
||||||
|
|
@ -22,6 +23,7 @@ function detectInteractionMode() {
|
||||||
/**
|
/**
|
||||||
* LionSelectRich: wraps the <lion-listbox> element
|
* LionSelectRich: wraps the <lion-listbox> element
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(LionListbox))) {
|
export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(LionListbox))) {
|
||||||
static get scopedElements() {
|
static get scopedElements() {
|
||||||
return {
|
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`
|
||||||
|
<div class="input-group__input">
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<div id="overlay-content-node-wrapper">
|
||||||
|
<slot name="input"></slot>
|
||||||
|
<slot id="options-outlet"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -55,23 +74,25 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {LionSelectInvoker} */
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {LionSelectInvoker}
|
||||||
|
*/
|
||||||
get _invokerNode() {
|
get _invokerNode() {
|
||||||
return /** @type {LionSelectInvoker} */ (Array.from(this.children).find(
|
return /** @type {LionSelectInvoker} */ (Array.from(this.children).find(
|
||||||
child => child.slot === 'invoker',
|
child => child.slot === 'invoker',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure ListboxMixin
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
get _scrollTargetNode() {
|
get _scrollTargetNode() {
|
||||||
return this._listboxNode._scrollTargetNode || this._listboxNode;
|
// 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;
|
||||||
get checkedIndex() {
|
|
||||||
return /** @type {number} */ (super.checkedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
set checkedIndex(i) {
|
|
||||||
super.checkedIndex = i;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -103,7 +124,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
this._invokerNode.selectedElement = this.formElements[
|
||||||
|
/** @type {number} */ (this.checkedIndex)
|
||||||
|
];
|
||||||
this.__setupInvokerNode();
|
this.__setupInvokerNode();
|
||||||
this.__toggleInvokerDisabled();
|
this.__toggleInvokerDisabled();
|
||||||
this.addEventListener('keyup', this.__onKeyUp);
|
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
|
* @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() {
|
toggle() {
|
||||||
this.opened = !this.opened;
|
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
|
_getCheckedElements() {
|
||||||
_inputGroupInputTemplate() {
|
return this.formElements.filter(el => el.checked);
|
||||||
return html`
|
}
|
||||||
<div class="input-group__input">
|
|
||||||
<slot name="invoker"></slot>
|
_onFormElementsChanged() {
|
||||||
<div id="overlay-content-node-wrapper">
|
this.singleOption = this.formElements.length === 1;
|
||||||
<slot name="input"></slot>
|
this._invokerNode.singleOption = this.singleOption;
|
||||||
<slot id="options-outlet"></slot>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
__initInteractionStates() {
|
||||||
`;
|
this.initInteractionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
__toggleInvokerDisabled() {
|
__toggleInvokerDisabled() {
|
||||||
|
|
@ -257,7 +262,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
__syncInvokerElement() {
|
__syncInvokerElement() {
|
||||||
// sync to invoker
|
// sync to invoker
|
||||||
if (this._invokerNode) {
|
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
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlayConfig() {
|
_defineOverlayConfig() {
|
||||||
|
|
@ -323,7 +330,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
|
|
||||||
__overlayOnShow() {
|
__overlayOnShow() {
|
||||||
if (this.checkedIndex != null) {
|
if (this.checkedIndex != null) {
|
||||||
this.activeIndex = this.checkedIndex;
|
this.activeIndex = /** @type {number} */ (this.checkedIndex);
|
||||||
}
|
}
|
||||||
this._listboxNode.focus();
|
this._listboxNode.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +339,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
this._invokerNode.focus();
|
this._invokerNode.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enhance OverlayMixin
|
||||||
|
*/
|
||||||
_setupOverlayCtrl() {
|
_setupOverlayCtrl() {
|
||||||
super._setupOverlayCtrl();
|
super._setupOverlayCtrl();
|
||||||
this._initialInheritsReferenceWidth = this._overlayCtrl.inheritsReferenceWidth;
|
this._initialInheritsReferenceWidth = this._overlayCtrl.inheritsReferenceWidth;
|
||||||
|
|
@ -342,6 +352,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
this._overlayCtrl.addEventListener('hide', this.__overlayOnHide);
|
this._overlayCtrl.addEventListener('hide', this.__overlayOnHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enhance OverlayMixin
|
||||||
|
*/
|
||||||
_teardownOverlayCtrl() {
|
_teardownOverlayCtrl() {
|
||||||
super._teardownOverlayCtrl();
|
super._teardownOverlayCtrl();
|
||||||
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
|
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() {
|
get _overlayInvokerNode() {
|
||||||
return this._invokerNode;
|
return this._invokerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override Configures OverlayMixin
|
* @configure OverlayMixin
|
||||||
*/
|
*/
|
||||||
get _overlayContentNode() {
|
get _overlayContentNode() {
|
||||||
return this._listboxNode;
|
return this._listboxNode;
|
||||||
|
|
@ -388,7 +401,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (this.navigateWithinInvoker) {
|
if (this.navigateWithinInvoker) {
|
||||||
this.setCheckedIndex(this._getPreviousEnabledOption(this.checkedIndex));
|
this.setCheckedIndex(
|
||||||
|
this._getPreviousEnabledOption(/** @type {number} */ (this.checkedIndex)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
|
|
@ -396,7 +411,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.navigateWithinInvoker) {
|
if (this.navigateWithinInvoker) {
|
||||||
this.setCheckedIndex(this._getNextEnabledOption(this.checkedIndex));
|
this.setCheckedIndex(
|
||||||
|
this._getNextEnabledOption(/** @type {number} */ (this.checkedIndex)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,21 @@ const selector = 'lion-select-rich';
|
||||||
describe('forms-select-rich', () => {
|
describe('forms-select-rich', () => {
|
||||||
it('main', async () => {
|
it('main', async () => {
|
||||||
const id = `forms-select-rich--main`;
|
const id = `forms-select-rich--main`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('main-opened', async () => {
|
it('main-opened', async () => {
|
||||||
const id = `forms-select-rich--main`;
|
const id = `forms-select-rich--main`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const el = document.querySelector('lion-select-rich');
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
// @ts-expect-error
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector,
|
selector,
|
||||||
id: `${id}-opened`,
|
id: `${id}-opened`,
|
||||||
|
|
@ -24,16 +29,21 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('options-with-html', async () => {
|
it('options-with-html', async () => {
|
||||||
const id = `forms-select-rich--options-with-html`;
|
const id = `forms-select-rich--options-with-html`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('options-with-html-opened', async () => {
|
it('options-with-html-opened', async () => {
|
||||||
const id = `forms-select-rich--options-with-html`;
|
const id = `forms-select-rich--options-with-html`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const el = document.querySelector('lion-select-rich');
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
// @ts-expect-error
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector,
|
selector,
|
||||||
id: `${id}-opened`,
|
id: `${id}-opened`,
|
||||||
|
|
@ -43,11 +53,14 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('many-options-with-scrolling-opened', async () => {
|
it('many-options-with-scrolling-opened', async () => {
|
||||||
const id = `forms-select-rich--many-options-with-scrolling`;
|
const id = `forms-select-rich--many-options-with-scrolling`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const el = document.querySelector('lion-select-rich');
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
// @ts-expect-error
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector,
|
selector,
|
||||||
id: `${id}-opened`,
|
id: `${id}-opened`,
|
||||||
|
|
@ -57,21 +70,28 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('read-only-prefilled', async () => {
|
it('read-only-prefilled', async () => {
|
||||||
const id = `forms-select-rich--read-only-prefilled`;
|
const id = `forms-select-rich--read-only-prefilled`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('disabled-select', async () => {
|
it('disabled-select', async () => {
|
||||||
const id = `forms-select-rich--disabled-select`;
|
const id = `forms-select-rich--disabled-select`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('disabled-option-opened', async () => {
|
it('disabled-option-opened', async () => {
|
||||||
const id = `forms-select-rich--disabled-option`;
|
const id = `forms-select-rich--disabled-option`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const el = document.querySelector('lion-select-rich');
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
// @ts-expect-error
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector,
|
selector,
|
||||||
id: `${id}-opened`,
|
id: `${id}-opened`,
|
||||||
|
|
@ -81,26 +101,35 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('validation', async () => {
|
it('validation', async () => {
|
||||||
const id = `forms-select-rich--validation`;
|
const id = `forms-select-rich--validation`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const el = document.querySelector('lion-select-rich');
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
// @ts-expect-error
|
||||||
el.updateComplete.then(() => {
|
el.updateComplete.then(() => {
|
||||||
|
// @ts-expect-error
|
||||||
el.touched = true;
|
el.touched = true;
|
||||||
|
// @ts-expect-error
|
||||||
el.dirty = true;
|
el.dirty = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('render-options', async () => {
|
it('render-options', async () => {
|
||||||
const id = `forms-select-rich--render-options`;
|
const id = `forms-select-rich--render-options`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('interaction-mode-mac', async () => {
|
it('interaction-mode-mac', async () => {
|
||||||
const id = `forms-select-rich--interaction-mode`;
|
const id = `forms-select-rich--interaction-mode`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.click('lion-select-rich');
|
await page.click('lion-select-rich');
|
||||||
await page.keyboard.press('ArrowDown');
|
await page.keyboard.press('ArrowDown');
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector,
|
selector,
|
||||||
id: `${id}-mac`,
|
id: `${id}-mac`,
|
||||||
|
|
@ -109,9 +138,11 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('interaction-mode-windows-linux', async () => {
|
it('interaction-mode-windows-linux', async () => {
|
||||||
const id = `forms-select-rich--interaction-mode`;
|
const id = `forms-select-rich--interaction-mode`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
await page.click('lion-select-rich:last-of-type');
|
await page.click('lion-select-rich:last-of-type');
|
||||||
await page.keyboard.press('ArrowDown');
|
await page.keyboard.press('ArrowDown');
|
||||||
|
// @ts-expect-error
|
||||||
await capture({
|
await capture({
|
||||||
selector: 'lion-select-rich:last-of-type',
|
selector: 'lion-select-rich:last-of-type',
|
||||||
id: `${id}-windows-linux`,
|
id: `${id}-windows-linux`,
|
||||||
|
|
@ -120,12 +151,16 @@ describe('forms-select-rich', () => {
|
||||||
});
|
});
|
||||||
it('no-default-selection', async () => {
|
it('no-default-selection', async () => {
|
||||||
const id = `forms-select-rich--no-default-selection`;
|
const id = `forms-select-rich--no-default-selection`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
it('single-option', async () => {
|
it('single-option', async () => {
|
||||||
const id = `forms-select-rich--single-option`;
|
const id = `forms-select-rich--single-option`;
|
||||||
|
// @ts-expect-error
|
||||||
const page = await getStoryPage(id);
|
const page = await getStoryPage(id);
|
||||||
|
// @ts-expect-error
|
||||||
await capture({ selector, id, page });
|
await capture({ selector, id, page });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ describe('lion-select-invoker', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders invoker info based on selectedElement child elements', async () => {
|
it('renders invoker info based on selectedElement child elements', async () => {
|
||||||
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
const el = /** @type {LionSelectInvoker} */ (await fixture(
|
||||||
el.selectedElement = await fixture(
|
html`<lion-select-invoker></lion-select-invoker>`,
|
||||||
|
));
|
||||||
|
el.selectedElement = /** @type {HTMLElement} */ (await fixture(
|
||||||
`<div class="option">Textnode<h2>I am</h2><p>2 lines</p></div>`,
|
`<div class="option">Textnode<h2>I am</h2><p>2 lines</p></div>`,
|
||||||
);
|
));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el._contentWrapperNode).lightDom.to.equal(
|
expect(el._contentWrapperNode).lightDom.to.equal(
|
||||||
|
|
@ -29,31 +31,40 @@ describe('lion-select-invoker', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders invoker info based on selectedElement textContent', async () => {
|
it('renders invoker info based on selectedElement textContent', async () => {
|
||||||
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
const el = /** @type {LionSelectInvoker} */ (await fixture(
|
||||||
el.selectedElement = await fixture(`<div class="option">just textContent</div>`);
|
html`<lion-select-invoker></lion-select-invoker>`,
|
||||||
|
));
|
||||||
|
el.selectedElement = /** @type {HTMLElement} */ (await fixture(
|
||||||
|
`<div class="option">just textContent</div>`,
|
||||||
|
));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el._contentWrapperNode).lightDom.to.equal('just textContent');
|
expect(el._contentWrapperNode).lightDom.to.equal('just textContent');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has tabindex="0"', async () => {
|
it('has tabindex="0"', async () => {
|
||||||
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
const el = /** @type {LionSelectInvoker} */ (await fixture(
|
||||||
|
html`<lion-select-invoker></lion-select-invoker>`,
|
||||||
|
));
|
||||||
expect(el.tabIndex).to.equal(0);
|
expect(el.tabIndex).to.equal(0);
|
||||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render after slot when singleOption is true', async () => {
|
it('should not render after slot when singleOption is true', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectInvoker} */ (await fixture(html`
|
||||||
<lion-select-invoker .singleOption="${true}"></lion-select-invoker>
|
<lion-select-invoker .singleOption="${true}"></lion-select-invoker>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
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 () => {
|
it('should render after slot when singleOption is not true', async () => {
|
||||||
const el = await fixture(html`<lion-select-invoker></lion-select-invoker>`);
|
const el = /** @type {LionSelectInvoker} */ (await fixture(
|
||||||
|
html`<lion-select-invoker></lion-select-invoker>`,
|
||||||
|
));
|
||||||
|
|
||||||
expect(el.shadowRoot.querySelector('slot[name="after"]')).to.exist;
|
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('slot[name="after"]')).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
|
|
@ -68,13 +79,17 @@ describe('lion-select-invoker', () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const el = await fixture(`<${myTag}></${myTag}>`);
|
const el = /** @type {LionSelectInvoker} */ (await fixture(`<${myTag}></${myTag}>`));
|
||||||
|
|
||||||
el.selectedElement = await fixture(`<div class="option">cat</div>`);
|
el.selectedElement = /** @type {HTMLElement} */ (await fixture(
|
||||||
|
`<div class="option">cat</div>`,
|
||||||
|
));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._contentWrapperNode).lightDom.to.equal('cat selected');
|
expect(el._contentWrapperNode).lightDom.to.equal('cat selected');
|
||||||
|
|
||||||
el.selectedElement = await fixture(`<div class="option">dog</div>`);
|
el.selectedElement = /** @type {HTMLElement} */ (await fixture(
|
||||||
|
`<div class="option">dog</div>`,
|
||||||
|
));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._contentWrapperNode).lightDom.to.equal('no valid selection');
|
expect(el._contentWrapperNode).lightDom.to.equal('no valid selection');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ import '@lion/listbox/lion-option.js';
|
||||||
import '@lion/listbox/lion-options.js';
|
import '@lion/listbox/lion-options.js';
|
||||||
import '../lion-select-rich.js';
|
import '../lion-select-rich.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/LionSelectRich').LionSelectRich} LionSelectRich
|
||||||
|
*/
|
||||||
|
|
||||||
const tagString = defineCE(
|
const tagString = defineCE(
|
||||||
class extends OverlayMixin(LitElement) {
|
class extends OverlayMixin(LitElement) {
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -24,14 +28,14 @@ describe('Select Rich Integration tests', () => {
|
||||||
let properlyInstantiated = false;
|
let properlyInstantiated = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nestedEl = await fixture(html`
|
const nestedEl = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-options slot="input">
|
<lion-options slot="input">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
await nestedEl.registrationComplete;
|
await nestedEl.registrationComplete;
|
||||||
|
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,82 @@
|
||||||
import { Required } from '@lion/form-core';
|
import { Required } from '@lion/form-core';
|
||||||
import { expect, html, triggerBlurFor, triggerFocusFor, fixture } from '@open-wc/testing';
|
import { expect, html, triggerBlurFor, triggerFocusFor, fixture } from '@open-wc/testing';
|
||||||
import { browserDetection } from '@lion/core';
|
import { browserDetection } from '@lion/core';
|
||||||
|
|
||||||
import '@lion/core/src/differentKeyEventNamesShimIE.js';
|
import '@lion/core/src/differentKeyEventNamesShimIE.js';
|
||||||
import '@lion/listbox/lion-option.js';
|
import '@lion/listbox/lion-option.js';
|
||||||
import '@lion/listbox/lion-options.js';
|
import '@lion/listbox/lion-options.js';
|
||||||
import '../lion-select-rich.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('lion-select-rich interactions', () => {
|
||||||
describe('Interaction mode', () => {
|
describe('Interaction mode', () => {
|
||||||
it('autodetects interactionMode if not defined', async () => {
|
it('autodetects interactionMode if not defined', async () => {
|
||||||
const originalIsMac = browserDetection.isMac;
|
const originalIsMac = browserDetection.isMac;
|
||||||
|
|
||||||
browserDetection.isMac = true;
|
browserDetection.isMac = true;
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich>
|
<lion-select-rich><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el.interactionMode).to.equal('mac');
|
expect(el.interactionMode).to.equal('mac');
|
||||||
const el2 = await fixture(html`
|
const el2 = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="windows/linux"
|
<lion-select-rich interaction-mode="windows/linux"
|
||||||
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
||||||
>
|
>
|
||||||
`);
|
`));
|
||||||
expect(el2.interactionMode).to.equal('windows/linux');
|
expect(el2.interactionMode).to.equal('windows/linux');
|
||||||
|
|
||||||
browserDetection.isMac = false;
|
browserDetection.isMac = false;
|
||||||
const el3 = await fixture(html`
|
const el3 = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich>
|
<lion-select-rich><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el3.interactionMode).to.equal('windows/linux');
|
expect(el3.interactionMode).to.equal('windows/linux');
|
||||||
const el4 = await fixture(html`
|
const el4 = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="mac"
|
<lion-select-rich interaction-mode="mac"
|
||||||
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
||||||
>
|
>
|
||||||
`);
|
`));
|
||||||
expect(el4.interactionMode).to.equal('mac');
|
expect(el4.interactionMode).to.equal('mac');
|
||||||
browserDetection.isMac = originalIsMac;
|
browserDetection.isMac = originalIsMac;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('derives selectionFollowsFocus and navigateWithinInvoker from interactionMode', async () => {
|
it('derives selectionFollowsFocus and navigateWithinInvoker from interactionMode', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="windows/linux"
|
<lion-select-rich interaction-mode="windows/linux"
|
||||||
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
||||||
>
|
>
|
||||||
`);
|
`));
|
||||||
expect(el.selectionFollowsFocus).to.be.true;
|
expect(el.selectionFollowsFocus).to.be.true;
|
||||||
expect(el.navigateWithinInvoker).to.be.true;
|
expect(el.navigateWithinInvoker).to.be.true;
|
||||||
|
|
||||||
const el2 = await fixture(html`
|
const el2 = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="mac"
|
<lion-select-rich interaction-mode="mac"
|
||||||
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
><lion-option .choiceValue=${10}>Item 1</lion-option></lion-select-rich
|
||||||
>
|
>
|
||||||
`);
|
`));
|
||||||
expect(el2.selectionFollowsFocus).to.be.false;
|
expect(el2.selectionFollowsFocus).to.be.false;
|
||||||
expect(el2.navigateWithinInvoker).to.be.false;
|
expect(el2.navigateWithinInvoker).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -59,7 +84,15 @@ describe('lion-select-rich interactions', () => {
|
||||||
|
|
||||||
describe('Invoker Keyboard navigation Windows', () => {
|
describe('Invoker Keyboard navigation Windows', () => {
|
||||||
it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => {
|
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) {
|
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
|
||||||
|
/**
|
||||||
|
* @param {{ checked: any; }} option
|
||||||
|
* @param {any} i
|
||||||
|
*/
|
||||||
options.forEach((option, i) => {
|
options.forEach((option, i) => {
|
||||||
if (i === selectedIndex) {
|
if (i === selectedIndex) {
|
||||||
expect(option.checked).to.be.true;
|
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`
|
||||||
<lion-select-rich interaction-mode="windows/linux">
|
<lion-select-rich interaction-mode="windows/linux">
|
||||||
<lion-options slot="input">
|
<lion-options slot="input">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
|
@ -77,9 +110,9 @@ describe('lion-select-rich interactions', () => {
|
||||||
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const options = Array.from(el.querySelectorAll('lion-option'));
|
const options = el.formElements;
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
|
||||||
|
|
@ -95,79 +128,83 @@ describe('lion-select-rich interactions', () => {
|
||||||
|
|
||||||
describe('Disabled', () => {
|
describe('Disabled', () => {
|
||||||
it('invoker cannot be focused if disabled', async () => {
|
it('invoker cannot be focused if disabled', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
<lion-options slot="input"></lion-options>
|
<lion-options slot="input"></lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
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 () => {
|
it('cannot be opened via click if disabled', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
<lion-options slot="input"></lion-options>
|
<lion-options slot="input"></lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
el._invokerNode.click();
|
const { invoker } = getNodes(el);
|
||||||
|
invoker.click();
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects disabled attribute to invoker', async () => {
|
it('reflects disabled attribute to invoker', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
<lion-options slot="input"></lion-options>
|
<lion-options slot="input"></lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el._invokerNode.hasAttribute('disabled')).to.be.true;
|
const { invoker } = getNodes(el);
|
||||||
|
expect(invoker.hasAttribute('disabled')).to.be.true;
|
||||||
el.removeAttribute('disabled');
|
el.removeAttribute('disabled');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._invokerNode.hasAttribute('disabled')).to.be.false;
|
expect(invoker.hasAttribute('disabled')).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Interaction states', () => {
|
describe('Interaction states', () => {
|
||||||
it('becomes touched if blurred once', async () => {
|
it('becomes touched if blurred once', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-options slot="input">
|
<lion-options slot="input">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
const { invoker } = getNodes(el);
|
||||||
expect(el.touched).to.be.false;
|
expect(el.touched).to.be.false;
|
||||||
await triggerFocusFor(el._invokerNode);
|
await triggerFocusFor(invoker);
|
||||||
await triggerBlurFor(el._invokerNode);
|
await triggerBlurFor(invoker);
|
||||||
expect(el.touched).to.be.true;
|
expect(el.touched).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('sets [aria-invalid="true"] to "._invokerNode" when there is an error', async () => {
|
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`
|
||||||
<lion-select-rich .validators=${[new Required()]}>
|
<lion-select-rich .validators=${[new Required()]}>
|
||||||
<lion-options slot="input">
|
<lion-options slot="input">
|
||||||
<lion-option .choiceValue=${null}>Please select a value</lion-option>
|
<lion-option .choiceValue=${null}>Please select a value</lion-option>
|
||||||
<lion-option .modelValue=${{ value: 10, checked: true }}>Item 1</lion-option>
|
<lion-option .modelValue=${{ value: 10, checked: true }}>Item 1</lion-option>
|
||||||
</lion-options>
|
</lion-options>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
const invokerNode = el._invokerNode;
|
const { invoker } = getNodes(el);
|
||||||
const options = el.querySelectorAll('lion-option');
|
const options = el.formElements;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(invokerNode.getAttribute('aria-invalid')).to.equal('false');
|
expect(invoker.getAttribute('aria-invalid')).to.equal('false');
|
||||||
|
|
||||||
options[0].checked = true;
|
options[0].checked = true;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(invokerNode.getAttribute('aria-invalid')).to.equal('true');
|
expect(invoker.getAttribute('aria-invalid')).to.equal('true');
|
||||||
|
|
||||||
options[1].checked = true;
|
options[1].checked = true;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(invokerNode.getAttribute('aria-invalid')).to.equal('false');
|
expect(invoker.getAttribute('aria-invalid')).to.equal('false');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,219 +16,297 @@ import '@lion/listbox/lion-option.js';
|
||||||
import '@lion/listbox/lion-options.js';
|
import '@lion/listbox/lion-options.js';
|
||||||
import '../lion-select-rich.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', () => {
|
describe('lion-select-rich', () => {
|
||||||
it('clicking the label should focus the invoker', async () => {
|
it('clicking the label should focus the invoker', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich label="foo"> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich label="foo"> </lion-select-rich> `,
|
||||||
|
));
|
||||||
expect(document.activeElement === document.body).to.be.true;
|
expect(document.activeElement === document.body).to.be.true;
|
||||||
el._labelNode.click();
|
el._labelNode.click();
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(document.activeElement === el._invokerNode).to.be.true;
|
expect(document.activeElement === el._invokerNode).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks the first enabled option', async () => {
|
it('checks the first enabled option', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${'Red'}></lion-option>
|
<lion-option .choiceValue=${'Red'}></lion-option>
|
||||||
<lion-option .choiceValue=${'Hotpink'}></lion-option>
|
<lion-option .choiceValue=${'Hotpink'}></lion-option>
|
||||||
<lion-option .choiceValue=${'Blue'}></lion-option>
|
<lion-option .choiceValue=${'Blue'}></lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('still has a checked value while disabled', async () => {
|
it('still has a checked value while disabled', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
<lion-option .choiceValue=${'Red'}>Red</lion-option>
|
<lion-option .choiceValue=${'Red'}>Red</lion-option>
|
||||||
<lion-option .choiceValue=${'Hotpink'}>Hotpink</lion-option>
|
<lion-option .choiceValue=${'Hotpink'}>Hotpink</lion-option>
|
||||||
<lion-option .choiceValue=${'Blue'}>Blue</lion-option>
|
<lion-option .choiceValue=${'Blue'}>Blue</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('Red');
|
expect(el.modelValue).to.equal('Red');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports having no default selection initially', async () => {
|
||||||
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
|
<lion-select-rich id="color" name="color" label="Favorite color" has-no-default-selected>
|
||||||
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
||||||
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
|
</lion-select-rich>
|
||||||
|
`));
|
||||||
|
const { invoker } = getProtectedMembers(el);
|
||||||
|
expect(invoker.selectedElement).to.be.undefined;
|
||||||
|
expect(el.modelValue).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
describe('Invoker', () => {
|
describe('Invoker', () => {
|
||||||
it('generates an lion-select-invoker if no invoker is provided', async () => {
|
it('generates an lion-select-invoker if no invoker is provided', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode).to.exist;
|
expect(el._invokerNode).to.exist;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
|
expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the first option as the selectedElement if no option is checked', async () => {
|
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`
|
||||||
<lion-select-rich name="foo">
|
<lion-select-rich name="foo">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
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]);
|
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs the selected element to the invoker', async () => {
|
it('syncs the selected element to the invoker', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich name="foo">
|
<lion-select-rich name="foo">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
const options = el.querySelectorAll('lion-option');
|
const options = el.querySelectorAll('lion-option');
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
|
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
|
||||||
|
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates readonly to the invoker', async () => {
|
it('delegates readonly to the invoker', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich readonly>
|
<lion-select-rich readonly>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.hasAttribute('readonly')).to.be.true;
|
expect(el.hasAttribute('readonly')).to.be.true;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.hasAttribute('readonly')).to.be.true;
|
expect(el._invokerNode.hasAttribute('readonly')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates singleOption to the invoker', async () => {
|
it('delegates singleOption to the invoker', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
expect(el.singleOption).to.be.true;
|
expect(el.singleOption).to.be.true;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.hasAttribute('single-option')).to.be.true;
|
expect(el._invokerNode.hasAttribute('single-option')).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('overlay', () => {
|
describe('overlay', () => {
|
||||||
it('should be closed by default', async () => {
|
it('should be closed by default', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich></lion-select-rich> `,
|
||||||
|
));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows/hides the listbox via opened attribute', async () => {
|
it('shows/hides the listbox via opened attribute', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich></lion-select-rich> `,
|
||||||
|
));
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.isShown).to.be.true;
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.isShown).to.be.false;
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs opened state with overlay shown', async () => {
|
it('syncs opened state with overlay shown', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich .opened=${true}></lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
const outerEl = await fixture('<button>somewhere</button>');
|
html` <lion-select-rich .opened=${true}></lion-select-rich> `,
|
||||||
|
));
|
||||||
|
const outerEl = /** @type {HTMLButtonElement} */ (await fixture(
|
||||||
|
'<button>somewhere</button>',
|
||||||
|
));
|
||||||
|
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
// a click on the button will trigger hide on outside click
|
// a click on the button will trigger hide on outside click
|
||||||
// which we then need to sync back to "opened"
|
// which we then need to sync back to "opened"
|
||||||
outerEl.click();
|
outerEl.click();
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will focus the listbox on open and invoker on close', async () => {
|
it('will focus the listbox on open and invoker on close', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich></lion-select-rich> `,
|
||||||
|
));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
await el._overlayCtrl.show();
|
await el._overlayCtrl.show();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(document.activeElement === el._listboxNode).to.be.true;
|
expect(document.activeElement === el._listboxNode).to.be.true;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(document.activeElement === el._invokerNode).to.be.false;
|
expect(document.activeElement === el._invokerNode).to.be.false;
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(document.activeElement === el._listboxNode).to.be.false;
|
expect(document.activeElement === el._listboxNode).to.be.false;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(document.activeElement === el._invokerNode).to.be.true;
|
expect(document.activeElement === el._invokerNode).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with checked option as active', async () => {
|
it('opens the listbox with checked option as active', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
await el._overlayCtrl.show();
|
await el._overlayCtrl.show();
|
||||||
await el.updateComplete;
|
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].active).to.be.true;
|
||||||
expect(options[1].checked).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 () => {
|
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`
|
||||||
<lion-select-rich readonly>
|
<lion-select-rich readonly>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const elDisabled = await fixture(html`
|
const elDisabled = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const elSingleoption = await fixture(html`
|
const elSingleoption = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
elReadOnly._invokerNode.click();
|
elReadOnly._invokerNode.click();
|
||||||
await elReadOnly.updateComplete;
|
await elReadOnly.updateComplete;
|
||||||
expect(elReadOnly.opened).to.be.false;
|
expect(elReadOnly.opened).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
elDisabled._invokerNode.click();
|
elDisabled._invokerNode.click();
|
||||||
await elDisabled.updateComplete;
|
await elDisabled.updateComplete;
|
||||||
expect(elDisabled.opened).to.be.false;
|
expect(elDisabled.opened).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
elSingleoption._invokerNode.click();
|
elSingleoption._invokerNode.click();
|
||||||
await elSingleoption.updateComplete;
|
await elSingleoption.updateComplete;
|
||||||
expect(elSingleoption.opened).to.be.false;
|
expect(elSingleoption.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets inheritsReferenceWidth to min by default', async () => {
|
it('sets inheritsReferenceWidth to min by default', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich name="favoriteColor" label="Favorite color">
|
<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
||||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override the inheritsWidth prop when no default selected feature is used', async () => {
|
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`
|
||||||
<lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
|
<lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
|
||||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
||||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
// The default is min, so we override that behavior here
|
// The default is min, so we override that behavior here
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._overlayCtrl.updateConfig({ inheritsReferenceWidth: 'full' });
|
el._overlayCtrl.updateConfig({ inheritsReferenceWidth: 'full' });
|
||||||
el._initialInheritsReferenceWidth = 'full';
|
el._initialInheritsReferenceWidth = 'full';
|
||||||
|
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// Opens while hasNoDefaultSelected = true, so we expect an override
|
// Opens while hasNoDefaultSelected = true, so we expect an override
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
|
|
||||||
// Emulate selecting hotpink, it closing, and opening it again
|
// Emulate selecting hotpink, it closing, and opening it again
|
||||||
|
|
@ -239,69 +317,86 @@ describe('lion-select-rich', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// noDefaultSelected will now flip the override back to what was the initial reference width
|
// 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');
|
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have singleOption only if there is exactly one option', async () => {
|
it('should have singleOption only if there is exactly one option', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el.singleOption).to.be.false;
|
expect(el.singleOption).to.be.false;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.singleOption).to.be.false;
|
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);
|
optionELm.parentNode.removeChild(optionELm);
|
||||||
el.requestUpdate();
|
el.requestUpdate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.singleOption).to.be.true;
|
expect(el.singleOption).to.be.true;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.singleOption).to.be.true;
|
expect(el._invokerNode.singleOption).to.be.true;
|
||||||
|
|
||||||
const newOption = document.createElement('lion-option');
|
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
||||||
newOption.choiceValue = 30;
|
newOption.choiceValue = 30;
|
||||||
el._inputNode.appendChild(newOption);
|
el._inputNode.appendChild(newOption);
|
||||||
el.requestUpdate();
|
el.requestUpdate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.singleOption).to.be.false;
|
expect(el.singleOption).to.be.false;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
expect(el._invokerNode.singleOption).to.be.false;
|
expect(el._invokerNode.singleOption).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('interaction-mode', () => {
|
describe('interaction-mode', () => {
|
||||||
it('allows to specify an interaction-mode which determines other behaviors', async () => {
|
it('allows to specify an interaction-mode which determines other behaviors', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el.interactionMode).to.equal('mac');
|
expect(el.interactionMode).to.equal('mac');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Keyboard navigation', () => {
|
describe('Keyboard navigation', () => {
|
||||||
it('opens the listbox with [Enter] key via click handler', async () => {
|
it('opens the listbox with [Enter] key via click handler', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with [ ](Space) key via click handler', async () => {
|
it('opens the listbox with [ ](Space) key via click handler', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
await aTimeout();
|
await aTimeout(0);
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the listbox with [Escape] key once opened', async () => {
|
it('closes the listbox with [Escape] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich opened> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the listbox with [Tab] key once opened', async () => {
|
it('closes the listbox with [Tab] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich opened> </lion-select-rich> `,
|
||||||
|
));
|
||||||
// tab can only be caught via keydown
|
// tab can only be caught via keydown
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -309,28 +404,34 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
describe('Mouse navigation', () => {
|
describe('Mouse navigation', () => {
|
||||||
it('opens the listbox via click on invoker', async () => {
|
it('opens the listbox via click on invoker', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich> </lion-select-rich> `,
|
||||||
|
));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
await nextFrame(); // reflection of click takes some time
|
await nextFrame(); // reflection of click takes some time
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the listbox when an option gets clicked', async () => {
|
it('closes the listbox when an option gets clicked', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich opened>
|
<lion-select-rich opened>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
el.querySelector('lion-option').click();
|
el.formElements[0].click();
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Keyboard navigation Windows', () => {
|
describe('Keyboard navigation Windows', () => {
|
||||||
it('closes the listbox with [Enter] key once opened', async () => {
|
it('closes the listbox with [Enter] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
|
html` <lion-select-rich opened> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -338,35 +439,35 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
describe('Keyboard navigation Mac', () => {
|
describe('Keyboard navigation Mac', () => {
|
||||||
it('checks active item and closes the listbox with [Enter] key via click handler once opened', async () => {
|
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`
|
||||||
<lion-select-rich opened interaction-mode="mac">
|
<lion-select-rich opened interaction-mode="mac">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
// changes active but not checked
|
// changes active but not checked
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
// @ts-ignore allow protected access in tests
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with [ArrowUp] key', async () => {
|
it('opens the listbox with [ArrowUp] key', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
||||||
`);
|
`));
|
||||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with [ArrowDown] key', async () => {
|
it('opens the listbox with [ArrowDown] key', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
<lion-select-rich interaction-mode="mac"> </lion-select-rich>
|
||||||
`);
|
`));
|
||||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
|
|
@ -375,34 +476,41 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('has the right references to its inner elements', async () => {
|
it('has the right references to its inner elements', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<lion-select-rich label="age">
|
<lion-select-rich label="age">
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`));
|
||||||
expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._labelNode.id);
|
const { invoker, feedback, label, helpText } = getProtectedMembers(el);
|
||||||
expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._invokerNode.id);
|
|
||||||
expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._helpTextNode.id);
|
expect(invoker.getAttribute('aria-labelledby')).to.contain(label.id);
|
||||||
expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._feedbackNode.id);
|
expect(invoker.getAttribute('aria-labelledby')).to.contain(invoker.id);
|
||||||
expect(el._invokerNode.getAttribute('aria-haspopup')).to.equal('listbox');
|
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 () => {
|
it('notifies when the listbox is expanded or not', async () => {
|
||||||
// smoke test for overlay functionality
|
// smoke test for overlay functionality
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = /** @type {LionSelectRich} */ (await fixture(
|
||||||
expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('false');
|
html` <lion-select-rich> </lion-select-rich> `,
|
||||||
|
));
|
||||||
|
const { invoker } = getProtectedMembers(el);
|
||||||
|
|
||||||
|
expect(invoker.getAttribute('aria-expanded')).to.equal('false');
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
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', () => {
|
describe('Use cases', () => {
|
||||||
it('keeps showing the selected item after a new item has been added in the selectedIndex position', async () => {
|
it('keeps showing the selected item after a new item has been added in the selectedIndex position', async () => {
|
||||||
const mySelectContainerTagString = defineCE(
|
const mySelectContainerTagString = defineCE(
|
||||||
|
// @ts-expect-error
|
||||||
class extends LitElement {
|
class extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -447,26 +555,29 @@ describe('lion-select-rich', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const mySelectContainerTag = unsafeStatic(mySelectContainerTagString);
|
const mySelectContainerTag = unsafeStatic(mySelectContainerTagString);
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<${mySelectContainerTag}></${mySelectContainerTag}>
|
<${mySelectContainerTag}></${mySelectContainerTag}>
|
||||||
`);
|
`));
|
||||||
|
|
||||||
const selectRich = el.shadowRoot.querySelector('lion-select-rich');
|
const selectRich = /** @type {LionSelectRich} */ (
|
||||||
const invoker = selectRich._invokerNode;
|
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('lion-select-rich')
|
||||||
|
);
|
||||||
|
|
||||||
|
const { invoker, listbox } = getProtectedMembers(selectRich);
|
||||||
|
|
||||||
expect(selectRich.checkedIndex).to.equal(1);
|
expect(selectRich.checkedIndex).to.equal(1);
|
||||||
expect(selectRich.modelValue).to.equal('hotpink');
|
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.modelValue = { checked: false, value: 'blue' };
|
||||||
newOption.textContent = 'Blue';
|
newOption.textContent = 'Blue';
|
||||||
const hotpinkEl = selectRich._listboxNode.children[1];
|
const hotpinkEl = listbox.children[1];
|
||||||
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
|
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
|
||||||
|
|
||||||
expect(selectRich.checkedIndex).to.equal(2);
|
expect(selectRich.checkedIndex).to.equal(2);
|
||||||
expect(selectRich.modelValue).to.equal('hotpink');
|
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 () => {
|
it('allows to override the type of overlay', async () => {
|
||||||
const mySelectTagString = defineCE(
|
const mySelectTagString = defineCE(
|
||||||
class MySelect extends LionSelectRich {
|
class MySelect extends LionSelectRich {
|
||||||
|
// @ts-expect-error
|
||||||
_defineOverlay({ invokerNode, contentNode, contentWrapperNode }) {
|
_defineOverlay({ invokerNode, contentNode, contentWrapperNode }) {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
|
|
@ -491,7 +603,7 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
const mySelectTag = unsafeStatic(mySelectTagString);
|
const mySelectTag = unsafeStatic(mySelectTagString);
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionSelectRich} */ (await fixture(html`
|
||||||
<${mySelectTag} label="Favorite color" name="color">
|
<${mySelectTag} label="Favorite color" name="color">
|
||||||
|
|
||||||
${Array(2).map(
|
${Array(2).map(
|
||||||
|
|
@ -501,10 +613,12 @@ describe('lion-select-rich', () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</${mySelectTag}>
|
</${mySelectTag}>
|
||||||
`);
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
// @ts-ignore allow protected member access in tests
|
||||||
expect(el._overlayCtrl.placementMode).to.equal('global');
|
expect(el._overlayCtrl.placementMode).to.equal('global');
|
||||||
el.dispatchEvent(new Event('switch'));
|
el.dispatchEvent(new Event('switch'));
|
||||||
|
// @ts-ignore allow protected member access in tests
|
||||||
expect(el._overlayCtrl.placementMode).to.equal('local');
|
expect(el._overlayCtrl.placementMode).to.equal('local');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -530,7 +644,7 @@ describe('lion-select-rich', () => {
|
||||||
);
|
);
|
||||||
const selectTag = unsafeStatic(selectTagName);
|
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>
|
<${selectTag} id="color" name="color" label="Favorite color" has-no-default-selected>
|
||||||
|
|
||||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
|
@ -538,11 +652,12 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
|
|
||||||
</${selectTag}>
|
</${selectTag}>
|
||||||
`);
|
`));
|
||||||
|
const { invoker } = getProtectedMembers(el);
|
||||||
|
|
||||||
expect(el._invokerNode.shadowRoot.getElementById('content-wrapper')).dom.to.equal(
|
expect(
|
||||||
`<div id="content-wrapper">Please select an option..</div>`,
|
/** @type {ShadowRoot} */ (invoker.shadowRoot).getElementById('content-wrapper'),
|
||||||
);
|
).dom.to.equal(`<div id="content-wrapper">Please select an option..</div>`);
|
||||||
expect(el.modelValue).to.equal('');
|
expect(el.modelValue).to.equal('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"packages/accordion/**/*.js",
|
"packages/accordion/**/*.js",
|
||||||
"packages/button/src/**/*.js",
|
"packages/button/src/**/*.js",
|
||||||
"packages/calendar/**/*.js",
|
"packages/calendar/**/*.js",
|
||||||
|
"packages/button/index.js",
|
||||||
"packages/checkbox-group/**/*.js",
|
"packages/checkbox-group/**/*.js",
|
||||||
"packages/collapsible/**/*.js",
|
"packages/collapsible/**/*.js",
|
||||||
"packages/core/**/*.js",
|
"packages/core/**/*.js",
|
||||||
|
|
@ -33,13 +34,14 @@
|
||||||
"packages/input-iban/**/*.js",
|
"packages/input-iban/**/*.js",
|
||||||
"packages/input-range/**/*.js",
|
"packages/input-range/**/*.js",
|
||||||
"packages/input-stepper/**/*.js",
|
"packages/input-stepper/**/*.js",
|
||||||
"packages/listbox/src/*.js",
|
"packages/listbox/**/*.js",
|
||||||
"packages/localize/**/*.js",
|
"packages/localize/**/*.js",
|
||||||
"packages/overlays/**/*.js",
|
"packages/overlays/**/*.js",
|
||||||
"packages/pagination/**/*.js",
|
"packages/pagination/**/*.js",
|
||||||
"packages/progress-indicator/**/*.js",
|
"packages/progress-indicator/**/*.js",
|
||||||
"packages/radio-group/**/*.js",
|
"packages/radio-group/**/*.js",
|
||||||
"packages/select/**/*.js",
|
"packages/select/**/*.js",
|
||||||
|
"packages/select-rich/**/*.js",
|
||||||
"packages/singleton-manager/**/*.js",
|
"packages/singleton-manager/**/*.js",
|
||||||
"packages/steps/**/*.js",
|
"packages/steps/**/*.js",
|
||||||
"packages/switch/**/*.js",
|
"packages/switch/**/*.js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue