fix(combobox): when multiple choice reset all listbox options on select (#2332)
* fix(combobox): when multiple choice reset all listbox options on select * Apply suggestions from code review Co-authored-by: Thijs Louisse <thijs.louisse@ing.com> * chore: clear options also on click * chore: adopt code to show list when only when showAllOnEmpty is true --------- Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
This commit is contained in:
parent
5fa385e923
commit
02a9427a7d
4 changed files with 174 additions and 18 deletions
5
.changeset/brave-plums-drum.md
Normal file
5
.changeset/brave-plums-drum.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[combobox] reset listbox options on click/enter for multiple-choice
|
||||||
|
|
@ -243,7 +243,7 @@ export const multipleChoice = () => html`
|
||||||
${lazyRender(
|
${lazyRender(
|
||||||
listboxData.map(
|
listboxData.map(
|
||||||
(entry, i) => html`
|
(entry, i) => html`
|
||||||
<lion-option .choiceValue="${entry}" ?checked="${i === 0}>${entry}"</lion-option>
|
<lion-option .choiceValue="${entry}" ?checked="${i === 0}">${entry}</lion-option>
|
||||||
`,
|
`,
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|
@ -266,7 +266,7 @@ export const multipleCustomizableChoice = () => html`
|
||||||
${lazyRender(
|
${lazyRender(
|
||||||
listboxData.map(
|
listboxData.map(
|
||||||
(entry, i) => html`
|
(entry, i) => html`
|
||||||
<lion-option .choiceValue="${entry}" ?checked="${i === 0}>${entry}"</lion-option>
|
<lion-option .choiceValue="${entry}" ?checked="${i === 0}">${entry}</lion-option>
|
||||||
`,
|
`,
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const matchA11ySpanReverseFns = new WeakMap();
|
||||||
* @typedef {import('@lion/ui/types/form-core.js').ChoiceInputHost} ChoiceInputHost
|
* @typedef {import('@lion/ui/types/form-core.js').ChoiceInputHost} ChoiceInputHost
|
||||||
* @typedef {import('@lion/ui/types/form-core.js').FormControlHost} FormControlHost
|
* @typedef {import('@lion/ui/types/form-core.js').FormControlHost} FormControlHost
|
||||||
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
|
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
|
||||||
|
* @typedef {LionOption & { onFilterUnmatch?:function; onFilterMatch?:function }} OptionWithFilterFn
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -201,8 +202,30 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
super.reset();
|
super.reset();
|
||||||
// @ts-ignore _initialModelValue comes from ListboxMixin
|
if (!this.multipleChoice) {
|
||||||
this.value = this._initialModelValue;
|
// @ts-ignore _initialModelValue comes from ListboxMixin
|
||||||
|
this.value = this._initialModelValue;
|
||||||
|
}
|
||||||
|
this._resetListboxOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_resetListboxOptions() {
|
||||||
|
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, idx) => {
|
||||||
|
this._unhighlightMatchedOption(option);
|
||||||
|
if (!this.showAllOnEmpty || !this.opened) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
option.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
option.style.display = '';
|
||||||
|
option.setAttribute('aria-posinset', `${idx + 1}`);
|
||||||
|
option.setAttribute('aria-setsize', `${this.formElements.length}`);
|
||||||
|
option.removeAttribute('aria-hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -704,11 +727,13 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
*/
|
*/
|
||||||
_listboxOnClick(ev) {
|
_listboxOnClick(ev) {
|
||||||
super._listboxOnClick(ev);
|
super._listboxOnClick(ev);
|
||||||
|
|
||||||
this._inputNode.focus();
|
this._inputNode.focus();
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
this.activeIndex = -1;
|
this.activeIndex = -1;
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
|
} else {
|
||||||
|
this._inputNode.value = '';
|
||||||
|
this._resetListboxOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -897,7 +922,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
const autoselect = this._autoSelectCondition();
|
const autoselect = this._autoSelectCondition();
|
||||||
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
||||||
|
|
||||||
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
|
|
||||||
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
|
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
|
||||||
// [1]. Decide whether option should be shown
|
// [1]. Decide whether option should be shown
|
||||||
const matches = this.matchCondition(option, curValue);
|
const matches = this.matchCondition(option, curValue);
|
||||||
|
|
@ -983,7 +1007,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
|
|
||||||
// [8]. These values will help computing autofill intentions next autocomplete cycle
|
// [8]. These values will help computing autofill intentions next autocomplete cycle
|
||||||
this.__prevCboxValueNonSelected = curValue;
|
this.__prevCboxValueNonSelected = curValue;
|
||||||
// See test 'computation of "user intends autofill" works correctly afer autofill'
|
// See test 'computation of "user intends autofill" works correctly after autofill'
|
||||||
this.__prevCboxValue = this._inputNode.value;
|
this.__prevCboxValue = this._inputNode.value;
|
||||||
this.__hadSelectionLastAutofill =
|
this.__hadSelectionLastAutofill =
|
||||||
this._inputNode.value.length !== this._inputNode.selectionStart;
|
this._inputNode.value.length !== this._inputNode.selectionStart;
|
||||||
|
|
@ -1120,16 +1144,16 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
this.modelValue = this.parser([...this.modelValue, this._inputNode.value]);
|
this.modelValue = this.parser([...this.modelValue, this._inputNode.value]);
|
||||||
|
|
||||||
this._inputNode.value = '';
|
this._inputNode.value = '';
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
} else {
|
} else {
|
||||||
super._listboxOnKeyDown(ev);
|
super._listboxOnKeyDown(ev);
|
||||||
// TODO: should we clear the input value here when allowCustomChoice is false?
|
this._resetListboxOptions();
|
||||||
// For now, we don't...
|
|
||||||
}
|
}
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
|
} else {
|
||||||
|
this._inputNode.value = '';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -1168,7 +1192,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
||||||
.filter(option => diff.includes(option.choiceValue))
|
.filter(option => diff.includes(option.choiceValue))
|
||||||
.map(option => this._getTextboxValueFromOption(option))
|
.map(option => this._getTextboxValueFromOption(option))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
this._setTextboxValue(newValue); // or last selected value?
|
this._setTextboxValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,54 @@ describe('lion-combobox', () => {
|
||||||
await performChecks();
|
await performChecks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides all options on reset()', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = el.formElements;
|
||||||
|
const visibleOptions = () => options.filter(o => o.style.display !== 'none');
|
||||||
|
|
||||||
|
mimicUserTyping(el, 'cha');
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(visibleOptions().length).to.equal(1);
|
||||||
|
el.reset();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(visibleOptions().length).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows all options on reset() when showAllOnEmpty is set to true and overlay was open', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" show-all-on-empty>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = el.formElements;
|
||||||
|
const visibleOptions = () => options.filter(o => o.getAttribute('aria-hidden') !== 'true');
|
||||||
|
|
||||||
|
mimicUserTyping(el, 'cha');
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(visibleOptions().length).to.equal(1);
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
|
||||||
|
el.reset();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(visibleOptions().length).to.equal(4);
|
||||||
|
});
|
||||||
|
|
||||||
it('hides listbox on click/enter (when multiple-choice is false)', async () => {
|
it('hides listbox on click/enter (when multiple-choice is false)', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
|
|
@ -467,13 +515,12 @@ describe('lion-combobox', () => {
|
||||||
mimicUserTyping(el, 'a');
|
mimicUserTyping(el, 'a');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
const visibleOptions = options.filter(o => o.style.display !== 'none');
|
const visibleOptions = () => options.filter(o => o.style.display !== 'none');
|
||||||
expect(visibleOptions.length).to.equal(3, 'after input');
|
expect(visibleOptions().length).to.equal(3, 'after input');
|
||||||
|
|
||||||
el.clear();
|
el.clear();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const visibleOptions2 = options.filter(o => o.style.display !== 'none');
|
expect(visibleOptions().length).to.equal(0, 'after clear');
|
||||||
expect(visibleOptions2.length).to.equal(0, 'after clear');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets modelValue and textbox value on reset()', async () => {
|
it('resets modelValue and textbox value on reset()', async () => {
|
||||||
|
|
@ -508,7 +555,7 @@ describe('lion-combobox', () => {
|
||||||
el2.reset();
|
el2.reset();
|
||||||
expect(el2.modelValue).to.deep.equal(['Artichoke']);
|
expect(el2.modelValue).to.deep.equal(['Artichoke']);
|
||||||
// @ts-ignore [allow-protected] in test
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el2._inputNode.value).to.equal('Artichoke');
|
expect(el2._inputNode.value).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs textbox to modelValue', async () => {
|
it('syncs textbox to modelValue', async () => {
|
||||||
|
|
@ -735,7 +782,7 @@ describe('lion-combobox', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows manyu custom selections when multi-choice when requireOptionMatch is false', async () => {
|
it('allows many custom selections when multi-choice when requireOptionMatch is false', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<lion-combobox
|
<lion-combobox
|
||||||
|
|
@ -2088,7 +2135,7 @@ describe('lion-combobox', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('highlights first occcurence of matching option', async () => {
|
it('highlights first occurrence of matching option', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<lion-combobox name="foo" match-mode="all">
|
<lion-combobox name="foo" match-mode="all">
|
||||||
|
|
@ -2170,6 +2217,28 @@ describe('lion-combobox', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resets removes highlights on reset()', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" match-mode="all">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'c');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(`<span aria-label="Artichoke">Arti<b>c</b>hoke</span>`);
|
||||||
|
el.reset();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(`Artichoke`);
|
||||||
|
});
|
||||||
|
|
||||||
it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
|
it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
|
|
@ -3018,6 +3087,64 @@ describe('lion-combobox', () => {
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clears textbox after selection of a new item on [enter]', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" multiple-choice>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(el, 'art');
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.opened).to.equal(true);
|
||||||
|
const visibleOptions = () => options.filter(o => o.style.display !== 'none');
|
||||||
|
expect(visibleOptions().length).to.equal(1);
|
||||||
|
|
||||||
|
// N.B. we do only trigger keydown here (and not mimicKeypress (both keyup and down)),
|
||||||
|
// because this closely mimics what happens in the browser
|
||||||
|
_inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
|
expect(el.opened).to.equal(true);
|
||||||
|
expect(visibleOptions().length).to.equal(0);
|
||||||
|
expect(_inputNode.value).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears textbox after selection of a new item on click', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" multiple-choice>
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(el, 'art');
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.opened).to.equal(true);
|
||||||
|
const visibleOptions = () => options.filter(o => o.style.display !== 'none');
|
||||||
|
expect(visibleOptions().length).to.equal(1);
|
||||||
|
|
||||||
|
visibleOptions()[0].click();
|
||||||
|
expect(el.opened).to.equal(true);
|
||||||
|
expect(visibleOptions().length).to.equal(0);
|
||||||
|
expect(_inputNode.value).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
it('submits form on [Enter] when listbox is closed', async () => {
|
it('submits form on [Enter] when listbox is closed', async () => {
|
||||||
const submitSpy = sinon.spy(e => e.preventDefault());
|
const submitSpy = sinon.spy(e => e.preventDefault());
|
||||||
const el = /** @type {HTMLFormElement} */ (
|
const el = /** @type {HTMLFormElement} */ (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue