fix(combobox): update the code to when show and hide the overlay, to be more robust (#2301)
This commit is contained in:
parent
57597bb9ba
commit
b50b960daa
3 changed files with 149 additions and 88 deletions
5
.changeset/wet-eggs-reply.md
Normal file
5
.changeset/wet-eggs-reply.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
[combobox] update the code to when show and hide the overlay, to be more robust
|
||||
|
|
@ -456,10 +456,14 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
*/
|
||||
this.__listboxContentChanged = false;
|
||||
|
||||
/** @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._onKeyUp = this._onKeyUp.bind(this);
|
||||
/** @type {EventListener}
|
||||
* @private
|
||||
*/
|
||||
this.__requestShowOverlay = this.__requestShowOverlay.bind(this);
|
||||
this._textboxOnClick = this._textboxOnClick.bind(this);
|
||||
/** @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
|
|
@ -500,9 +504,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
}
|
||||
}
|
||||
}
|
||||
if (name === 'focused' && this.focused) {
|
||||
this.__requestShowOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -614,11 +615,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
* that wraps the listbox with options
|
||||
*
|
||||
* @example
|
||||
* _showOverlayCondition(options) {
|
||||
* return this.focused || super._showOverlayCondition(options);
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* _showOverlayCondition({ lastKey }) {
|
||||
* return lastKey === 'ArrowDown';
|
||||
* }
|
||||
|
|
@ -635,20 +631,21 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
// 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)) {
|
||||
const alwaysHideOn = ['Tab', 'Escape'];
|
||||
const notMultipleChoiceHideOn = ['Enter'];
|
||||
if (
|
||||
lastKey &&
|
||||
(alwaysHideOn.includes(lastKey) ||
|
||||
(!this.multipleChoice && notMultipleChoiceHideOn.includes(lastKey)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.showAllOnEmpty && this.focused) {
|
||||
if (this.filled || this.showAllOnEmpty) {
|
||||
return true;
|
||||
}
|
||||
// when no keyboard action involved (on focused change), return current opened state
|
||||
if (!lastKey) {
|
||||
return /** @type {boolean} */ (this.opened);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value to be used for the input value
|
||||
|
|
@ -678,9 +675,14 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
this.__listboxContentChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_textboxOnInput() {
|
||||
_textboxOnInput(ev) {
|
||||
this.__shouldAutocompleteNextUpdate = true;
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1068,7 +1070,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
*/
|
||||
_setupOpenCloseListeners() {
|
||||
super._setupOpenCloseListeners();
|
||||
this._inputNode.addEventListener('keyup', this.__requestShowOverlay);
|
||||
this._inputNode.addEventListener('keyup', this._onKeyUp);
|
||||
this._inputNode.addEventListener('click', this._textboxOnClick);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1077,7 +1080,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
*/
|
||||
_teardownOpenCloseListeners() {
|
||||
super._teardownOpenCloseListeners();
|
||||
this._inputNode.removeEventListener('keyup', this.__requestShowOverlay);
|
||||
this._inputNode.removeEventListener('keyup', this._onKeyUp);
|
||||
this._inputNode.removeEventListener('click', this._textboxOnClick);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1225,9 +1229,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} [ev]
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
__requestShowOverlay(ev) {
|
||||
_onKeyUp(ev) {
|
||||
const lastKey = ev && ev.key;
|
||||
this.opened = this._showOverlayCondition({
|
||||
lastKey,
|
||||
|
|
@ -1235,6 +1239,15 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FocusEvent} [ev]
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_textboxOnClick(ev) {
|
||||
this.opened = this._showOverlayCondition({});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.value = '';
|
||||
super.clear();
|
||||
|
|
|
|||
|
|
@ -93,11 +93,9 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode, _listboxNode } = getComboboxMembers(el);
|
||||
const { _listboxNode } = getComboboxMembers(el);
|
||||
|
||||
async function open() {
|
||||
// activate opened listbox
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
mimicUserTyping(el, 'ch');
|
||||
return el.updateComplete;
|
||||
}
|
||||
|
|
@ -150,7 +148,7 @@ describe('lion-combobox', () => {
|
|||
await performChecks();
|
||||
});
|
||||
|
||||
it('shows overlay on focusin', async () => {
|
||||
it('shows overlay on click', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
<lion-combobox name="foo" .showAllOnEmpty="${true}">
|
||||
|
|
@ -161,15 +159,15 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`)
|
||||
);
|
||||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
const { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
expect(el.opened).to.be.false;
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.true;
|
||||
});
|
||||
|
||||
it('hides overlay on focusin after [Escape]', async () => {
|
||||
it('hides overlay on [Escape] after being opened', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
<lion-combobox name="foo" .showAllOnEmpty="${true}">
|
||||
|
|
@ -183,7 +181,7 @@ describe('lion-combobox', () => {
|
|||
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||
|
||||
expect(el.opened).to.be.false;
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
_comboboxNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.true;
|
||||
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
||||
|
|
@ -203,11 +201,9 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode, _listboxNode, _inputNode } = getComboboxMembers(el);
|
||||
const { _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;
|
||||
}
|
||||
|
|
@ -870,10 +866,10 @@ describe('lion-combobox', () => {
|
|||
});
|
||||
|
||||
describe('Overlay visibility', () => {
|
||||
it('does not show overlay on focusin', async () => {
|
||||
it('does not show overlay on click', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
<lion-combobox name="foo" multiple-choice>
|
||||
<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>
|
||||
|
|
@ -884,7 +880,26 @@ describe('lion-combobox', () => {
|
|||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
|
||||
expect(el.opened).to.equal(false);
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
_comboboxNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
});
|
||||
|
||||
it('shows overlay on click when filled', 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 } = getComboboxMembers(el);
|
||||
el.modelValue = 'Art';
|
||||
expect(el.opened).to.equal(false);
|
||||
_comboboxNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
});
|
||||
|
|
@ -914,7 +929,7 @@ describe('lion-combobox', () => {
|
|||
expect(el.opened).to.equal(false);
|
||||
|
||||
// step [1]
|
||||
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
|
|
@ -948,10 +963,7 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||
|
||||
// open
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
const { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
|
|
@ -963,6 +975,32 @@ describe('lion-combobox', () => {
|
|||
expect(_inputNode.value).to.equal('');
|
||||
});
|
||||
|
||||
it('hides overlay on [Enter]', 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 { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(_inputNode.value).to.equal('Artichoke');
|
||||
|
||||
// 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(false);
|
||||
expect(_inputNode.value).to.equal('Artichoke');
|
||||
});
|
||||
|
||||
it('hides overlay on [Tab]', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
|
|
@ -975,10 +1013,7 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||
|
||||
// open
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
const { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
|
|
@ -992,6 +1027,32 @@ describe('lion-combobox', () => {
|
|||
expect(_inputNode.value).to.equal('Artichoke');
|
||||
});
|
||||
|
||||
it('keeps overlay open on [Shift]', 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 { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(_inputNode.value).to.equal('Artichoke');
|
||||
|
||||
// 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: 'Shift' }));
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(_inputNode.value).to.equal('Artichoke');
|
||||
});
|
||||
|
||||
it('clears checkedIndex on empty text', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
|
|
@ -1004,10 +1065,7 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||
|
||||
// open
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
const { _inputNode } = getComboboxMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
|
|
@ -1043,34 +1101,6 @@ describe('lion-combobox', () => {
|
|||
|
||||
// NB: If this becomes a suite, move to separate file
|
||||
describe('Subclassers', () => {
|
||||
it('allows to control overlay visibility via "_showOverlayCondition"', async () => {
|
||||
class ShowOverlayConditionCombobox extends LionCombobox {
|
||||
/** @param {{ currentValue: string, lastKey:string }} options */
|
||||
_showOverlayCondition(options) {
|
||||
return this.focused || super._showOverlayCondition(options);
|
||||
}
|
||||
}
|
||||
const tagName = defineCE(ShowOverlayConditionCombobox);
|
||||
const tag = unsafeStatic(tagName);
|
||||
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
<${tag} 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>
|
||||
</${tag}>
|
||||
`)
|
||||
);
|
||||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
|
||||
expect(el.opened).to.equal(false);
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
});
|
||||
|
||||
it('allows to control overlay visibility via "_showOverlayCondition": should not display overlay if currentValue length condition is not fulfilled', async () => {
|
||||
class ShowOverlayConditionCombobox extends LionCombobox {
|
||||
/** @param {{ currentValue: string, lastKey:string }} options */
|
||||
|
|
@ -1164,11 +1194,8 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
const options = el.formElements;
|
||||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
|
|
@ -1208,11 +1235,8 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
const options = el.formElements;
|
||||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
expect(el.opened).to.equal(true);
|
||||
await el.updateComplete;
|
||||
|
|
@ -2948,7 +2972,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', async () => {
|
||||
it('does not close listbox on click', async () => {
|
||||
const el = /** @type {LionCombobox} */ (
|
||||
await fixture(html`
|
||||
<lion-combobox name="foo" multiple-choice>
|
||||
|
|
@ -2960,10 +2984,7 @@ describe('lion-combobox', () => {
|
|||
`)
|
||||
);
|
||||
|
||||
const { _comboboxNode } = getComboboxMembers(el);
|
||||
|
||||
// activate opened listbox
|
||||
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
|
||||
|
|
@ -2971,7 +2992,29 @@ describe('lion-combobox', () => {
|
|||
const visibleOptions = el.formElements.filter(o => o.style.display !== 'none');
|
||||
visibleOptions[0].click();
|
||||
expect(el.opened).to.equal(true);
|
||||
mimicKeyPress(visibleOptions[1], 'Enter');
|
||||
});
|
||||
|
||||
it('does not close listbox 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);
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue