fix(combobox): no sync unmatched checkedIndex [autocomplet=none
This commit is contained in:
parent
11ec31c62e
commit
edb43c4e05
4 changed files with 80 additions and 47 deletions
6
.changeset/three-apples-appear.md
Normal file
6
.changeset/three-apples-appear.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@lion/combobox': patch
|
||||||
|
'@lion/listbox': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
syncs last selected choice value for [autocomplet="none|list"] on close
|
||||||
|
|
@ -430,7 +430,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
if (!lastKey) {
|
if (!lastKey) {
|
||||||
return /** @type {boolean} */ (this.opened);
|
return /** @type {boolean} */ (this.opened);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,7 +465,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
this._inputNode.focus();
|
this._inputNode.focus();
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
this.activeIndex = -1;
|
this.activeIndex = -1;
|
||||||
this._setOpenedWithoutPropertyEffects(false);
|
this.opened = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -584,8 +583,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_handleAutocompletion() {
|
_handleAutocompletion() {
|
||||||
if (this.autocomplete === 'none') {
|
if ((!this.multipleChoice && this.autocomplete === 'none') || this.autocomplete === 'list') {
|
||||||
return;
|
if (!this._inputNode.value.startsWith(this.modelValue)) {
|
||||||
|
this.checkedIndex = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
|
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 userIntendsInlineAutoFill = this.__computeUserIntendsAutoFill({ prevValue, curValue });
|
||||||
const isInlineAutoFillCandidate =
|
const isInlineAutoFillCandidate =
|
||||||
this.autocomplete === 'both' || this.autocomplete === 'inline';
|
this.autocomplete === 'both' || this.autocomplete === 'inline';
|
||||||
const autoselect = this._autoSelectCondition();
|
const autoselect = this.autocomplete !== 'none' && this._autoSelectCondition();
|
||||||
// @ts-ignore this.autocomplete === 'none' needs to be there if statement above is removed
|
|
||||||
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
|
||||||
|
|
||||||
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
|
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
|
||||||
|
|
@ -795,7 +795,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
const { key } = ev;
|
const { key } = ev;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this._setOpenedWithoutPropertyEffects(false);
|
this.opened = false;
|
||||||
this._setTextboxValue('');
|
this._setTextboxValue('');
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
|
|
@ -803,7 +803,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
this._setOpenedWithoutPropertyEffects(false);
|
this.opened = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
/* no default */
|
/* no default */
|
||||||
|
|
@ -903,12 +903,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
__requestShowOverlay(ev) {
|
__requestShowOverlay(ev) {
|
||||||
const lastKey = ev && ev.key;
|
const lastKey = ev && ev.key;
|
||||||
this._setOpenedWithoutPropertyEffects(
|
this.opened = this._showOverlayCondition({
|
||||||
this._showOverlayCondition({
|
|
||||||
lastKey,
|
lastKey,
|
||||||
currentValue: this._inputNode.value,
|
currentValue: this._inputNode.value,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ function getComboboxMembers(el) {
|
||||||
* @param {LionCombobox} el
|
* @param {LionCombobox} el
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
|
// TODO: add keys that actually make sense...
|
||||||
function mimicUserTyping(el, value) {
|
function mimicUserTyping(el, value) {
|
||||||
const { _inputNode } = getComboboxMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||||
|
|
@ -71,45 +72,44 @@ function mimicKeyPress(el, key) {
|
||||||
*/
|
*/
|
||||||
async function mimicUserTypingAdvanced(el, values) {
|
async function mimicUserTypingAdvanced(el, values) {
|
||||||
const { _inputNode } = getComboboxMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (_inputNode);
|
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||||
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
||||||
|
|
||||||
for (const key of values) {
|
for (const key of values) {
|
||||||
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
const hasSelection = inputNodeLoc.selectionStart !== inputNodeLoc.selectionEnd;
|
const hasSelection = _inputNode.selectionStart !== _inputNode.selectionEnd;
|
||||||
|
|
||||||
if (key === 'Backspace') {
|
if (key === 'Backspace') {
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
inputNodeLoc.value =
|
_inputNode.value =
|
||||||
inputNodeLoc.value.slice(
|
_inputNode.value.slice(
|
||||||
0,
|
0,
|
||||||
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
|
_inputNode.selectionStart ? _inputNode.selectionStart : undefined,
|
||||||
) +
|
) +
|
||||||
inputNodeLoc.value.slice(
|
_inputNode.value.slice(
|
||||||
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
|
_inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
|
||||||
inputNodeLoc.value.length,
|
_inputNode.value.length,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
inputNodeLoc.value = inputNodeLoc.value.slice(0, -1);
|
_inputNode.value = _inputNode.value.slice(0, -1);
|
||||||
}
|
}
|
||||||
} else if (hasSelection) {
|
} else if (hasSelection) {
|
||||||
inputNodeLoc.value =
|
_inputNode.value =
|
||||||
inputNodeLoc.value.slice(
|
_inputNode.value.slice(
|
||||||
0,
|
0,
|
||||||
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
|
_inputNode.selectionStart ? _inputNode.selectionStart : undefined,
|
||||||
) +
|
) +
|
||||||
key +
|
key +
|
||||||
inputNodeLoc.value.slice(
|
_inputNode.value.slice(
|
||||||
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
|
_inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
|
||||||
inputNodeLoc.value.length,
|
_inputNode.value.length,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
inputNodeLoc.value += key;
|
_inputNode.value += key;
|
||||||
}
|
}
|
||||||
|
|
||||||
mimicKeyPress(inputNodeLoc, key);
|
mimicKeyPress(_inputNode, key);
|
||||||
inputNodeLoc.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
_inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
el.updateComplete.then(() => {
|
el.updateComplete.then(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -180,9 +180,8 @@ describe('lion-combobox', () => {
|
||||||
expect(visibleOptions().length).to.equal(0);
|
expect(visibleOptions().length).to.equal(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: autocomplete 'none' should have this behavior as well
|
el.autocomplete = 'none';
|
||||||
// el.autocomplete = 'none';
|
await performChecks();
|
||||||
// await performChecks();
|
|
||||||
el.autocomplete = 'list';
|
el.autocomplete = 'list';
|
||||||
await performChecks();
|
await performChecks();
|
||||||
el.autocomplete = 'inline';
|
el.autocomplete = 'inline';
|
||||||
|
|
@ -321,9 +320,9 @@ describe('lion-combobox', () => {
|
||||||
|
|
||||||
_inputNode.value = '';
|
_inputNode.value = '';
|
||||||
_inputNode.blur();
|
_inputNode.blur();
|
||||||
|
|
||||||
await open();
|
await open();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.be.true;
|
|
||||||
|
|
||||||
el.activeIndex = el.formElements.indexOf(visibleOptions[0]);
|
el.activeIndex = el.formElements.indexOf(visibleOptions[0]);
|
||||||
mimicKeyPress(_listboxNode, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
|
|
@ -443,6 +442,36 @@ describe('lion-combobox', () => {
|
||||||
expect(el2.modelValue).to.eql([]);
|
expect(el2.modelValue).to.eql([]);
|
||||||
expect(_inputNode.value).to.equal('');
|
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', () => {
|
describe('Overlay visibility', () => {
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
it('has a reference to the active option', async () => {
|
it('has a reference to the active option', async () => {
|
||||||
const el = await fixture(html`
|
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=${'10'} id="first">Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
|
|
@ -387,7 +387,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
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 () => {
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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=${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}>
|
||||||
|
|
@ -400,7 +400,7 @@ 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 = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} autocomplete="none">
|
<${tag} autocomplete="none" show-all-on-empty>
|
||||||
<${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}>
|
||||||
|
|
@ -588,7 +588,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
describe('Enter', () => {
|
describe('Enter', () => {
|
||||||
it('[Enter] selects active option', async () => {
|
it('[Enter] selects active option', async () => {
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
|
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${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
|
// When listbox is not focusable (in case of a combobox), the user should be allowed
|
||||||
// to enter a space in the focusable element (texbox)
|
// to enter a space in the focusable element (texbox)
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
|
<${optionTag} .choiceValue="${'Bla'}">Bla</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${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 () => {
|
it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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 1'}>Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${'Item 2'}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'Item 2'}>Item 2</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
|
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
|
||||||
|
|
@ -715,7 +715,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
describe('Orientation', () => {
|
describe('Orientation', () => {
|
||||||
it('has a default value of "vertical"', async () => {
|
it('has a default value of "vertical"', async () => {
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
|
|
@ -755,7 +755,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
it('uses [ArrowLeft] and [ArrowRight] keys when "horizontal"', async () => {
|
it('uses [ArrowLeft] and [ArrowRight] keys when "horizontal"', async () => {
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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="${'Artichoke'}">Artichoke</${optionTag}>
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
|
|
@ -932,7 +932,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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=${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}>
|
||||||
|
|
@ -972,7 +972,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const el = /** @type {LionListbox} */ (await fixture(html`
|
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=${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}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue