fix(combobox): make the first occurrence of a string highlighted, instead of the last
This commit is contained in:
parent
974e9ea4ce
commit
1c18057cee
7 changed files with 382 additions and 81 deletions
5
.changeset/fast-taxis-kneel.md
Normal file
5
.changeset/fast-taxis-kneel.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[combobox] make the first occurrence of a string highlighted, instead of the last.
|
||||||
|
|
@ -14,7 +14,7 @@ availability of the popup.
|
||||||
|
|
||||||
```js script
|
```js script
|
||||||
import { LitElement, html, repeat } from '@mdjs/mdjs-preview';
|
import { LitElement, html, repeat } from '@mdjs/mdjs-preview';
|
||||||
import { listboxData } from '../listbox/src/listboxData.js';
|
import { listboxData, listboxComplexData } from '../listbox/src/listboxData.js';
|
||||||
import { LionCombobox } from '@lion/ui/combobox.js';
|
import { LionCombobox } from '@lion/ui/combobox.js';
|
||||||
import '@lion/ui/define/lion-combobox.js';
|
import '@lion/ui/define/lion-combobox.js';
|
||||||
import '@lion/ui/define/lion-option.js';
|
import '@lion/ui/define/lion-option.js';
|
||||||
|
|
@ -328,20 +328,45 @@ 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
|
## Complex options
|
||||||
|
|
||||||
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.
|
For performance reasons a complex object in the choiceValue property is unwanted. But it is possible to create more complex options.
|
||||||
|
|
||||||
|
To highlight the correct elements of the option, each element should be tagged with a `data-key` attribute. Which will be used in the `_onFilterMatch` and `_onFilterUnmatch` functions.
|
||||||
|
|
||||||
```js preview-story
|
```js preview-story
|
||||||
class ComplexObjectCombobox extends LionCombobox {
|
class ComplexObjectCombobox extends LionCombobox {
|
||||||
/**
|
/**
|
||||||
* Override how the input text is filled
|
* @overridable
|
||||||
* @param {LionOption} option
|
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||||
* @returns {string}
|
* @param {string} matchingString
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
_onFilterMatch(option, matchingString) {
|
||||||
_getTextboxValueFromOption(option) {
|
Array.from(option.children).forEach(child => {
|
||||||
return option.label;
|
if (child.hasAttribute('data-key')) {
|
||||||
|
this._highlightMatchedOption(child, matchingString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Alternatively, an extension can add an animation here
|
||||||
|
option.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||||
|
* @param {string} [curValue]
|
||||||
|
* @param {string} [prevValue]
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_onFilterUnmatch(option, curValue, prevValue) {
|
||||||
|
Array.from(option.children).forEach(child => {
|
||||||
|
if (child.hasAttribute('data-key')) {
|
||||||
|
this._unhighlightMatchedOption(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Alternatively, an extension can add an animation here
|
||||||
|
option.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,20 +376,19 @@ const onModelValueChanged = event => {
|
||||||
console.log(`event.target.modelValue: ${JSON.stringify(event.target.modelValue)}`);
|
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
|
export const complexObjectChoiceValue = () => html` <complex-object-combobox
|
||||||
name="combo"
|
name="combo"
|
||||||
label="Display only the label once selected"
|
label="Display only the label once selected"
|
||||||
@model-value-changed="${onModelValueChanged}"
|
@model-value-changed="${onModelValueChanged}"
|
||||||
>
|
>
|
||||||
${lazyRender(
|
${lazyRender(
|
||||||
listboxDataObject.map(
|
listboxComplexData.map(
|
||||||
entry =>
|
entry =>
|
||||||
html`
|
html`
|
||||||
<lion-option .choiceValue="${entry}" .label="${entry.label}">${entry.label}</lion-option>
|
<lion-option .choiceValue="${entry.label}">
|
||||||
|
<div data-key>${entry.label}</div>
|
||||||
|
<small>${entry.description}</small>
|
||||||
|
</lion-option>
|
||||||
`,
|
`,
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,69 @@ export const listboxData = [
|
||||||
'Yam',
|
'Yam',
|
||||||
'Zucchini',
|
'Zucchini',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const listboxComplexData = [
|
||||||
|
{ label: 'Apple', description: 'Rosaceae' },
|
||||||
|
{ label: 'Artichoke', description: 'Cardoon' },
|
||||||
|
{ label: 'Asparagus', description: 'Liliopsida' },
|
||||||
|
{ label: 'Banana', description: 'Bananas' },
|
||||||
|
{ label: 'Beets', description: 'Beet' },
|
||||||
|
{ label: 'Bell pepper', description: 'Solanaceae' },
|
||||||
|
{ label: 'Broccoli', description: 'Wild cabbage' },
|
||||||
|
{ label: 'Brussels sprout', description: 'Wild cabbage' },
|
||||||
|
{ label: 'Cabbage', description: 'Wild cabbage' },
|
||||||
|
{ label: 'Carrot', description: 'Apiales' },
|
||||||
|
{ label: 'Cauliflower', description: 'Wild cabbage' },
|
||||||
|
{ label: 'Celery', description: 'Apium' },
|
||||||
|
{ label: 'Chard', description: 'Beet' },
|
||||||
|
{ label: 'Chicory', description: 'Chicory' },
|
||||||
|
{ label: 'Corn', description: 'Corn' },
|
||||||
|
{ label: 'Cucumber', description: 'Cucumis' },
|
||||||
|
{ label: 'Daikon', description: 'Radishes' },
|
||||||
|
{ label: 'Date', description: 'Arecaceae' },
|
||||||
|
{ label: 'Edamame', description: 'Wild bean' },
|
||||||
|
{ label: 'Eggplant', description: 'Nightshade' },
|
||||||
|
{ label: 'Elderberry', description: 'Moschatel' },
|
||||||
|
{ label: 'Fennel', description: 'Fennels' },
|
||||||
|
{ label: 'Fig', description: 'Fig trees' },
|
||||||
|
{ label: 'Garlic', description: 'Allium' },
|
||||||
|
{ label: 'Grape', description: 'Vitis vinifera' },
|
||||||
|
{ label: 'Honeydew melon', description: 'Citrullus' },
|
||||||
|
{ label: 'Iceberg lettuce', description: 'Lactuca' },
|
||||||
|
{ label: 'Jerusalem artichoke', description: 'Sunflowers' },
|
||||||
|
{ label: 'Kale', description: 'Brassicaceae' },
|
||||||
|
{ label: 'Kiwi', description: 'Ratites' },
|
||||||
|
{ label: 'Leek', description: 'Onion' },
|
||||||
|
{ label: 'Lemon', description: 'Citrus' },
|
||||||
|
{ label: 'Mango', description: 'Mangifera' },
|
||||||
|
{ label: 'Mangosteen', description: 'Saptrees' },
|
||||||
|
{ label: 'Melon', description: 'Citrullus' },
|
||||||
|
{ label: 'Mushroom', description: 'Eumycota' },
|
||||||
|
{ label: 'Nectarine', description: 'Citrus' },
|
||||||
|
{ label: 'Okra', description: 'Mallows' },
|
||||||
|
{ label: 'Olive', description: 'Olives' },
|
||||||
|
{ label: 'Onion', description: 'Onion' },
|
||||||
|
{ label: 'Orange', description: 'Citrus' },
|
||||||
|
{ label: 'Parship', description: 'Umbellifers' },
|
||||||
|
{ label: 'Pea', description: 'Peas' },
|
||||||
|
{ label: 'Pear', description: 'Malinae' },
|
||||||
|
{ label: 'Pineapple', description: 'Pineapples' },
|
||||||
|
{ label: 'Potato', description: 'Nightshade' },
|
||||||
|
{ label: 'Pumpkin', description: 'Cucurbitaceae' },
|
||||||
|
{ label: 'Quince', description: 'Cydonia' },
|
||||||
|
{ label: 'Radish', description: 'Radishes' },
|
||||||
|
{ label: 'Rhubarb', description: 'Rhubarb' },
|
||||||
|
{ label: 'Shallot', description: 'Onion' },
|
||||||
|
{ label: 'Spinach', description: 'Spinacia' },
|
||||||
|
{ label: 'Squash', description: 'Cucurbiteae' },
|
||||||
|
{ label: 'Strawberry', description: 'Fragaria' },
|
||||||
|
{ label: 'Sweet potato', description: 'Ipomoea' },
|
||||||
|
{ label: 'Tomato', description: 'Nightshade' },
|
||||||
|
{ label: 'Turnip', description: 'Field mustard' },
|
||||||
|
{ label: 'Ugli fruit', description: 'Citrus reticulata' },
|
||||||
|
{ label: 'Victoria plum', description: 'Prunus domestica' },
|
||||||
|
{ label: 'Watercress', description: 'Nasturtium' },
|
||||||
|
{ label: 'Watermelon', description: 'Citrullus' },
|
||||||
|
{ label: 'Yam', description: 'Dioscorea' },
|
||||||
|
{ label: 'Zucchini', description: 'Cucurbitaceae' },
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,11 @@
|
||||||
import { html, css } from 'lit';
|
|
||||||
import { browserDetection } from '@lion/ui/core.js';
|
import { browserDetection } from '@lion/ui/core.js';
|
||||||
import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
|
|
||||||
import { LionListbox } from '@lion/ui/listbox.js';
|
import { LionListbox } from '@lion/ui/listbox.js';
|
||||||
|
import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
|
||||||
|
import { css, html } from 'lit';
|
||||||
|
import { makeMatchingTextBold, unmakeMatchingTextBold } from './utils/makeMatchingTextBold.js';
|
||||||
|
|
||||||
const matchReverseFns = new WeakMap();
|
|
||||||
const matchA11ySpanReverseFns = new WeakMap();
|
const matchA11ySpanReverseFns = new WeakMap();
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Node} root
|
|
||||||
* @param {string} matchingString
|
|
||||||
* @param {Node} option
|
|
||||||
*/
|
|
||||||
function makeMatchingTextBold(root, matchingString, option) {
|
|
||||||
Array.from(root.childNodes).forEach(childNode => {
|
|
||||||
if (childNode.nodeName === '#text') {
|
|
||||||
// check for match based on nodeValue
|
|
||||||
|
|
||||||
const re = new RegExp(`^(.*)(${matchingString})(.*)$`, 'i');
|
|
||||||
// @ts-ignore
|
|
||||||
const match = childNode.nodeValue.match(re);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
// 1. textContent before match
|
|
||||||
|
|
||||||
const textBefore = document.createTextNode(match[1]);
|
|
||||||
root.appendChild(textBefore);
|
|
||||||
|
|
||||||
// 2. matched part
|
|
||||||
const boldElement = document.createElement('b');
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
boldElement.textContent = match[2];
|
|
||||||
root.appendChild(boldElement);
|
|
||||||
|
|
||||||
// 3. textContent after match
|
|
||||||
|
|
||||||
const textAfter = document.createTextNode(match[3]);
|
|
||||||
root.appendChild(textAfter);
|
|
||||||
root.removeChild(childNode);
|
|
||||||
|
|
||||||
matchReverseFns.set(option, () => {
|
|
||||||
root.appendChild(childNode);
|
|
||||||
if (root.contains(textBefore) && textBefore.parentNode !== null) {
|
|
||||||
textBefore.parentNode.removeChild(textBefore);
|
|
||||||
}
|
|
||||||
if (root.contains(boldElement) && boldElement.parentNode !== null) {
|
|
||||||
boldElement.parentNode.removeChild(boldElement);
|
|
||||||
}
|
|
||||||
if (root.contains(textAfter) && textAfter.parentNode !== null) {
|
|
||||||
textAfter.parentNode.removeChild(textAfter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
makeMatchingTextBold(childNode, matchingString, option);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make ListboxOverlayMixin that is shared between SelectRich and Combobox
|
// TODO: make ListboxOverlayMixin that is shared between SelectRich and Combobox
|
||||||
// TODO: extract option matching based on 'typed character cache' and share that logic
|
// TODO: extract option matching based on 'typed character cache' and share that logic
|
||||||
// on Listbox or ListNavigationWithActiveDescendantMixin
|
// on Listbox or ListNavigationWithActiveDescendantMixin
|
||||||
|
|
@ -304,7 +253,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @type {'begin'|'all'}
|
* @type {'begin'|'all'}
|
||||||
*/
|
*/
|
||||||
this.matchMode = 'all';
|
this.matchMode = 'all';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When true, the listbox is open and textbox goes from a value to empty, all options are shown.
|
* When true, the listbox is open and textbox goes from a value to empty, all options are shown.
|
||||||
* By default, the listbox closes on empty, similar to wai-aria example and <datalist>
|
* By default, the listbox closes on empty, similar to wai-aria example and <datalist>
|
||||||
|
|
@ -642,14 +590,27 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @param {string} matchingString
|
* @param {string} matchingString
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_onFilterMatch(option, matchingString) {
|
_onFilterMatch(option, matchingString) {
|
||||||
makeMatchingTextBold(option, matchingString, option);
|
this._highlightMatchedOption(option, matchingString);
|
||||||
|
|
||||||
|
// Alternatively, an extension can add an animation here
|
||||||
|
option.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {Element} option
|
||||||
|
* @param {string} matchingString
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_highlightMatchedOption(option, matchingString) {
|
||||||
|
makeMatchingTextBold(option, matchingString);
|
||||||
|
|
||||||
// For Safari, we need to add a label to the element
|
// For Safari, we need to add a label to the element
|
||||||
if (option.textContent) {
|
if (option.textContent) {
|
||||||
const a11ySpan = document.createElement('span');
|
const a11ySpan = document.createElement('span');
|
||||||
a11ySpan.setAttribute('aria-label', option.textContent);
|
a11ySpan.setAttribute('aria-label', option.textContent.replace(/\s+/g, ' '));
|
||||||
Array.from(option.childNodes).forEach(childNode => {
|
Array.from(option.childNodes).forEach(childNode => {
|
||||||
a11ySpan.appendChild(childNode);
|
a11ySpan.appendChild(childNode);
|
||||||
});
|
});
|
||||||
|
|
@ -664,8 +625,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Alternatively, an extension can add an animation here
|
|
||||||
option.style.display = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -677,14 +636,24 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||||
_onFilterUnmatch(option, curValue, prevValue) {
|
_onFilterUnmatch(option, curValue, prevValue) {
|
||||||
if (matchReverseFns.has(option)) {
|
this._unhighlightMatchedOption(option);
|
||||||
matchReverseFns.get(option)();
|
|
||||||
}
|
// Alternatively, an extension can add an animation here
|
||||||
|
option.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {Element} option
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_unhighlightMatchedOption(option) {
|
||||||
|
unmakeMatchingTextBold(option);
|
||||||
|
|
||||||
if (matchA11ySpanReverseFns.has(option)) {
|
if (matchA11ySpanReverseFns.has(option)) {
|
||||||
matchA11ySpanReverseFns.get(option)();
|
matchA11ySpanReverseFns.get(option)();
|
||||||
}
|
}
|
||||||
// Alternatively, an extension can add an animation here
|
|
||||||
option.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
/* eslint-enable no-param-reassign */
|
/* eslint-enable no-param-reassign */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
const matchReverseFns = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} root
|
||||||
|
* @param {string} matchingString
|
||||||
|
*/
|
||||||
|
export function makeMatchingTextBold(root, matchingString) {
|
||||||
|
Array.from(root.childNodes).forEach(childNode => {
|
||||||
|
if (childNode.nodeName === '#text') {
|
||||||
|
// check for match based on nodeValue
|
||||||
|
|
||||||
|
const re = new RegExp(`^(.*?)(${matchingString})(.*)$`, 'i');
|
||||||
|
// @ts-ignore
|
||||||
|
const match = childNode.nodeValue.match(re);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// 1. textContent before match
|
||||||
|
const textBefore = document.createTextNode(match[1]);
|
||||||
|
root.appendChild(textBefore);
|
||||||
|
|
||||||
|
// 2. matched part
|
||||||
|
const boldElement = document.createElement('b');
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
boldElement.textContent = match[2];
|
||||||
|
root.appendChild(boldElement);
|
||||||
|
|
||||||
|
// 3. textContent after match
|
||||||
|
const textAfter = document.createTextNode(match[3]);
|
||||||
|
root.appendChild(textAfter);
|
||||||
|
|
||||||
|
root.removeChild(childNode);
|
||||||
|
|
||||||
|
matchReverseFns.set(root, () => {
|
||||||
|
root.appendChild(childNode);
|
||||||
|
if (root.contains(textBefore) && textBefore.parentNode !== null) {
|
||||||
|
textBefore.parentNode.removeChild(textBefore);
|
||||||
|
}
|
||||||
|
if (root.contains(boldElement) && boldElement.parentNode !== null) {
|
||||||
|
boldElement.parentNode.removeChild(boldElement);
|
||||||
|
}
|
||||||
|
if (root.contains(textAfter) && textAfter.parentNode !== null) {
|
||||||
|
textAfter.parentNode.removeChild(textAfter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
makeMatchingTextBold(childNode, matchingString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} root
|
||||||
|
*/
|
||||||
|
export function unmakeMatchingTextBold(root) {
|
||||||
|
if (matchReverseFns.has(root)) {
|
||||||
|
matchReverseFns.get(root)();
|
||||||
|
}
|
||||||
|
Array.from(root.childNodes).forEach(childNode => {
|
||||||
|
if (childNode.nodeName === '#text') {
|
||||||
|
if (matchReverseFns.has(childNode)) {
|
||||||
|
matchReverseFns.get(childNode)();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unmakeMatchingTextBold(childNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import { LionCombobox } from '@lion/ui/combobox.js';
|
||||||
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
|
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
|
||||||
* @typedef {import('../../listbox/types/ListboxMixinTypes.js').ListboxHost} ListboxHost
|
* @typedef {import('../../listbox/types/ListboxMixinTypes.js').ListboxHost} ListboxHost
|
||||||
* @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
* @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
||||||
|
* @typedef {import('@lion/ui/listbox.js').LionOption} LionOption
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1632,7 +1633,7 @@ describe('lion-combobox', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('highlights matching options', async () => {
|
it('highlights first occcurence of matching option', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<lion-combobox name="foo" match-mode="all">
|
<lion-combobox name="foo" match-mode="all">
|
||||||
|
|
@ -1645,6 +1646,16 @@ describe('lion-combobox', () => {
|
||||||
);
|
);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'c');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(`<span aria-label="Artichoke">Arti<b>c</b>hoke</span>`);
|
||||||
|
expect(options[1]).lightDom.to.equal(`<span aria-label="Chard"><b>C</b>hard</span>`);
|
||||||
|
expect(options[2]).lightDom.to.equal(`<span aria-label="Chicory"><b>C</b>hicory</span>`);
|
||||||
|
expect(options[3]).lightDom.to.equal(
|
||||||
|
`<span aria-label="Victoria Plum">Vi<b>c</b>toria Plum</span>`,
|
||||||
|
);
|
||||||
|
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -1662,6 +1673,48 @@ describe('lion-combobox', () => {
|
||||||
expect(options[3]).lightDom.to.equal(`Victoria Plum`);
|
expect(options[3]).lightDom.to.equal(`Victoria Plum`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('highlights matching complex options', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" match-mode="all">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">
|
||||||
|
<div>Artichoke</div>
|
||||||
|
<small>Cardoon</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">
|
||||||
|
<div>Chard</div>
|
||||||
|
<small>Beet</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">
|
||||||
|
<div>Chicory</div>
|
||||||
|
<small>Chicory</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">
|
||||||
|
<div>Victoria Plum</div>
|
||||||
|
<small>Prunus domestica</small>
|
||||||
|
</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(
|
||||||
|
`<span aria-label=" Artichoke Cardoon "><div>Arti<b>ch</b>oke</div><small>Cardoon</small>`,
|
||||||
|
);
|
||||||
|
expect(options[1]).lightDom.to.equal(
|
||||||
|
`<span aria-label=" Chard Beet "><div><b>Ch</b>ard</div><small>Beet</small>`,
|
||||||
|
);
|
||||||
|
expect(options[2]).lightDom.to.equal(
|
||||||
|
`<span aria-label=" Chicory Chicory "><div><b>Ch</b>icory</div><small><b>Ch</b>icory</small>`,
|
||||||
|
);
|
||||||
|
expect(options[3]).lightDom.to.equal(
|
||||||
|
`<div>Victoria Plum</div><small>Prunus domestica</small>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
|
it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
|
||||||
const el = /** @type {LionCombobox} */ (
|
const el = /** @type {LionCombobox} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
|
|
@ -1999,6 +2052,98 @@ describe('lion-combobox', () => {
|
||||||
await performChecks();
|
await performChecks();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows to override "_onFilterMatch" and "_onFilterUmatch"', async () => {
|
||||||
|
class X extends LionCombobox {
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||||
|
* @param {string} matchingString
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_onFilterMatch(option, matchingString) {
|
||||||
|
Array.from(option.children).forEach(child => {
|
||||||
|
if (child.hasAttribute('data-key')) {
|
||||||
|
this._highlightMatchedOption(child, matchingString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Alternatively, an extension can add an animation here
|
||||||
|
// option.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||||
|
* @param {string} [curValue]
|
||||||
|
* @param {string} [prevValue]
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_onFilterUnmatch(option, curValue, prevValue) {
|
||||||
|
Array.from(option.children).forEach(child => {
|
||||||
|
if (child.hasAttribute('data-key')) {
|
||||||
|
this._unhighlightMatchedOption(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Alternatively, an extension can add an animation here
|
||||||
|
// option.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tagName = defineCE(X);
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag} name="foo" autocomplete="both">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">
|
||||||
|
<div data-key>Artichoke</div>
|
||||||
|
<small>Cardoon</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chard'}">
|
||||||
|
<div data-key>Chard</div>
|
||||||
|
<small>Beet</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Chicory'}">
|
||||||
|
<div data-key>Chicory</div>
|
||||||
|
<small>Chicory</small>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .choiceValue="${'Victoria Plum'}">
|
||||||
|
<div data-key>Victoria Plum</div>
|
||||||
|
<small>Prunus domestica</small>
|
||||||
|
</lion-option>
|
||||||
|
</${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(
|
||||||
|
`<div data-key><span aria-label="Artichoke">Arti<b>ch</b>oke</span></div><small>Cardoon</small>`,
|
||||||
|
);
|
||||||
|
expect(options[1]).lightDom.to.equal(
|
||||||
|
`<div data-key><span aria-label="Chard"><b>Ch</b>ard</div></span><small>Beet</small>`,
|
||||||
|
);
|
||||||
|
expect(options[2]).lightDom.to.equal(
|
||||||
|
`<div data-key><span aria-label="Chicory"><b>Ch</b>icory</span></div><small>Chicory</small>`,
|
||||||
|
);
|
||||||
|
expect(options[3]).lightDom.to.equal(
|
||||||
|
`<div data-key>Victoria Plum</div><small>Prunus domestica</small>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'D');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0]).lightDom.to.equal(`<div data-key>Artichoke</div><small>Cardoon</small>`);
|
||||||
|
expect(options[1]).lightDom.to.equal(
|
||||||
|
`<div data-key><span aria-label="Chard">Char<b>d</b></div></span><small>Beet</small>`,
|
||||||
|
);
|
||||||
|
expect(options[2]).lightDom.to.equal(`<div data-key>Chicory</div><small>Chicory</small>`);
|
||||||
|
expect(options[3]).lightDom.to.equal(
|
||||||
|
`<div data-key>Victoria Plum</div><small>Prunus domestica</small>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Active index behavior', () => {
|
describe('Active index behavior', () => {
|
||||||
|
|
@ -2278,6 +2423,26 @@ describe('lion-combobox', () => {
|
||||||
expect(labelledElement).to.not.be.null;
|
expect(labelledElement).to.not.be.null;
|
||||||
expect(labelledElement.innerText).to.equal('Artichoke');
|
expect(labelledElement.innerText).to.equal('Artichoke');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds aria-label to highlighted complex options', async () => {
|
||||||
|
const el = /** @type {LionCombobox} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-combobox name="foo" match-mode="all">
|
||||||
|
<lion-option .choiceValue="${'Artichoke'}">
|
||||||
|
<div>Artichoke</div>
|
||||||
|
<small>Cardoon</small>
|
||||||
|
</lion-option>
|
||||||
|
</lion-combobox>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const options = el.formElements;
|
||||||
|
|
||||||
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'choke');
|
||||||
|
await el.updateComplete;
|
||||||
|
const labelledElement = options[0].querySelector('span[aria-label=" Artichoke Cardoon "]');
|
||||||
|
expect(labelledElement).to.not.be.null;
|
||||||
|
expect(labelledElement.innerText).to.equal('Artichoke\nCardoon');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
export { LionCombobox } from '../components/combobox/src/LionCombobox.js';
|
export { LionCombobox } from '../components/combobox/src/LionCombobox.js';
|
||||||
|
export {
|
||||||
|
makeMatchingTextBold,
|
||||||
|
unmakeMatchingTextBold,
|
||||||
|
} from '../components/combobox/src/utils/makeMatchingTextBold.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue