feat(combobox): add requireOptionMatch flag and MatchesOption validator
Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
This commit is contained in:
parent
1f018bafdf
commit
a2b81b2693
28 changed files with 695 additions and 213 deletions
9
.changeset/soft-eagles-reply.md
Normal file
9
.changeset/soft-eagles-reply.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
packages/ui/components/combobox/src/validators.js
Normal file
18
packages/ui/components/combobox/src/validators.js
Normal 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;
|
||||
}
|
||||
}
|
||||
125
packages/ui/components/combobox/test-helpers/combobox-helpers.js
Normal file
125
packages/ui/components/combobox/test-helpers/combobox-helpers.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
62
packages/ui/components/combobox/test/validators.test.js
Normal file
62
packages/ui/components/combobox/test/validators.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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: 'Добре',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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ă',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'Добре',
|
||||
|
|
|
|||
|
|
@ -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: '好的',
|
||||
|
|
|
|||
1
packages/ui/exports/combobox-test-helpers.js
Normal file
1
packages/ui/exports/combobox-test-helpers.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from '../components/combobox/test-helpers/combobox-helpers.js';
|
||||
|
|
@ -3,3 +3,4 @@ export {
|
|||
makeMatchingTextBold,
|
||||
unmakeMatchingTextBold,
|
||||
} from '../components/combobox/src/utils/makeMatchingTextBold.js';
|
||||
export { MatchesOption } from '../components/combobox/src/validators.js';
|
||||
|
|
|
|||
Loading…
Reference in a new issue