feat(combobox): add requireOptionMatch flag and MatchesOption validator

Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
This commit is contained in:
gerjanvangeest 2023-03-16 18:18:40 +01:00 committed by GitHub
parent 1f018bafdf
commit a2b81b2693
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 695 additions and 213 deletions

View file

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

View file

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

View file

@ -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`
<lion-combobox name="search" label="Search" .requireOptionMatch=${false}>
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
## 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`
<lion-combobox name="combo" label="Validation" .validators=${[new Required()]}>
${lazyRender(
listboxData.map(entry => html` <lion-option .choiceValue="${entry}">${entry}</lion-option> `),
)}
</lion-combobox>
`;
```
## Invoker button
```js preview-story

View file

@ -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 <datalist>
*/
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 <datalist> rotate
*/
@ -336,7 +399,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
* @configure ListboxMixin: the wai-aria pattern and <datalist> 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 <input>
* @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 = '';
}
}

View file

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

View file

@ -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);
}

View file

@ -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`
<lion-combobox name="foo">
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`)
);
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`
<lion-combobox name="foo" .modelValue="${'Artichoke'}">
@ -419,12 +319,59 @@ describe('lion-combobox', () => {
</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`
<lion-combobox name="foo" .modelValue="${'Artichoke'}" .requireOptionMatch="${false}">
<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>
`)
);
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', () => {
</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`
<lion-combobox name="foo" multiple-choice>
<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>
`)
);
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`
<lion-combobox name="foo" .modelValue="${'Artichoke'}">
<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>
`)
);
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`
<lion-combobox name="foo" multiple-choice .modelValue="${['Artichoke']}">
<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>
`)
);
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`
<lion-combobox name="foo" .validators=${[new Required()]}>
<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>
`)
);
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`
<lion-combobox name="foo" .validators=${[new Required()]}>
<lion-option checked .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>
`)
);
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`
<lion-combobox name="foo">
<lion-option checked .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>
`)
);
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`
<lion-combobox
name="foo"
.parser="${/** @param {string} value */ value => value.replace(/[0-9]/g, '')}"
>
<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>
`)
);
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`
<lion-combobox name="foo" .validators=${[new Required()]}>
<lion-option checked .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>
`)
);
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>
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
@ -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;

View file

@ -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`
<lion-combobox name="foo">
<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>
`)
);
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`
<lion-combobox name="foo">
<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>
`)
);
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;
});
});

View file

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

View file

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

View file

@ -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();

View file

@ -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: 'Добре',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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ă',

View file

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

View file

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

View file

@ -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: 'Добре',

View file

@ -17,6 +17,7 @@ export default {
'请在{params.mindateYYYYMMDD}和{params.maxdateYYYYMMDD}之间输入{fieldName}。',
IsDateDisabled: '此日期不可用,请选择其他日期。',
IsEmail: '请输入格式为"name@example.com"的有效{fieldName}。',
MatchesOption: '无匹配结果。请尝试其他关键词或类别。',
},
warning: {
Required: '請輸入{fieldName}。',
@ -35,6 +36,7 @@ export default {
'请在{params.mindateYYYYMMDD}和{params.maxdateYYYYMMDD}之间输入{fieldName}。',
IsDateDisabled: '此日期不可用,请选择其他日期。',
IsEmail: '请输入格式为"name@example.com"的有效{fieldName}。',
MatchesOption: '无匹配结果。请尝试其他关键词或类别。',
},
success: {
DefaultOk: '好的',

View file

@ -0,0 +1 @@
export * from '../components/combobox/test-helpers/combobox-helpers.js';

View file

@ -3,3 +3,4 @@ export {
makeMatchingTextBold,
unmakeMatchingTextBold,
} from '../components/combobox/src/utils/makeMatchingTextBold.js';
export { MatchesOption } from '../components/combobox/src/validators.js';