feat(combobox): improvements api, demos and autocompletion ux

This commit is contained in:
Thijs Louisse 2020-09-30 12:05:36 +02:00
parent c03ebde5b5
commit 0ebca5b47d
7 changed files with 491 additions and 51 deletions

View file

@ -0,0 +1,11 @@
---
'@lion/combobox': patch
---
Combobox api, demo and ux improvements
- renamed `filterOptionCondition ` (similarity to `match-mode`, since this is basically an override)
- demos for `matchCondition`
- inline autocompletion edge cases solved (that would be inconsistent ux otherwise)
- demos took a long time render: introduced a lazyRender directive that only adds (expensive) lionOptions after first meaningful paint has happened
- made clearer from the code that selectionDisplay component is for demo purposes only at this moment

View file

@ -19,7 +19,9 @@ import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
import { listboxData } from '@lion/listbox/docs/listboxData.js';
import '@lion/listbox/lion-option.js';
import './lion-combobox.js';
import './docs/lion-combobox-selection-display.js';
import './docs/demo-selection-display.js';
import { lazyRender } from './docs/lazyRender.js';
import levenshtein from './docs/levenshtein.js';
export default {
title: 'Forms/Combobox',
@ -29,7 +31,9 @@ export default {
```js preview-story
export const main = () => html`
<lion-combobox name="combo" label="Default">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -47,7 +51,7 @@ to the configurable values `none`, `list`, `inline` and `both`.
| both | ✓ | ✓ | ✓ | ✓ | ✓ |
- **list** shows a list on keydown character press
- **filter** filters list of potential matches according to `matchmode` or provided `filterOptionCondition`
- **filter** filters list of potential matches according to `matchmode` or provided `matchCondition`
- **focus** automatically focuses closest match (makes it the activedescendant)
- **check** automatically checks/selects closest match when `selection-follows-focus` is enabled (this is the default configuration)
- **complete** completes the textbox value inline (the 'missing characters' will be added as selected text)
@ -59,7 +63,9 @@ Selection will happen manually by the user.
```js preview-story
export const autocompleteNone = () => html`
<lion-combobox name="combo" label="Autocomplete 'none'" autocomplete="none">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -69,7 +75,9 @@ When `autocomplete="list"` is configured, it will filter listbox suggestions bas
```js preview-story
export const autocompleteList = () => html`
<lion-combobox name="combo" label="Autocomplete 'list'" autocomplete="list">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -80,7 +88,9 @@ It does NOT filter list of potential matches.
```js preview-story
export const autocompleteInline = () => html`
<lion-combobox name="combo" label="Autocomplete 'inline'" autocomplete="inline">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -91,7 +101,9 @@ This is the default value for `autocomplete`.
```js preview-story
export const autocompleteBoth = () => html`
<lion-combobox name="combo" label="Autocomplete 'both'" autocomplete="both">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -107,7 +119,9 @@ So 'ch' will both match 'Chard' and 'Artichoke'.
```js preview-story
export const matchModeBegin = () => html`
<lion-combobox name="combo" label="Match Mode 'begin'" match-mode="begin">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -115,7 +129,33 @@ export const matchModeBegin = () => html`
```js preview-story
export const matchModeAll = () => html`
<lion-combobox name="combo" label="Match Mode 'all'" match-mode="all">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
When the preconfigurable `match-mode` conditions are not sufficient,
one can define a custom matching function.
The example below matches when the Levenshtein distance is below 3 (including some other conditions).
```js preview-story
export const customMatchCondition = () => html`
<lion-combobox
name="combo"
label="Custom Match Mode 'levenshtein'"
help-text="Spelling mistakes will be forgiven. Try typing 'Aple' instead of 'Apple'"
.matchCondition="${({ choiceValue }, textboxValue) => {
const oVal = choiceValue.toLowerCase();
const tVal = textboxValue.toLowerCase();
const t = 1; // treshold
return oVal.slice(0, t) === tVal.slice(0, t) && levenshtein(oVal, tVal) < 3;
}}"
>
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -133,7 +173,9 @@ will be kept track of independently.
```js preview-story
export const noSelectionFollowsFocus = () => html`
<lion-combobox name="combo" label="No Selection Follows focus" .selectionFollowsFocus="${false}">
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -149,7 +191,9 @@ export const noRotateKeyboardNavigation = () => html`
label="No Rotate Keyboard Navigation"
.rotateKeyboardNavigation="${false}"
>
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
@ -170,10 +214,12 @@ This will:
```js preview-story
export const multipleChoice = () => html`
<lion-combobox name="combo" label="Multiple" multiple-choice>
<lion-combobox-selection-display slot="selection-display"></lion-combobox-selection-display>
${listboxData.map(
(entry, i) =>
html` <lion-option .choiceValue="${entry}" ?checked=${i === 0}>${entry}</lion-option> `,
<demo-selection-display slot="selection-display"></demo-selection-display>
${lazyRender(
listboxData.map(
(entry, i) =>
html` <lion-option .choiceValue="${entry}" ?checked=${i === 0}>${entry}</lion-option> `,
),
)}
</lion-combobox>
`;
@ -193,7 +239,9 @@ export const invokerButton = () => html`
}}"
>
<button slot="suffix" type="button" tabindex="-1"></button>
${listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `)}
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```

View file

@ -0,0 +1,191 @@
// eslint-disable-next-line max-classes-per-file
import { LitElement, html, css, nothing } from '@lion/core';
/**
* Disclaimer: this is just an example component demoing the selection display of LionCombobox
* It needs an 'a11y plan' and tests before it could be released
*/
/**
* @typedef {import('../src/LionCombobox.js').LionCombobox} LionCombobox
*/
/**
* Renders the wrapper containing the textbox that triggers the listbox with filtered options.
* Optionally, shows 'chips' that indicate the selection.
* Should be considered an internal/protected web component to be used in conjunction with
* LionCombobox
*
*/
export class DemoSelectionDisplay extends LitElement {
static get properties() {
return {
comboboxElement: Object,
/**
* Can be used to visually indicate the next
*/
removeChipOnNextBackspace: Boolean,
selectedElements: Array,
};
}
static get styles() {
return css`
:host {
display: flex;
}
.combobox__selection {
flex: none;
}
.combobox__input {
display: block;
}
.selection-chip {
border-radius: 4px;
background-color: #eee;
padding: 4px;
font-size: 10px;
}
.selection-chip--highlighted {
background-color: #ccc;
}
* > ::slotted([slot='_textbox']) {
outline: none;
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
border-bottom: 1px solid;
}
`;
}
/**
* @configure FocusMixin
*/
get _inputNode() {
return this.comboboxElement._inputNode;
}
_computeSelectedElements() {
const { formElements, checkedIndex } = /** @type {LionCombobox} */ (this.comboboxElement);
const checkedIndexes = Array.isArray(checkedIndex) ? checkedIndex : [checkedIndex];
return formElements.filter((_, i) => checkedIndexes.includes(i));
}
get multipleChoice() {
return this.comboboxElement?.multipleChoice;
}
constructor() {
super();
this.selectedElements = [];
/** @type {EventListener} */
this.__textboxOnKeyup = this.__textboxOnKeyup.bind(this);
/** @type {EventListener} */
this.__restoreBackspace = this.__restoreBackspace.bind(this);
}
/**
* @param {import('lit-element').PropertyValues } changedProperties
*/
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
if (this.multipleChoice) {
this._inputNode.addEventListener('keyup', this.__textboxOnKeyup);
this._inputNode.addEventListener('focusout', this.__restoreBackspace);
}
}
/**
* @param {import('lit-element').PropertyValues } changedProperties
*/
onComboboxElementUpdated(changedProperties) {
if (changedProperties.has('modelValue')) {
this.selectedElements = this._computeSelectedElements();
}
}
/**
* Whenever selectedElements are updated, makes sure that latest added elements
* are shown latest, and deleted elements respect existing order of chips.
*/
__reorderChips() {
const { selectedElements } = this;
if (this.__prevSelectedEls) {
const addedEls = selectedElements.filter(e => !this.__prevSelectedEls.includes(e));
const deletedEls = this.__prevSelectedEls.filter(e => !selectedElements.includes(e));
if (addedEls.length) {
this.selectedElements = [...this.__prevSelectedEls, ...addedEls];
} else if (deletedEls.length) {
deletedEls.forEach(delEl => {
this.__prevSelectedEls.splice(this.__prevSelectedEls.indexOf(delEl), 1);
});
this.selectedElements = this.__prevSelectedEls;
}
}
this.__prevSelectedEls = this.selectedElements;
}
/**
* @param {import("@lion/listbox").LionOption} option
* @param {boolean} highlight
*/
// eslint-disable-next-line class-methods-use-this
_selectedElementTemplate(option, highlight) {
return html`
<span class="selection-chip ${highlight ? 'selection-chip--highlighted' : ''}">
${option.value}
</span>
`;
}
_selectedElementsTemplate() {
if (!this.multipleChoice) {
return nothing;
}
return html`
<div class="combobox__selection">
${this.selectedElements.map((option, i) => {
const highlight = Boolean(
this.removeChipOnNextBackspace && i === this.selectedElements.length - 1,
);
return this._selectedElementTemplate(option, highlight);
})}
</div>
`;
}
render() {
return html` ${this._selectedElementsTemplate()} `;
}
/**
* @param {{ key: string; }} ev
*/
__textboxOnKeyup(ev) {
if (ev.key === 'Backspace') {
if (!this._inputNode.value) {
if (this.removeChipOnNextBackspace && this.selectedElements.length) {
this.selectedElements[this.selectedElements.length - 1].checked = false;
}
this.removeChipOnNextBackspace = true;
}
} else {
this.removeChipOnNextBackspace = false;
}
}
__restoreBackspace() {
this.removeChipOnNextBackspace = false;
}
}
customElements.define('demo-selection-display', DemoSelectionDisplay);

View file

@ -0,0 +1,23 @@
import { directive } from '@lion/core';
/**
* In order to speed up the first meaningful paint, use this directive
* on content that is:
* - (visually) hidden
* - out of the page flow (having position: 'absolute|fixed')
*
* A good practice would be to use it in overlays,
* For hidden tab panels, collapsible content etc. it's also useful
* @example
* <lion-combobox name="combo" label="Combo">
* ${lazyRender(
* largeListOfData.map(entry => html` <expensive-option>${entry}</expensive-option> `),
* )}
* </lion-combobox>
*/
export const lazyRender = directive(tplResult => part => {
setTimeout(() => {
part.setValue(tplResult);
part.commit();
});
});

View file

@ -0,0 +1,95 @@
/* eslint-disable*/
// https://github.com/gustf/js-levenshtein/blob/master/index.js
function _min(d0, d1, d2, bx, ay) {
return d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1;
}
export default function (a, b) {
if (a === b) {
return 0;
}
if (a.length > b.length) {
var tmp = a;
a = b;
b = tmp;
}
var la = a.length;
var lb = b.length;
while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) {
la--;
lb--;
}
var offset = 0;
while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) {
offset++;
}
la -= offset;
lb -= offset;
if (la === 0 || lb < 3) {
return lb;
}
var x = 0;
var y;
var d0;
var d1;
var d2;
var d3;
var dd;
var dy;
var ay;
var bx0;
var bx1;
var bx2;
var bx3;
var vector = [];
for (y = 0; y < la; y++) {
vector.push(y + 1);
vector.push(a.charCodeAt(offset + y));
}
var len = vector.length - 1;
for (; x < lb - 3; ) {
bx0 = b.charCodeAt(offset + (d0 = x));
bx1 = b.charCodeAt(offset + (d1 = x + 1));
bx2 = b.charCodeAt(offset + (d2 = x + 2));
bx3 = b.charCodeAt(offset + (d3 = x + 3));
dd = x += 4;
for (y = 0; y < len; y += 2) {
dy = vector[y];
ay = vector[y + 1];
d0 = _min(dy, d0, d1, bx0, ay);
d1 = _min(d0, d1, d2, bx1, ay);
d2 = _min(d1, d2, d3, bx2, ay);
dd = _min(d2, d3, dd, bx3, ay);
vector[y] = dd;
d3 = d2;
d2 = d1;
d1 = d0;
d0 = dy;
}
}
for (; x < lb; ) {
bx0 = b.charCodeAt(offset + (d0 = x));
dd = ++x;
for (y = 0; y < len; y += 2) {
dy = vector[y];
vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]);
d0 = dy;
}
}
return dd;
}

