fix(combobox): do not reopen on focusin edge cases

This commit is contained in:
Thijs Louisse 2021-04-17 00:00:32 +02:00
parent 6aa7fc29c8
commit 351e9598fe
3 changed files with 165 additions and 59 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/combobox': patch
---
do not reopen listbox on focusin edge cases

View file

@ -307,7 +307,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
/**
* @param {'disabled'|'modelValue'|'readOnly'} name
* @param {'disabled'|'modelValue'|'readOnly'|'focused'} name
* @param {unknown} oldValue
*/
requestUpdateInternal(name, oldValue) {
@ -324,6 +324,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
}
if (name === 'focused' && this.focused) {
this.__requestShowOverlay();
}
}
/**
@ -331,11 +334,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('focused')) {
if (this.focused) {
this.__requestShowOverlay();
}
}
if (changedProperties.has('opened')) {
if (this.opened) {
@ -413,12 +411,18 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* return options.currentValue.length > 4 && super._showOverlayCondition(options);
* }
*
* @param {{ currentValue: string, lastKey:string|undefined }} options
* @param {{ currentValue?: string, lastKey?: string }} options
* @protected
* @returns {boolean}
*/
// TODO: batch all pending condition triggers in __pendingShowTriggers, reducing race conditions
// eslint-disable-next-line class-methods-use-this
_showOverlayCondition({ lastKey }) {
const hideOn = ['Tab', 'Escape', 'Enter'];
if (lastKey && hideOn.includes(lastKey)) {
return false;
}
if (this.showAllOnEmpty && this.focused) {
return true;
}
@ -426,8 +430,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
if (!lastKey) {
return /** @type {boolean} */ (this.opened);
}
const doNotShowOn = ['Tab', 'Esc', 'Enter'];
return !doNotShowOn.includes(lastKey);
return true;
}
/**
@ -449,11 +453,8 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @param {KeyboardEvent} ev
* @protected
*/
_textboxOnKeydown(ev) {
if (ev.key === 'Tab') {
this.opened = false;
}
}
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_textboxOnKeydown(ev) {}
/**
* @param {MouseEvent} ev
@ -461,11 +462,12 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
_listboxOnClick(ev) {
super._listboxOnClick(ev);
this._inputNode.focus();
if (!this.multipleChoice) {
this.activeIndex = -1;
this.opened = false;
this._setOpenedWithoutPropertyEffects(false);
}
this._inputNode.focus();
}
/**
@ -756,6 +758,15 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this.__setupCombobox();
}
/**
* @enhance OverlayMixin
* @protected
*/
_teardownOverlayCtrl() {
super._teardownOverlayCtrl();
this.__teardownCombobox();
}
/**
* @enhance OverlayMixin
* @protected
@ -784,7 +795,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
const { key } = ev;
switch (key) {
case 'Escape':
this.opened = false;
this._setOpenedWithoutPropertyEffects(false);
this._setTextboxValue('');
break;
case 'Enter':
@ -792,7 +803,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
return;
}
if (!this.multipleChoice) {
this.opened = false;
this._setOpenedWithoutPropertyEffects(false);
}
break;
/* no default */
@ -891,10 +902,13 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @private
*/
__requestShowOverlay(ev) {
this.opened = this._showOverlayCondition({
lastKey: ev && ev.key,
const lastKey = ev && ev.key;
this._setOpenedWithoutPropertyEffects(
this._showOverlayCondition({
lastKey,
currentValue: this._inputNode.value,
});
}),
);
}
clear() {

View file

@ -57,7 +57,7 @@ function mimicUserTyping(el, value) {
}
/**
* @param {HTMLInputElement} el
* @param {HTMLElement} el
* @param {string} key
*/
function mimicKeyPress(el, key) {
@ -157,8 +157,7 @@ async function fruitFixture({ autocomplete, matchMode } = {}) {
}
describe('lion-combobox', () => {
describe('Options', () => {
describe('showAllOnEmpty', () => {
describe('Options visibility', () => {
it('hides options when text in input node is cleared after typing something by default', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo">
@ -192,6 +191,39 @@ describe('lion-combobox', () => {
await performChecks();
});
it('hides listbox on click/enter (when multiple-choice is false)', 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 { _comboboxNode, _listboxNode } = getComboboxMembers(el);
async function open() {
// activate opened listbox
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'ch');
return el.updateComplete;
}
await open();
expect(el.opened).to.be.true;
const visibleOptions = el.formElements.filter(o => o.style.display !== 'none');
visibleOptions[0].click();
expect(el.opened).to.be.false;
await open();
expect(el.opened).to.be.true;
el.activeIndex = el.formElements.indexOf(visibleOptions[0]);
mimicKeyPress(_listboxNode, 'Enter');
await el.updateComplete;
expect(el.opened).to.be.false;
});
describe('With ".showAllOnEmpty"', () => {
it('keeps showing options when text in input node is cleared after typing something', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="list" show-all-on-empty>
@ -240,6 +272,63 @@ describe('lion-combobox', () => {
await el.updateComplete;
expect(el.opened).to.be.true;
});
it('hides overlay on focusin after [Escape]', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" .showAllOnEmpty="${true}">
<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 { _comboboxNode, _inputNode } = getComboboxMembers(el);
expect(el.opened).to.be.false;
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.be.true;
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
await el.updateComplete;
expect(el.opened).to.be.false;
});
it('hides listbox on click/enter (when multiple-choice is false)', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" .showAllOnEmpty="${true}">
<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 { _comboboxNode, _listboxNode, _inputNode } = getComboboxMembers(el);
async function open() {
// activate opened listbox
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'ch');
return el.updateComplete;
}
await open();
expect(el.opened).to.be.true;
const visibleOptions = el.formElements.filter(o => o.style.display !== 'none');
visibleOptions[0].click();
await el.updateComplete;
expect(el.opened).to.be.false;
_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');
expect(el.opened).to.be.false;
});
});
});
@ -1766,7 +1855,7 @@ describe('lion-combobox', () => {
describe('Multiple Choice', () => {
// TODO: possibly later share test with select-rich if it officially supports multipleChoice
it('does not close listbox on click/enter/space', async () => {
it('does not close listbox on click/enter', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" multiple-choice>
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
@ -1787,10 +1876,8 @@ describe('lion-combobox', () => {
const visibleOptions = el.formElements.filter(o => o.style.display !== 'none');
visibleOptions[0].click();
expect(el.opened).to.equal(true);
// visibleOptions[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
// expect(el.opened).to.equal(true);
// visibleOptions[2].dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
// expect(el.opened).to.equal(true);
mimicKeyPress(visibleOptions[1], 'Enter');
expect(el.opened).to.equal(true);
});
});