Merge pull request #1377 from MatthieuLebigre/feat/combobox-complex-object
feat: add _getTextboxValueFromOption method
This commit is contained in:
commit
c4af8a9b15
4 changed files with 292 additions and 14 deletions
5
.changeset/wet-crabs-shop.md
Normal file
5
.changeset/wet-crabs-shop.md
Normal 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
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue