Merge pull request #1377 from MatthieuLebigre/feat/combobox-complex-object

feat: add _getTextboxValueFromOption method
This commit is contained in:
Thijs Louisse 2021-05-21 00:08:05 +02:00 committed by GitHub
commit c4af8a9b15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 292 additions and 14 deletions

View file

@ -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

View file

@ -15,6 +15,7 @@ availability of the popup.
```js script ```js script
import { LitElement, html, repeat } from '@lion/core'; import { LitElement, html, repeat } from '@lion/core';
import { listboxData } from '../../../../packages/listbox/docs/listboxData.js'; import { listboxData } from '../../../../packages/listbox/docs/listboxData.js';
import { LionCombobox } from '../../../../packages/combobox/src/LionCombobox.js';
import '@lion/listbox/define'; import '@lion/listbox/define';
import '@lion/combobox/define'; import '@lion/combobox/define';
import './src/demo-selection-display.js'; import './src/demo-selection-display.js';
@ -327,6 +328,49 @@ customElements.define('demo-server-side', DemoServerSide);
export const serverSideCompletion = () => html`<demo-server-side></demo-server-side>`; export const serverSideCompletion = () => html`<demo-server-side></demo-server-side>`;
``` ```
## 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` <complex-object-combobox
name="combo"
label="Display only the label once selected"
@model-value-changed="${onModelValueChanged}"
>
${lazyRender(
listboxDataObject.map(
entry =>
html`
<lion-option .choiceValue="${entry}" .label="${entry.label}">${entry.label}</lion-option>
`,
),
)}
</complex-object-combobox>`;
```
## Listbox compatibility ## Listbox compatibility
All configurations that can be applied to `lion-listbox`, can be applied to `lion-combobox` as well. All configurations that can be applied to `lion-listbox`, can be applied to `lion-combobox` as well.

View file