View file

@ -246,6 +246,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this._listboxReceivesNoFocus = true;
this.__prevCboxValueNonSelected = '';
this.__prevCboxValue = '';
/** @type {EventListener} */
this.__showOverlay = this.__showOverlay.bind(this);
@ -307,27 +308,27 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this.__shouldAutocompleteNextUpdate
) {
// Only update list in render cycle
this._handleAutocompletion({
curValue: this._inputNode.value,
prevValue: this.__prevCboxValueNonSelected,
});
this._handleAutocompletion();
this.__shouldAutocompleteNextUpdate = false;
}
if (this._selectionDisplayNode) {
if (typeof this._selectionDisplayNode?.onComboboxElementUpdated === 'function') {
this._selectionDisplayNode.onComboboxElementUpdated(changedProperties);
}
}
/**
* When the preconfigurable `match-mode` conditions are not sufficient,
* one can define a custom matching function.
*
* @overridable
* @param {LionOption} option
* @param {string} curValue current ._inputNode value
* @param {string} textboxValue current ._inputNode value
*/
filterOptionCondition(option, curValue) {
matchCondition(option, textboxValue) {
let idx = -1;
if (typeof option.choiceValue === 'string' && typeof curValue === 'string') {
idx = option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase());
if (typeof option.choiceValue === 'string' && typeof textboxValue === 'string') {
idx = option.choiceValue.toLowerCase().indexOf(textboxValue.toLowerCase());
}
if (this.matchMode === 'all') {
@ -353,6 +354,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
if (ev.key === 'Tab') {
this.opened = false;
}
this.__hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
}
/**
@ -424,9 +426,16 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
option.style.display = 'none';
}
/**
*
* @overridable whether a user int
*/
_computeUserIntendsAutoFill({ prevValue, curValue }) {
const userIsAddingChars = prevValue.length < curValue.length;
const userStartsNewWord = prevValue.length && curValue.length && prevValue[0] !== curValue[0];
const userStartsNewWord =
prevValue.length &&
curValue.length &&
prevValue[0].toLowerCase() !== curValue[0].toLowerCase();
return userIsAddingChars || userStartsNewWord;
}
@ -434,15 +443,15 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* Matches visibility of listbox options against current ._inputNode contents
* @param {object} config
* @param {string} config.curValue current ._inputNode value
* @param {string} config.prevValue previous ._inputNode value
*/
_handleAutocompletion({ curValue, prevValue }) {
_handleAutocompletion() {
if (this.autocomplete === 'none') {
return;
}
const curValue = this._inputNode.value;
const prevValue = this.__hasSelection ? this.__prevCboxValueNonSelected : this.__prevCboxValue;
/**
* The filtered list of options that will match in this autocompletion cycle
* @type {LionOption[]}
@ -454,8 +463,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
const show =
this.autocomplete === 'inline' ? true : this.filterOptionCondition(option, curValue);
const show = this.autocomplete === 'inline' ? true : this.matchCondition(option, curValue);
// [1]. Synchronize ._inputNode value and active descendant with closest match
if (isAutoFillCandidate) {
@ -515,8 +523,11 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
option.removeAttribute('aria-hidden');
});
/** @type {number} */
const { selectionStart } = this._inputNode;
this.__prevCboxValueNonSelected = curValue.slice(0, selectionStart);
this.__prevCboxValueNonSelected = curValue;
// See test "computation of "user intends autofill" works correctly afer autofill"
this.__prevCboxValue = this._inputNode.value;
this.__hasSelection = hasAutoFilled;
if (this._overlayCtrl && this._overlayCtrl._popper) {
this._overlayCtrl._popper.update();
@ -600,10 +611,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
__initFilterListbox() {
this._handleAutocompletion({
curValue: this._inputNode.value,
prevValue: this.__prevCboxValueNonSelected,
});
this._handleAutocompletion();
}
__setComboboxDisabledAndReadOnly() {

View file

@ -21,6 +21,45 @@ function mimicUserTyping(el, value) {
el._overlayInvokerNode.dispatchEvent(new Event('keydown'));
}
/**
* @param {LionCombobox} el
* @param {string[]} value
*/
async function mimicUserTypingAdvanced(el, values) {
const inputNode = el._inputNode;
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
let hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
for (const key of values) {
// eslint-disable-next-line no-await-in-loop, no-loop-func
await new Promise(resolve => {
setTimeout(() => {
if (key === 'Backspace') {
if (hasSelection) {
inputNode.value =
inputNode.value.slice(0, inputNode.selectionStart) +
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
} else {
inputNode.value = inputNode.value.slice(0, -1);
}
} else if (hasSelection) {
inputNode.value =
inputNode.value.slice(0, inputNode.selectionStart) +
key +
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
} else {
inputNode.value += key;
}
hasSelection = false;
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key }));
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
resolve();
});
});
}
}
/**
* @param {LionCombobox} el
*/
@ -589,7 +628,7 @@ describe('lion-combobox', () => {
expect(el._inputNode.selectionStart).to.equal('ch'.length);
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
mimicUserTyping(el, 'chic');
await mimicUserTypingAdvanced(el, ['i', 'c']);
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chicory');
expect(el._inputNode.selectionStart).to.equal('chic'.length);
@ -599,8 +638,8 @@ describe('lion-combobox', () => {
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('ch');
expect(el._inputNode.selectionStart).to.equal(2);
expect(el._inputNode.selectionEnd).to.equal(2);
expect(el._inputNode.selectionStart).to.equal('ch'.length);
expect(el._inputNode.selectionEnd).to.equal('ch'.length);
});
it('does autocompletion when adding chars', async () => {
@ -613,20 +652,20 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
mimicUserTyping(el, 'ch');
await el.updateComplete;
mimicUserTyping(el, 'ch'); // ch
await el.updateComplete; // Ch[ard]
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(1);
mimicUserTyping(el, 'chic');
await el.updateComplete;
await mimicUserTypingAdvanced(el, ['i', 'c']); // Chic
await el.updateComplete; // Chic[ory]
expect(el.activeIndex).to.equal(2);
expect(el.checkedIndex).to.equal(2);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el.activeIndex).to.equal(2);
expect(el.checkedIndex).to.equal(-1);
await mimicUserTypingAdvanced(el, ['Backspace', 'Backspace', 'Backspace', 'Backspace', 'h']); // Ch
await el.updateComplete; // Ch[ard]
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(1);
});
it('does autocompletion when changing the word', async () => {
@ -644,7 +683,7 @@ describe('lion-combobox', () => {
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(1);
mimicUserTyping(el, 'chic');
await mimicUserTypingAdvanced(el, 'ic'.split(''));
await el.updateComplete;
expect(el.activeIndex).to.equal(2);
expect(el.checkedIndex).to.equal(2);
@ -656,6 +695,31 @@ describe('lion-combobox', () => {
expect(el.checkedIndex).to.equal(0);
});
it('computation of "user intends autofill" works correctly afer autofill', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="inline">
<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>
`));
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chard');
expect(el._inputNode.selectionStart).to.equal('ch'.length);
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
// Autocompletion happened. When we go backwards ('Char'), we should not
// autocomplete to 'Chard' anymore.
mimicUserTyping(el, 'Char');
await el.updateComplete;
expect(el._inputNode.value).to.equal('Char'); // so not 'Chard'
expect(el._inputNode.selectionStart).to.equal('Char'.length);
expect(el._inputNode.selectionEnd).to.equal('Char'.length);
});
it('highlights matching options', async () => {
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" match-mode="all">
@ -1008,7 +1072,7 @@ describe('lion-combobox', () => {
function onlyExactMatches(option, curValue) {
return option.value === curValue;
}
el.filterOptionCondition = onlyExactMatches;
el.matchCondition = onlyExactMatches;
mimicUserTyping(/** @type {LionCombobox} */ (el), 'Chicory');
await el.updateComplete;
expect(getFilteredOptionValues(/** @type {LionCombobox} */ (el))).to.eql(['Chicory']);