From 6cdefd883331d24edfd6507308361002d07dec74 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 11 May 2021 19:53:55 -0400 Subject: [PATCH] feat: add _getTextboxValueFromOption method --- .changeset/wet-crabs-shop.md | 5 + docs/components/inputs/combobox/features.md | 44 ++++ packages/combobox/src/LionCombobox.js | 52 +++-- packages/combobox/test/lion-combobox.test.js | 205 +++++++++++++++++++ 4 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 .changeset/wet-crabs-shop.md diff --git a/.changeset/wet-crabs-shop.md b/.changeset/wet-crabs-shop.md new file mode 100644 index 000000000..a260ed8f7 --- /dev/null +++ b/.changeset/wet-crabs-shop.md @@ -0,0 +1,5 @@ +--- +'@lion/combobox': minor +--- + +Add a new \_getTextboxValueFromOption method on LionCombobox, to be able to overide how the modelValue is displayed in the textbox diff --git a/docs/components/inputs/combobox/features.md b/docs/components/inputs/combobox/features.md index 5fe918228..65129b97b 100644 --- a/docs/components/inputs/combobox/features.md +++ b/docs/components/inputs/combobox/features.md @@ -15,6 +15,7 @@ availability of the popup. ```js script import { LitElement, html, repeat } from '@lion/core'; import { listboxData } from '../../../../packages/listbox/docs/listboxData.js'; +import { LionCombobox } from '../../../../packages/combobox/src/LionCombobox.js'; import '@lion/listbox/define'; import '@lion/combobox/define'; import './src/demo-selection-display.js'; @@ -327,6 +328,49 @@ customElements.define('demo-server-side', DemoServerSide); export const serverSideCompletion = () => html``; ``` +## Set complex object in choiceValue + +A common use case is to set a complex object in the `choiceValue` property. By default, `LionCombobox` will display in the text input the `modelValue`. By overriding the `_getTextboxValueFromOption` method, you can choose which value will be used for the input value. + +```js preview-story +class ComplexObjectCombobox extends LionCombobox { + /** + * Override how the input text is filled + * @param {LionOption} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.label; + } +} + +customElements.define('complex-object-combobox', ComplexObjectCombobox); + +const onModelValueChanged = event => { + console.log(`event.target.modelValue: ${JSON.stringify(event.target.modelValue)}`); +}; + +const listboxDataObject = listboxData.map(e => { + return { label: e, name: e }; +}); + +export const complexObjectChoiceValue = () => html` + ${lazyRender( + listboxDataObject.map( + entry => + html` + ${entry.label} + `, + ), + )} +`; +``` + ## Listbox compatibility All configurations that can be applied to `lion-listbox`, can be applied to `lion-combobox` as well. diff --git a/packages/combobox/src/LionCombobox.js b/packages/combobox/src/LionCombobox.js index 4dd59dc24..53ed0beb4 100644 --- a/packages/combobox/src/LionCombobox.js +++ b/packages/combobox/src/LionCombobox.js @@ -320,7 +320,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) { if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) { if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) { if (!this.multipleChoice) { - this._setTextboxValue(this.modelValue); + const textboxValue = this._getTextboxValueFromOption( + this.formElements[/** @type {number} */ (this.checkedIndex)], + ); + this._setTextboxValue(textboxValue); } else { this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); } @@ -337,8 +340,12 @@ export class LionCombobox extends OverlayMixin(LionListbox) { */ __unsyncCheckedIndexOnInputChange() { const autoselect = this._autoSelectCondition(); - if (!this.multipleChoice && !autoselect && !this._inputNode.value.startsWith(this.modelValue)) { - this.checkedIndex = -1; + const checkedElement = this.formElements[/** @type {number} */ (this.checkedIndex)]; + if (!this.multipleChoice && !autoselect && checkedElement) { + const textboxValue = this._getTextboxValueFromOption(checkedElement); + if (!this._inputNode.value.startsWith(textboxValue)) { + this.checkedIndex = -1; + } } } @@ -396,8 +403,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) { */ matchCondition(option, textboxValue) { let idx = -1; - if (typeof option.choiceValue === 'string' && typeof textboxValue === 'string') { - idx = option.choiceValue.toLowerCase().indexOf(textboxValue.toLowerCase()); + const inputValue = this._getTextboxValueFromOption(option); + if (typeof inputValue === 'string' && typeof textboxValue === 'string') { + idx = inputValue.toLowerCase().indexOf(textboxValue.toLowerCase()); } if (this.matchMode === 'all') { @@ -452,6 +460,17 @@ export class LionCombobox extends OverlayMixin(LionListbox) { return true; } + /** + * Return the value to be used for the input value + * @overridable + * @param {LionOption} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.choiceValue; + } + /** * @configure ListboxMixin whenever the options are changed (potentially due to external causes * like server side filtering of nodes), schedule autocompletion for proper highlighting @@ -516,8 +535,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) { phase: 'overlay-close', }) ) { - this._inputNode.value = - this.formElements[/** @type {number} */ (this.checkedIndex)].choiceValue; + this._inputNode.value = this._getTextboxValueFromOption( + this.formElements[/** @type {number} */ (this.checkedIndex)], + ); } } else { this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); @@ -656,11 +676,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) { // and _inputNode.value if (isInlineAutoFillCandidate) { - const stringValues = - typeof option.choiceValue === 'string' && typeof curValue === 'string'; + const textboxValue = this._getTextboxValueFromOption(option); + const stringValues = typeof textboxValue === 'string' && typeof curValue === 'string'; const beginsWith = - stringValues && - option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0; + stringValues && textboxValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0; // We only can do proper inline autofilling when the beginning of the word matches if (beginsWith) { this.__textboxInlineComplete(option); @@ -727,7 +746,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) { */ __textboxInlineComplete(option = this.formElements[this.activeIndex]) { const prevLen = this._inputNode.value.length; - this._inputNode.value = option.choiceValue; + this._inputNode.value = this._getTextboxValueFromOption(option); this._inputNode.selectionStart = prevLen; this._inputNode.selectionEnd = this._inputNode.value.length; } @@ -849,9 +868,14 @@ export class LionCombobox extends OverlayMixin(LionListbox) { * @param {string[]} oldModelValue * @protected */ + // eslint-disable-next-line no-unused-vars _syncToTextboxMultiple(modelValue, oldModelValue = []) { - const diff = modelValue.filter(x => !oldModelValue.includes(x)).toString(); - this._setTextboxValue(diff); // or last selected value? + const diff = modelValue.filter(x => !oldModelValue.includes(x)); + const newValue = this.formElements + .filter(option => diff.includes(option.choiceValue)) + .map(option => this._getTextboxValueFromOption(option)) + .join(' '); + this._setTextboxValue(newValue); // or last selected value? } /** diff --git a/packages/combobox/test/lion-combobox.test.js b/packages/combobox/test/lion-combobox.test.js index 8cccbd9c4..7d1b3230c 100644 --- a/packages/combobox/test/lion-combobox.test.js +++ b/packages/combobox/test/lion-combobox.test.js @@ -1680,6 +1680,211 @@ describe('lion-combobox', () => { el.setCheckedIndex([0]); expect(_inputNode.value).to.equal('Artichoke--multi'); }); + + describe('Override _getTextboxValueFromOption', () => { + it('allows to override "_getTextboxValueFromOption" and sync to textbox when multiple', async () => { + class X extends LionCombobox { + /** + * Return the value to be used for the input value + * @overridable + * @param {?} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.label; + } + } + const tagName = defineCE(X); + const tag = unsafeStatic(tagName); + + const el = /** @type {LionCombobox} */ ( + await fixture(html` + <${tag} name="foo" autocomplete="both" multiple-choice> + Artichoke + Chard + Chicory + Victoria Plum + + `) + ); + const { _inputNode } = getComboboxMembers(el); + el.setCheckedIndex(-1); + + // Act + el.setCheckedIndex([0]); + + // Assert + expect(_inputNode.value).to.equal('Artichoke as label'); + + // Act + el.setCheckedIndex([3]); + + // Assert + expect(_inputNode.value).to.equal('Victoria Plum as label'); + }); + + it('allows to override "_getTextboxValueFromOption" and sync modelValue with textbox', async () => { + class X extends LionCombobox { + /** + * Return the value to be used for the input value + * @overridable + * @param {?} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.label; + } + } + const tagName = defineCE(X); + const tag = unsafeStatic(tagName); + + const el = /** @type {LionCombobox} */ ( + await fixture(html` + <${tag} name="foo"> + Artichoke + Chard + Chicory + Victoria Plum + + `) + ); + const { _inputNode } = getComboboxMembers(el); + + // Assume + expect(_inputNode.value).to.equal('Chard as label'); + + // Act + el.modelValue = { value: 'Chicory' }; + await el.updateComplete; + + // Assert + expect(_inputNode.value).to.equal('Chicory as label'); + }); + + it('allows to override "_getTextboxValueFromOption" and clears modelValue and textbox value on clear()', async () => { + class X extends LionCombobox { + /** + * Return the value to be used for the input value + * @overridable + * @param {?} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.label; + } + } + const tagName = defineCE(X); + const tag = unsafeStatic(tagName); + + const el = /** @type {LionCombobox} */ ( + await fixture(html` + <${tag} name="foo" .modelValue="${{ value: 'Artichoke' }}"> + Artichoke + Chard + Chicory + Victoria Plum + + `) + ); + const { _inputNode } = getComboboxMembers(el); + + // Assume + expect(_inputNode.value).to.equal('Artichoke as label'); + + // Act + el.clear(); + + // Assert + expect(el.modelValue).to.equal(''); + expect(_inputNode.value).to.equal(''); + }); + + it('allows to override "_getTextboxValueFromOption" and syncs textbox to modelValue', async () => { + class X extends LionCombobox { + /** + * Return the value to be used for the input value + * @overridable + * @param {?} option + * @returns {string} + */ + // eslint-disable-next-line class-methods-use-this + _getTextboxValueFromOption(option) { + return option.label; + } + } + const tagName = defineCE(X); + const tag = unsafeStatic(tagName); + + const el = /** @type {LionCombobox} */ ( + await fixture(html` + <${tag} name="foo"> + Artichoke + Chard + Chicory + Victoria Plum + + `) + ); + const { _inputNode } = getComboboxMembers(el); + + async function performChecks() { + el.formElements[0].click(); + await el.updateComplete; + + // FIXME: fix properly for Webkit + // expect(_inputNode.value).to.equal('Aha'); + expect(el.checkedIndex).to.equal(0); + + mimicUserTyping(el, 'Arti'); + await el.updateComplete; + expect(_inputNode.value).to.equal('Arti'); + + await el.updateComplete; + expect(el.checkedIndex).to.equal(-1); + } + + el.autocomplete = 'none'; + await performChecks(); + + el.autocomplete = 'list'; + await performChecks(); + }); + }); }); describe('Active index behavior', () => {