@ -320,7 +320,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) { if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) {
if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) { if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) {
if (!this.multipleChoice) { if (!this.multipleChoice) {
this._setTextboxValue(this.modelValue); const textboxValue = this._getTextboxValueFromOption(
this.formElements[/** @type {number} */ (this.checkedIndex)],
);
this._setTextboxValue(textboxValue);
} else { } else {
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
} }
@ -337,8 +340,12 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/ */
__unsyncCheckedIndexOnInputChange() { __unsyncCheckedIndexOnInputChange() {
const autoselect = this._autoSelectCondition(); const autoselect = this._autoSelectCondition();
if (!this.multipleChoice && !autoselect && !this._inputNode.value.startsWith(this.modelValue)) { const checkedElement = this.formElements[/** @type {number} */ (this.checkedIndex)];
this.checkedIndex = -1; 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) { matchCondition(option, textboxValue) {
let idx = -1; let idx = -1;
if (typeof option.choiceValue === 'string' && typeof textboxValue === 'string') { const inputValue = this._getTextboxValueFromOption(option);
idx = option.choiceValue.toLowerCase().indexOf(textboxValue.toLowerCase()); if (typeof inputValue === 'string' && typeof textboxValue === 'string') {
idx = inputValue.toLowerCase().indexOf(textboxValue.toLowerCase());
} }
if (this.matchMode === 'all') { if (this.matchMode === 'all') {
@ -452,6 +460,17 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
return true; 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 * @configure ListboxMixin whenever the options are changed (potentially due to external causes
* like server side filtering of nodes), schedule autocompletion for proper highlighting * like server side filtering of nodes), schedule autocompletion for proper highlighting
@ -516,8 +535,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
phase: 'overlay-close', phase: 'overlay-close',
}) })
) { ) {
this._inputNode.value = this._inputNode.value = this._getTextboxValueFromOption(
this.formElements[/** @type {number} */ (this.checkedIndex)].choiceValue; this.formElements[/** @type {number} */ (this.checkedIndex)],
);
} }
} else { } else {
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
@ -656,11 +676,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
// and _inputNode.value // and _inputNode.value
if (isInlineAutoFillCandidate) { if (isInlineAutoFillCandidate) {
const stringValues = const textboxValue = this._getTextboxValueFromOption(option);
typeof option.choiceValue === 'string' && typeof curValue === 'string'; const stringValues = typeof textboxValue === 'string' && typeof curValue === 'string';
const beginsWith = const beginsWith =
stringValues && stringValues && textboxValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
// We only can do proper inline autofilling when the beginning of the word matches // We only can do proper inline autofilling when the beginning of the word matches
if (beginsWith) { if (beginsWith) {
this.__textboxInlineComplete(option); this.__textboxInlineComplete(option);
@ -727,7 +746,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/ */
__textboxInlineComplete(option = this.formElements[this.activeIndex]) { __textboxInlineComplete(option = this.formElements[this.activeIndex]) {
const prevLen = this._inputNode.value.length; const prevLen = this._inputNode.value.length;
this._inputNode.value = option.choiceValue; this._inputNode.value = this._getTextboxValueFromOption(option);
this._inputNode.selectionStart = prevLen; this._inputNode.selectionStart = prevLen;
this._inputNode.selectionEnd = this._inputNode.value.length; this._inputNode.selectionEnd = this._inputNode.value.length;
} }
@ -849,9 +868,14 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @param {string[]} oldModelValue * @param {string[]} oldModelValue
* @protected * @protected
*/ */
// eslint-disable-next-line no-unused-vars
_syncToTextboxMultiple(modelValue, oldModelValue = []) { _syncToTextboxMultiple(modelValue, oldModelValue = []) {
const diff = modelValue.filter(x => !oldModelValue.includes(x)).toString(); const diff = modelValue.filter(x => !oldModelValue.includes(x));
this._setTextboxValue(diff); // or last selected value? const newValue = this.formElements
.filter(option => diff.includes(option.choiceValue))
.map(option => this._getTextboxValueFromOption(option))
.join(' ');
this._setTextboxValue(newValue); // or last selected value?
} }
/** /**

View file

@ -1680,6 +1680,211 @@ describe('lion-combobox', () => {
el.setCheckedIndex([0]); el.setCheckedIndex([0]);
expect(_inputNode.value).to.equal('Artichoke--multi'); 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>
<lion-option .label="${'Artichoke as label'}" .choiceValue="${{
value: 'Artichoke',
}}">Artichoke</lion-option>
<lion-option .label="${'Chard as label'}" .choiceValue="${{
value: 'Chard',
}}">Chard</lion-option>
<lion-option .label="${'Chicory as label'}" .choiceValue="${{
value: 'Chicory',
}}">Chicory</lion-option>
<lion-option .label="${'Victoria Plum as label'}" .choiceValue="${{
value: 'Victoria Plum',
}}">Victoria Plum</lion-option>
</${tag}>
`)
);
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">
<lion-option .label="${'Artichoke as label'}" .choiceValue="${{
value: 'Artichoke',
}}">Artichoke</lion-option>
<lion-option .label="${'Chard as label'}" .choiceValue="${{
value: 'Chard',
}}" checked>Chard</lion-option>
<lion-option .label="${'Chicory as label'}" .choiceValue="${{
value: 'Chicory',
}}">Chicory</lion-option>
<lion-option .label="${'Victoria Plum as label'}" .choiceValue="${{
value: 'Victoria Plum',
}}">Victoria Plum</lion-option>
</${tag}>
`)
);
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' }}">
<lion-option .label="${'Artichoke as label'}" .choiceValue="${{
value: 'Artichoke',
}}">Artichoke</lion-option>
<lion-option .label="${'Chard as label'}" .choiceValue="${{
value: 'Chard',
}}">Chard</lion-option>
<lion-option .label="${'Chicory as label'}" .choiceValue="${{
value: 'Chicory',
}}">Chicory</lion-option>
<lion-option .label="${'Victoria Plum as label'}" .choiceValue="${{
value: 'Victoria Plum',
}}">Victoria Plum</lion-option>
</${tag}>
`)
);
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">
<lion-option .label="${'Artichoke as label'}" .choiceValue="${{
value: 'Artichoke',
}}">Artichoke</lion-option>
<lion-option .label="${'Chard as label'}" .choiceValue="${{
value: 'Chard',
}}">Chard</lion-option>
<lion-option .label="${'Chicory as label'}" .choiceValue="${{
value: 'Chicory',
}}">Chicory</lion-option>
<lion-option .label="${'Victoria Plum as label'}" .choiceValue="${{
value: 'Victoria Plum',
}}">Victoria Plum</lion-option>
</${tag}>
`)
);
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', () => { describe('Active index behavior', () => {