fix(combobox): no sync unmatched checkedIndex [autocomplet=none

This commit is contained in:
Thijs Louisse 2021-04-19 11:02:07 +02:00
parent 11ec31c62e
commit edb43c4e05
4 changed files with 80 additions and 47 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/combobox': patch
'@lion/listbox': patch
---
syncs last selected choice value for [autocomplet="none|list"] on close

View file

@ -430,7 +430,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
if (!lastKey) {
return /** @type {boolean} */ (this.opened);
}
return true;
}
@ -466,7 +465,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this._inputNode.focus();
if (!this.multipleChoice) {
this.activeIndex = -1;
this._setOpenedWithoutPropertyEffects(false);
this.opened = false;
}
}
@ -584,8 +583,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @protected
*/
_handleAutocompletion() {
if (this.autocomplete === 'none') {
return;
if ((!this.multipleChoice && this.autocomplete === 'none') || this.autocomplete === 'list') {
if (!this._inputNode.value.startsWith(this.modelValue)) {
this.checkedIndex = -1;
}
}
const hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
@ -607,8 +608,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
const userIntendsInlineAutoFill = this.__computeUserIntendsAutoFill({ prevValue, curValue });
const isInlineAutoFillCandidate =
this.autocomplete === 'both' || this.autocomplete === 'inline';
const autoselect = this._autoSelectCondition();
// @ts-ignore this.autocomplete === 'none' needs to be there if statement above is removed
const autoselect = this.autocomplete !== 'none' && this._autoSelectCondition();
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
@ -795,7 +795,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
const { key } = ev;
switch (key) {
case 'Escape':
this._setOpenedWithoutPropertyEffects(false);
this.opened = false;
this._setTextboxValue('');
break;
case 'Enter':
@ -803,7 +803,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
return;
}
if (!this.multipleChoice) {
this._setOpenedWithoutPropertyEffects(false);
this.opened = false;
}
break;
/* no default */
@ -903,12 +903,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
__requestShowOverlay(ev) {
const lastKey = ev && ev.key;
this._setOpenedWithoutPropertyEffects(
this._showOverlayCondition({
this.opened = this._showOverlayCondition({
lastKey,
currentValue: this._inputNode.value,
}),
);
});
}
clear() {

View file

@ -46,6 +46,7 @@ function getComboboxMembers(el) {
* @param {LionCombobox} el
* @param {string} value
*/
// TODO: add keys that actually make sense...
function mimicUserTyping(el, value) {
const { _inputNode } = getComboboxMembers(el);
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
@ -71,45 +72,44 @@ function mimicKeyPress(el, key) {
*/
async function mimicUserTypingAdvanced(el, values) {
const { _inputNode } = getComboboxMembers(el);
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (_inputNode);
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
for (const key of values) {
// eslint-disable-next-line no-await-in-loop, no-loop-func
await new Promise(resolve => {
const hasSelection = inputNodeLoc.selectionStart !== inputNodeLoc.selectionEnd;
const hasSelection = _inputNode.selectionStart !== _inputNode.selectionEnd;
if (key === 'Backspace') {
if (hasSelection) {
inputNodeLoc.value =
inputNodeLoc.value.slice(
_inputNode.value =
_inputNode.value.slice(
0,
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
_inputNode.selectionStart ? _inputNode.selectionStart : undefined,
) +
inputNodeLoc.value.slice(
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
inputNodeLoc.value.length,
_inputNode.value.slice(
_inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
_inputNode.value.length,
);
} else {
inputNodeLoc.value = inputNodeLoc.value.slice(0, -1);
_inputNode.value = _inputNode.value.slice(0, -1);
}
} else if (hasSelection) {
inputNodeLoc.value =
inputNodeLoc.value.slice(
_inputNode.value =
_inputNode.value.slice(
0,
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
_inputNode.selectionStart ? _inputNode.selectionStart : undefined,
) +
key +
inputNodeLoc.value.slice(
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
inputNodeLoc.value.length,
_inputNode.value.slice(
_inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
_inputNode.value.length,
);
} else {
inputNodeLoc.value += key;
_inputNode.value += key;
}
mimicKeyPress(inputNodeLoc, key);
inputNodeLoc.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
mimicKeyPress(_inputNode, key);
_inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
el.updateComplete.then(() => {
// @ts-ignore
@ -180,9 +180,8 @@ describe('lion-combobox', () => {
expect(visibleOptions().length).to.equal(0);
}
// FIXME: autocomplete 'none' should have this behavior as well
// el.autocomplete = 'none';
// await performChecks();
el.autocomplete = 'none';
await performChecks();
el.autocomplete = 'list';
await performChecks();
el.autocomplete = 'inline';
@ -321,9 +320,9 @@ describe('lion-combobox', () => {
_inputNode.value = '';
_inputNode.blur();
await open();
await el.updateComplete;
expect(el.opened).to.be.true;
el.activeIndex = el.formElements.indexOf(visibleOptions[0]);
mimicKeyPress(_listboxNode, 'Enter');
@ -443,6 +442,36 @@ describe('lion-combobox', () => {
expect(el2.modelValue).to.eql([]);
expect(_inputNode.value).to.equal('');
});
it('syncs textbox to modelValue', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" show-all-on-empty>
<lion-option .choiceValue="${'Aha'}" checked>Aha</lion-option>
<lion-option .choiceValue="${'Bhb'}">Bhb</lion-option>
</lion-combobox>
`));
const { _inputNode } = getComboboxMembers(el);
async function performChecks() {
el.formElements[0].click();
await el.updateComplete;
expect(_inputNode.value).to.equal('Aha');
expect(el.checkedIndex).to.equal(0);
await mimicUserTyping(el, 'Ah');
await el.updateComplete;
await el.updateComplete;
expect(el.checkedIndex).to.equal(-1);
}
el.autocomplete = 'none';
await performChecks();
el.autocomplete = 'list';
await performChecks();
});
});
describe('Overlay visibility', () => {

View file

@ -365,7 +365,7 @@ export function runListboxMixinSuite(customConfig = {}) {
it('has a reference to the active option', async () => {
const el = await fixture(html`
<${tag} opened has-no-default-selected autocomplete="none">
<${tag} opened has-no-default-selected autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${'10'} id="first">Item 1</${optionTag}>
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
</${tag}>
@ -387,7 +387,7 @@ export function runListboxMixinSuite(customConfig = {}) {
it('puts "aria-setsize" on all options to indicate the total amount of options', async () => {
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} autocomplete="none">
<${tag} autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
@ -400,7 +400,7 @@ export function runListboxMixinSuite(customConfig = {}) {
it('puts "aria-posinset" on all options to indicate their position in the listbox', async () => {
const el = await fixture(html`
<${tag} autocomplete="none">
<${tag} autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
@ -588,7 +588,7 @@ export function runListboxMixinSuite(customConfig = {}) {
describe('Enter', () => {
it('[Enter] selects active option', async () => {
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened name="foo" autocomplete="none">
<${tag} opened name="foo" autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
@ -611,7 +611,7 @@ export function runListboxMixinSuite(customConfig = {}) {
// When listbox is not focusable (in case of a combobox), the user should be allowed
// to enter a space in the focusable element (texbox)
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened name="foo" ._listboxReceivesNoFocus="${false}" autocomplete="none">
<${tag} opened name="foo" ._listboxReceivesNoFocus="${false}" autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
@ -687,7 +687,7 @@ export function runListboxMixinSuite(customConfig = {}) {
});
it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened has-no-default-selected autocomplete="none">
<${tag} opened has-no-default-selected autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${'Item 1'}>Item 1</${optionTag}>
<${optionTag} .choiceValue=${'Item 2'}>Item 2</${optionTag}>
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
@ -715,7 +715,7 @@ export function runListboxMixinSuite(customConfig = {}) {
describe('Orientation', () => {
it('has a default value of "vertical"', async () => {
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened name="foo" autocomplete="none">
<${tag} opened name="foo" autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
@ -755,7 +755,7 @@ export function runListboxMixinSuite(customConfig = {}) {
it('uses [ArrowLeft] and [ArrowRight] keys when "horizontal"', async () => {
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened name="foo" orientation="horizontal" autocomplete="none">
<${tag} opened name="foo" orientation="horizontal" autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
@ -932,7 +932,7 @@ export function runListboxMixinSuite(customConfig = {}) {
});
}
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened selection-follows-focus autocomplete="none">
<${tag} opened selection-follows-focus autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
@ -972,7 +972,7 @@ export function runListboxMixinSuite(customConfig = {}) {
});
}
const el = /** @type {LionListbox} */ (await fixture(html`
<${tag} opened selection-follows-focus orientation="horizontal" autocomplete="none">
<${tag} opened selection-follows-focus orientation="horizontal" autocomplete="none" show-all-on-empty>
<${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
<${optionTag} .choiceValue=${30}>Item 3</${optionTag}>