diff --git a/.changeset/soft-eagles-reply.md b/.changeset/soft-eagles-reply.md
new file mode 100644
index 000000000..fe9115ea5
--- /dev/null
+++ b/.changeset/soft-eagles-reply.md
@@ -0,0 +1,9 @@
+---
+'@lion/ui': patch
+---
+
+[combobox] Multiple improvements:
+
+- Allow textbox values to be entered that do not match a listbox option, via `requireOptionMatch` flag.
+- Added an `MatchesOption` validator to check if the value is matching an option.
+- Exports combobox test helpers
diff --git a/docs/components/combobox/overview.md b/docs/components/combobox/overview.md
index 957b21646..51f6fd4b3 100644
--- a/docs/components/combobox/overview.md
+++ b/docs/components/combobox/overview.md
@@ -37,7 +37,17 @@ export const main = () => html`
## Features
-> tbd
+The combobox has many configurable properties to fine-tune its behaviour:
+
+- **Multiple choice** - Allows multiselection of options.
+- **requireOptionMatch**
+ - **true** (default) - The listbox is leading, the textbox is a helping aid to quickly select an option/options. Unmatching input values become Unparseable, with the `MatchesOption` set as a default validator.
+ - **false** - The textbox is leading, with the listbox as an aid to supply suggestions, e.g. a search input.
+- **Autocomplete** - When the autocompletion will happen: `none`, `list`, `inline` and `both`.
+- **Matchmode** - Which part of the value should match: `begin` and `all`.
+- **Show all on empty** - Shows the options list on empty.
+- **Selection follows focus** - When false the active/focused and checked/selected values will be kept track of independently.
+- **Rotate keyboard Navigation** - When false it won't rotate the navigation.
## Installation
diff --git a/docs/components/combobox/use-cases.md b/docs/components/combobox/use-cases.md
index 9dbc6a548..abda92a4a 100644
--- a/docs/components/combobox/use-cases.md
+++ b/docs/components/combobox/use-cases.md
@@ -16,11 +16,30 @@ availability of the popup.
import { LitElement, html, repeat } from '@mdjs/mdjs-preview';
import { listboxData, listboxComplexData } from '../listbox/src/listboxData.js';
import { LionCombobox } from '@lion/ui/combobox.js';
+import { Required } from '@lion/ui/form-core.js';
import '@lion/ui/define/lion-combobox.js';
import '@lion/ui/define/lion-option.js';
import './src/demo-selection-display.js';
import { lazyRender } from './src/lazyRender.js';
import levenshtein from './src/levenshtein.js';
+import { loadDefaultFeedbackMessages } from '@lion/ui/validate-messages.js';
+loadDefaultFeedbackMessages();
+```
+
+## Require option match
+
+By default `requireOptionMatch` is set to true, which means that the listbox is leading. The textbox is a helping aid to quickly select an option/options. Unmatching input values become Unparseable, with the `MatchesOption` set as a default validator.
+
+When `requireOptionMatch` is set to false the textbox is leading, with the listbox as an aid to supply suggestions, e.g. a search input. This means that all input values are allowed.
+
+```js preview-story
+export const optionMatch = () => html`
+
+ ${lazyRender(
+ listboxData.map(entry => html` ${entry} `),
+ )}
+
+`;
```
## Autocomplete
@@ -230,6 +249,22 @@ export const multipleChoice = () => html`
`;
```
+## Validation
+
+The combobox works with a `Required` validator to check if it is empty.
+
+By default the a check is made which makes sure the value matches an option. This only works if `requireOptionMatch` is set to true.
+
+```js preview-story
+export const validation = () => html`
+
+ ${lazyRender(
+ listboxData.map(entry => html` ${entry} `),
+ )}
+
+`;
+```
+
## Invoker button
```js preview-story
diff --git a/packages/ui/components/combobox/src/LionCombobox.js b/packages/ui/components/combobox/src/LionCombobox.js
index c79b2045d..80412f46e 100644
--- a/packages/ui/components/combobox/src/LionCombobox.js
+++ b/packages/ui/components/combobox/src/LionCombobox.js
@@ -1,9 +1,11 @@
import { browserDetection } from '@lion/ui/core.js';
+import { Unparseable } from '@lion/ui/form-core.js';
import { LionListbox } from '@lion/ui/listbox.js';
import { LocalizeMixin } from '@lion/ui/localize-no-side-effects.js';
import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
import { css, html } from 'lit';
import { makeMatchingTextBold, unmakeMatchingTextBold } from './utils/makeMatchingTextBold.js';
+import { MatchesOption } from './validators.js';
const matchA11ySpanReverseFns = new WeakMap();
@@ -12,12 +14,12 @@ const matchA11ySpanReverseFns = new WeakMap();
// on Listbox or ListNavigationWithActiveDescendantMixin
/**
- * @typedef {import('../../listbox/src/LionOption.js').LionOption} LionOption
- * @typedef {import('../../listbox/src/LionOptions.js').LionOptions} LionOptions
- * @typedef {import('../../overlays/types/OverlayConfig.js').OverlayConfig} OverlayConfig
- * @typedef {import('../../core/types/SlotMixinTypes.js').SlotsMap} SlotsMap
- * @typedef {import('../../form-core/types/choice-group/ChoiceInputMixinTypes.js').ChoiceInputHost} ChoiceInputHost
- * @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
+ * @typedef {import('@lion/ui/listbox.js').LionOption} LionOption
+ * @typedef {import('@lion/ui/listbox.js').LionOptions} LionOptions
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
+ * @typedef {import('@lion/ui/types/core.js').SlotsMap} SlotsMap
+ * @typedef {import('@lion/ui/types/form-core.js').ChoiceInputHost} ChoiceInputHost
+ * @typedef {import('@lion/ui/types/form-core.js').FormControlHost} FormControlHost
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
*/
@@ -38,6 +40,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
type: Boolean,
attribute: 'show-all-on-empty',
},
+ requireOptionMatch: {
+ type: Boolean,
+ },
__shouldAutocompleteNextUpdate: Boolean,
};
}
@@ -141,6 +146,60 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
];
}
+ /**
+ * @override ChoiceGroupMixin
+ */
+ // @ts-ignore
+ get modelValue() {
+ const choiceGroupModelValue = super.modelValue;
+ if (choiceGroupModelValue !== '') {
+ return choiceGroupModelValue;
+ }
+ // Since the FormatMixin can't be applied to a [FormGroup](https://github.com/ing-bank/lion/blob/master/packages/ui/components/form-core/src/form-group/FormGroupMixin.js)
+ // atm, we treat it in a way analogue to InteractionStateMixin (basically same apis, w/o Mixin applied).
+ // Hence, modelValue has no reactivity by default and we need to call parser manually here...
+ return this.parser(this.value);
+ }
+
+ // Duplicating from ChoiceGroupMixin, because you cannot independently inherit/override getter + setter.
+ // If you override one, gotta override the other, they go in pairs.
+ /**
+ * @override ChoiceGroupMixin
+ */
+ set modelValue(value) {
+ super.modelValue = value;
+ }
+
+ /**
+ * We define the value getter/setter below as also defined in LionField (via FormatMixin).
+ * Since FormatMixin is meant for Formgroups/ChoiceGroup it's not applied on Combobox;
+ * Combobox is somewhat of a hybrid between a ChoiceGroup and LionField, therefore we copy over
+ * some of the LionField members to align with its interface.
+ *
+ * The view value. Will be delegated to `._inputNode.value`
+ */
+ get value() {
+ return this._inputNode?.value || this.__value || '';
+ }
+
+ /** @param {string} value */
+ set value(value) {
+ // if not yet connected to dom can't change the value
+ if (this._inputNode) {
+ this._inputNode.value = value;
+ /** @type {string | undefined} */
+ this.__value = undefined;
+ } else {
+ this.__value = value;
+ }
+ }
+
+ reset() {
+ super.reset();
+ // @ts-ignore _initialModelValue comes from ListboxMixin
+ this.value = this._initialModelValue;
+ }
+
/**
* @enhance FormControlMixin - add slot[name=selection-display]
* @protected
@@ -256,7 +315,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
* @protected
*/
get _inputNode() {
- if (this._ariaVersion === '1.1') {
+ if (this._ariaVersion === '1.1' && this._comboboxNode) {
return /** @type {HTMLInputElement} */ (this._comboboxNode.querySelector('input'));
}
return /** @type {HTMLInputElement} */ (this._comboboxNode);
@@ -327,7 +386,11 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
* By default, the listbox closes on empty, similar to wai-aria example and
*/
this.showAllOnEmpty = false;
-
+ /**
+ * If set to false, the value is allowed to not match any of the options.
+ * We set the default to true for backwards compatibility
+ */
+ this.requireOptionMatch = true;
/**
* @configure ListboxMixin: the wai-aria pattern and rotate
*/
@@ -336,7 +399,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
* @configure ListboxMixin: the wai-aria pattern and have selection follow focus
*/
this.selectionFollowsFocus = true;
-
+ this.defaultValidators.push(new MatchesOption());
/**
* For optimal support, we allow aria v1.1 on newer browsers
* @type {'1.1'|'1.0'}
@@ -422,6 +485,18 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
}
}
+ /**
+ * Converts viewValue to modelValue
+ * @param {string} value - viewValue: the formatted value inside
+ * @returns {*} modelValue
+ */
+ parser(value) {
+ if (this.requireOptionMatch && this.checkedIndex === -1 && value !== '') {
+ return new Unparseable(value);
+ }
+ return value;
+ }
+
/**
* When textbox value doesn't match checkedIndex anymore, update accordingly...
* @protected
@@ -432,7 +507,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
if (!this.multipleChoice && !autoselect && checkedElement) {
const textboxValue = this._getTextboxValueFromOption(checkedElement);
if (!this._inputNode.value.startsWith(textboxValue)) {
- this.checkedIndex = -1;
+ this.setCheckedIndex(-1);
}
}
}
@@ -480,6 +555,15 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
if (typeof this._selectionDisplayNode?.onComboboxElementUpdated === 'function') {
this._selectionDisplayNode.onComboboxElementUpdated(changedProperties);
}
+
+ if (changedProperties.has('requireOptionMatch') || changedProperties.has('multipleChoice')) {
+ if (!this.requireOptionMatch && this.multipleChoice) {
+ // TODO implement !requireOptionMatch and multipleChoice flow
+ throw new Error(
+ "multipleChoice and requireOptionMatch=false can't be used at the same time (yet).",
+ );
+ }
+ }
}
/**
@@ -557,7 +641,13 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
*/
// eslint-disable-next-line class-methods-use-this
_getTextboxValueFromOption(option) {
- return option.choiceValue;
+ if (option) {
+ return option.choiceValue;
+ }
+ if (this.modelValue instanceof Unparseable) {
+ return this.modelValue.viewValue;
+ }
+ return this.modelValue;
}
/**
@@ -814,7 +904,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
if (isInlineAutoFillCandidate) {
const textboxValue = this._getTextboxValueFromOption(option);
- const stringValues = typeof textboxValue === 'string' && typeof curValue === 'string';
+ const stringValues =
+ textboxValue && typeof textboxValue === 'string' && typeof curValue === 'string';
const beginsWith =
stringValues && textboxValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
// We only can do proper inline autofilling when the beginning of the word matches
@@ -862,7 +953,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
// [7]. If no autofill took place, we are left with the previously matched option; correct this
if (autoselect && !hasAutoFilled && !this.multipleChoice) {
// This means there is no match for checkedIndex
- this.checkedIndex = -1;
+ this.setCheckedIndex(-1);
+ this.modelValue = this.parser(inputValue);
}
// [8]. These values will help computing autofill intentions next autocomplete cycle
@@ -1090,6 +1182,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
clear() {
super.clear();
- this._inputNode.value = '';
+ this.value = '';
}
}
diff --git a/packages/ui/components/combobox/src/validators.js b/packages/ui/components/combobox/src/validators.js
new file mode 100644
index 000000000..cde94fa96
--- /dev/null
+++ b/packages/ui/components/combobox/src/validators.js
@@ -0,0 +1,18 @@
+/* eslint-disable max-classes-per-file */
+import { Unparseable, Validator } from '@lion/ui/form-core.js';
+
+export class MatchesOption extends Validator {
+ static get validatorName() {
+ return 'MatchesOption';
+ }
+
+ /**
+ * @param {unknown} [value]
+ * @param {string | undefined} [options]
+ * @param {{ node: any }} [config]
+ */
+ // eslint-disable-next-line class-methods-use-this
+ execute(value, options, config) {
+ return config?.node.modelValue instanceof Unparseable;
+ }
+}
diff --git a/packages/ui/components/combobox/test-helpers/combobox-helpers.js b/packages/ui/components/combobox/test-helpers/combobox-helpers.js
new file mode 100644
index 000000000..89fc0da67
--- /dev/null
+++ b/packages/ui/components/combobox/test-helpers/combobox-helpers.js
@@ -0,0 +1,125 @@
+import { getListboxMembers } from '@lion/ui/listbox-test-helpers.js';
+
+/**
+ * @typedef {import('@lion/ui/combobox.js').LionCombobox} LionCombobox
+ */
+
+/**
+ * @param { LionCombobox } el
+ */
+export function getComboboxMembers(el) {
+ const obj = getListboxMembers(el);
+ return {
+ ...obj,
+ ...{
+ // @ts-ignore [allow-protected] in test
+ _invokerNode: el._invokerNode,
+ // @ts-ignore [allow-protected] in test
+ _overlayCtrl: el._overlayCtrl,
+ // @ts-ignore [allow-protected] in test
+ _comboboxNode: el._comboboxNode,
+ // @ts-ignore [allow-protected] in test
+ _inputNode: el._inputNode,
+ // @ts-ignore [allow-protected] in test
+ _listboxNode: el._listboxNode,
+ // @ts-ignore [allow-protected] in test
+ _selectionDisplayNode: el._selectionDisplayNode,
+ // @ts-ignore [allow-protected] in test
+ _activeDescendantOwnerNode: el._activeDescendantOwnerNode,
+ // @ts-ignore [allow-protected] in test
+ _ariaVersion: el._ariaVersion,
+ },
+ };
+}
+
+/**
+ * @param {LionCombobox} el
+ * @param {string} value
+ */
+// TODO: add keys that actually make sense...
+export function mimicUserTyping(el, value) {
+ const { _inputNode } = getComboboxMembers(el);
+ _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
+ // eslint-disable-next-line no-param-reassign
+ _inputNode.value = value;
+ _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+ _inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
+ _inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
+}
+
+/**
+ * @param {HTMLElement} el
+ * @param {string} key
+ */
+export function mimicKeyPress(el, key) {
+ el.dispatchEvent(new KeyboardEvent('keydown', { key }));
+ el.dispatchEvent(new KeyboardEvent('keyup', { key }));
+}
+
+/**
+ * @param {LionCombobox} el
+ * @param {string[]} values
+ */
+export async function mimicUserTypingAdvanced(el, values) {
+ const { _inputNode } = getComboboxMembers(el);
+ _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
+
+ for (const key of values) {
+ // eslint-disable-next-line no-await-in-loop, no-loop-func
+ await new Promise(resolve => {
+ const hasSelection = _inputNode.selectionStart !== _inputNode.selectionEnd;
+
+ if (key === 'Backspace') {
+ if (hasSelection) {
+ _inputNode.value =
+ _inputNode.value.slice(
+ 0,
+ _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
+ ) +
+ _inputNode.value.slice(
+ _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
+ _inputNode.value.length,
+ );
+ } else {
+ _inputNode.value = _inputNode.value.slice(0, -1);
+ }
+ } else if (hasSelection) {
+ _inputNode.value =
+ _inputNode.value.slice(
+ 0,
+ _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
+ ) +
+ key +
+ _inputNode.value.slice(
+ _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
+ _inputNode.value.length,
+ );
+ } else {
+ _inputNode.value += key;
+ }
+
+ mimicKeyPress(_inputNode, key);
+ _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+
+ el.updateComplete.then(() => {
+ // @ts-ignore
+ resolve();
+ });
+ });
+ }
+}
+
+/**
+ * @param {LionCombobox} el
+ */
+export function getFilteredOptionValues(el) {
+ const options = el.formElements;
+ /**
+ * @param {{ style: { display: string; }; }} option
+ */
+ const filtered = options.filter(option => option.getAttribute('aria-hidden') !== 'true');
+ /**
+ * @param {{ value: any; }} option
+ */
+ return filtered.map(option => option.value);
+}
diff --git a/packages/ui/components/combobox/test/lion-combobox.test.js b/packages/ui/components/combobox/test/lion-combobox.test.js
index ec6391fce..b3e60a6ea 100644
--- a/packages/ui/components/combobox/test/lion-combobox.test.js
+++ b/packages/ui/components/combobox/test/lion-combobox.test.js
@@ -1,141 +1,25 @@
-import '@lion/ui/define/lion-combobox.js';
-import { LitElement } from 'lit';
+import {
+ getComboboxMembers,
+ getFilteredOptionValues,
+ mimicKeyPress,
+ mimicUserTyping,
+ mimicUserTypingAdvanced,
+} from '@lion/ui/combobox-test-helpers.js';
+import { LionCombobox } from '@lion/ui/combobox.js';
import { browserDetection } from '@lion/ui/core.js';
-import { Required } from '@lion/ui/form-core.js';
+import '@lion/ui/define/lion-combobox.js';
import '@lion/ui/define/lion-listbox.js';
import '@lion/ui/define/lion-option.js';
-import { getListboxMembers } from '@lion/ui/listbox-test-helpers.js';
+import { Required, Unparseable } from '@lion/ui/form-core.js';
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
+import { LitElement } from 'lit';
import sinon from 'sinon';
-import { LionCombobox } from '@lion/ui/combobox.js';
/**
* @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
- * @typedef {import('../../listbox/types/ListboxMixinTypes.js').ListboxHost} ListboxHost
- * @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
* @typedef {import('@lion/ui/listbox.js').LionOption} LionOption
*/
-/**
- * @param { LionCombobox } el
- */
-function getComboboxMembers(el) {
- const obj = getListboxMembers(el);
- return {
- ...obj,
- ...{
- // @ts-ignore [allow-protected] in test
- _invokerNode: el._invokerNode,
- // @ts-ignore [allow-protected] in test
- _overlayCtrl: el._overlayCtrl,
- // @ts-ignore [allow-protected] in test
- _comboboxNode: el._comboboxNode,
- // @ts-ignore [allow-protected] in test
- _inputNode: el._inputNode,
- // @ts-ignore [allow-protected] in test
- _listboxNode: el._listboxNode,
- // @ts-ignore [allow-protected] in test
- _selectionDisplayNode: el._selectionDisplayNode,
- // @ts-ignore [allow-protected] in test
- _activeDescendantOwnerNode: el._activeDescendantOwnerNode,
- // @ts-ignore [allow-protected] in test
- _ariaVersion: el._ariaVersion,
- },
- };
-}
-
-/**
- * @param {LionCombobox} el
- * @param {string} value
- */
-// TODO: add keys that actually make sense...
-function mimicUserTyping(el, value) {
- const { _inputNode } = getComboboxMembers(el);
- _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
- // eslint-disable-next-line no-param-reassign
- _inputNode.value = value;
- _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
- _inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
- _inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
-}
-
-/**
- * @param {HTMLElement} el
- * @param {string} key
- */
-function mimicKeyPress(el, key) {
- el.dispatchEvent(new KeyboardEvent('keydown', { key }));
- el.dispatchEvent(new KeyboardEvent('keyup', { key }));
-}
-
-/**
- * @param {LionCombobox} el
- * @param {string[]} values
- */
-async function mimicUserTypingAdvanced(el, values) {
- const { _inputNode } = getComboboxMembers(el);
- _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
-
- for (const key of values) {
- // eslint-disable-next-line no-await-in-loop, no-loop-func
- await new Promise(resolve => {
- const hasSelection = _inputNode.selectionStart !== _inputNode.selectionEnd;
-
- if (key === 'Backspace') {
- if (hasSelection) {
- _inputNode.value =
- _inputNode.value.slice(
- 0,
- _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
- ) +
- _inputNode.value.slice(
- _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
- _inputNode.value.length,
- );
- } else {
- _inputNode.value = _inputNode.value.slice(0, -1);
- }
- } else if (hasSelection) {
- _inputNode.value =
- _inputNode.value.slice(
- 0,
- _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
- ) +
- key +
- _inputNode.value.slice(
- _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
- _inputNode.value.length,
- );
- } else {
- _inputNode.value += key;
- }
-
- mimicKeyPress(_inputNode, key);
- _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
-
- el.updateComplete.then(() => {
- // @ts-ignore
- resolve();
- });
- });
- }
-}
-
-/**
- * @param {LionCombobox} el
- */
-function getFilteredOptionValues(el) {
- const options = el.formElements;
- /**
- * @param {{ style: { display: string; }; }} option
- */
- const filtered = options.filter(option => option.getAttribute('aria-hidden') !== 'true');
- /**
- * @param {{ value: any; }} option
- */
- return filtered.map(option => option.value);
-}
-
/**
* @param {{ autocomplete?:'none'|'list'|'both', matchMode?:'begin'|'all' }} config
*/
@@ -387,6 +271,22 @@ describe('lion-combobox', () => {
expect(_comboboxNode).to.exist;
expect(el.querySelector('[role=combobox]')).to.equal(_comboboxNode);
});
+
+ it('has validator "MatchesOption" applied by default', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Item 1
+ Item 2
+
+ `)
+ );
+ mimicUserTyping(el, '30');
+ await el.updateComplete;
+ expect(el.hasFeedbackFor).to.include('error');
+ expect(el.validationStates).to.have.property('error');
+ expect(el.validationStates.error).to.have.property('MatchesOption');
+ });
});
describe('Values', () => {
@@ -408,7 +308,7 @@ describe('lion-combobox', () => {
expect(_inputNode.value).to.equal('20');
});
- it('sets modelValue to empty string if no option is selected', async () => {
+ it('sets modelValue to Unparseable if no option is selected', async () => {
const el = /** @type {LionCombobox} */ (
await fixture(html`
@@ -419,12 +319,59 @@ describe('lion-combobox', () => {
`)
);
+ const { _inputNode } = getComboboxMembers(el);
expect(el.modelValue).to.equal('Artichoke');
expect(el.formElements[0].checked).to.be.true;
- el.checkedIndex = -1;
+ el.setCheckedIndex(-1);
+ el.__shouldAutocompleteNextUpdate = true;
await el.updateComplete;
- expect(el.modelValue).to.equal('');
+ expect(el.modelValue instanceof Unparseable).to.be.true;
+ expect(el.modelValue.viewValue).to.equal('Artichoke');
+ expect(el.formElements[0].checked).to.be.false;
+
+ el.setCheckedIndex(-1);
+ _inputNode.value = 'Foo';
+ el.__shouldAutocompleteNextUpdate = true;
+ await el.updateComplete;
+ expect(el.modelValue instanceof Unparseable).to.be.true;
+ expect(el.modelValue.viewValue).to.equal('Foo');
+ expect(el.formElements[0].checked).to.be.false;
+
+ el.setCheckedIndex(0);
+ el.__shouldAutocompleteNextUpdate = true;
+ await el.updateComplete;
+ expect(el.modelValue instanceof Unparseable).to.be.false;
+ expect(el.modelValue).to.equal('Artichoke');
+ expect(el.formElements[0].checked).to.be.true;
+ });
+
+ it('sets modelValue to _inputNode.value if no option is selected when requireOptionMatch is false', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+
+ expect(el.modelValue).to.equal('Artichoke');
+ expect(el.formElements[0].checked).to.be.true;
+ el.setCheckedIndex(-1);
+ el.__shouldAutocompleteNextUpdate = true;
+ await el.updateComplete;
+ expect(el.modelValue).to.equal('Artichoke');
+ expect(el.formElements[0].checked).to.be.true;
+
+ el.setCheckedIndex(-1);
+ _inputNode.value = 'Foo';
+ el.__shouldAutocompleteNextUpdate = true;
+ await el.updateComplete;
+ expect(el.modelValue).to.equal('Foo');
expect(el.formElements[0].checked).to.be.false;
});
@@ -439,15 +386,37 @@ describe('lion-combobox', () => {
`)
);
-
- expect(el.modelValue).to.eql(['Artichoke']);
+ expect(el.modelValue).to.deep.equal(['Artichoke']);
expect(el.formElements[0].checked).to.be.true;
- el.checkedIndex = [];
+ el.setCheckedIndex([]);
await el.updateComplete;
- expect(el.modelValue).to.eql([]);
+ expect(el.modelValue).to.deep.equal([]);
expect(el.formElements[0].checked).to.be.false;
});
+ it('multiple choice and requireOptionMatch is false do not work together yet', async () => {
+ const errorMessage = `multipleChoice and requireOptionMatch=false can't be used at the same time (yet).`;
+ let error;
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ try {
+ el.requireOptionMatch = false;
+ await el.updateComplete;
+ } catch (err) {
+ error = err;
+ }
+ expect(error).to.be.instanceOf(Error);
+ expect(/** @type {Error} */ (error).message).to.equal(errorMessage);
+ });
+
it('clears modelValue and textbox value on clear()', async () => {
const el = /** @type {LionCombobox} */ (
await fixture(html`
@@ -479,7 +448,43 @@ describe('lion-combobox', () => {
el2.clear();
expect(el2.modelValue).to.eql([]);
- expect(_inputNode.value).to.equal('');
+ // @ts-ignore [allow-protected] in test
+ expect(el2._inputNode.value).to.equal('');
+ });
+
+ it('resets modelValue and textbox value on reset()', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+
+ const { _inputNode } = getComboboxMembers(el);
+ el.modelValue = 'Chard';
+ el.reset();
+ expect(el.modelValue).to.equal('Artichoke');
+ expect(_inputNode.value).to.equal('Artichoke');
+
+ const el2 = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ el2.modelValue = ['Artichoke', 'Chard'];
+ el2.reset();
+ expect(el2.modelValue).to.deep.equal(['Artichoke']);
+ // @ts-ignore [allow-protected] in test
+ expect(el2._inputNode.value).to.equal('Artichoke');
});
it('syncs textbox to modelValue', async () => {
@@ -493,27 +498,123 @@ describe('lion-combobox', () => {
);
const { _inputNode } = getComboboxMembers(el);
- async function performChecks() {
+ /** @param {string} autocompleteMode */
+ async function performChecks(autocompleteMode) {
el.formElements[0].click();
await el.updateComplete;
// FIXME: fix properly for Webkit
- // expect(_inputNode.value).to.equal('Aha');
- expect(el.checkedIndex).to.equal(0);
+ // expect(_inputNode.value).to.equal('Aha', `autocomplete mode ${autocompleteMode}`);
+ expect(el.checkedIndex).to.equal(0, `autocomplete mode ${autocompleteMode}`);
mimicUserTyping(el, 'Ah');
await el.updateComplete;
- expect(_inputNode.value).to.equal('Ah');
+ expect(_inputNode.value).to.equal('Ah', `autocomplete mode ${autocompleteMode}`);
await el.updateComplete;
- expect(el.checkedIndex).to.equal(-1);
+ expect(el.checkedIndex).to.equal(-1, `autocomplete mode ${autocompleteMode}`);
}
el.autocomplete = 'none';
- await performChecks();
+ await performChecks('none');
el.autocomplete = 'list';
- await performChecks();
+ await performChecks('list');
+ });
+
+ it('works with Required validator', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ el.submitted = true;
+ await el.feedbackComplete;
+ expect(el.hasFeedbackFor).to.include('error', 'hasFeedbackFor');
+ await el.feedbackComplete;
+ expect(el.showsFeedbackFor).to.include('error', 'showsFeedbackFor');
+ });
+
+ it('allows a value outside of the option list when requireOptionMatch is false', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ el.requireOptionMatch = false;
+ const { _inputNode } = getComboboxMembers(el);
+ expect(el.checkedIndex).to.equal(0);
+
+ mimicUserTyping(el, 'Foo');
+ _inputNode.dispatchEvent(new Event('input'));
+ await el.updateComplete;
+
+ expect(el.checkedIndex).to.equal(-1);
+ expect(el.modelValue).to.equal('Foo');
+ expect(_inputNode.value).to.equal('Foo');
+ });
+
+ it("when removing a letter it won't select the option", async () => {
+ // We don't autocomplete when characters are removed
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+ expect(el.checkedIndex).to.equal(0);
+
+ // Simulate backspace deleting the char at the end of the string
+ mimicKeyPress(_inputNode, 'Backspace');
+ _inputNode.dispatchEvent(new Event('input'));
+ const arr = _inputNode.value.split('');
+ arr.splice(_inputNode.value.length - 1, 1);
+ _inputNode.value = arr.join('');
+ await el.updateComplete;
+ el.dispatchEvent(new Event('blur'));
+
+ expect(el.checkedIndex).to.equal(-1);
+ expect(el.modelValue instanceof Unparseable).to.be.true;
+ expect(el.modelValue.viewValue).to.equal('Artichok');
+ });
+
+ it('allows the user to override the parser', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+
+ el.setCheckedIndex(-1);
+ _inputNode.value = 'Foo123';
+ el.__shouldAutocompleteNextUpdate = true;
+ await el.updateComplete;
+ expect(el.modelValue).to.equal('Foo');
+ expect(el.formElements[0].checked).to.be.false;
});
});
@@ -874,36 +975,6 @@ describe('lion-combobox', () => {
});
});
- it('works with validation', async () => {
- const el = /** @type {LionCombobox} */ (
- await fixture(html`
-
- Artichoke
- Chard
- Chicory
- Victoria Plum
-
- `)
- );
- const { _inputNode } = getComboboxMembers(el);
- expect(el.checkedIndex).to.equal(0);
-
- // Simulate backspace deleting the char at the end of the string
- mimicKeyPress(_inputNode, 'Backspace');
- _inputNode.dispatchEvent(new Event('input'));
- const arr = _inputNode.value.split('');
- arr.splice(_inputNode.value.length - 1, 1);
- _inputNode.value = arr.join('');
- await el.updateComplete;
- el.dispatchEvent(new Event('blur'));
-
- expect(el.checkedIndex).to.equal(-1);
- await el.feedbackComplete;
- expect(el.hasFeedbackFor).to.include('error', 'hasFeedbackFor');
- await el.feedbackComplete;
- expect(el.showsFeedbackFor).to.include('error', 'showsFeedbackFor');
- });
-
it('dropdown has a label', async () => {
const el = /** @type {LionCombobox} */ (
await fixture(html`
@@ -1656,7 +1727,7 @@ describe('lion-combobox', () => {
const el = /** @type {LionCombobox} */ (
await fixture(html`
- <${tag} name="foo" autocomplete="list" opened>
+ <${tag} name="foo" opened>
Artichoke
Chard
Chicory
@@ -1948,7 +2019,10 @@ describe('lion-combobox', () => {
*/
// eslint-disable-next-line class-methods-use-this
_getTextboxValueFromOption(option) {
- return option.label;
+ if (option && option.label) {
+ return option.label;
+ }
+ return this.modelValue;
}
}
const tagName = defineCE(X);
@@ -1995,7 +2069,10 @@ describe('lion-combobox', () => {
*/
// eslint-disable-next-line class-methods-use-this
_getTextboxValueFromOption(option) {
- return option.label;
+ if (option && option.label) {
+ return option.label;
+ }
+ return this.modelValue;
}
}
const tagName = defineCE(X);
@@ -2068,29 +2145,29 @@ describe('lion-combobox', () => {
);
const { _inputNode } = getComboboxMembers(el);
- async function performChecks() {
+ /** @param {string} autocompleteMode */
+ async function performChecks(autocompleteMode) {
await el.updateComplete;
el.formElements[0].click();
- await el.updateComplete;
// FIXME: fix properly for Webkit
- // expect(_inputNode.value).to.equal('Aha');
- expect(el.checkedIndex).to.equal(0);
+ // expect(_inputNode.value).to.equal('Aha', autocompleteMode);
+ expect(el.checkedIndex).to.equal(0, autocompleteMode);
mimicUserTyping(el, 'Arti');
await el.updateComplete;
- expect(_inputNode.value).to.equal('Arti');
+ expect(_inputNode.value).to.equal('Arti', autocompleteMode);
await el.updateComplete;
- expect(el.checkedIndex).to.equal(-1);
+ expect(el.checkedIndex).to.equal(-1, autocompleteMode);
}
el.autocomplete = 'none';
- await performChecks();
+ await performChecks('none');
el.autocomplete = 'list';
- await performChecks();
+ await performChecks('list');
});
});
@@ -2233,7 +2310,7 @@ describe('lion-combobox', () => {
// eslint-disable-next-line no-param-reassign
elm.activeIndex = -1;
// eslint-disable-next-line no-param-reassign
- elm.checkedIndex = -1;
+ elm.setCheckedIndex(-1);
// eslint-disable-next-line no-param-reassign
elm.opened = true;
await elm.updateComplete;
diff --git a/packages/ui/components/combobox/test/validators.test.js b/packages/ui/components/combobox/test/validators.test.js
new file mode 100644
index 000000000..9fa99e43c
--- /dev/null
+++ b/packages/ui/components/combobox/test/validators.test.js
@@ -0,0 +1,62 @@
+import { mimicUserTyping } from '@lion/ui/combobox-test-helpers.js';
+import { MatchesOption } from '@lion/ui/combobox.js';
+import '@lion/ui/define/lion-combobox.js';
+import '@lion/ui/define/lion-option.js';
+import { expect, fixture, html } from '@open-wc/testing';
+
+/**
+ * @typedef {import('@lion/ui/combobox.js').LionCombobox} LionCombobox
+ */
+
+describe('MatchesOption validation', () => {
+ it('is enabled when an invalid value is set', async () => {
+ let isEnabled;
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ const config = {};
+ config.node = el;
+ const validator = new MatchesOption();
+
+ mimicUserTyping(el, 'Artichoke');
+ await el.updateComplete;
+
+ isEnabled = validator.execute('Artichoke', undefined, config);
+ expect(isEnabled).to.be.false;
+
+ mimicUserTyping(el, 'Foo');
+ await el.updateComplete;
+
+ isEnabled = validator.execute('Foo', undefined, config);
+ expect(isEnabled).to.be.true;
+ });
+
+ it('is not enabled when empty is set', async () => {
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+
+ `)
+ );
+ const config = {};
+ config.node = el;
+ const validator = new MatchesOption();
+
+ el.modelValue = '';
+ await el.updateComplete;
+
+ const isEnabled = validator.execute('', undefined, config);
+ expect(isEnabled).to.be.false;
+ });
+});
diff --git a/packages/ui/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts b/packages/ui/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts
index 02289d001..ad203dca2 100644
--- a/packages/ui/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts
+++ b/packages/ui/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts
@@ -1,9 +1,10 @@
import { Constructor } from '@open-wc/dedupe-mixin';
import { LitElement } from 'lit';
-import { FormControlHost } from '../FormControlMixinTypes.js';
import { FormControl } from '../form-group/FormGroupMixinTypes.js';
-import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes.js';
+import { FormControlHost } from '../FormControlMixinTypes.js';
import { InteractionStateHost } from '../InteractionStateMixinTypes.js';
+import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes.js';
+import { ChoiceInputHost } from './ChoiceInputMixinTypes.js';
export declare class ChoiceGroupHost {
multipleChoice: boolean;
@@ -26,7 +27,7 @@ export declare class ChoiceGroupHost {
protected _throwWhenInvalidChildModelValue(child: FormControlHost): void;
protected _isEmpty(): void;
protected _checkSingleChoiceElements(ev: Event): void;
- protected _getCheckedElements(): void;
+ protected _getCheckedElements(): ChoiceInputHost[];
protected _setCheckedElements(value: any, check: boolean): void;
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
diff --git a/packages/ui/components/listbox/test-suites/ListboxMixin.suite.js b/packages/ui/components/listbox/test-suites/ListboxMixin.suite.js
index bc3923abe..107a803c7 100644
--- a/packages/ui/components/listbox/test-suites/ListboxMixin.suite.js
+++ b/packages/ui/components/listbox/test-suites/ListboxMixin.suite.js
@@ -1459,7 +1459,7 @@ export function runListboxMixinSuite(customConfig = {}) {
)}
${tag}>
`);
- el.checkedIndex = 0;
+ el.setCheckedIndex(0);
expect(el.modelValue).to.deep.equal({
type: 'mastercard',
label: 'Master Card',
@@ -1467,7 +1467,7 @@ export function runListboxMixinSuite(customConfig = {}) {
active: true,
});
- el.checkedIndex = 1;
+ el.setCheckedIndex(1);
expect(el.modelValue).to.deep.equal({
type: 'visacard',
label: 'Visa Card',
diff --git a/packages/ui/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.js b/packages/ui/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.js
index b383cf939..0662ef3d4 100644
--- a/packages/ui/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.js
+++ b/packages/ui/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.js
@@ -1,22 +1,23 @@
/* eslint-disable import/no-extraneous-dependencies */
+import { MatchesOption } from '@lion/ui/combobox.js';
import {
DefaultSuccess,
+ EqualsLength,
IsDate,
IsDateDisabled,
- MaxDate,
- MinDate,
- MinMaxDate,
+ IsEmail,
IsNumber,
+ MaxDate,
+ MaxLength,
MaxNumber,
+ MinDate,
+ MinLength,
+ MinMaxDate,
+ MinMaxLength,
MinMaxNumber,
MinNumber,
- Required,
- EqualsLength,
- IsEmail,
- MaxLength,
- MinLength,
- MinMaxLength,
Pattern,
+ Required,
} from '@lion/ui/form-core.js';
import { PhoneNumber } from '@lion/ui/input-tel.js';
@@ -162,6 +163,8 @@ export function loadDefaultFeedbackMessagesNoSideEffects({ localize }) {
MinMaxDate.getMessage = async data => getLocalizedMessage(data);
/** @param {FeedbackMessageData} data */
IsDateDisabled.getMessage = async data => getLocalizedMessage(data);
+ /** @param {FeedbackMessageData} data */
+ MatchesOption.getMessage = async data => getLocalizedMessage(data);
DefaultSuccess.getMessage = async data => {
await forMessagesToBeReady();
diff --git a/packages/ui/components/validate-messages/translations/bg.js b/packages/ui/components/validate-messages/translations/bg.js
index 909a3209c..a55677685 100644
--- a/packages/ui/components/validate-messages/translations/bg.js
+++ b/packages/ui/components/validate-messages/translations/bg.js
@@ -17,6 +17,8 @@ export default {
'Моля, въведете {fieldName} между {params.min, date, YYYYMMDD} и {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Тази дата не е на разположение, моля, изберете друга.',
IsEmail: 'Моля, въведете валиден {fieldName} с формат "name@example.com".',
+ MatchesOption:
+ 'Не са открити съответстващи резултати. Моля, опитайте с друга ключова дума или категория.',
},
warning: {
Required: 'Моля, въведете също {fieldName}.',
@@ -35,6 +37,8 @@ export default {
'Моля, въведете {fieldName} между {params.min, date, YYYYMMDD} и {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Тази дата не е на разположение, моля, изберете друга.',
IsEmail: 'Моля, въведете валиден {fieldName} с формат "name@example.com".',
+ MatchesOption:
+ 'Не са открити съответстващи резултати. Моля, опитайте с друга ключова дума или категория.',
},
success: {
DefaultOk: 'Добре',
diff --git a/packages/ui/components/validate-messages/translations/cs.js b/packages/ui/components/validate-messages/translations/cs.js
index ed20aeaa3..1ee9a9e37 100644
--- a/packages/ui/components/validate-messages/translations/cs.js
+++ b/packages/ui/components/validate-messages/translations/cs.js
@@ -17,6 +17,7 @@ export default {
'Zadejte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Toto datum je nedostupné, vyberte jiné.',
IsEmail: 'Zadejte platný {fieldName} ve formátu "name@example.com".',
+ MatchesOption: 'Žádné odpovídající výsledky. Zkuste jiné klíčové slovo nebo kategorii.',
},
warning: {
Required: 'Zadejte rovněž {fieldName}.',
@@ -35,6 +36,7 @@ export default {
'Zadejte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Toto datum je nedostupné, vyberte jiné.',
IsEmail: 'Zadejte platný {fieldName} ve formátu "name@example.com".',
+ MatchesOption: 'Žádné odpovídající výsledky. Zkuste jiné klíčové slovo nebo kategorii.',
},
success: {
DefaultOk: 'Dobře',
diff --git a/packages/ui/components/validate-messages/translations/de.js b/packages/ui/components/validate-messages/translations/de.js
index 5a8107ba8..f54cb875c 100644
--- a/packages/ui/components/validate-messages/translations/de.js
+++ b/packages/ui/components/validate-messages/translations/de.js
@@ -18,6 +18,8 @@ export default {
'Geben Sie für {fieldName} einen Wert zwischen {params.min, date, YYYYMMDD} und {params.max, date, YYYYMMDD} ein.',
IsDateDisabled: 'Dieses Datum ist nicht verfügbar, bitte wählen Sie ein anderes Datum.',
IsEmail: 'Geben Sie einen gültige {fieldName} im Format „name@example.com“ ein.',
+ MatchesOption:
+ 'Keine übereinstimmenden Ergebnisse. Bitte versuchen Sie es mit einem anderen Schlüsselbegriff oder einer anderen Kategorie.',
},
warning: {
Required: '{fieldName} sollte ausgefüllt werden.',
@@ -37,6 +39,8 @@ export default {
'Geben Sie für {fieldName} einen Wert zwischen {params.min, date, YYYYMMDD} und {params.max, date, YYYYMMDD} ein.',
IsDateDisabled: 'Dieses Datum ist nicht verfügbar, bitte wählen Sie ein anderes Datum.',
IsEmail: 'Geben Sie einen gültige {fieldName} im Format „name@example.com“ ein.',
+ MatchesOption:
+ 'Keine übereinstimmenden Ergebnisse. Bitte versuchen Sie es mit einem anderen Schlüsselbegriff oder einer anderen Kategorie.',
},
success: {
DefaultOk: 'OK',
diff --git a/packages/ui/components/validate-messages/translations/en.js b/packages/ui/components/validate-messages/translations/en.js
index 4acce51c8..ac6c6b224 100644
--- a/packages/ui/components/validate-messages/translations/en.js
+++ b/packages/ui/components/validate-messages/translations/en.js
@@ -18,6 +18,7 @@ export default {
'Please enter a {fieldName} between {params.min, date, YYYYMMDD} and {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'This date is unavailable, please choose another one.',
IsEmail: 'Please enter a valid {fieldName} in the format "name@example.com".',
+ MatchesOption: 'No matching results. Please try a different keyword or category.',
},
warning: {
Required: 'Please enter a(n) {fieldName}.',
@@ -37,6 +38,7 @@ export default {
'Please enter a {fieldName} between {params.min, date, YYYYMMDD} and {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'This date is unavailable, please choose another one.',
IsEmail: 'Please enter a valid {fieldName} in the format "name@example.com".',
+ MatchesOption: 'No matching results. Please try a different keyword or category.',
},
success: {
DefaultOk: 'Okay',
diff --git a/packages/ui/components/validate-messages/translations/es.js b/packages/ui/components/validate-messages/translations/es.js
index fd79e1608..75eaa4a48 100644
--- a/packages/ui/components/validate-messages/translations/es.js
+++ b/packages/ui/components/validate-messages/translations/es.js
@@ -18,6 +18,8 @@ export default {
'Introduzca un/a {fieldName} entre {params.min, date, YYYYMMDD} y {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Esta fecha no está disponible. Elija otra.',
IsEmail: 'Introduzca un/a {fieldName} válido/a con el formato "nombre@ejemplo.com".',
+ MatchesOption:
+ 'No hay resultados que coincidan. Pruebe con una palabra clave o categoría diferente.',
},
warning: {
Required: 'Introduzca también un/a {fieldName}.',
@@ -37,6 +39,8 @@ export default {
'Introduzca un/a {fieldName} entre {params.min, date, YYYYMMDD} y {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Esta fecha no está disponible. Elija otra.',
IsEmail: 'Introduzca un/a {fieldName} válido/a con el formato "nombre@ejemplo.com".',
+ MatchesOption:
+ 'No hay resultados que coincidan. Pruebe con una palabra clave o categoría diferente.',
},
success: {
DefaultOk: 'Vale',
diff --git a/packages/ui/components/validate-messages/translations/fr.js b/packages/ui/components/validate-messages/translations/fr.js
index 3ec59ad39..0c0e8b511 100644
--- a/packages/ui/components/validate-messages/translations/fr.js
+++ b/packages/ui/components/validate-messages/translations/fr.js
@@ -19,6 +19,8 @@ export default {
'Veuillez indiquer un(e) {fieldName} entre {params.min, date, YYYYMMDD} et {params.max, date, YYYYMMDD}.',
IsDateDisabled: "Cette date n'est pas disponible, veuillez en choisir une autre.",
IsEmail: 'Veuillez indiquer un(e) {fieldName} au format "nom@exemple.com".',
+ MatchesOption:
+ 'Aucun résultat correspondant. Veuillez essayer un autre mot-clé ou une autre catégorie.',
},
warning: {
Required: 'Veuillez également indiquer un(e) {fieldName}.',
@@ -39,6 +41,8 @@ export default {
'Veuillez indiquer un(e) {fieldName} entre {params.min, date, YYYYMMDD} et {params.max, date, YYYYMMDD}.',
IsDateDisabled: "Cette date n'est pas disponible, veuillez en choisir une autre.",
IsEmail: 'Veuillez indiquer un(e) {fieldName} au format "nom@exemple.com".',
+ MatchesOption:
+ 'Aucun résultat correspondant. Veuillez essayer un autre mot-clé ou une autre catégorie.',
},
success: {
DefaultOk: 'Ok',
diff --git a/packages/ui/components/validate-messages/translations/hu.js b/packages/ui/components/validate-messages/translations/hu.js
index bd3b7ffa2..9ddbe0189 100644
--- a/packages/ui/components/validate-messages/translations/hu.js
+++ b/packages/ui/components/validate-messages/translations/hu.js
@@ -19,6 +19,7 @@ export default {
IsDateDisabled: 'Ez a dátum nem áll rendelkezésre, válasszon egy másikat.',
IsEmail:
'Adjon meg egy érvényes {fieldName} értéket, a következő formátumban: „név@példa.com”.',
+ MatchesOption: 'Nincs egyező találat. Próbálkozzon másik kulcsszóval vagy kategóriával.',
},
warning: {
Required: 'Továbbá adjon meg egy {fieldName} értéket.',
@@ -39,6 +40,7 @@ export default {
IsDateDisabled: 'Ez a dátum nem áll rendelkezésre, válasszon egy másikat.',
IsEmail:
'Adjon meg egy érvényes {fieldName} értéket, a következő formátumban: „név@példa.com”.',
+ MatchesOption: 'Nincs egyező találat. Próbálkozzon másik kulcsszóval vagy kategóriával.',
},
success: {
DefaultOk: 'Rendben',
diff --git a/packages/ui/components/validate-messages/translations/it.js b/packages/ui/components/validate-messages/translations/it.js
index 096891345..c57f71afd 100644
--- a/packages/ui/components/validate-messages/translations/it.js
+++ b/packages/ui/components/validate-messages/translations/it.js
@@ -18,6 +18,8 @@ export default {
'Inserire un(a) {fieldName} tra {params.min, date, YYYYMMDD} e {params.max, date, YYYYMMDD}.',
IsDateDisabled: "ТQuesta data non è disponibile, sceglierne un'altra.",
IsEmail: 'Inserire un valore valido per {fieldName} nel formato "name@example.com".',
+ MatchesOption:
+ 'Nessun risultato corrispondente. Provare con una parola chiave o una categoria diversa.',
},
warning: {
Required: 'Inserire anche un(a) {fieldName}.',
@@ -37,6 +39,8 @@ export default {
'Inserire un(a) {fieldName} tra {params.min, date, YYYYMMDD} e {params.max, date, YYYYMMDD}.',
IsDateDisabled: "ТQuesta data non è disponibile, sceglierne un'altra.",
IsEmail: 'Inserire un valore valido per {fieldName} nel formato "name@example.com".',
+ MatchesOption:
+ 'Nessun risultato corrispondente. Provare con una parola chiave o una categoria diversa.',
},
success: {
DefaultOk: 'OK',
diff --git a/packages/ui/components/validate-messages/translations/nl.js b/packages/ui/components/validate-messages/translations/nl.js
index 40253fc09..4cc1ba3ba 100644
--- a/packages/ui/components/validate-messages/translations/nl.js
+++ b/packages/ui/components/validate-messages/translations/nl.js
@@ -17,6 +17,8 @@ export default {
'Vul een {fieldName} in tussen {params.min, date, YYYYMMDD} en {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Deze datum is niet mogelijk, kies een andere.',
IsEmail: 'Vul een {fieldName} in formaat "name@example.com".',
+ MatchesOption:
+ 'Geen overeenkomende resultaten. Probeer een ander trefwoord of een andere categorie.',
},
warning: {
Required: 'Vul een {fieldName} in.',
@@ -35,6 +37,8 @@ export default {
'Vul een {fieldName} in tussen {params.min, date, YYYYMMDD} en {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Deze datum is niet mogelijk, kies een andere.',
IsEmail: 'Vul een {fieldName} in formaat "name@example.com".',
+ MatchesOption:
+ 'Geen overeenkomende resultaten. Probeer een ander trefwoord of een andere categorie.',
},
success: {
DefaultOk: 'Okee',
diff --git a/packages/ui/components/validate-messages/translations/pl.js b/packages/ui/components/validate-messages/translations/pl.js
index 9c33cfd98..8217d2271 100644
--- a/packages/ui/components/validate-messages/translations/pl.js
+++ b/packages/ui/components/validate-messages/translations/pl.js
@@ -18,6 +18,7 @@ export default {
'Proszę podać wartość {fieldName} między {params.min, date, YYYYMMDD} a {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Ta data jest niedostępna, wybierz inną.',
IsEmail: 'Proszę podać prawidłowy {fieldName} w formacie „nazwa@example.com”.',
+ MatchesOption: 'Brak pasujących wyników. Spróbuj użyć innego słowa kluczowego lub kategorii.',
},
warning: {
Required: 'Proszę również podać wartość {fieldName}.',
@@ -37,6 +38,7 @@ export default {
'Proszę podać wartość {fieldName} między {params.min, date, YYYYMMDD} a {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Ta data jest niedostępna, wybierz inną.',
IsEmail: 'Proszę podać prawidłowy {fieldName} w formacie „nazwa@example.com”.',
+ MatchesOption: 'Brak pasujących wyników. Spróbuj użyć innego słowa kluczowego lub kategorii.',
},
success: {
DefaultOk: 'Ok',
diff --git a/packages/ui/components/validate-messages/translations/ro.js b/packages/ui/components/validate-messages/translations/ro.js
index 8ffa29561..01787944f 100644
--- a/packages/ui/components/validate-messages/translations/ro.js
+++ b/packages/ui/components/validate-messages/translations/ro.js
@@ -18,6 +18,8 @@ export default {
'Introduceți un/o {fieldName} cuprins(ă) între {params.min, date, YYYYMMDD} și {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Această dată nu este disponibilă, alegeți alta.',
IsEmail: 'Introduceți un/o {fieldName} valid(ă) în formatul „nume@exemplu.com”.',
+ MatchesOption:
+ 'Niciun rezultat corespunzător. Încercaţi un cuvânt cheie sau o categorie diferită.',
},
warning: {
Required: 'Introduceți un/o {fieldName}.',
@@ -37,6 +39,8 @@ export default {
'Introduceți un/o {fieldName} cuprins(ă) între {params.min, date, YYYYMMDD} și {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Această dată nu este disponibilă, alegeți alta.',
IsEmail: 'Introduceți un/o {fieldName} valid(ă) în formatul „nume@exemplu.com”.',
+ MatchesOption:
+ 'Niciun rezultat corespunzător. Încercaţi un cuvânt cheie sau o categorie diferită.',
},
success: {
DefaultOk: 'În regulă',
diff --git a/packages/ui/components/validate-messages/translations/ru.js b/packages/ui/components/validate-messages/translations/ru.js
index 6b06a6104..e956760c3 100644
--- a/packages/ui/components/validate-messages/translations/ru.js
+++ b/packages/ui/components/validate-messages/translations/ru.js
@@ -18,6 +18,8 @@ export default {
'Введите значение поля {fieldName} от {params.min, date, YYYYMMDD} до {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Эта дата недоступна, выберите другой вариант.',
IsEmail: 'Введите действительное значение поля {fieldName} в формате «name@example.com».',
+ MatchesOption:
+ 'Нет соответствующих результатов. Попробуйте указать другое ключевое слово или категорию.',
},
warning: {
Required: 'Введите значение поля {fieldName}.',
@@ -37,6 +39,8 @@ export default {
'Введите значение поля {fieldName} от {params.min, date, YYYYMMDD} до {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Эта дата недоступна, выберите другой вариант.',
IsEmail: 'Введите действительное значение поля {fieldName} в формате «name@example.com».',
+ MatchesOption:
+ 'Нет соответствующих результатов. Попробуйте указать другое ключевое слово или категорию.',
},
success: {
DefaultOk: 'OK',
diff --git a/packages/ui/components/validate-messages/translations/sk.js b/packages/ui/components/validate-messages/translations/sk.js
index 73d8b58a2..2fd63ce7d 100644
--- a/packages/ui/components/validate-messages/translations/sk.js
+++ b/packages/ui/components/validate-messages/translations/sk.js
@@ -17,6 +17,7 @@ export default {
'Uveďte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Tento dátum je nedostupný, vyberte iný.',
IsEmail: 'Uveďte platnú položku {fieldName} vo formáte „meno@príklad.com“.',
+ MatchesOption: 'Žiadne vyhovujúce výsledky. Skúste iné kľúčové slovo alebo kategóriu.',
},
warning: {
Required: 'Uveďte aj {fieldName}.',
@@ -35,6 +36,7 @@ export default {
'Uveďte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Tento dátum je nedostupný, vyberte iný.',
IsEmail: 'Uveďte platnú položku {fieldName} vo formáte „meno@príklad.com“.',
+ MatchesOption: 'Žiadne vyhovujúce výsledky. Skúste iné kľúčové slovo alebo kategóriu.',
},
success: {
DefaultOk: 'Dobre',
diff --git a/packages/ui/components/validate-messages/translations/uk.js b/packages/ui/components/validate-messages/translations/uk.js
index 1dac856bc..f1f0e2379 100644
--- a/packages/ui/components/validate-messages/translations/uk.js
+++ b/packages/ui/components/validate-messages/translations/uk.js
@@ -19,6 +19,8 @@ export default {
'Уведіть значення {fieldName} між {params.min, date, YYYYMMDD} та {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Ця дата недоступна, виберіть іншу.',
IsEmail: 'Уведіть допустиме значення {fieldName} у форматі name@example.com.',
+ MatchesOption:
+ 'Не знайдено відповідних результатів. Спробуйте інше ключове слово чи категорію.',
},
warning: {
Required: 'Уведіть також значення {fieldName}.',
@@ -39,6 +41,8 @@ export default {
'Уведіть значення {fieldName} між {params.min, date, YYYYMMDD} та {params.max, date, YYYYMMDD}.',
IsDateDisabled: 'Ця дата недоступна, виберіть іншу.',
IsEmail: 'Уведіть допустиме значення {fieldName} у форматі name@example.com.',
+ MatchesOption:
+ 'Не знайдено відповідних результатів. Спробуйте інше ключове слово чи категорію.',
},
success: {
DefaultOk: 'Добре',
diff --git a/packages/ui/components/validate-messages/translations/zh.js b/packages/ui/components/validate-messages/translations/zh.js
index d9ede8a02..0850e0e9e 100644
--- a/packages/ui/components/validate-messages/translations/zh.js
+++ b/packages/ui/components/validate-messages/translations/zh.js
@@ -17,6 +17,7 @@ export default {
'请在{params.min,date,YYYYMMDD}和{params.max,date,YYYYMMDD}之间输入{fieldName}。',
IsDateDisabled: '此日期不可用,请选择其他日期。',
IsEmail: '请输入格式为"name@example.com"的有效{fieldName}。',
+ MatchesOption: '无匹配结果。请尝试其他关键词或类别。',
},
warning: {
Required: '請輸入{fieldName}。',
@@ -35,6 +36,7 @@ export default {
'请在{params.min,date,YYYYMMDD}和{params.max,date,YYYYMMDD}之间输入{fieldName}。',
IsDateDisabled: '此日期不可用,请选择其他日期。',
IsEmail: '请输入格式为"name@example.com"的有效{fieldName}。',
+ MatchesOption: '无匹配结果。请尝试其他关键词或类别。',
},
success: {
DefaultOk: '好的',
diff --git a/packages/ui/exports/combobox-test-helpers.js b/packages/ui/exports/combobox-test-helpers.js
new file mode 100644
index 000000000..243bfde9a
--- /dev/null
+++ b/packages/ui/exports/combobox-test-helpers.js
@@ -0,0 +1 @@
+export * from '../components/combobox/test-helpers/combobox-helpers.js';
diff --git a/packages/ui/exports/combobox.js b/packages/ui/exports/combobox.js
index 8309c9357..726f3abdc 100644
--- a/packages/ui/exports/combobox.js
+++ b/packages/ui/exports/combobox.js
@@ -3,3 +3,4 @@ export {
makeMatchingTextBold,
unmakeMatchingTextBold,
} from '../components/combobox/src/utils/makeMatchingTextBold.js';
+export { MatchesOption } from '../components/combobox/src/validators.js';