diff --git a/.changeset/big-geese-run.md b/.changeset/big-geese-run.md
new file mode 100644
index 000000000..916c3afd8
--- /dev/null
+++ b/.changeset/big-geese-run.md
@@ -0,0 +1,5 @@
+---
+'@lion/input-tel-dropdown': minor
+---
+
+New component LionInpuTelDropdown
diff --git a/.changeset/fifty-comics-attend.md b/.changeset/fifty-comics-attend.md
new file mode 100644
index 000000000..628df1f0a
--- /dev/null
+++ b/.changeset/fifty-comics-attend.md
@@ -0,0 +1,5 @@
+---
+'@lion/validate-messages': patch
+---
+
+fix(validate-messages): typo IsData message
diff --git a/.changeset/four-avocados-confess.md b/.changeset/four-avocados-confess.md
new file mode 100644
index 000000000..46a136368
--- /dev/null
+++ b/.changeset/four-avocados-confess.md
@@ -0,0 +1,5 @@
+---
+'@lion/form-core': patch
+---
+
+FormControl: allow a label-sr-only flag to provide visually hidden labels
diff --git a/.changeset/green-ads-fold.md b/.changeset/green-ads-fold.md
new file mode 100644
index 000000000..5437bd4c2
--- /dev/null
+++ b/.changeset/green-ads-fold.md
@@ -0,0 +1,5 @@
+---
+'@lion/form-core': patch
+---
+
+form-core: expose 'mimicUserInput' test-helper
diff --git a/.changeset/proud-geese-suffer.md b/.changeset/proud-geese-suffer.md
new file mode 100644
index 000000000..4e373eac9
--- /dev/null
+++ b/.changeset/proud-geese-suffer.md
@@ -0,0 +1,5 @@
+---
+'@lion/form-core': minor
+---
+
+Validation: allow enums as outcome of a Validator
diff --git a/.changeset/rare-panthers-crash.md b/.changeset/rare-panthers-crash.md
new file mode 100644
index 000000000..732c593b1
--- /dev/null
+++ b/.changeset/rare-panthers-crash.md
@@ -0,0 +1,5 @@
+---
+'@lion/input-tel': minor
+---
+
+New component "LionInputTel"
diff --git a/.prettierignore b/.prettierignore
index 63ecf735b..bc9266d1b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,3 +3,4 @@ CHANGELOG.md
bundlesize/
_site
_site-dev
+.history
diff --git a/docs/components/inputs/input-tel-dropdown/examples.md b/docs/components/inputs/input-tel-dropdown/examples.md
new file mode 100644
index 000000000..60d258b4b
--- /dev/null
+++ b/docs/components/inputs/input-tel-dropdown/examples.md
@@ -0,0 +1,28 @@
+# Inputs >> Input Tel Dropdown >> Examples ||30
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import '@lion/select-rich/define';
+import './src/intl-input-tel-dropdown.js';
+```
+
+## Input Tel International
+
+A visually advanced Subclasser implementation of `LionInputTelDropdown`.
+
+Inspired by:
+
+- [intl-tel-input](https://intl-tel-input.com/)
+- [react-phone-input-2](https://github.com/bl00mber/react-phone-input-2)
+
+```js preview-story
+export const IntlInputTelDropdown = () => html`
+
+`;
+```
diff --git a/docs/components/inputs/input-tel-dropdown/features.md b/docs/components/inputs/input-tel-dropdown/features.md
new file mode 100644
index 000000000..42478e6d5
--- /dev/null
+++ b/docs/components/inputs/input-tel-dropdown/features.md
@@ -0,0 +1,72 @@
+# Inputs >> Input Tel Dropdown >> Features ||20
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import { ref, createRef } from '@lion/core';
+import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
+import { PhoneUtilManager } from '@lion/input-tel';
+import '@lion/input-tel-dropdown/define';
+import '../../../docs/systems/form/assets/h-output.js';
+```
+
+## Input Tel Dropdown
+
+When `.allowedRegions` is not configured, all regions/countries will be available in the dropdown
+list. Once a region is chosen, its country/dial code will be adjusted with that of the new locale.
+
+```js preview-story
+export const InputTelDropdown = () => html`
+
+
+`;
+```
+
+## Allowed regions
+
+When `.allowedRegions` is configured, only those regions/countries will be available in the dropdown
+list.
+
+```js preview-story
+export const allowedRegions = () => html`
+
+
+`;
+```
+
+## Preferred regions
+
+When `.preferredRegions` is configured, they will show up on top of the dropdown list to
+enhance user experience.
+
+```js preview-story
+export const preferredRegionCodes = () => html`
+
+
+`;
+```
diff --git a/docs/components/inputs/input-tel-dropdown/index.md b/docs/components/inputs/input-tel-dropdown/index.md
new file mode 100644
index 000000000..f2453e8bd
--- /dev/null
+++ b/docs/components/inputs/input-tel-dropdown/index.md
@@ -0,0 +1,3 @@
+# Inputs >> Input Tel Dropdown ||20
+
+-> go to Overview
diff --git a/docs/components/inputs/input-tel-dropdown/overview.md b/docs/components/inputs/input-tel-dropdown/overview.md
new file mode 100644
index 000000000..7a6d46dba
--- /dev/null
+++ b/docs/components/inputs/input-tel-dropdown/overview.md
@@ -0,0 +1,36 @@
+# Inputs >> Input Tel Dropdown >> Overview ||10
+
+Extension of Input Tel that prefixes a dropdown list that shows all possible regions / countries.
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import '@lion/input-tel-dropdown/define';
+```
+
+```js preview-story
+export const main = () => {
+ return html`
+
+ `;
+};
+```
+
+## Features
+
+- Extends our [input-tel](../input-tel/overview.md)
+- Shows dropdown list with all possible regions
+- Shows only allowed regions in dropdown list when .allowedRegions is configured
+- Highlights regions on top of dropdown list when .preferredRegions is configured
+- Generates template meta data for advanced
+
+## Installation
+
+```bash
+npm i --save @lion/input-tel-dropdown
+```
+
+```js
+import { LionInputTelDropdown } from '@lion/input-tel-dropdown';
+// or
+import '@lion/input-tel-dropdown/define';
+```
diff --git a/docs/components/inputs/input-tel-dropdown/src/intl-input-tel-dropdown.js b/docs/components/inputs/input-tel-dropdown/src/intl-input-tel-dropdown.js
new file mode 100644
index 000000000..08bdefb21
--- /dev/null
+++ b/docs/components/inputs/input-tel-dropdown/src/intl-input-tel-dropdown.js
@@ -0,0 +1,124 @@
+import { html, css, ScopedElementsMixin, ref, repeat } from '@lion/core';
+import { LionInputTelDropdown } from '@lion/input-tel-dropdown';
+import {
+ IntlSelectRich,
+ IntlOption,
+ IntlSeparator,
+} from '../../select-rich/src/intl-select-rich.js';
+
+/**
+ * @typedef {import('@lion/input-tel-dropdown/types').TemplateDataForDropdownInputTel}TemplateDataForDropdownInputTel
+ */
+
+// Example implementation for https://intl-tel-input.com/
+export class IntlInputTelDropdown extends ScopedElementsMixin(LionInputTelDropdown) {
+ /**
+ * @configure LitElement
+ * @enhance LionInputTelDropdown
+ */
+ static styles = [
+ super.styles,
+ css`
+ :host,
+ ::slotted(*) {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ color: #333;
+ }
+
+ :host {
+ max-width: 300px;
+ }
+
+ .input-group__container {
+ width: 100%;
+ height: 34px;
+ font-size: 14px;
+ line-height: 1.42857143;
+ color: #555;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
+ box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
+ -webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
+ -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ }
+
+ .input-group__input {
+ padding: 6px;
+ box-sizing: border-box;
+ }
+
+ .input-group__input ::slotted(input) {
+ border: none;
+ outline: none;
+ }
+
+ :host([focused]) .input-group__container {
+ border-color: #66afe9;
+ outline: 0;
+ -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%);
+ box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%);
+ }
+ `,
+ ];
+
+ static templates = {
+ ...(super.templates || {}),
+ /**
+ * @param {TemplateDataForDropdownInputTel} templateDataForDropdown
+ */
+ dropdown: templateDataForDropdown => {
+ const { refs, data } = templateDataForDropdown;
+ // TODO: once spread directive available, use it per ref (like ref(refs?.dropdown?.ref))
+ return html`
+
+ ${data?.regionMetaListPreferred?.length
+ ? html` ${repeat(
+ data.regionMetaListPreferred,
+ regionMeta => regionMeta.regionCode,
+ regionMeta =>
+ html`${this.templates.dropdownOption(templateDataForDropdown, regionMeta)} `,
+ )}`
+ : ''}
+ ${repeat(
+ data.regionMetaList,
+ regionMeta => regionMeta.regionCode,
+ regionMeta =>
+ html`${this.templates.dropdownOption(templateDataForDropdown, regionMeta)} `,
+ )}
+
+ `;
+ },
+ /**
+ * @param {TemplateDataForDropdownInputTel} templateDataForDropdown
+ * @param {RegionMeta} regionMeta
+ */
+ // eslint-disable-next-line class-methods-use-this
+ dropdownOption: (templateDataForDropdown, regionMeta) => html`
+
+
+ `,
+ };
+
+ /**
+ * @configure ScopedElementsMixin
+ */
+ static scopedElements = {
+ ...super.scopedElements,
+ 'intl-select-rich': IntlSelectRich,
+ 'intl-option': IntlOption,
+ 'intl-separator': IntlSeparator,
+ };
+}
+customElements.define('intl-input-tel-dropdown', IntlInputTelDropdown);
diff --git a/docs/components/inputs/input-tel/features.md b/docs/components/inputs/input-tel/features.md
new file mode 100644
index 000000000..483e36624
--- /dev/null
+++ b/docs/components/inputs/input-tel/features.md
@@ -0,0 +1,254 @@
+# Inputs >> Input Tel >> Features ||20
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import { ref, createRef } from '@lion/core';
+import { Unparseable } from '@lion/form-core';
+import { localize } from '@lion/localize';
+import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
+import { PhoneUtilManager } from '@lion/input-tel';
+import '@lion/input-tel/define';
+import './src/h-region-code-table.js';
+import '../../../docs/systems/form/assets/h-output.js';
+```
+
+## Regions: some context
+
+Say we have the following telephone number from Madrid, Spain: `+34919930432`.
+
+It contains a [country code](https://en.wikipedia.org/wiki/Country_code) (34), an [area code](https://en.wikipedia.org/wiki/Telephone_numbering_plan#Area_code) (91) and a [dial code](https://en.wikipedia.org/wiki/Mobile_dial_code) (+34 91).
+Input Tel interprets phone numbers based on their [region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2): a two character long region representation('ES' in the telephone number above).
+
+The table below lists all possible regions worldwide. When [allowed regions](#allowed-regions) are not configured,
+all of them will be supported as values of Input Tel.
+
+```js story
+export const regionCodesTable = () => html``;
+```
+
+### Active region
+
+The active region (accessible via readonly accessor `.activeRegion`) determines how validation and formatting
+should be applied. It is dependent on the following factors:
+
+- [allowed regions](#allowed-regions): a list that determines what is allowed to become .activeRegion. If
+ [.allowedRegions has only one entry](#restrict-to-one-region), .activeRegion will always be this value.
+- the modelValue or viewValue: once it contains sufficient info to derive its region code (and
+ the derived code is inside [allowed regions](#allowed-regions) if configured)
+- active locale (and the derived locale is inside [allowed regions](#allowed-regions) if configured)
+
+What follows from the list above is that .activeRegion can change dynamically, after a value
+change in the text box by the user (or when locales or allowed regions would be changed by the
+Application Developer).
+
+### How active region is computed
+
+The following heuristic will be applied:
+
+1. check for **allowed regions**: if one region defined in .allowedRegions, use it.
+2. check for **user input**: try to derive active region from user input
+3. check for **locale**: try to get the region from locale (`html[lang]` attribute)
+
+```js preview-story
+export const heuristic = () => {
+ const initialAllowedRegions = ['CN', 'ES'];
+ const [inputTelRef, outputRef, selectRef] = [createRef(), createRef(), createRef()];
+
+ const setDerivedActiveRegionScenario = (
+ scenarioToSet,
+ inputTel = inputTelRef.value,
+ output = outputRef.value,
+ ) => {
+ if (scenarioToSet === 'only-allowed-region') {
+ // activeRegion will be the top allowed region, which is 'NL'
+ inputTel.modelValue = undefined;
+ inputTel.allowedRegions = ['NL']; // activeRegion will always be the only option
+ output.innerText = '.activeRegion (NL) is only allowed region';
+ } else if (scenarioToSet === 'user-input') {
+ // activeRegion will be based on phone number => 'BE'
+ inputTel.allowedRegions = ['NL', 'BE', 'DE'];
+ inputTel.modelValue = '+3261234567'; // BE number
+ output.innerText = '.activeRegion (BE) is derived (since within allowedRegions)';
+ } else if (scenarioToSet === 'locale') {
+ localize.locale = 'en-GB';
+ // activeRegion will be `html[lang]`
+ inputTel.modelValue = undefined;
+ inputTel.allowedRegions = undefined;
+ output.innerText = `.activeRegion (${inputTel._langIso}) set to locale when inside allowed or all regions`;
+ } else {
+ output.innerText = '';
+ }
+ };
+ return html`
+
+
+ {
+ if (detail.isTriggeredByUser && selectRef.value) {
+ selectRef.value.value = '';
+ }
+ }}"
+ name="phoneNumber"
+ label="Active region"
+ .allowedRegions="${initialAllowedRegions}"
+ >
+
+ `;
+};
+```
+
+## Allowed regions
+
+`.allowedRegions` is an array of one or more region codes.
+Once it is configured, validation and formatting will be restricted to those
+values that are present in this list.
+
+> Note that for [InputTelDropdown](../input-tel-dropdown/index.md), only allowed regions will
+> be shown in the dropdown list.
+
+```js preview-story
+export const allowedRegions = () => html`
+
+
+`;
+```
+
+### Restrict to one region
+
+When one allowed region is configured, validation and formatting will be restricted to just that
+region (that means that changes of the region via viewValue won't have effect).
+
+```js preview-story
+export const oneAllowedRegion = () => html`
+
+
+`;
+```
+
+## Format strategy
+
+Determines what the formatter output should look like.
+Formatting strategies as provided by awesome-phonenumber / google-libphonenumber.
+
+Possible values:
+
+| strategy | output |
+| :------------ | ---------------------: |
+| e164 | `+46707123456` |
+| international | `+46 70 712 34 56` |
+| national | `070-712 34 56` |
+| significant | `707123456` |
+| rfc3966 | `tel:+46-70-712-34-56` |
+
+Also see:
+
+- [awesome-phonenumber documentation](https://www.npmjs.com/package/awesome-phonenumber)
+
+```js preview-story
+export const formatStrategy = () => {
+ const inputTel = createRef();
+ return html`
+
+
+
+ `;
+};
+```
+
+## Live format
+
+Type '6' in the example below to see how the phone number is formatted during typing.
+
+See [awesome-phonenumber documentation](https://www.npmjs.com/package/awesome-phonenumber)
+
+```js preview-story
+export const liveFormat = () => html`
+
+`;
+```
+
+## Active phone number type
+
+The readonly acessor `.activePhoneNumberType` outputs the current phone number type, based on
+the textbox value.
+
+Possible types: `fixed-line`, `fixed-line-or-mobile`, `mobile`, `pager`, `personal-number`, `premium-rate`, `shared-cost`, `toll-free`, `uan`, `voip`, `unknown`
+
+Also see:
+
+- [awesome-phonenumber documentation](https://www.npmjs.com/package/awesome-phonenumber)
+
+```js preview-story
+export const activePhoneNumberType = () => html`
+
+
+`;
+```
diff --git a/docs/components/inputs/input-tel/index.md b/docs/components/inputs/input-tel/index.md
new file mode 100644
index 000000000..f8548f9c5
--- /dev/null
+++ b/docs/components/inputs/input-tel/index.md
@@ -0,0 +1,3 @@
+# Inputs >> Input Tel ||20
+
+-> go to Overview
diff --git a/docs/components/inputs/input-tel/overview.md b/docs/components/inputs/input-tel/overview.md
new file mode 100644
index 000000000..58b6f3a91
--- /dev/null
+++ b/docs/components/inputs/input-tel/overview.md
@@ -0,0 +1,59 @@
+# Inputs >> Input Tel >> Overview ||10
+
+Input field for entering phone numbers, including validation, formatting and mobile keyboard support.
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import { ref, createRef } from '@lion/core';
+import { PhoneUtilManager } from '@lion/input-tel';
+import '@lion/input-tel/define';
+import '../../../docs/systems/form/assets/h-output.js';
+```
+
+```js preview-story
+export const main = () => {
+ return html`
+
+
+ `;
+};
+```
+
+## Features
+
+- Extends our [input](../input/overview.md)
+- Shows a mobile telephone keypad on mobile (by having a native `` inside)
+- Can be configured with a list of allowed region codes
+- Will be preconfigured with region derived from locale
+- Has the [e164 standard format](https://en.wikipedia.org/wiki/E.164) as modelValue
+- Uses [awesome-phonenumber](https://www.npmjs.com/package/awesome-phonenumber) (a performant, concise version of [google-lib-phonenumber](https://www.npmjs.com/package/google-libphonenumber)):
+ - Formats phone numbers, based on region code
+ - Validates phone numbers, based on region code
+- Lazy loads awesome-phonenumber, so that the first paint of this component will be brought to your screen as quick as possible
+
+## Installation
+
+```bash
+npm i --save @lion/input-tel
+```
+
+```js
+import { LionInputTel } from '@lion/input-tel';
+// or
+import '@lion/input-tel/define';
+```
diff --git a/docs/components/inputs/input-tel/src/h-region-code-table.js b/docs/components/inputs/input-tel/src/h-region-code-table.js
new file mode 100644
index 000000000..84c348a00
--- /dev/null
+++ b/docs/components/inputs/input-tel/src/h-region-code-table.js
@@ -0,0 +1,107 @@
+import { LitElement, css, html, repeat, ScopedStylesController } from '@lion/core';
+import { regionMetaList } from '../../select-rich/src/regionMetaList.js';
+
+export class HRegionCodeTable extends LitElement {
+ static properties = {
+ regionMeta: Object,
+ };
+
+ constructor() {
+ super();
+ /** @type {ScopedStylesController} */
+ this.scopedStylesController = new ScopedStylesController(this);
+ }
+
+ /**
+ * @param {CSSResult} scope
+ */
+ static scopedStyles(scope) {
+ return css`
+ /* Custom input range styling comes here, be aware that this won't work for polyfilled browsers */
+ .${scope} .sr-only {
+ position: absolute;
+ top: 0;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
+
+ .${scope} table {
+ position: relative;
+ height: 300px;
+ }
+
+ .${scope} th {
+ border-left: none;
+ border-right: none;
+ position: sticky;
+ top: -1px;
+ }
+
+ .${scope} th .backdrop {
+ background-color: white;
+ opacity: 0.95;
+ filter: blur(4px);
+ position: absolute;
+ inset: -5px;
+ }
+
+ .${scope} th .content {
+ position: relative;
+ }
+
+ .${scope} td {
+ border-left: none;
+ border-right: none;
+ }
+ `;
+ }
+
+ // Render to light dom, so global table styling will be applied
+ createRenderRoot() {
+ return this;
+ }
+
+ render() {
+ const finalRegionMetaList = this.regionMetaList || regionMetaList;
+ return html`
+
+
+ Region codes
+
+
+
+ |
+ country name
+ |
+
+ region code
+ |
+
+ country code
+ |
+
+
+
+ ${repeat(
+ finalRegionMetaList,
+ regionMeta => regionMeta.regionCode,
+ ({ regionCode, countryCode, flagSymbol, nameForLocale }) =>
+ html`
+ | ${flagSymbol} ${nameForLocale} |
+ ${regionCode} |
+ ${countryCode} |
+
`,
+ )}
+
+
+ `;
+ }
+}
+customElements.define('h-region-code-table', HRegionCodeTable);
diff --git a/docs/components/inputs/select-rich/examples.md b/docs/components/inputs/select-rich/examples.md
new file mode 100644
index 000000000..32a23a961
--- /dev/null
+++ b/docs/components/inputs/select-rich/examples.md
@@ -0,0 +1,31 @@
+# Inputs >> Select Rich >> Examples ||30
+
+```js script
+import { html } from '@mdjs/mdjs-preview';
+import { repeat } from '@lion/core';
+import '@lion/select-rich/define';
+import './src/intl-select-rich.js';
+import { regionMetaList } from './src/regionMetaList.js';
+```
+
+## Select Rich International
+
+A visually advanced Subclasser implementation of `LionSelectRich`.
+
+Inspired by:
+
+- [intl-tel-input](https://intl-tel-input.com/)
+
+```js story
+export const IntlSelectRich = () => html`
+
+ ${repeat(
+ regionMetaList,
+ regionMeta => regionMeta.regionCode,
+ regionMeta =>
+ html`
+ `,
+ )}
+
+`;
+```
diff --git a/docs/components/inputs/select-rich/features.md b/docs/components/inputs/select-rich/features.md
index 58e384e6f..e1d5412cf 100644
--- a/docs/components/inputs/select-rich/features.md
+++ b/docs/components/inputs/select-rich/features.md
@@ -11,7 +11,7 @@ import '@lion/select-rich/define';
You can set the full `modelValue` for each option, which includes the checked property for whether it is checked or not.
```html
-Red
+Red
```
## Options with HTML
@@ -21,15 +21,15 @@ The main feature of this rich select that makes it rich, is that your options ca
```js preview-story
export const optionsWithHTML = () => html`
-
+
I am red
and multi Line
-
+
I am hotpink
and multi Line
-
+
I am teal
and multi Line
@@ -49,19 +49,19 @@ export const manyOptionsWithScrolling = () => html`
}
-
+
I am red
-
+
I am hotpink
-
+
I am teal
-
+
I am green
-
+
I am blue
@@ -126,7 +126,7 @@ export const renderOptions = ({ shadowRoot }) => {
{ type: 'visacard', label: 'Visa Card', amount: 0, active: false },
];
function showOutput(ev) {
- shadowRoot.getElementById('demoRenderOutput').innerHTML = JSON.stringify(
+ shadowRoot.querySelector('#demoRenderOutput').innerHTML = JSON.stringify(
ev.target.modelValue,
null,
2,
@@ -305,49 +305,63 @@ export const singleOptionRemoveAdd = () => {
## Custom Invoker
You can provide a custom invoker using the invoker slot.
-This means it will get the selected value(s) as an input property `.selectedElement`.
+LionSelectRich will give it acces to:
-You can use this `selectedElement` to then render the content to your own invoker.
+- the currently selected option via `.selectedElement`
+- LionSelectRich itself, via `.hostElement`
+
+Code of an advanced custom invoker is shown below (this is the code for the
+invoker used in [IntlSelectRich](./examples.md)).
+The invoker is usually added in the invoker slot of the LionSelectRich subclass.
+However, it would also be possible for an application developer to provide the invoker
+by putting it in light dom:
```html
-
+
...
```
-An example of how such a custom invoker class could look like:
-
```js
-class MyInvokerButton extends LitElement() {
- static get properties() {
- return {
- selectedElement: {
- type: Object,
- };
- }
- }
-
- _contentTemplate() {
- if (this.selectedElement) {
- const labelNodes = Array.from(this.selectedElement.childNodes);
- // Nested html in the selected option
- if (labelNodes.length > 0) {
- // Cloning is important if you plan on passing nodes straight to a lit template
- return labelNodes.map(node => node.cloneNode(true));
- }
- // Or if it is just text inside the selected option, no html
- return this.selectedElement.textContent;
- }
- return ``;
+import { LionSelectInvoker } from '@lion/select-rich';
+
+class IntlSelectInvoker extends LionSelectInvoker {
+ /**
+ * 1. Add your own styles
+ * @configure LitElement
+ * @enhance LionSelectInvoker
+ */
+ static styles = [
+ /** see IntlSelectRich listed above */
+ ];
+
+ /**
+ * 2. Take back control of slots (LionSelectInvoker adds slots you most likely don't want)
+ * @configure SlotMixin
+ * @override LionSelectInvoker
+ */
+ get slots() {
+ return {};
}
+ /**
+ * 3. Add you custom render function
+ * @override LionSelectInvoker
+ */
render() {
- return html`
-
- ${this._contentTemplate()}
-
- `;
+ const ctor = /** @type {typeof LionSelectInvoker} */ (this.constructor);
+ return ctor._mainTemplate(this._templateData);
+ }
+
+ get _templateData() {
+ return {
+ data: { selectedElement: this.selectedElement, hostElement: this.hostElement },
+ };
+ }
+
+ static _mainTemplate(templateData) {
+ /** see IntlSelectRich listed above */
}
}
```
diff --git a/docs/components/inputs/select-rich/src/flagStyles.js b/docs/components/inputs/select-rich/src/flagStyles.js
new file mode 100644
index 000000000..03632c6fb
--- /dev/null
+++ b/docs/components/inputs/select-rich/src/flagStyles.js
@@ -0,0 +1,1250 @@
+import { css, unsafeCSS } from '@lion/core';
+
+const flagsImgUrl = unsafeCSS(new URL('./flags.png', import.meta.url).href);
+const flagsImg2xUrl = unsafeCSS(new URL('./flags@2x.png', import.meta.url).href);
+
+export const flagStyles = css`
+ .iti {
+ position: relative;
+ display: inline-block;
+ }
+ .iti * {
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ }
+ .iti__hide {
+ display: none;
+ }
+ .iti__v-hide {
+ visibility: hidden;
+ }
+ .iti input,
+ .iti input[type='text'],
+ .iti input[type='tel'] {
+ position: relative;
+ z-index: 0;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ padding-right: 36px;
+ margin-right: 0;
+ }
+ .iti__flag-container {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ padding: 1px;
+ }
+ .iti__selected-flag {
+ z-index: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 100%;
+ padding: 0 6px 0 8px;
+ }
+ .iti__arrow {
+ margin-left: 6px;
+ width: 0;
+ height: 0;
+ border-left: 3px solid transparent;
+ border-right: 3px solid transparent;
+ border-top: 4px solid #555;
+ }
+ .iti__arrow--up {
+ border-top: none;
+ border-bottom: 4px solid #555;
+ }
+ .iti__country-list {
+ position: absolute;
+ z-index: 2;
+ list-style: none;
+ text-align: left;
+ padding: 0;
+ margin: 0 0 0 -1px;
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2);
+ background-color: white;
+ border: 1px solid #ccc;
+ white-space: nowrap;
+ max-height: 200px;
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+ }
+ .iti__country-list--dropup {
+ bottom: 100%;
+ margin-bottom: -1px;
+ }
+ @media (max-width: 500px) {
+ .iti__country-list {
+ white-space: normal;
+ }
+ }
+ .iti__flag-box {
+ display: inline-block;
+ width: 20px;
+ }
+ .iti__divider {
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+ border-bottom: 1px solid #ccc;
+ }
+ .iti__country {
+ padding: 5px 10px;
+ outline: none;
+ }
+ .iti__dial-code {
+ color: #999;
+ }
+ .iti__country.iti__highlight {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ .iti__flag-box,
+ .iti__country-name,
+ .iti__dial-code {
+ vertical-align: middle;
+ }
+ .iti__flag-box,
+ .iti__country-name {
+ margin-right: 6px;
+ }
+ .iti--allow-dropdown input,
+ .iti--allow-dropdown input[type='text'],
+ .iti--allow-dropdown input[type='tel'],
+ .iti--separate-dial-code input,
+ .iti--separate-dial-code input[type='text'],
+ .iti--separate-dial-code input[type='tel'] {
+ padding-right: 6px;
+ padding-left: 52px;
+ margin-left: 0;
+ }
+ .iti--allow-dropdown .iti__flag-container,
+ .iti--separate-dial-code .iti__flag-container {
+ right: auto;
+ left: 0;
+ }
+ .iti--allow-dropdown .iti__flag-container:hover {
+ cursor: pointer;
+ }
+ .iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ .iti--allow-dropdown input[disabled] + .iti__flag-container:hover,
+ .iti--allow-dropdown input[readonly] + .iti__flag-container:hover {
+ cursor: default;
+ }
+ .iti--allow-dropdown input[disabled] + .iti__flag-container:hover .iti__selected-flag,
+ .iti--allow-dropdown input[readonly] + .iti__flag-container:hover .iti__selected-flag {
+ background-color: transparent;
+ }
+ .iti--separate-dial-code .iti__selected-flag {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ .iti--separate-dial-code .iti__selected-dial-code {
+ margin-left: 6px;
+ }
+ .iti--container {
+ position: absolute;
+ top: -1000px;
+ left: -1000px;
+ z-index: 1060;
+ padding: 1px;
+ }
+ .iti--container:hover {
+ cursor: pointer;
+ }
+
+ .iti-mobile .iti--container {
+ top: 30px;
+ bottom: 30px;
+ left: 30px;
+ right: 30px;
+ position: fixed;
+ }
+
+ .iti-mobile .iti__country-list {
+ max-height: 100%;
+ width: 100%;
+ }
+
+ .iti-mobile .iti__country {
+ padding: 10px 10px;
+ line-height: 1.5em;
+ }
+
+ .iti__flag {
+ width: 20px;
+ }
+ .iti__flag.iti__be {
+ width: 18px;
+ }
+ .iti__flag.iti__ch {
+ width: 15px;
+ }
+ .iti__flag.iti__mc {
+ width: 19px;
+ }
+ .iti__flag.iti__ne {
+ width: 18px;
+ }
+ .iti__flag.iti__np {
+ width: 13px;
+ }
+ .iti__flag.iti__va {
+ width: 15px;
+ }
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .iti__flag {
+ background-size: 5652px 15px;
+ }
+ }
+ .iti__flag.iti__ac {
+ height: 10px;
+ background-position: 0px 0px;
+ }
+ .iti__flag.iti__ad {
+ height: 14px;
+ background-position: -22px 0px;
+ }
+ .iti__flag.iti__ae {
+ height: 10px;
+ background-position: -44px 0px;
+ }
+ .iti__flag.iti__af {
+ height: 14px;
+ background-position: -66px 0px;
+ }
+ .iti__flag.iti__ag {
+ height: 14px;
+ background-position: -88px 0px;
+ }
+ .iti__flag.iti__ai {
+ height: 10px;
+ background-position: -110px 0px;
+ }
+ .iti__flag.iti__al {
+ height: 15px;
+ background-position: -132px 0px;
+ }
+ .iti__flag.iti__am {
+ height: 10px;
+ background-position: -154px 0px;
+ }
+ .iti__flag.iti__ao {
+ height: 14px;
+ background-position: -176px 0px;
+ }
+ .iti__flag.iti__aq {
+ height: 14px;
+ background-position: -198px 0px;
+ }
+ .iti__flag.iti__ar {
+ height: 13px;
+ background-position: -220px 0px;
+ }
+ .iti__flag.iti__as {
+ height: 10px;
+ background-position: -242px 0px;
+ }
+ .iti__flag.iti__at {
+ height: 14px;
+ background-position: -264px 0px;
+ }
+ .iti__flag.iti__au {
+ height: 10px;
+ background-position: -286px 0px;
+ }
+ .iti__flag.iti__aw {
+ height: 14px;
+ background-position: -308px 0px;
+ }
+ .iti__flag.iti__ax {
+ height: 13px;
+ background-position: -330px 0px;
+ }
+ .iti__flag.iti__az {
+ height: 10px;
+ background-position: -352px 0px;
+ }
+ .iti__flag.iti__ba {
+ height: 10px;
+ background-position: -374px 0px;
+ }
+ .iti__flag.iti__bb {
+ height: 14px;
+ background-position: -396px 0px;
+ }
+ .iti__flag.iti__bd {
+ height: 12px;
+ background-position: -418px 0px;
+ }
+ .iti__flag.iti__be {
+ height: 15px;
+ background-position: -440px 0px;
+ }
+ .iti__flag.iti__bf {
+ height: 14px;
+ background-position: -460px 0px;
+ }
+ .iti__flag.iti__bg {
+ height: 12px;
+ background-position: -482px 0px;
+ }
+ .iti__flag.iti__bh {
+ height: 12px;
+ background-position: -504px 0px;
+ }
+ .iti__flag.iti__bi {
+ height: 12px;
+ background-position: -526px 0px;
+ }
+ .iti__flag.iti__bj {
+ height: 14px;
+ background-position: -548px 0px;
+ }
+ .iti__flag.iti__bl {
+ height: 14px;
+ background-position: -570px 0px;
+ }
+ .iti__flag.iti__bm {
+ height: 10px;
+ background-position: -592px 0px;
+ }
+ .iti__flag.iti__bn {
+ height: 10px;
+ background-position: -614px 0px;
+ }
+ .iti__flag.iti__bo {
+ height: 14px;
+ background-position: -636px 0px;
+ }
+ .iti__flag.iti__bq {
+ height: 14px;
+ background-position: -658px 0px;
+ }
+ .iti__flag.iti__br {
+ height: 14px;
+ background-position: -680px 0px;
+ }
+ .iti__flag.iti__bs {
+ height: 10px;
+ background-position: -702px 0px;
+ }
+ .iti__flag.iti__bt {
+ height: 14px;
+ background-position: -724px 0px;
+ }
+ .iti__flag.iti__bv {
+ height: 15px;
+ background-position: -746px 0px;
+ }
+ .iti__flag.iti__bw {
+ height: 14px;
+ background-position: -768px 0px;
+ }
+ .iti__flag.iti__by {
+ height: 10px;
+ background-position: -790px 0px;
+ }
+ .iti__flag.iti__bz {
+ height: 14px;
+ background-position: -812px 0px;
+ }
+ .iti__flag.iti__ca {
+ height: 10px;
+ background-position: -834px 0px;
+ }
+ .iti__flag.iti__cc {
+ height: 10px;
+ background-position: -856px 0px;
+ }
+ .iti__flag.iti__cd {
+ height: 15px;
+ background-position: -878px 0px;
+ }
+ .iti__flag.iti__cf {
+ height: 14px;
+ background-position: -900px 0px;
+ }
+ .iti__flag.iti__cg {
+ height: 14px;
+ background-position: -922px 0px;
+ }
+ .iti__flag.iti__ch {
+ height: 15px;
+ background-position: -944px 0px;
+ }
+ .iti__flag.iti__ci {
+ height: 14px;
+ background-position: -961px 0px;
+ }
+ .iti__flag.iti__ck {
+ height: 10px;
+ background-position: -983px 0px;
+ }
+ .iti__flag.iti__cl {
+ height: 14px;
+ background-position: -1005px 0px;
+ }
+ .iti__flag.iti__cm {
+ height: 14px;
+ background-position: -1027px 0px;
+ }
+ .iti__flag.iti__cn {
+ height: 14px;
+ background-position: -1049px 0px;
+ }
+ .iti__flag.iti__co {
+ height: 14px;
+ background-position: -1071px 0px;
+ }
+ .iti__flag.iti__cp {
+ height: 14px;
+ background-position: -1093px 0px;
+ }
+ .iti__flag.iti__cr {
+ height: 12px;
+ background-position: -1115px 0px;
+ }
+ .iti__flag.iti__cu {
+ height: 10px;
+ background-position: -1137px 0px;
+ }
+ .iti__flag.iti__cv {
+ height: 12px;
+ background-position: -1159px 0px;
+ }
+ .iti__flag.iti__cw {
+ height: 14px;
+ background-position: -1181px 0px;
+ }
+ .iti__flag.iti__cx {
+ height: 10px;
+ background-position: -1203px 0px;
+ }
+ .iti__flag.iti__cy {
+ height: 14px;
+ background-position: -1225px 0px;
+ }
+ .iti__flag.iti__cz {
+ height: 14px;
+ background-position: -1247px 0px;
+ }
+ .iti__flag.iti__de {
+ height: 12px;
+ background-position: -1269px 0px;
+ }
+ .iti__flag.iti__dg {
+ height: 10px;
+ background-position: -1291px 0px;
+ }
+ .iti__flag.iti__dj {
+ height: 14px;
+ background-position: -1313px 0px;
+ }
+ .iti__flag.iti__dk {
+ height: 15px;
+ background-position: -1335px 0px;
+ }
+ .iti__flag.iti__dm {
+ height: 10px;
+ background-position: -1357px 0px;
+ }
+ .iti__flag.iti__do {
+ height: 14px;
+ background-position: -1379px 0px;
+ }
+ .iti__flag.iti__dz {
+ height: 14px;
+ background-position: -1401px 0px;
+ }
+ .iti__flag.iti__ea {
+ height: 14px;
+ background-position: -1423px 0px;
+ }
+ .iti__flag.iti__ec {
+ height: 14px;
+ background-position: -1445px 0px;
+ }
+ .iti__flag.iti__ee {
+ height: 13px;
+ background-position: -1467px 0px;
+ }
+ .iti__flag.iti__eg {
+ height: 14px;
+ background-position: -1489px 0px;
+ }
+ .iti__flag.iti__eh {
+ height: 10px;
+ background-position: -1511px 0px;
+ }
+ .iti__flag.iti__er {
+ height: 10px;
+ background-position: -1533px 0px;
+ }
+ .iti__flag.iti__es {
+ height: 14px;
+ background-position: -1555px 0px;
+ }
+ .iti__flag.iti__et {
+ height: 10px;
+ background-position: -1577px 0px;
+ }
+ .iti__flag.iti__eu {
+ height: 14px;
+ background-position: -1599px 0px;
+ }
+ .iti__flag.iti__fi {
+ height: 12px;
+ background-position: -1621px 0px;
+ }
+ .iti__flag.iti__fj {
+ height: 10px;
+ background-position: -1643px 0px;
+ }
+ .iti__flag.iti__fk {
+ height: 10px;
+ background-position: -1665px 0px;
+ }
+ .iti__flag.iti__fm {
+ height: 11px;
+ background-position: -1687px 0px;
+ }
+ .iti__flag.iti__fo {
+ height: 15px;
+ background-position: -1709px 0px;
+ }
+ .iti__flag.iti__fr {
+ height: 14px;
+ background-position: -1731px 0px;
+ }
+ .iti__flag.iti__ga {
+ height: 15px;
+ background-position: -1753px 0px;
+ }
+ .iti__flag.iti__gb {
+ height: 10px;
+ background-position: -1775px 0px;
+ }
+ .iti__flag.iti__gd {
+ height: 12px;
+ background-position: -1797px 0px;
+ }
+ .iti__flag.iti__ge {
+ height: 14px;
+ background-position: -1819px 0px;
+ }
+ .iti__flag.iti__gf {
+ height: 14px;
+ background-position: -1841px 0px;
+ }
+ .iti__flag.iti__gg {
+ height: 14px;
+ background-position: -1863px 0px;
+ }
+ .iti__flag.iti__gh {
+ height: 14px;
+ background-position: -1885px 0px;
+ }
+ .iti__flag.iti__gi {
+ height: 10px;
+ background-position: -1907px 0px;
+ }
+ .iti__flag.iti__gl {
+ height: 14px;
+ background-position: -1929px 0px;
+ }
+ .iti__flag.iti__gm {
+ height: 14px;
+ background-position: -1951px 0px;
+ }
+ .iti__flag.iti__gn {
+ height: 14px;
+ background-position: -1973px 0px;
+ }
+ .iti__flag.iti__gp {
+ height: 14px;
+ background-position: -1995px 0px;
+ }
+ .iti__flag.iti__gq {
+ height: 14px;
+ background-position: -2017px 0px;
+ }
+ .iti__flag.iti__gr {
+ height: 14px;
+ background-position: -2039px 0px;
+ }
+ .iti__flag.iti__gs {
+ height: 10px;
+ background-position: -2061px 0px;
+ }
+ .iti__flag.iti__gt {
+ height: 13px;
+ background-position: -2083px 0px;
+ }
+ .iti__flag.iti__gu {
+ height: 11px;
+ background-position: -2105px 0px;
+ }
+ .iti__flag.iti__gw {
+ height: 10px;
+ background-position: -2127px 0px;
+ }
+ .iti__flag.iti__gy {
+ height: 12px;
+ background-position: -2149px 0px;
+ }
+ .iti__flag.iti__hk {
+ height: 14px;
+ background-position: -2171px 0px;
+ }
+ .iti__flag.iti__hm {
+ height: 10px;
+ background-position: -2193px 0px;
+ }
+ .iti__flag.iti__hn {
+ height: 10px;
+ background-position: -2215px 0px;
+ }
+ .iti__flag.iti__hr {
+ height: 10px;
+ background-position: -2237px 0px;
+ }
+ .iti__flag.iti__ht {
+ height: 12px;
+ background-position: -2259px 0px;
+ }
+ .iti__flag.iti__hu {
+ height: 10px;
+ background-position: -2281px 0px;
+ }
+ .iti__flag.iti__ic {
+ height: 14px;
+ background-position: -2303px 0px;
+ }
+ .iti__flag.iti__id {
+ height: 14px;
+ background-position: -2325px 0px;
+ }
+ .iti__flag.iti__ie {
+ height: 10px;
+ background-position: -2347px 0px;
+ }
+ .iti__flag.iti__il {
+ height: 15px;
+ background-position: -2369px 0px;
+ }
+ .iti__flag.iti__im {
+ height: 10px;
+ background-position: -2391px 0px;
+ }
+ .iti__flag.iti__in {
+ height: 14px;
+ background-position: -2413px 0px;
+ }
+ .iti__flag.iti__io {
+ height: 10px;
+ background-position: -2435px 0px;
+ }
+ .iti__flag.iti__iq {
+ height: 14px;
+ background-position: -2457px 0px;
+ }
+ .iti__flag.iti__ir {
+ height: 12px;
+ background-position: -2479px 0px;
+ }
+ .iti__flag.iti__is {
+ height: 15px;
+ background-position: -2501px 0px;
+ }
+ .iti__flag.iti__it {
+ height: 14px;
+ background-position: -2523px 0px;
+ }
+ .iti__flag.iti__je {
+ height: 12px;
+ background-position: -2545px 0px;
+ }
+ .iti__flag.iti__jm {
+ height: 10px;
+ background-position: -2567px 0px;
+ }
+ .iti__flag.iti__jo {
+ height: 10px;
+ background-position: -2589px 0px;
+ }
+ .iti__flag.iti__jp {
+ height: 14px;
+ background-position: -2611px 0px;
+ }
+ .iti__flag.iti__ke {
+ height: 14px;
+ background-position: -2633px 0px;
+ }
+ .iti__flag.iti__kg {
+ height: 12px;
+ background-position: -2655px 0px;
+ }
+ .iti__flag.iti__kh {
+ height: 13px;
+ background-position: -2677px 0px;
+ }
+ .iti__flag.iti__ki {
+ height: 10px;
+ background-position: -2699px 0px;
+ }
+ .iti__flag.iti__km {
+ height: 12px;
+ background-position: -2721px 0px;
+ }
+ .iti__flag.iti__kn {
+ height: 14px;
+ background-position: -2743px 0px;
+ }
+ .iti__flag.iti__kp {
+ height: 10px;
+ background-position: -2765px 0px;
+ }
+ .iti__flag.iti__kr {
+ height: 14px;
+ background-position: -2787px 0px;
+ }
+ .iti__flag.iti__kw {
+ height: 10px;
+ background-position: -2809px 0px;
+ }
+ .iti__flag.iti__ky {
+ height: 10px;
+ background-position: -2831px 0px;
+ }
+ .iti__flag.iti__kz {
+ height: 10px;
+ background-position: -2853px 0px;
+ }
+ .iti__flag.iti__la {
+ height: 14px;
+ background-position: -2875px 0px;
+ }
+ .iti__flag.iti__lb {
+ height: 14px;
+ background-position: -2897px 0px;
+ }
+ .iti__flag.iti__lc {
+ height: 10px;
+ background-position: -2919px 0px;
+ }
+ .iti__flag.iti__li {
+ height: 12px;
+ background-position: -2941px 0px;
+ }
+ .iti__flag.iti__lk {
+ height: 10px;
+ background-position: -2963px 0px;
+ }
+ .iti__flag.iti__lr {
+ height: 11px;
+ background-position: -2985px 0px;
+ }
+ .iti__flag.iti__ls {
+ height: 14px;
+ background-position: -3007px 0px;
+ }
+ .iti__flag.iti__lt {
+ height: 12px;
+ background-position: -3029px 0px;
+ }
+ .iti__flag.iti__lu {
+ height: 12px;
+ background-position: -3051px 0px;
+ }
+ .iti__flag.iti__lv {
+ height: 10px;
+ background-position: -3073px 0px;
+ }
+ .iti__flag.iti__ly {
+ height: 10px;
+ background-position: -3095px 0px;
+ }
+ .iti__flag.iti__ma {
+ height: 14px;
+ background-position: -3117px 0px;
+ }
+ .iti__flag.iti__mc {
+ height: 15px;
+ background-position: -3139px 0px;
+ }
+ .iti__flag.iti__md {
+ height: 10px;
+ background-position: -3160px 0px;
+ }
+ .iti__flag.iti__me {
+ height: 10px;
+ background-position: -3182px 0px;
+ }
+ .iti__flag.iti__mf {
+ height: 14px;
+ background-position: -3204px 0px;
+ }
+ .iti__flag.iti__mg {
+ height: 14px;
+ background-position: -3226px 0px;
+ }
+ .iti__flag.iti__mh {
+ height: 11px;
+ background-position: -3248px 0px;
+ }
+ .iti__flag.iti__mk {
+ height: 10px;
+ background-position: -3270px 0px;
+ }
+ .iti__flag.iti__ml {
+ height: 14px;
+ background-position: -3292px 0px;
+ }
+ .iti__flag.iti__mm {
+ height: 14px;
+ background-position: -3314px 0px;
+ }
+ .iti__flag.iti__mn {
+ height: 10px;
+ background-position: -3336px 0px;
+ }
+ .iti__flag.iti__mo {
+ height: 14px;
+ background-position: -3358px 0px;
+ }
+ .iti__flag.iti__mp {
+ height: 10px;
+ background-position: -3380px 0px;
+ }
+ .iti__flag.iti__mq {
+ height: 14px;
+ background-position: -3402px 0px;
+ }
+ .iti__flag.iti__mr {
+ height: 14px;
+ background-position: -3424px 0px;
+ }
+ .iti__flag.iti__ms {
+ height: 10px;
+ background-position: -3446px 0px;
+ }
+ .iti__flag.iti__mt {
+ height: 14px;
+ background-position: -3468px 0px;
+ }
+ .iti__flag.iti__mu {
+ height: 14px;
+ background-position: -3490px 0px;
+ }
+ .iti__flag.iti__mv {
+ height: 14px;
+ background-position: -3512px 0px;
+ }
+ .iti__flag.iti__mw {
+ height: 14px;
+ background-position: -3534px 0px;
+ }
+ .iti__flag.iti__mx {
+ height: 12px;
+ background-position: -3556px 0px;
+ }
+ .iti__flag.iti__my {
+ height: 10px;
+ background-position: -3578px 0px;
+ }
+ .iti__flag.iti__mz {
+ height: 14px;
+ background-position: -3600px 0px;
+ }
+ .iti__flag.iti__na {
+ height: 14px;
+ background-position: -3622px 0px;
+ }
+ .iti__flag.iti__nc {
+ height: 10px;
+ background-position: -3644px 0px;
+ }
+ .iti__flag.iti__ne {
+ height: 15px;
+ background-position: -3666px 0px;
+ }
+ .iti__flag.iti__nf {
+ height: 10px;
+ background-position: -3686px 0px;
+ }
+ .iti__flag.iti__ng {
+ height: 10px;
+ background-position: -3708px 0px;
+ }
+ .iti__flag.iti__ni {
+ height: 12px;
+ background-position: -3730px 0px;
+ }
+ .iti__flag.iti__nl {
+ height: 14px;
+ background-position: -3752px 0px;
+ }
+ .iti__flag.iti__no {
+ height: 15px;
+ background-position: -3774px 0px;
+ }
+ .iti__flag.iti__np {
+ height: 15px;
+ background-position: -3796px 0px;
+ }
+ .iti__flag.iti__nr {
+ height: 10px;
+ background-position: -3811px 0px;
+ }
+ .iti__flag.iti__nu {
+ height: 10px;
+ background-position: -3833px 0px;
+ }
+ .iti__flag.iti__nz {
+ height: 10px;
+ background-position: -3855px 0px;
+ }
+ .iti__flag.iti__om {
+ height: 10px;
+ background-position: -3877px 0px;
+ }
+ .iti__flag.iti__pa {
+ height: 14px;
+ background-position: -3899px 0px;
+ }
+ .iti__flag.iti__pe {
+ height: 14px;
+ background-position: -3921px 0px;
+ }
+ .iti__flag.iti__pf {
+ height: 14px;
+ background-position: -3943px 0px;
+ }
+ .iti__flag.iti__pg {
+ height: 15px;
+ background-position: -3965px 0px;
+ }
+ .iti__flag.iti__ph {
+ height: 10px;
+ background-position: -3987px 0px;
+ }
+ .iti__flag.iti__pk {
+ height: 14px;
+ background-position: -4009px 0px;
+ }
+ .iti__flag.iti__pl {
+ height: 13px;
+ background-position: -4031px 0px;
+ }
+ .iti__flag.iti__pm {
+ height: 14px;
+ background-position: -4053px 0px;
+ }
+ .iti__flag.iti__pn {
+ height: 10px;
+ background-position: -4075px 0px;
+ }
+ .iti__flag.iti__pr {
+ height: 14px;
+ background-position: -4097px 0px;
+ }
+ .iti__flag.iti__ps {
+ height: 10px;
+ background-position: -4119px 0px;
+ }
+ .iti__flag.iti__pt {
+ height: 14px;
+ background-position: -4141px 0px;
+ }
+ .iti__flag.iti__pw {
+ height: 13px;
+ background-position: -4163px 0px;
+ }
+ .iti__flag.iti__py {
+ height: 11px;
+ background-position: -4185px 0px;
+ }
+ .iti__flag.iti__qa {
+ height: 8px;
+ background-position: -4207px 0px;
+ }
+ .iti__flag.iti__re {
+ height: 14px;
+ background-position: -4229px 0px;
+ }
+ .iti__flag.iti__ro {
+ height: 14px;
+ background-position: -4251px 0px;
+ }
+ .iti__flag.iti__rs {
+ height: 14px;
+ background-position: -4273px 0px;
+ }
+ .iti__flag.iti__ru {
+ height: 14px;
+ background-position: -4295px 0px;
+ }
+ .iti__flag.iti__rw {
+ height: 14px;
+ background-position: -4317px 0px;
+ }
+ .iti__flag.iti__sa {
+ height: 14px;
+ background-position: -4339px 0px;
+ }
+ .iti__flag.iti__sb {
+ height: 10px;
+ background-position: -4361px 0px;
+ }
+ .iti__flag.iti__sc {
+ height: 10px;
+ background-position: -4383px 0px;
+ }
+ .iti__flag.iti__sd {
+ height: 10px;
+ background-position: -4405px 0px;
+ }
+ .iti__flag.iti__se {
+ height: 13px;
+ background-position: -4427px 0px;
+ }
+ .iti__flag.iti__sg {
+ height: 14px;
+ background-position: -4449px 0px;
+ }
+ .iti__flag.iti__sh {
+ height: 10px;
+ background-position: -4471px 0px;
+ }
+ .iti__flag.iti__si {
+ height: 10px;
+ background-position: -4493px 0px;
+ }
+ .iti__flag.iti__sj {
+ height: 15px;
+ background-position: -4515px 0px;
+ }
+ .iti__flag.iti__sk {
+ height: 14px;
+ background-position: -4537px 0px;
+ }
+ .iti__flag.iti__sl {
+ height: 14px;
+ background-position: -4559px 0px;
+ }
+ .iti__flag.iti__sm {
+ height: 15px;
+ background-position: -4581px 0px;
+ }
+ .iti__flag.iti__sn {
+ height: 14px;
+ background-position: -4603px 0px;
+ }
+ .iti__flag.iti__so {
+ height: 14px;
+ background-position: -4625px 0px;
+ }
+ .iti__flag.iti__sr {
+ height: 14px;
+ background-position: -4647px 0px;
+ }
+ .iti__flag.iti__ss {
+ height: 10px;
+ background-position: -4669px 0px;
+ }
+ .iti__flag.iti__st {
+ height: 10px;
+ background-position: -4691px 0px;
+ }
+ .iti__flag.iti__sv {
+ height: 12px;
+ background-position: -4713px 0px;
+ }
+ .iti__flag.iti__sx {
+ height: 14px;
+ background-position: -4735px 0px;
+ }
+ .iti__flag.iti__sy {
+ height: 14px;
+ background-position: -4757px 0px;
+ }
+ .iti__flag.iti__sz {
+ height: 14px;
+ background-position: -4779px 0px;
+ }
+ .iti__flag.iti__ta {
+ height: 10px;
+ background-position: -4801px 0px;
+ }
+ .iti__flag.iti__tc {
+ height: 10px;
+ background-position: -4823px 0px;
+ }
+ .iti__flag.iti__td {
+ height: 14px;
+ background-position: -4845px 0px;
+ }
+ .iti__flag.iti__tf {
+ height: 14px;
+ background-position: -4867px 0px;
+ }
+ .iti__flag.iti__tg {
+ height: 13px;
+ background-position: -4889px 0px;
+ }
+ .iti__flag.iti__th {
+ height: 14px;
+ background-position: -4911px 0px;
+ }
+ .iti__flag.iti__tj {
+ height: 10px;
+ background-position: -4933px 0px;
+ }
+ .iti__flag.iti__tk {
+ height: 10px;
+ background-position: -4955px 0px;
+ }
+ .iti__flag.iti__tl {
+ height: 10px;
+ background-position: -4977px 0px;
+ }
+ .iti__flag.iti__tm {
+ height: 14px;
+ background-position: -4999px 0px;
+ }
+ .iti__flag.iti__tn {
+ height: 14px;
+ background-position: -5021px 0px;
+ }
+ .iti__flag.iti__to {
+ height: 10px;
+ background-position: -5043px 0px;
+ }
+ .iti__flag.iti__tr {
+ height: 14px;
+ background-position: -5065px 0px;
+ }
+ .iti__flag.iti__tt {
+ height: 12px;
+ background-position: -5087px 0px;
+ }
+ .iti__flag.iti__tv {
+ height: 10px;
+ background-position: -5109px 0px;
+ }
+ .iti__flag.iti__tw {
+ height: 14px;
+ background-position: -5131px 0px;
+ }
+ .iti__flag.iti__tz {
+ height: 14px;
+ background-position: -5153px 0px;
+ }
+ .iti__flag.iti__ua {
+ height: 14px;
+ background-position: -5175px 0px;
+ }
+ .iti__flag.iti__ug {
+ height: 14px;
+ background-position: -5197px 0px;
+ }
+ .iti__flag.iti__um {
+ height: 11px;
+ background-position: -5219px 0px;
+ }
+ .iti__flag.iti__un {
+ height: 14px;
+ background-position: -5241px 0px;
+ }
+ .iti__flag.iti__us {
+ height: 11px;
+ background-position: -5263px 0px;
+ }
+ .iti__flag.iti__uy {
+ height: 14px;
+ background-position: -5285px 0px;
+ }
+ .iti__flag.iti__uz {
+ height: 10px;
+ background-position: -5307px 0px;
+ }
+ .iti__flag.iti__va {
+ height: 15px;
+ background-position: -5329px 0px;
+ }
+ .iti__flag.iti__vc {
+ height: 14px;
+ background-position: -5346px 0px;
+ }
+ .iti__flag.iti__ve {
+ height: 14px;
+ background-position: -5368px 0px;
+ }
+ .iti__flag.iti__vg {
+ height: 10px;
+ background-position: -5390px 0px;
+ }
+ .iti__flag.iti__vi {
+ height: 14px;
+ background-position: -5412px 0px;
+ }
+ .iti__flag.iti__vn {
+ height: 14px;
+ background-position: -5434px 0px;
+ }
+ .iti__flag.iti__vu {
+ height: 12px;
+ background-position: -5456px 0px;
+ }
+ .iti__flag.iti__wf {
+ height: 14px;
+ background-position: -5478px 0px;
+ }
+ .iti__flag.iti__ws {
+ height: 10px;
+ background-position: -5500px 0px;
+ }
+ .iti__flag.iti__xk {
+ height: 15px;
+ background-position: -5522px 0px;
+ }
+ .iti__flag.iti__ye {
+ height: 14px;
+ background-position: -5544px 0px;
+ }
+ .iti__flag.iti__yt {
+ height: 14px;
+ background-position: -5566px 0px;
+ }
+ .iti__flag.iti__za {
+ height: 14px;
+ background-position: -5588px 0px;
+ }
+ .iti__flag.iti__zm {
+ height: 14px;
+ background-position: -5610px 0px;
+ }
+ .iti__flag.iti__zw {
+ height: 10px;
+ background-position: -5632px 0px;
+ }
+
+ .iti__flag {
+ height: 15px;
+ box-shadow: 0px 0px 1px 0px #888;
+ background-image: url(${flagsImgUrl});
+ background-repeat: no-repeat;
+ background-color: #dbdbdb;
+ background-position: 20px 0;
+ }
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .iti__flag {
+ background-image: url(${flagsImg2xUrl});
+ }
+ }
+
+ .iti__flag.iti__np {
+ background-color: transparent;
+ }
+`;
diff --git a/docs/components/inputs/select-rich/src/flags.png b/docs/components/inputs/select-rich/src/flags.png
new file mode 100644
index 000000000..3fa1bec9f
Binary files /dev/null and b/docs/components/inputs/select-rich/src/flags.png differ
diff --git a/docs/components/inputs/select-rich/src/flags@2x.png b/docs/components/inputs/select-rich/src/flags@2x.png
new file mode 100644
index 000000000..7cc3ed3fb
Binary files /dev/null and b/docs/components/inputs/select-rich/src/flags@2x.png differ
diff --git a/docs/components/inputs/select-rich/src/intl-select-rich.js b/docs/components/inputs/select-rich/src/intl-select-rich.js
new file mode 100644
index 000000000..699f1247b
--- /dev/null
+++ b/docs/components/inputs/select-rich/src/intl-select-rich.js
@@ -0,0 +1,193 @@
+import { html, css, LitElement } from '@lion/core';
+import { LionSelectRich, LionOption, LionSelectInvoker } from '@lion/select-rich';
+import { flagStyles } from './flagStyles.js';
+
+/**
+ * @typedef {import('@lion/core').RenderOptions} RenderOptions
+ * @typedef {import('@lion/input-tel/types/types').RegionAndCountryCode} RegionAndCountryCode
+ * @typedef {import('@lion/input-tel/types/types').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
+ * @typedef {{countryCode: string; regionCode: string; nameForRegion: string; nameForLocale: string}} RegionMetaList
+ * @typedef {TemplateDataForDropdownInputTel & {data: {regionMetaList:RegionMetaList}}} TemplateDataForIntlInputTel
+ */
+
+export class IntlOption extends LionOption {
+ static properties = { regionMeta: { type: Object } };
+
+ static styles = [
+ super.styles,
+ flagStyles,
+ css`
+ :host {
+ padding: 5px 10px;
+ outline: none;
+ }
+
+ :host(:hover),
+ :host([active]),
+ :host([checked]) {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ `,
+ ];
+
+ get _regionCode() {
+ return this.choiceValue?.toUpperCase();
+ }
+
+ render() {
+ const ctor = /** @type {typeof IntlOption} */ (this.constructor);
+ return ctor._contentTemplate({
+ data: this.regionMeta,
+ });
+ }
+
+ static _contentTemplate({ data: { regionCode, countryCode, nameForLocale, nameForRegion } }) {
+ return html`
+
+ ${nameForLocale} (${nameForRegion})
+ +${countryCode}
+ `;
+ }
+}
+customElements.define('intl-option', IntlOption);
+
+class IntlSelectInvoker extends LionSelectInvoker {
+ /**
+ * @configure LitElement
+ * @enhance LionSelectInvoker
+ */
+ static styles = [
+ super.styles,
+ flagStyles,
+ css`
+ :host {
+ /** TODO: avoid importants; should actually be configured in overlay */
+ width: auto !important;
+ background-color: transparent;
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ }
+
+ #content-wrapper {
+ display: flex;
+ align-items: center;
+ }
+ `,
+ ];
+
+ /**
+ * @configure SlotMixin
+ * @override LionSelectInvoker
+ */
+ get slots() {
+ return {};
+ }
+
+ /**
+ * @override LionSelectInvoker
+ */
+ render() {
+ const ctor = /** @type {typeof LionSelectInvoker} */ (this.constructor);
+ return ctor._mainTemplate(this._templateData);
+ }
+
+ get _templateData() {
+ return {
+ data: { selectedElement: this.selectedElement, hostElement: this.hostElement },
+ };
+ }
+
+ static _mainTemplate(templateData) {
+ return html` ${this._contentTemplate(templateData)}
`;
+ }
+
+ static _contentTemplate({ data: { selectedElement, hostElement } }) {
+ if (!selectedElement) {
+ return ``;
+ }
+ return html`
+
+
+ `;
+ }
+}
+customElements.define('intl-select-invoker', IntlSelectInvoker);
+
+export class IntlSeparator extends LitElement {
+ static styles = [
+ css`
+ :host {
+ display: block;
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+ border-bottom: 1px solid #ccc;
+ }
+ `,
+ ];
+
+ /**
+ * @lifecycle platform
+ */
+ connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute('role', 'separator');
+ }
+}
+customElements.define('intl-separator', IntlSeparator);
+
+export class IntlSelectRich extends LionSelectRich {
+ static styles = [
+ super.styles,
+ css`
+ :host,
+ ::slotted(*) {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ color: #333;
+ }
+
+ ::slotted([role='listbox']) {
+ margin-left: -3px;
+ display: block;
+ white-space: nowrap;
+ max-height: 200px;
+ overflow-y: scroll;
+ position: absolute;
+ z-index: 2;
+ list-style: none;
+ text-align: left;
+ padding: 0;
+ margin: 0 0 0 -1px;
+ box-shadow: 1px 1px 4px rgb(0 0 0 / 20%);
+ background-color: white;
+ border: 1px solid #ccc;
+ -webkit-overflow-scrolling: touch;
+ outline: none;
+ }
+
+ .form-field__group-two,
+ .input-group,
+ .input-group__container,
+ .input-group__input {
+ height: 100%;
+ }
+ `,
+ ];
+
+ /**
+ * @configure ScopedElementsMixin
+ */
+ static scopedElements = { 'intl-select-invoker': IntlSelectInvoker };
+
+ /**
+ * @configure SlotMixin
+ */
+ slots = {
+ ...super.slots,
+ invoker: () => html``,
+ };
+}
+customElements.define('intl-select-rich', IntlSelectRich);
diff --git a/docs/components/inputs/select-rich/src/regionMetaList.js b/docs/components/inputs/select-rich/src/regionMetaList.js
new file mode 100644
index 000000000..6fee46ece
--- /dev/null
+++ b/docs/components/inputs/select-rich/src/regionMetaList.js
@@ -0,0 +1,1717 @@
+export const regionMetaList = [
+ {
+ regionCode: 'AC',
+ countryCode: 247,
+ flagSymbol: '🇦🇨',
+ nameForLocale: 'Ascension Island',
+ nameForRegion: 'Ascension Island',
+ },
+ {
+ regionCode: 'AD',
+ countryCode: 376,
+ flagSymbol: '🇦🇩',
+ nameForLocale: 'Andorra',
+ nameForRegion: 'Andorra',
+ },
+ {
+ regionCode: 'AE',
+ countryCode: 971,
+ flagSymbol: '🇦🇪',
+ nameForLocale: 'United Arab Emirates',
+ nameForRegion: 'United Arab Emirates',
+ },
+ {
+ regionCode: 'AF',
+ countryCode: 93,
+ flagSymbol: '🇦🇫',
+ nameForLocale: 'Afghanistan',
+ nameForRegion: 'Afghanistan',
+ },
+ {
+ regionCode: 'AG',
+ countryCode: 1,
+ flagSymbol: '🇦🇬',
+ nameForLocale: 'Antigua & Barbuda',
+ nameForRegion: 'Antigua & Barbuda',
+ },
+ {
+ regionCode: 'AI',
+ countryCode: 1,
+ flagSymbol: '🇦🇮',
+ nameForLocale: 'Anguilla',
+ nameForRegion: 'Anguilla',
+ },
+ {
+ regionCode: 'AL',
+ countryCode: 355,
+ flagSymbol: '🇦🇱',
+ nameForLocale: 'Albania',
+ nameForRegion: 'Albania',
+ },
+ {
+ regionCode: 'AM',
+ countryCode: 374,
+ flagSymbol: '🇦🇲',
+ nameForLocale: 'Armenia',
+ nameForRegion: 'አርሜኒያ',
+ },
+ {
+ regionCode: 'AO',
+ countryCode: 244,
+ flagSymbol: '🇦🇴',
+ nameForLocale: 'Angola',
+ nameForRegion: 'Angola',
+ },
+ {
+ regionCode: 'AR',
+ countryCode: 54,
+ flagSymbol: '🇦🇷',
+ nameForLocale: 'Argentina',
+ nameForRegion: 'الأرجنتين',
+ },
+ {
+ regionCode: 'AS',
+ countryCode: 1,
+ flagSymbol: '🇦🇸',
+ nameForLocale: 'American Samoa',
+ nameForRegion: 'American Samoa',
+ },
+ {
+ regionCode: 'AT',
+ countryCode: 43,
+ flagSymbol: '🇦🇹',
+ nameForLocale: 'Austria',
+ nameForRegion: 'Austria',
+ },
+ {
+ regionCode: 'AU',
+ countryCode: 61,
+ flagSymbol: '🇦🇺',
+ nameForLocale: 'Australia',
+ nameForRegion: 'Australia',
+ },
+ {
+ regionCode: 'AW',
+ countryCode: 297,
+ flagSymbol: '🇦🇼',
+ nameForLocale: 'Aruba',
+ nameForRegion: 'Aruba',
+ },
+ {
+ regionCode: 'AX',
+ countryCode: 358,
+ flagSymbol: '🇦🇽',
+ nameForLocale: 'Åland Islands',
+ nameForRegion: 'Åland Islands',
+ },
+ {
+ regionCode: 'AZ',
+ countryCode: 994,
+ flagSymbol: '🇦🇿',
+ nameForLocale: 'Azerbaijan',
+ nameForRegion: 'Azerbaijan',
+ },
+ {
+ regionCode: 'BA',
+ countryCode: 387,
+ flagSymbol: '🇧🇦',
+ nameForLocale: 'Bosnia & Herzegovina',
+ nameForRegion: 'Bosnia & Herzegovina',
+ },
+ {
+ regionCode: 'BB',
+ countryCode: 1,
+ flagSymbol: '🇧🇧',
+ nameForLocale: 'Barbados',
+ nameForRegion: 'Barbados',
+ },
+ {
+ regionCode: 'BD',
+ countryCode: 880,
+ flagSymbol: '🇧🇩',
+ nameForLocale: 'Bangladesh',
+ nameForRegion: 'Bangladesh',
+ },
+ {
+ regionCode: 'BE',
+ countryCode: 32,
+ flagSymbol: '🇧🇪',
+ nameForLocale: 'Belgium',
+ nameForRegion: 'Belgium',
+ },
+ {
+ regionCode: 'BF',
+ countryCode: 226,
+ flagSymbol: '🇧🇫',
+ nameForLocale: 'Burkina Faso',
+ nameForRegion: 'Burkina Faso',
+ },
+ {
+ regionCode: 'BG',
+ countryCode: 359,
+ flagSymbol: '🇧🇬',
+ nameForLocale: 'Bulgaria',
+ nameForRegion: 'България',
+ },
+ {
+ regionCode: 'BH',
+ countryCode: 973,
+ flagSymbol: '🇧🇭',
+ nameForLocale: 'Bahrain',
+ nameForRegion: 'Bahrain',
+ },
+ {
+ regionCode: 'BI',
+ countryCode: 257,
+ flagSymbol: '🇧🇮',
+ nameForLocale: 'Burundi',
+ nameForRegion: 'Burundi',
+ },
+ {
+ regionCode: 'BJ',
+ countryCode: 229,
+ flagSymbol: '🇧🇯',
+ nameForLocale: 'Benin',
+ nameForRegion: 'Benin',
+ },
+ {
+ regionCode: 'BL',
+ countryCode: 590,
+ flagSymbol: '🇧🇱',
+ nameForLocale: 'St. Barthélemy',
+ nameForRegion: 'St Barthélemy',
+ },
+ {
+ regionCode: 'BM',
+ countryCode: 1,
+ flagSymbol: '🇧🇲',
+ nameForLocale: 'Bermuda',
+ nameForRegion: 'Bermuda',
+ },
+ {
+ regionCode: 'BN',
+ countryCode: 673,
+ flagSymbol: '🇧🇳',
+ nameForLocale: 'Brunei',
+ nameForRegion: 'ব্রুনেই',
+ },
+ {
+ regionCode: 'BO',
+ countryCode: 591,
+ flagSymbol: '🇧🇴',
+ nameForLocale: 'Bolivia',
+ nameForRegion: 'Bolivia',
+ },
+ {
+ regionCode: 'BQ',
+ countryCode: 599,
+ flagSymbol: '🇧🇶',
+ nameForLocale: 'Caribbean Netherlands',
+ nameForRegion: 'Caribbean Netherlands',
+ },
+ {
+ regionCode: 'BR',
+ countryCode: 55,
+ flagSymbol: '🇧🇷',
+ nameForLocale: 'Brazil',
+ nameForRegion: 'Brazil',
+ },
+ {
+ regionCode: 'BS',
+ countryCode: 1,
+ flagSymbol: '🇧🇸',
+ nameForLocale: 'Bahamas',
+ nameForRegion: 'Bahamas',
+ },
+ {
+ regionCode: 'BT',
+ countryCode: 975,
+ flagSymbol: '🇧🇹',
+ nameForLocale: 'Bhutan',
+ nameForRegion: 'Bhutan',
+ },
+ {
+ regionCode: 'BW',
+ countryCode: 267,
+ flagSymbol: '🇧🇼',
+ nameForLocale: 'Botswana',
+ nameForRegion: 'Botswana',
+ },
+ {
+ regionCode: 'BY',
+ countryCode: 375,
+ flagSymbol: '🇧🇾',
+ nameForLocale: 'Belarus',
+ nameForRegion: 'Belarus',
+ },
+ {
+ regionCode: 'BZ',
+ countryCode: 501,
+ flagSymbol: '🇧🇿',
+ nameForLocale: 'Belize',
+ nameForRegion: 'Belize',
+ },
+ {
+ regionCode: 'CA',
+ countryCode: 1,
+ flagSymbol: '🇨🇦',
+ nameForLocale: 'Canada',
+ nameForRegion: 'Canadà',
+ },
+ {
+ regionCode: 'CC',
+ countryCode: 61,
+ flagSymbol: '🇨🇨',
+ nameForLocale: 'Cocos (Keeling) Islands',
+ nameForRegion: 'Cocos (Keeling) Islands',
+ },
+ {
+ regionCode: 'CD',
+ countryCode: 243,
+ flagSymbol: '🇨🇩',
+ nameForLocale: 'Congo - Kinshasa',
+ nameForRegion: 'Congo - Kinshasa',
+ },
+ {
+ regionCode: 'CF',
+ countryCode: 236,
+ flagSymbol: '🇨🇫',
+ nameForLocale: 'Central African Republic',
+ nameForRegion: 'Central African Republic',
+ },
+ {
+ regionCode: 'CG',
+ countryCode: 242,
+ flagSymbol: '🇨🇬',
+ nameForLocale: 'Congo - Brazzaville',
+ nameForRegion: 'Congo - Brazzaville',
+ },
+ {
+ regionCode: 'CH',
+ countryCode: 41,
+ flagSymbol: '🇨🇭',
+ nameForLocale: 'Switzerland',
+ nameForRegion: 'Switzerland',
+ },
+ {
+ regionCode: 'CI',
+ countryCode: 225,
+ flagSymbol: '🇨🇮',
+ nameForLocale: 'Côte d’Ivoire',
+ nameForRegion: 'Côte d’Ivoire',
+ },
+ {
+ regionCode: 'CK',
+ countryCode: 682,
+ flagSymbol: '🇨🇰',
+ nameForLocale: 'Cook Islands',
+ nameForRegion: 'Cook Islands',
+ },
+ {
+ regionCode: 'CL',
+ countryCode: 56,
+ flagSymbol: '🇨🇱',
+ nameForLocale: 'Chile',
+ nameForRegion: 'Chile',
+ },
+ {
+ regionCode: 'CM',
+ countryCode: 237,
+ flagSymbol: '🇨🇲',
+ nameForLocale: 'Cameroon',
+ nameForRegion: 'Cameroon',
+ },
+ {
+ regionCode: 'CN',
+ countryCode: 86,
+ flagSymbol: '🇨🇳',
+ nameForLocale: 'China',
+ nameForRegion: 'China',
+ },
+ {
+ regionCode: 'CO',
+ countryCode: 57,
+ flagSymbol: '🇨🇴',
+ nameForLocale: 'Colombia',
+ nameForRegion: 'Colombia',
+ },
+ {
+ regionCode: 'CR',
+ countryCode: 506,
+ flagSymbol: '🇨🇷',
+ nameForLocale: 'Costa Rica',
+ nameForRegion: 'Costa Rica',
+ },
+ {
+ regionCode: 'CU',
+ countryCode: 53,
+ flagSymbol: '🇨🇺',
+ nameForLocale: 'Cuba',
+ nameForRegion: 'Cuba',
+ },
+ {
+ regionCode: 'CV',
+ countryCode: 238,
+ flagSymbol: '🇨🇻',
+ nameForLocale: 'Cape Verde',
+ nameForRegion: 'Cape Verde',
+ },
+ {
+ regionCode: 'CW',
+ countryCode: 599,
+ flagSymbol: '🇨🇼',
+ nameForLocale: 'Curaçao',
+ nameForRegion: 'Curaçao',
+ },
+ {
+ regionCode: 'CX',
+ countryCode: 61,
+ flagSymbol: '🇨🇽',
+ nameForLocale: 'Christmas Island',
+ nameForRegion: 'Christmas Island',
+ },
+ {
+ regionCode: 'CY',
+ countryCode: 357,
+ flagSymbol: '🇨🇾',
+ nameForLocale: 'Cyprus',
+ nameForRegion: 'Cyprus',
+ },
+ {
+ regionCode: 'CZ',
+ countryCode: 420,
+ flagSymbol: '🇨🇿',
+ nameForLocale: 'Czechia',
+ nameForRegion: 'Czechia',
+ },
+ {
+ regionCode: 'DE',
+ countryCode: 49,
+ flagSymbol: '🇩🇪',
+ nameForLocale: 'Germany',
+ nameForRegion: 'Deutschland',
+ },
+ {
+ regionCode: 'DJ',
+ countryCode: 253,
+ flagSymbol: '🇩🇯',
+ nameForLocale: 'Djibouti',
+ nameForRegion: 'Djibouti',
+ },
+ {
+ regionCode: 'DK',
+ countryCode: 45,
+ flagSymbol: '🇩🇰',
+ nameForLocale: 'Denmark',
+ nameForRegion: 'Denmark',
+ },
+ {
+ regionCode: 'DM',
+ countryCode: 1,
+ flagSymbol: '🇩🇲',
+ nameForLocale: 'Dominica',
+ nameForRegion: 'Dominica',
+ },
+ {
+ regionCode: 'DO',
+ countryCode: 1,
+ flagSymbol: '🇩🇴',
+ nameForLocale: 'Dominican Republic',
+ nameForRegion: 'Dominican Republic',
+ },
+ {
+ regionCode: 'DZ',
+ countryCode: 213,
+ flagSymbol: '🇩🇿',
+ nameForLocale: 'Algeria',
+ nameForRegion: 'Algeria',
+ },
+ {
+ regionCode: 'EC',
+ countryCode: 593,
+ flagSymbol: '🇪🇨',
+ nameForLocale: 'Ecuador',
+ nameForRegion: 'Ecuador',
+ },
+ {
+ regionCode: 'EE',
+ countryCode: 372,
+ flagSymbol: '🇪🇪',
+ nameForLocale: 'Estonia',
+ nameForRegion: 'Estonia',
+ },
+ {
+ regionCode: 'EG',
+ countryCode: 20,
+ flagSymbol: '🇪🇬',
+ nameForLocale: 'Egypt',
+ nameForRegion: 'Egypt',
+ },
+ {
+ regionCode: 'EH',
+ countryCode: 212,
+ flagSymbol: '🇪🇭',
+ nameForLocale: 'Western Sahara',
+ nameForRegion: 'Western Sahara',
+ },
+ {
+ regionCode: 'ER',
+ countryCode: 291,
+ flagSymbol: '🇪🇷',
+ nameForLocale: 'Eritrea',
+ nameForRegion: 'Eritrea',
+ },
+ {
+ regionCode: 'ES',
+ countryCode: 34,
+ flagSymbol: '🇪🇸',
+ nameForLocale: 'Spain',
+ nameForRegion: 'España',
+ },
+ {
+ regionCode: 'ET',
+ countryCode: 251,
+ flagSymbol: '🇪🇹',
+ nameForLocale: 'Ethiopia',
+ nameForRegion: 'Etioopia',
+ },
+ {
+ regionCode: 'FI',
+ countryCode: 358,
+ flagSymbol: '🇫🇮',
+ nameForLocale: 'Finland',
+ nameForRegion: 'Suomi',
+ },
+ {
+ regionCode: 'FJ',
+ countryCode: 679,
+ flagSymbol: '🇫🇯',
+ nameForLocale: 'Fiji',
+ nameForRegion: 'Fiji',
+ },
+ {
+ regionCode: 'FK',
+ countryCode: 500,
+ flagSymbol: '🇫🇰',
+ nameForLocale: 'Falkland Islands (Islas Malvinas)',
+ nameForRegion: 'Falkland Islands (Islas Malvinas)',
+ },
+ {
+ regionCode: 'FM',
+ countryCode: 691,
+ flagSymbol: '🇫🇲',
+ nameForLocale: 'Micronesia',
+ nameForRegion: 'Micronesia',
+ },
+ {
+ regionCode: 'FO',
+ countryCode: 298,
+ flagSymbol: '🇫🇴',
+ nameForLocale: 'Faroe Islands',
+ nameForRegion: 'Faroe Islands',
+ },
+ {
+ regionCode: 'FR',
+ countryCode: 33,
+ flagSymbol: '🇫🇷',
+ nameForLocale: 'France',
+ nameForRegion: 'France',
+ },
+ {
+ regionCode: 'GA',
+ countryCode: 241,
+ flagSymbol: '🇬🇦',
+ nameForLocale: 'Gabon',
+ nameForRegion: 'Gabon',
+ },
+ {
+ regionCode: 'GB',
+ countryCode: 44,
+ flagSymbol: '🇬🇧',
+ nameForLocale: 'United Kingdom',
+ nameForRegion: 'United Kingdom',
+ },
+ {
+ regionCode: 'GD',
+ countryCode: 1,
+ flagSymbol: '🇬🇩',
+ nameForLocale: 'Grenada',
+ nameForRegion: 'Grenada',
+ },
+ {
+ regionCode: 'GE',
+ countryCode: 995,
+ flagSymbol: '🇬🇪',
+ nameForLocale: 'Georgia',
+ nameForRegion: 'Georgia',
+ },
+ {
+ regionCode: 'GF',
+ countryCode: 594,
+ flagSymbol: '🇬🇫',
+ nameForLocale: 'French Guiana',
+ nameForRegion: 'French Guiana',
+ },
+ {
+ regionCode: 'GG',
+ countryCode: 44,
+ flagSymbol: '🇬🇬',
+ nameForLocale: 'Guernsey',
+ nameForRegion: 'Guernsey',
+ },
+ {
+ regionCode: 'GH',
+ countryCode: 233,
+ flagSymbol: '🇬🇭',
+ nameForLocale: 'Ghana',
+ nameForRegion: 'Ghana',
+ },
+ {
+ regionCode: 'GI',
+ countryCode: 350,
+ flagSymbol: '🇬🇮',
+ nameForLocale: 'Gibraltar',
+ nameForRegion: 'Gibraltar',
+ },
+ {
+ regionCode: 'GL',
+ countryCode: 299,
+ flagSymbol: '🇬🇱',
+ nameForLocale: 'Greenland',
+ nameForRegion: 'Greenland',
+ },
+ {
+ regionCode: 'GM',
+ countryCode: 220,
+ flagSymbol: '🇬🇲',
+ nameForLocale: 'Gambia',
+ nameForRegion: 'Gambia',
+ },
+ {
+ regionCode: 'GN',
+ countryCode: 224,
+ flagSymbol: '🇬🇳',
+ nameForLocale: 'Guinea',
+ nameForRegion: 'Guinea',
+ },
+ {
+ regionCode: 'GP',
+ countryCode: 590,
+ flagSymbol: '🇬🇵',
+ nameForLocale: 'Guadeloupe',
+ nameForRegion: 'Guadeloupe',
+ },
+ {
+ regionCode: 'GQ',
+ countryCode: 240,
+ flagSymbol: '🇬🇶',
+ nameForLocale: 'Equatorial Guinea',
+ nameForRegion: 'Equatorial Guinea',
+ },
+ {
+ regionCode: 'GR',
+ countryCode: 30,
+ flagSymbol: '🇬🇷',
+ nameForLocale: 'Greece',
+ nameForRegion: 'Greece',
+ },
+ {
+ regionCode: 'GT',
+ countryCode: 502,
+ flagSymbol: '🇬🇹',
+ nameForLocale: 'Guatemala',
+ nameForRegion: 'Guatemala',
+ },
+ {
+ regionCode: 'GU',
+ countryCode: 1,
+ flagSymbol: '🇬🇺',
+ nameForLocale: 'Guam',
+ nameForRegion: 'ગ્વામ',
+ },
+ {
+ regionCode: 'GW',
+ countryCode: 245,
+ flagSymbol: '🇬🇼',
+ nameForLocale: 'Guinea-Bissau',
+ nameForRegion: 'Guinea-Bissau',
+ },
+ {
+ regionCode: 'GY',
+ countryCode: 592,
+ flagSymbol: '🇬🇾',
+ nameForLocale: 'Guyana',
+ nameForRegion: 'Guyana',
+ },
+ {
+ regionCode: 'HK',
+ countryCode: 852,
+ flagSymbol: '🇭🇰',
+ nameForLocale: 'Hong Kong',
+ nameForRegion: 'Hong Kong',
+ },
+ {
+ regionCode: 'HN',
+ countryCode: 504,
+ flagSymbol: '🇭🇳',
+ nameForLocale: 'Honduras',
+ nameForRegion: 'Honduras',
+ },
+ {
+ regionCode: 'HR',
+ countryCode: 385,
+ flagSymbol: '🇭🇷',
+ nameForLocale: 'Croatia',
+ nameForRegion: 'Hrvatska',
+ },
+ {
+ regionCode: 'HT',
+ countryCode: 509,
+ flagSymbol: '🇭🇹',
+ nameForLocale: 'Haiti',
+ nameForRegion: 'Haiti',
+ },
+ {
+ regionCode: 'HU',
+ countryCode: 36,
+ flagSymbol: '🇭🇺',
+ nameForLocale: 'Hungary',
+ nameForRegion: 'Magyarország',
+ },
+ {
+ regionCode: 'ID',
+ countryCode: 62,
+ flagSymbol: '🇮🇩',
+ nameForLocale: 'Indonesia',
+ nameForRegion: 'Indonesia',
+ },
+ {
+ regionCode: 'IE',
+ countryCode: 353,
+ flagSymbol: '🇮🇪',
+ nameForLocale: 'Ireland',
+ nameForRegion: 'Ireland',
+ },
+ {
+ regionCode: 'IL',
+ countryCode: 972,
+ flagSymbol: '🇮🇱',
+ nameForLocale: 'Israel',
+ nameForRegion: 'Israel',
+ },
+ {
+ regionCode: 'IM',
+ countryCode: 44,
+ flagSymbol: '🇮🇲',
+ nameForLocale: 'Isle of Man',
+ nameForRegion: 'Isle of Man',
+ },
+ {
+ regionCode: 'IN',
+ countryCode: 91,
+ flagSymbol: '🇮🇳',
+ nameForLocale: 'India',
+ nameForRegion: 'India',
+ },
+ {
+ regionCode: 'IO',
+ countryCode: 246,
+ flagSymbol: '🇮🇴',
+ nameForLocale: 'British Indian Ocean Territory',
+ nameForRegion: 'British Indian Ocean Territory',
+ },
+ {
+ regionCode: 'IQ',
+ countryCode: 964,
+ flagSymbol: '🇮🇶',
+ nameForLocale: 'Iraq',
+ nameForRegion: 'Iraq',
+ },
+ {
+ regionCode: 'IR',
+ countryCode: 98,
+ flagSymbol: '🇮🇷',
+ nameForLocale: 'Iran',
+ nameForRegion: 'Iran',
+ },
+ {
+ regionCode: 'IS',
+ countryCode: 354,
+ flagSymbol: '🇮🇸',
+ nameForLocale: 'Iceland',
+ nameForRegion: 'Iceland',
+ },
+ {
+ regionCode: 'IT',
+ countryCode: 39,
+ flagSymbol: '🇮🇹',
+ nameForLocale: 'Italy',
+ nameForRegion: 'Italia',
+ },
+ {
+ regionCode: 'JE',
+ countryCode: 44,
+ flagSymbol: '🇯🇪',
+ nameForLocale: 'Jersey',
+ nameForRegion: 'Jersey',
+ },
+ {
+ regionCode: 'JM',
+ countryCode: 1,
+ flagSymbol: '🇯🇲',
+ nameForLocale: 'Jamaica',
+ nameForRegion: 'Jamaica',
+ },
+ {
+ regionCode: 'JO',
+ countryCode: 962,
+ flagSymbol: '🇯🇴',
+ nameForLocale: 'Jordan',
+ nameForRegion: 'Jordan',
+ },
+ {
+ regionCode: 'JP',
+ countryCode: 81,
+ flagSymbol: '🇯🇵',
+ nameForLocale: 'Japan',
+ nameForRegion: 'Japan',
+ },
+ {
+ regionCode: 'KE',
+ countryCode: 254,
+ flagSymbol: '🇰🇪',
+ nameForLocale: 'Kenya',
+ nameForRegion: 'Kenya',
+ },
+ {
+ regionCode: 'KG',
+ countryCode: 996,
+ flagSymbol: '🇰🇬',
+ nameForLocale: 'Kyrgyzstan',
+ nameForRegion: 'Kyrgyzstan',
+ },
+ {
+ regionCode: 'KH',
+ countryCode: 855,
+ flagSymbol: '🇰🇭',
+ nameForLocale: 'Cambodia',
+ nameForRegion: 'Cambodia',
+ },
+ {
+ regionCode: 'KI',
+ countryCode: 686,
+ flagSymbol: '🇰🇮',
+ nameForLocale: 'Kiribati',
+ nameForRegion: 'Kiribati',
+ },
+ {
+ regionCode: 'KM',
+ countryCode: 269,
+ flagSymbol: '🇰🇲',
+ nameForLocale: 'Comoros',
+ nameForRegion: 'Comoros',
+ },
+ {
+ regionCode: 'KN',
+ countryCode: 1,
+ flagSymbol: '🇰🇳',
+ nameForLocale: 'St. Kitts & Nevis',
+ nameForRegion: 'ಸೇಂಟ್ ಕಿಟ್ಸ್ ಮತ್ತು ನೆವಿಸ್',
+ },
+ {
+ regionCode: 'KP',
+ countryCode: 850,
+ flagSymbol: '🇰🇵',
+ nameForLocale: 'North Korea',
+ nameForRegion: 'North Korea',
+ },
+ {
+ regionCode: 'KR',
+ countryCode: 82,
+ flagSymbol: '🇰🇷',
+ nameForLocale: 'South Korea',
+ nameForRegion: 'South Korea',
+ },
+ {
+ regionCode: 'KW',
+ countryCode: 965,
+ flagSymbol: '🇰🇼',
+ nameForLocale: 'Kuwait',
+ nameForRegion: 'Kuwait',
+ },
+ {
+ regionCode: 'KY',
+ countryCode: 1,
+ flagSymbol: '🇰🇾',
+ nameForLocale: 'Cayman Islands',
+ nameForRegion: 'Cayman Islands',
+ },
+ {
+ regionCode: 'KZ',
+ countryCode: 7,
+ flagSymbol: '🇰🇿',
+ nameForLocale: 'Kazakhstan',
+ nameForRegion: 'Kazakhstan',
+ },
+ {
+ regionCode: 'LA',
+ countryCode: 856,
+ flagSymbol: '🇱🇦',
+ nameForLocale: 'Laos',
+ nameForRegion: 'Laos',
+ },
+ {
+ regionCode: 'LB',
+ countryCode: 961,
+ flagSymbol: '🇱🇧',
+ nameForLocale: 'Lebanon',
+ nameForRegion: 'Lebanon',
+ },
+ {
+ regionCode: 'LC',
+ countryCode: 1,
+ flagSymbol: '🇱🇨',
+ nameForLocale: 'St. Lucia',
+ nameForRegion: 'St Lucia',
+ },
+ {
+ regionCode: 'LI',
+ countryCode: 423,
+ flagSymbol: '🇱🇮',
+ nameForLocale: 'Liechtenstein',
+ nameForRegion: 'Liechtenstein',
+ },
+ {
+ regionCode: 'LK',
+ countryCode: 94,
+ flagSymbol: '🇱🇰',
+ nameForLocale: 'Sri Lanka',
+ nameForRegion: 'Sri Lanka',
+ },
+ {
+ regionCode: 'LR',
+ countryCode: 231,
+ flagSymbol: '🇱🇷',
+ nameForLocale: 'Liberia',
+ nameForRegion: 'Liberia',
+ },
+ {
+ regionCode: 'LS',
+ countryCode: 266,
+ flagSymbol: '🇱🇸',
+ nameForLocale: 'Lesotho',
+ nameForRegion: 'Lesotho',
+ },
+ {
+ regionCode: 'LT',
+ countryCode: 370,
+ flagSymbol: '🇱🇹',
+ nameForLocale: 'Lithuania',
+ nameForRegion: 'Lietuva',
+ },
+ {
+ regionCode: 'LU',
+ countryCode: 352,
+ flagSymbol: '🇱🇺',
+ nameForLocale: 'Luxembourg',
+ nameForRegion: 'Luxembourg',
+ },
+ {
+ regionCode: 'LV',
+ countryCode: 371,
+ flagSymbol: '🇱🇻',
+ nameForLocale: 'Latvia',
+ nameForRegion: 'Latvija',
+ },
+ {
+ regionCode: 'LY',
+ countryCode: 218,
+ flagSymbol: '🇱🇾',
+ nameForLocale: 'Libya',
+ nameForRegion: 'Libya',
+ },
+ {
+ regionCode: 'MA',
+ countryCode: 212,
+ flagSymbol: '🇲🇦',
+ nameForLocale: 'Morocco',
+ nameForRegion: 'Morocco',
+ },
+ {
+ regionCode: 'MC',
+ countryCode: 377,
+ flagSymbol: '🇲🇨',
+ nameForLocale: 'Monaco',
+ nameForRegion: 'Monaco',
+ },
+ {
+ regionCode: 'MD',
+ countryCode: 373,
+ flagSymbol: '🇲🇩',
+ nameForLocale: 'Moldova',
+ nameForRegion: 'Moldova',
+ },
+ {
+ regionCode: 'ME',
+ countryCode: 382,
+ flagSymbol: '🇲🇪',
+ nameForLocale: 'Montenegro',
+ nameForRegion: 'Montenegro',
+ },
+ {
+ regionCode: 'MF',
+ countryCode: 590,
+ flagSymbol: '🇲🇫',
+ nameForLocale: 'St. Martin',
+ nameForRegion: 'St Martin',
+ },
+ {
+ regionCode: 'MG',
+ countryCode: 261,
+ flagSymbol: '🇲🇬',
+ nameForLocale: 'Madagascar',
+ nameForRegion: 'Madagascar',
+ },
+ {
+ regionCode: 'MH',
+ countryCode: 692,
+ flagSymbol: '🇲🇭',
+ nameForLocale: 'Marshall Islands',
+ nameForRegion: 'Marshall Islands',
+ },
+ {
+ regionCode: 'MK',
+ countryCode: 389,
+ flagSymbol: '🇲🇰',
+ nameForLocale: 'North Macedonia',
+ nameForRegion: 'North Macedonia',
+ },
+ {
+ regionCode: 'ML',
+ countryCode: 223,
+ flagSymbol: '🇲🇱',
+ nameForLocale: 'Mali',
+ nameForRegion: 'മാലി',
+ },
+ {
+ regionCode: 'MM',
+ countryCode: 95,
+ flagSymbol: '🇲🇲',
+ nameForLocale: 'Myanmar (Burma)',
+ nameForRegion: 'Myanmar (Burma)',
+ },
+ {
+ regionCode: 'MN',
+ countryCode: 976,
+ flagSymbol: '🇲🇳',
+ nameForLocale: 'Mongolia',
+ nameForRegion: 'Mongolia',
+ },
+ {
+ regionCode: 'MO',
+ countryCode: 853,
+ flagSymbol: '🇲🇴',
+ nameForLocale: 'Macao',
+ nameForRegion: 'Macao',
+ },
+ {
+ regionCode: 'MP',
+ countryCode: 1,
+ flagSymbol: '🇲🇵',
+ nameForLocale: 'Northern Mariana Islands',
+ nameForRegion: 'Northern Mariana Islands',
+ },
+ {
+ regionCode: 'MQ',
+ countryCode: 596,
+ flagSymbol: '🇲🇶',
+ nameForLocale: 'Martinique',
+ nameForRegion: 'Martinique',
+ },
+ {
+ regionCode: 'MR',
+ countryCode: 222,
+ flagSymbol: '🇲🇷',
+ nameForLocale: 'Mauritania',
+ nameForRegion: 'मॉरिटानिया',
+ },
+ {
+ regionCode: 'MS',
+ countryCode: 1,
+ flagSymbol: '🇲🇸',
+ nameForLocale: 'Montserrat',
+ nameForRegion: 'Montserrat',
+ },
+ {
+ regionCode: 'MT',
+ countryCode: 356,
+ flagSymbol: '🇲🇹',
+ nameForLocale: 'Malta',
+ nameForRegion: 'Malta',
+ },
+ {
+ regionCode: 'MU',
+ countryCode: 230,
+ flagSymbol: '🇲🇺',
+ nameForLocale: 'Mauritius',
+ nameForRegion: 'Mauritius',
+ },
+ {
+ regionCode: 'MV',
+ countryCode: 960,
+ flagSymbol: '🇲🇻',
+ nameForLocale: 'Maldives',
+ nameForRegion: 'Maldives',
+ },
+ {
+ regionCode: 'MW',
+ countryCode: 265,
+ flagSymbol: '🇲🇼',
+ nameForLocale: 'Malawi',
+ nameForRegion: 'Malawi',
+ },
+ {
+ regionCode: 'MX',
+ countryCode: 52,
+ flagSymbol: '🇲🇽',
+ nameForLocale: 'Mexico',
+ nameForRegion: 'Mexico',
+ },
+ {
+ regionCode: 'MY',
+ countryCode: 60,
+ flagSymbol: '🇲🇾',
+ nameForLocale: 'Malaysia',
+ nameForRegion: 'Malaysia',
+ },
+ {
+ regionCode: 'MZ',
+ countryCode: 258,
+ flagSymbol: '🇲🇿',
+ nameForLocale: 'Mozambique',
+ nameForRegion: 'Mozambique',
+ },
+ {
+ regionCode: 'NA',
+ countryCode: 264,
+ flagSymbol: '🇳🇦',
+ nameForLocale: 'Namibia',
+ nameForRegion: 'Namibia',
+ },
+ {
+ regionCode: 'NC',
+ countryCode: 687,
+ flagSymbol: '🇳🇨',
+ nameForLocale: 'New Caledonia',
+ nameForRegion: 'New Caledonia',
+ },
+ {
+ regionCode: 'NE',
+ countryCode: 227,
+ flagSymbol: '🇳🇪',
+ nameForLocale: 'Niger',
+ nameForRegion: 'Niger',
+ },
+ {
+ regionCode: 'NF',
+ countryCode: 672,
+ flagSymbol: '🇳🇫',
+ nameForLocale: 'Norfolk Island',
+ nameForRegion: 'Norfolk Island',
+ },
+ {
+ regionCode: 'NG',
+ countryCode: 234,
+ flagSymbol: '🇳🇬',
+ nameForLocale: 'Nigeria',
+ nameForRegion: 'Nigeria',
+ },
+ {
+ regionCode: 'NI',
+ countryCode: 505,
+ flagSymbol: '🇳🇮',
+ nameForLocale: 'Nicaragua',
+ nameForRegion: 'Nicaragua',
+ },
+ {
+ regionCode: 'NL',
+ countryCode: 31,
+ flagSymbol: '🇳🇱',
+ nameForLocale: 'Netherlands',
+ nameForRegion: 'Nederland',
+ },
+ {
+ regionCode: 'NO',
+ countryCode: 47,
+ flagSymbol: '🇳🇴',
+ nameForLocale: 'Norway',
+ nameForRegion: 'Norge',
+ },
+ {
+ regionCode: 'NP',
+ countryCode: 977,
+ flagSymbol: '🇳🇵',
+ nameForLocale: 'Nepal',
+ nameForRegion: 'Nepal',
+ },
+ {
+ regionCode: 'NR',
+ countryCode: 674,
+ flagSymbol: '🇳🇷',
+ nameForLocale: 'Nauru',
+ nameForRegion: 'Nauru',
+ },
+ {
+ regionCode: 'NU',
+ countryCode: 683,
+ flagSymbol: '🇳🇺',
+ nameForLocale: 'Niue',
+ nameForRegion: 'Niue',
+ },
+ {
+ regionCode: 'NZ',
+ countryCode: 64,
+ flagSymbol: '🇳🇿',
+ nameForLocale: 'New Zealand',
+ nameForRegion: 'New Zealand',
+ },
+ {
+ regionCode: 'OM',
+ countryCode: 968,
+ flagSymbol: '🇴🇲',
+ nameForLocale: 'Oman',
+ nameForRegion: 'Oman',
+ },
+ {
+ regionCode: 'PA',
+ countryCode: 507,
+ flagSymbol: '🇵🇦',
+ nameForLocale: 'Panama',
+ nameForRegion: 'Panama',
+ },
+ {
+ regionCode: 'PE',
+ countryCode: 51,
+ flagSymbol: '🇵🇪',
+ nameForLocale: 'Peru',
+ nameForRegion: 'Peru',
+ },
+ {
+ regionCode: 'PF',
+ countryCode: 689,
+ flagSymbol: '🇵🇫',
+ nameForLocale: 'French Polynesia',
+ nameForRegion: 'French Polynesia',
+ },
+ {
+ regionCode: 'PG',
+ countryCode: 675,
+ flagSymbol: '🇵🇬',
+ nameForLocale: 'Papua New Guinea',
+ nameForRegion: 'Papua New Guinea',
+ },
+ {
+ regionCode: 'PH',
+ countryCode: 63,
+ flagSymbol: '🇵🇭',
+ nameForLocale: 'Philippines',
+ nameForRegion: 'Philippines',
+ },
+ {
+ regionCode: 'PK',
+ countryCode: 92,
+ flagSymbol: '🇵🇰',
+ nameForLocale: 'Pakistan',
+ nameForRegion: 'Pakistan',
+ },
+ {
+ regionCode: 'PL',
+ countryCode: 48,
+ flagSymbol: '🇵🇱',
+ nameForLocale: 'Poland',
+ nameForRegion: 'Polska',
+ },
+ {
+ regionCode: 'PM',
+ countryCode: 508,
+ flagSymbol: '🇵🇲',
+ nameForLocale: 'St. Pierre & Miquelon',
+ nameForRegion: 'St Pierre & Miquelon',
+ },
+ {
+ regionCode: 'PR',
+ countryCode: 1,
+ flagSymbol: '🇵🇷',
+ nameForLocale: 'Puerto Rico',
+ nameForRegion: 'Puerto Rico',
+ },
+ {
+ regionCode: 'PS',
+ countryCode: 970,
+ flagSymbol: '🇵🇸',
+ nameForLocale: 'Palestine',
+ nameForRegion: 'Palestine',
+ },
+ {
+ regionCode: 'PT',
+ countryCode: 351,
+ flagSymbol: '🇵🇹',
+ nameForLocale: 'Portugal',
+ nameForRegion: 'Portugal',
+ },
+ {
+ regionCode: 'PW',
+ countryCode: 680,
+ flagSymbol: '🇵🇼',
+ nameForLocale: 'Palau',
+ nameForRegion: 'Palau',
+ },
+ {
+ regionCode: 'PY',
+ countryCode: 595,
+ flagSymbol: '🇵🇾',
+ nameForLocale: 'Paraguay',
+ nameForRegion: 'Paraguay',
+ },
+ {
+ regionCode: 'QA',
+ countryCode: 974,
+ flagSymbol: '🇶🇦',
+ nameForLocale: 'Qatar',
+ nameForRegion: 'Qatar',
+ },
+ {
+ regionCode: 'RE',
+ countryCode: 262,
+ flagSymbol: '🇷🇪',
+ nameForLocale: 'Réunion',
+ nameForRegion: 'Réunion',
+ },
+ {
+ regionCode: 'RO',
+ countryCode: 40,
+ flagSymbol: '🇷🇴',
+ nameForLocale: 'Romania',
+ nameForRegion: 'România',
+ },
+ {
+ regionCode: 'RS',
+ countryCode: 381,
+ flagSymbol: '🇷🇸',
+ nameForLocale: 'Serbia',
+ nameForRegion: 'Serbia',
+ },
+ {
+ regionCode: 'RU',
+ countryCode: 7,
+ flagSymbol: '🇷🇺',
+ nameForLocale: 'Russia',
+ nameForRegion: 'Россия',
+ },
+ {
+ regionCode: 'RW',
+ countryCode: 250,
+ flagSymbol: '🇷🇼',
+ nameForLocale: 'Rwanda',
+ nameForRegion: 'Rwanda',
+ },
+ {
+ regionCode: 'SA',
+ countryCode: 966,
+ flagSymbol: '🇸🇦',
+ nameForLocale: 'Saudi Arabia',
+ nameForRegion: 'Saudi Arabia',
+ },
+ {
+ regionCode: 'SB',
+ countryCode: 677,
+ flagSymbol: '🇸🇧',
+ nameForLocale: 'Solomon Islands',
+ nameForRegion: 'Solomon Islands',
+ },
+ {
+ regionCode: 'SC',
+ countryCode: 248,
+ flagSymbol: '🇸🇨',
+ nameForLocale: 'Seychelles',
+ nameForRegion: 'Seychelles',
+ },
+ {
+ regionCode: 'SD',
+ countryCode: 249,
+ flagSymbol: '🇸🇩',
+ nameForLocale: 'Sudan',
+ nameForRegion: 'Sudan',
+ },
+ {
+ regionCode: 'SE',
+ countryCode: 46,
+ flagSymbol: '🇸🇪',
+ nameForLocale: 'Sweden',
+ nameForRegion: 'Sweden',
+ },
+ {
+ regionCode: 'SG',
+ countryCode: 65,
+ flagSymbol: '🇸🇬',
+ nameForLocale: 'Singapore',
+ nameForRegion: 'Singapore',
+ },
+ {
+ regionCode: 'SH',
+ countryCode: 290,
+ flagSymbol: '🇸🇭',
+ nameForLocale: 'St. Helena',
+ nameForRegion: 'Sveta Jelena',
+ },
+ {
+ regionCode: 'SI',
+ countryCode: 386,
+ flagSymbol: '🇸🇮',
+ nameForLocale: 'Slovenia',
+ nameForRegion: 'Slovenia',
+ },
+ {
+ regionCode: 'SJ',
+ countryCode: 47,
+ flagSymbol: '🇸🇯',
+ nameForLocale: 'Svalbard & Jan Mayen',
+ nameForRegion: 'Svalbard & Jan Mayen',
+ },
+ {
+ regionCode: 'SK',
+ countryCode: 421,
+ flagSymbol: '🇸🇰',
+ nameForLocale: 'Slovakia',
+ nameForRegion: 'Slovensko',
+ },
+ {
+ regionCode: 'SL',
+ countryCode: 232,
+ flagSymbol: '🇸🇱',
+ nameForLocale: 'Sierra Leone',
+ nameForRegion: 'Sierra Leone',
+ },
+ {
+ regionCode: 'SM',
+ countryCode: 378,
+ flagSymbol: '🇸🇲',
+ nameForLocale: 'San Marino',
+ nameForRegion: 'San Marino',
+ },
+ {
+ regionCode: 'SN',
+ countryCode: 221,
+ flagSymbol: '🇸🇳',
+ nameForLocale: 'Senegal',
+ nameForRegion: 'Senegal',
+ },
+ {
+ regionCode: 'SO',
+ countryCode: 252,
+ flagSymbol: '🇸🇴',
+ nameForLocale: 'Somalia',
+ nameForRegion: 'Somalia',
+ },
+ {
+ regionCode: 'SR',
+ countryCode: 597,
+ flagSymbol: '🇸🇷',
+ nameForLocale: 'Suriname',
+ nameForRegion: 'Суринам',
+ },
+ {
+ regionCode: 'SS',
+ countryCode: 211,
+ flagSymbol: '🇸🇸',
+ nameForLocale: 'South Sudan',
+ nameForRegion: 'South Sudan',
+ },
+ {
+ regionCode: 'ST',
+ countryCode: 239,
+ flagSymbol: '🇸🇹',
+ nameForLocale: 'São Tomé & Príncipe',
+ nameForRegion: 'São Tomé & Príncipe',
+ },
+ {
+ regionCode: 'SV',
+ countryCode: 503,
+ flagSymbol: '🇸🇻',
+ nameForLocale: 'El Salvador',
+ nameForRegion: 'El Salvador',
+ },
+ {
+ regionCode: 'SX',
+ countryCode: 1,
+ flagSymbol: '🇸🇽',
+ nameForLocale: 'Sint Maarten',
+ nameForRegion: 'Sint Maarten',
+ },
+ {
+ regionCode: 'SY',
+ countryCode: 963,
+ flagSymbol: '🇸🇾',
+ nameForLocale: 'Syria',
+ nameForRegion: 'Syria',
+ },
+ {
+ regionCode: 'SZ',
+ countryCode: 268,
+ flagSymbol: '🇸🇿',
+ nameForLocale: 'Eswatini',
+ nameForRegion: 'Eswatini',
+ },
+ {
+ regionCode: 'TA',
+ countryCode: 290,
+ flagSymbol: '🇹🇦',
+ nameForLocale: 'Tristan da Cunha',
+ nameForRegion: 'டிரிஸ்டன் டா குன்ஹா',
+ },
+ {
+ regionCode: 'TC',
+ countryCode: 1,
+ flagSymbol: '🇹🇨',
+ nameForLocale: 'Turks & Caicos Islands',
+ nameForRegion: 'Turks & Caicos Islands',
+ },
+ {
+ regionCode: 'TD',
+ countryCode: 235,
+ flagSymbol: '🇹🇩',
+ nameForLocale: 'Chad',
+ nameForRegion: 'Chad',
+ },
+ {
+ regionCode: 'TG',
+ countryCode: 228,
+ flagSymbol: '🇹🇬',
+ nameForLocale: 'Togo',
+ nameForRegion: 'Togo',
+ },
+ {
+ regionCode: 'TH',
+ countryCode: 66,
+ flagSymbol: '🇹🇭',
+ nameForLocale: 'Thailand',
+ nameForRegion: 'ไทย',
+ },
+ {
+ regionCode: 'TJ',
+ countryCode: 992,
+ flagSymbol: '🇹🇯',
+ nameForLocale: 'Tajikistan',
+ nameForRegion: 'Tajikistan',
+ },
+ {
+ regionCode: 'TK',
+ countryCode: 690,
+ flagSymbol: '🇹🇰',
+ nameForLocale: 'Tokelau',
+ nameForRegion: 'Tokelau',
+ },
+ {
+ regionCode: 'TL',
+ countryCode: 670,
+ flagSymbol: '🇹🇱',
+ nameForLocale: 'Timor-Leste',
+ nameForRegion: 'Timor-Leste',
+ },
+ {
+ regionCode: 'TM',
+ countryCode: 993,
+ flagSymbol: '🇹🇲',
+ nameForLocale: 'Turkmenistan',
+ nameForRegion: 'Turkmenistan',
+ },
+ {
+ regionCode: 'TN',
+ countryCode: 216,
+ flagSymbol: '🇹🇳',
+ nameForLocale: 'Tunisia',
+ nameForRegion: 'Tunisia',
+ },
+ {
+ regionCode: 'TO',
+ countryCode: 676,
+ flagSymbol: '🇹🇴',
+ nameForLocale: 'Tonga',
+ nameForRegion: 'Tonga',
+ },
+ {
+ regionCode: 'TR',
+ countryCode: 90,
+ flagSymbol: '🇹🇷',
+ nameForLocale: 'Turkey',
+ nameForRegion: 'Türkiye',
+ },
+ {
+ regionCode: 'TT',
+ countryCode: 1,
+ flagSymbol: '🇹🇹',
+ nameForLocale: 'Trinidad & Tobago',
+ nameForRegion: 'Trinidad & Tobago',
+ },
+ {
+ regionCode: 'TV',
+ countryCode: 688,
+ flagSymbol: '🇹🇻',
+ nameForLocale: 'Tuvalu',
+ nameForRegion: 'Tuvalu',
+ },
+ {
+ regionCode: 'TW',
+ countryCode: 886,
+ flagSymbol: '🇹🇼',
+ nameForLocale: 'Taiwan',
+ nameForRegion: 'Taiwan',
+ },
+ {
+ regionCode: 'TZ',
+ countryCode: 255,
+ flagSymbol: '🇹🇿',
+ nameForLocale: 'Tanzania',
+ nameForRegion: 'Tanzania',
+ },
+ {
+ regionCode: 'UA',
+ countryCode: 380,
+ flagSymbol: '🇺🇦',
+ nameForLocale: 'Ukraine',
+ nameForRegion: 'Ukraine',
+ },
+ {
+ regionCode: 'UG',
+ countryCode: 256,
+ flagSymbol: '🇺🇬',
+ nameForLocale: 'Uganda',
+ nameForRegion: 'Uganda',
+ },
+ {
+ regionCode: 'US',
+ countryCode: 1,
+ flagSymbol: '🇺🇸',
+ nameForLocale: 'United States',
+ nameForRegion: 'United States',
+ },
+ {
+ regionCode: 'UY',
+ countryCode: 598,
+ flagSymbol: '🇺🇾',
+ nameForLocale: 'Uruguay',
+ nameForRegion: 'Uruguay',
+ },
+ {
+ regionCode: 'UZ',
+ countryCode: 998,
+ flagSymbol: '🇺🇿',
+ nameForLocale: 'Uzbekistan',
+ nameForRegion: 'Uzbekistan',
+ },
+ {
+ regionCode: 'VA',
+ countryCode: 39,
+ flagSymbol: '🇻🇦',
+ nameForLocale: 'Vatican City',
+ nameForRegion: 'Vatican City',
+ },
+ {
+ regionCode: 'VC',
+ countryCode: 1,
+ flagSymbol: '🇻🇨',
+ nameForLocale: 'St. Vincent & Grenadines',
+ nameForRegion: 'St Vincent & the Grenadines',
+ },
+ {
+ regionCode: 'VE',
+ countryCode: 58,
+ flagSymbol: '🇻🇪',
+ nameForLocale: 'Venezuela',
+ nameForRegion: 'Venezuela',
+ },
+ {
+ regionCode: 'VG',
+ countryCode: 1,
+ flagSymbol: '🇻🇬',
+ nameForLocale: 'British Virgin Islands',
+ nameForRegion: 'British Virgin Islands',
+ },
+ {
+ regionCode: 'VI',
+ countryCode: 1,
+ flagSymbol: '🇻🇮',
+ nameForLocale: 'U.S. Virgin Islands',
+ nameForRegion: 'Quần đảo Virgin thuộc Hoa Kỳ',
+ },
+ {
+ regionCode: 'VN',
+ countryCode: 84,
+ flagSymbol: '🇻🇳',
+ nameForLocale: 'Vietnam',
+ nameForRegion: 'Vietnam',
+ },
+ {
+ regionCode: 'VU',
+ countryCode: 678,
+ flagSymbol: '🇻🇺',
+ nameForLocale: 'Vanuatu',
+ nameForRegion: 'Vanuatu',
+ },
+ {
+ regionCode: 'WF',
+ countryCode: 681,
+ flagSymbol: '🇼🇫',
+ nameForLocale: 'Wallis & Futuna',
+ nameForRegion: 'Wallis & Futuna',
+ },
+ {
+ regionCode: 'WS',
+ countryCode: 685,
+ flagSymbol: '🇼🇸',
+ nameForLocale: 'Samoa',
+ nameForRegion: 'Samoa',
+ },
+ {
+ regionCode: 'XK',
+ countryCode: 383,
+ flagSymbol: '🇽🇰',
+ nameForLocale: 'Kosovo',
+ nameForRegion: 'Kosovo',
+ },
+ {
+ regionCode: 'YE',
+ countryCode: 967,
+ flagSymbol: '🇾🇪',
+ nameForLocale: 'Yemen',
+ nameForRegion: 'Yemen',
+ },
+ {
+ regionCode: 'YT',
+ countryCode: 262,
+ flagSymbol: '🇾🇹',
+ nameForLocale: 'Mayotte',
+ nameForRegion: 'Mayotte',
+ },
+ {
+ regionCode: 'ZA',
+ countryCode: 27,
+ flagSymbol: '🇿🇦',
+ nameForLocale: 'South Africa',
+ nameForRegion: 'South Africa',
+ },
+ {
+ regionCode: 'ZM',
+ countryCode: 260,
+ flagSymbol: '🇿🇲',
+ nameForLocale: 'Zambia',
+ nameForRegion: 'Zambia',
+ },
+ {
+ regionCode: 'ZW',
+ countryCode: 263,
+ flagSymbol: '🇿🇼',
+ nameForLocale: 'Zimbabwe',
+ nameForRegion: 'Zimbabwe',
+ },
+];
diff --git a/docs/docs/systems/form/assets/h-output.js b/docs/docs/systems/form/assets/h-output.js
index f172c94cb..7c8ccc2af 100644
--- a/docs/docs/systems/form/assets/h-output.js
+++ b/docs/docs/systems/form/assets/h-output.js
@@ -3,39 +3,67 @@ import { LionField } from '@lion/form-core';
import { LionFieldset } from '@lion/fieldset';
export class HelperOutput extends LitElement {
- static get properties() {
- return {
- field: Object,
- show: Array,
- };
- }
+ static properties = {
+ field: Object,
+ show: Array,
+ title: String,
+ readyPromise: Object,
+ };
- static get styles() {
- return [
- css`
- :host {
- display: block;
- margin-top: 16px;
- }
+ static styles = [
+ css`
+ :host {
+ display: block;
+ margin-top: 8px;
+ }
- table,
- th,
- td {
- border: 1px solid #ccc;
- padding: 4px;
- font-size: 12px;
- }
+ code {
+ font-size: 8px;
+ background-color: #eee;
+ }
- table {
- border-collapse: collapse;
- }
+ caption {
+ position: absolute;
+ top: 0;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
- caption {
- text-align: left;
- }
- `,
- ];
- }
+ table,
+ th,
+ td {
+ border-bottom: 1px solid rgb(204, 204, 204);
+ border-image: initial;
+ padding: 4px;
+ font-size: 12px;
+ border-left: none;
+ border-right: none;
+ text-align: left;
+ }
+
+ td {
+ max-width: 200px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ table {
+ border-collapse: collapse;
+ }
+
+ caption {
+ text-align: left;
+ }
+ `,
+ ];
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
@@ -47,15 +75,25 @@ export class HelperOutput extends LitElement {
}
}
this.__rerender = this.__rerender.bind(this);
- this.field.addEventListener('model-value-changed', this.__rerender);
- this.field.addEventListener('mousemove', this.__rerender);
- this.field.addEventListener('blur', this.__rerender);
- this.field.addEventListener('focusin', this.__rerender);
- this.field.addEventListener('focusout', this.__rerender);
+
+ const storyRoot = this.field.closest('[mdjs-story-name]');
+
+ storyRoot.addEventListener('model-value-changed', this.__rerender);
+ storyRoot.addEventListener('mousemove', this.__rerender);
+ // this.field.addEventListener('blur', this.__rerender);
+ storyRoot.addEventListener('focusin', this.__rerender);
+ storyRoot.addEventListener('focusout', this.__rerender);
+ storyRoot.addEventListener('change', this.__rerender);
if (this.field._inputNode.form) {
this.field._inputNode.form.addEventListener('submit', this.__rerender);
}
+
+ if (this.readyPromise) {
+ this.readyPromise.then(() => {
+ this.__rerender();
+ });
+ }
}
__rerender() {
@@ -67,29 +105,47 @@ export class HelperOutput extends LitElement {
}
// eslint-disable-next-line class-methods-use-this
- __renderProp(p) {
- if (typeof p === 'boolean') {
- return p === true ? '✓' : '';
+ __renderProp(prop) {
+ const field = this.field || {};
+ let resultText = '';
+
+ if (typeof prop === 'string') {
+ const p = field[prop];
+ if (typeof p === 'boolean') {
+ return p === true ? '✓' : '';
+ }
+ if (typeof p === 'undefined') {
+ return html`undefined`;
+ }
+ if (typeof p === 'object' && p !== null) {
+ return JSON.stringify(p);
+ }
+ resultText = p;
+ } else {
+ resultText = prop.processor(field);
}
- if (typeof p === 'undefined') {
- return '?';
- }
- return p;
+
+ return html`${resultText}`;
+ }
+
+ constructor() {
+ super();
+ this.title = 'States';
}
render() {
- const field = this.field || {};
+ const computePropName = prop => (typeof prop === 'string' ? prop : prop.name);
return html`
- Interaction States
+ ${this.title}
- ${this.show.map(prop => html`| ${prop} | `)}
+ ${this.show.map(prop => html`${computePropName(prop)} | `)}
- ${this.show.map(prop => html`| ${this.__renderProp(field[prop])} | `)}
+ ${this.show.map(prop => html`${this.__renderProp(prop)} | `)}
`;
diff --git a/docs/docs/systems/form/features.md b/docs/docs/systems/form/features.md
index e41422ebd..7f1fa38c1 100644
--- a/docs/docs/systems/form/features.md
+++ b/docs/docs/systems/form/features.md
@@ -14,6 +14,7 @@ import '@lion/input-amount/define';
import '@lion/input-date/define';
import '@lion/input-datepicker/define';
import '@lion/input-email/define';
+import '@lion/input-tel/define';
import '@lion/input-iban/define';
import '@lion/input-range/define';
import '@lion/input-stepper/define';
@@ -70,6 +71,7 @@ export const main = () => {
+
{
};
```
+### Live formatters
+
+Live formatters are a specific type of preprocessor, that format a view value during typing.
+Examples:
+
+- a phone number that, during typing formats `+316` as `+31 6`
+- a date that follows a date mask and automatically inserts '-' characters
+
+Type '6' in the example below and see that a space will be added and the caret in the text box
+will be automatically moved along.
+
+```js preview-story
+export const liveFormatters = () => {
+ return html`
+ {
+ return liveFormatPhoneNumber(viewValue, {
+ regionCode: 'NL',
+ formatStrategy: 'international',
+ currentCaretIndex,
+ prevViewValue,
+ });
+ }}
+ >
+
+ `;
+};
+```
+
+Note that these live formatters need to make an educated guess based on the current (incomplete) view
+value what the users intentions are. When implemented correctly, they can create a huge improvement
+in user experience.
+Next to a changed viewValue, they are also responsible for taking care of the
+caretIndex. For instance, if `+316` is changed to `+31 6`, the caret needs to be moved one position
+to the right (to compensate for the extra inserted space).
+
+#### When to use a live formatter and when a regular formatter?
+
+Although it might feel more logical to configure live formatters inside the `.formatter` function,
+it should be configured inside the `.preprocessor` function. The table below shows differences
+between the two mentioned methods
+
+| Function | Value type recieved | Reflected back to user on | Supports incomplete values | Supports caret index |
+| :------------ | :------------------ | :------------------------ | :------------------------- | :------------------- |
+| .formatter | modelValue | blur (leave) | No | No |
+| .preprocessor | viewValue | keyup (live) | Yes | Yes |
+
## Flow Diagrams
Below we show three flow diagrams to show the flow of formatting, serializing and parsing user input, with the example of a date input:
diff --git a/docs/guides/how-to/create-a-custom-field.md b/docs/guides/how-to/create-a-custom-field.md
index 671f819b7..f719fdec0 100644
--- a/docs/guides/how-to/create-a-custom-field.md
+++ b/docs/guides/how-to/create-a-custom-field.md
@@ -141,6 +141,12 @@ Then we follow the steps below:
the glue to integrate all other functionality like parsing/formatting/serializing, validating,
tracking interaction states etc.
+> N.B. Make sure you never override other property getters than the one mentioned in this tutorial,
+> because those properties will loose their reactivity (they won't be considered anymore in the
+> update loop of LitElement).
+> Whenever a .modelValue/.formattedValue/.serializedValue needs to be computed, use
+> [parser/formatter/serializer](../../docs/systems/form/formatting-and-parsing.md)
+
Implement with the following code:
```js preview-story
@@ -165,6 +171,7 @@ export const createAField = () => {
}
get value() {
+ // Remember we should always return type 'string' here
return this._inputNode.value;
}
@@ -183,6 +190,9 @@ export const createAField = () => {
```
That was all!
+Now that your .modelValue is connected your component is fully compatible with our form system
+
+> Is your `.modelValue` still undefined? Please make sure you're `.value` is of type 'string'.
Now you can enhance your slider by writing custom Validators for it or by
writing a parser to get a custom modelValue type.
diff --git a/packages/core/src/ScopedStylesController.js b/packages/core/src/ScopedStylesController.js
index 6e9ff7790..d746263ac 100644
--- a/packages/core/src/ScopedStylesController.js
+++ b/packages/core/src/ScopedStylesController.js
@@ -35,10 +35,14 @@ export class ScopedStylesController {
}
__setupStyleTag() {
+ // Make it win from other elements on the page.
+ // TODO: consider adding an id here to always win, since we are simulating shadow dom
+ // behavior here.
+ const highSpecifictyScope = `${this.scopedClass}.${this.scopedClass}`;
this.__styleTag.textContent = /** @type {typeof ScopedStylesController} */ (
this.host.constructor
)
- .scopedStyles(unsafeCSS(this.scopedClass))
+ .scopedStyles(unsafeCSS(highSpecifictyScope))
.toString();
this.host.insertBefore(this.__styleTag, this.host.childNodes[0]);
}
diff --git a/packages/form-core/src/FormControlMixin.js b/packages/form-core/src/FormControlMixin.js
index 21eb8b63a..7b5d18311 100644
--- a/packages/form-core/src/FormControlMixin.js
+++ b/packages/form-core/src/FormControlMixin.js
@@ -43,6 +43,7 @@ const FormControlMixinImplementation = superclass =>
name: { type: String, reflect: true },
readOnly: { type: Boolean, attribute: 'readonly', reflect: true },
label: String, // FIXME: { attribute: false } breaks a bunch of tests, but shouldn't...
+ labelSrOnly: { type: Boolean, attribute: 'label-sr-only', reflect: true },
helpText: { type: String, attribute: 'help-text' },
modelValue: { attribute: false },
_ariaLabelledNodes: { attribute: false },
@@ -186,6 +187,12 @@ const FormControlMixinImplementation = superclass =>
*/
this.label = '';
+ /**
+ * The label will only be visible for srceen readers when true
+ * @type {boolean}
+ */
+ this.labelSrOnly = false;
+
/**
* The helpt text for the input node.
* When no value is defined, textContent of [slot=help-text] will be used
@@ -699,6 +706,20 @@ const FormControlMixinImplementation = superclass =>
color: var(--disabled-text-color, #767676);
}
+ :host([label-sr-only]) .form-field__label {
+ position: absolute;
+ top: 0;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
+
/***********************
{block} .input-group
*********************/
diff --git a/packages/form-core/src/FormatMixin.js b/packages/form-core/src/FormatMixin.js
index 62ce2f2e8..5c6a53820 100644
--- a/packages/form-core/src/FormatMixin.js
+++ b/packages/form-core/src/FormatMixin.js
@@ -7,7 +7,7 @@ import { ValidateMixin } from './validate/ValidateMixin.js';
/**
* @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin
- * @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
+ * @typedef {import('../types/FormatMixinTypes').FormatOptions} FormatOptions
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
*/
@@ -107,20 +107,26 @@ const FormatMixinImplementation = superclass =>
}
/**
- * Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter
- * invalid input amongst others.
+ * Preprocessors could be considered 'live formatters'. Their result is shown to the user
+ * on keyup instead of after blurring the field. The biggest difference between preprocessors
+ * and formatters is their moment of execution: preprocessors are run before modelValue is
+ * computed (and work based on view value), whereas formatters are run after the parser (and
+ * are based on modelValue)
+ * Automatically formats code while typing. It depends on a preprocessro that smartly
+ * updates the viewValue and caret position for best UX.
* @example
* ```js
* preprocessor(viewValue) {
* // only use digits
* return viewValue.replace(/\D/g, '');
* }
- * ```
* @param {string} v - the raw value from the after keyUp/Down event
- * @returns {string} preprocessedValue: the result of preprocessing for invalid input
+ * @param {FormatOptions & { prevViewValue: string; currentCaretIndex: number }} opts - the raw value from the after keyUp/Down event
+ * @returns {{ viewValue:string; caretIndex:number; }|string|undefined} preprocessedValue: the result of preprocessing for invalid input
*/
- preprocessor(v) {
- return v;
+ // eslint-disable-next-line no-unused-vars
+ preprocessor(v, opts) {
+ return undefined;
}
/**
@@ -204,6 +210,7 @@ const FormatMixinImplementation = superclass =>
}
this._reflectBackFormattedValueToUser();
this.__preventRecursiveTrigger = false;
+ this.__prevViewValue = this.value;
}
/**
@@ -218,13 +225,12 @@ const FormatMixinImplementation = superclass =>
if (value === '') {
// Ideally, modelValue should be undefined for empty strings.
// For backwards compatibility we return an empty string:
- // - it triggers validation for required validators (see ValidateMixin.validate())
// - it can be expected by 3rd parties (for instance unit tests)
// TODO(@tlouisse): In a breaking refactor of the Validation System, this behavior can be corrected.
return '';
}
- // A.2) Handle edge cases We might have no view value yet, for instance because
+ // A.2) Handle edge cases. We might have no view value yet, for instance because
// _inputNode.value was not available yet
if (typeof value !== 'string') {
// This means there is nothing to find inside the view that can be of
@@ -263,8 +269,7 @@ const FormatMixinImplementation = superclass =>
if (
this._isHandlingUserInput &&
- this.hasFeedbackFor &&
- this.hasFeedbackFor.length &&
+ this.hasFeedbackFor?.length &&
this.hasFeedbackFor.includes('error') &&
this._inputNode
) {
@@ -282,6 +287,8 @@ const FormatMixinImplementation = superclass =>
}
/**
+ * Responds to modelValue changes in the synchronous cycle (most subclassers should listen to
+ * the asynchronous cycle ('modelValue' in the .updated lifecycle))
* @param {{ modelValue: unknown; }[]} args
* @protected
*/
@@ -320,7 +327,7 @@ const FormatMixinImplementation = superclass =>
*/
_syncValueUpwards() {
if (!this.__isHandlingComposition) {
- this.value = this.preprocessor(this.value);
+ this.__handlePreprocessor();
}
const prevFormatted = this.formattedValue;
this.modelValue = this._callParser(this.value);
@@ -330,8 +337,45 @@ const FormatMixinImplementation = superclass =>
if (prevFormatted === this.formattedValue && this.__prevViewValue !== this.value) {
this._calculateValues();
}
- /** @type {string} */
- this.__prevViewValue = this.value;
+ }
+
+ /**
+ * Handle view value and caretIndex, depending on return type of .preprocessor.
+ * @private
+ */
+ __handlePreprocessor() {
+ const unprocessedValue = this.value;
+ let currentCaretIndex = this.value.length;
+ // Be gentle with Safari
+ if (
+ this._inputNode &&
+ 'selectionStart' in this._inputNode &&
+ /** @type {HTMLInputElement} */ (this._inputNode)?.type !== 'range'
+ ) {
+ currentCaretIndex = /** @type {number} */ (this._inputNode.selectionStart);
+ }
+ const preprocessedValue = this.preprocessor(this.value, {
+ ...this.formatOptions,
+ currentCaretIndex,
+ prevViewValue: this.__prevViewValue,
+ });
+
+ this.__prevViewValue = unprocessedValue;
+ if (preprocessedValue === undefined) {
+ // Make sure we do no set back original value, so we preserve
+ // caret index (== selectionStart/selectionEnd)
+ return;
+ }
+ if (typeof preprocessedValue === 'string') {
+ this.value = preprocessedValue;
+ } else if (typeof preprocessedValue === 'object') {
+ const { viewValue, caretIndex } = preprocessedValue;
+ this.value = viewValue;
+ if (caretIndex && this._inputNode && 'selectionStart' in this._inputNode) {
+ this._inputNode.selectionStart = caretIndex;
+ this._inputNode.selectionEnd = caretIndex;
+ }
+ }
}
/**
@@ -351,7 +395,7 @@ const FormatMixinImplementation = superclass =>
/**
* Every time .formattedValue is attempted to sync to the view value (on change/blur and on
* modelValue change), this condition is checked. When enhancing it, it's recommended to
- * call `super._reflectBackOn()`
+ * call via `return this._myExtraCondition && super._reflectBackOn()`
* @overridable
* @return {boolean}
* @protected
@@ -490,6 +534,9 @@ const FormatMixinImplementation = superclass =>
};
}
+ /**
+ * @private
+ */
__onPaste() {
this._isPasting = true;
this.formatOptions.mode = 'pasted';
@@ -510,6 +557,9 @@ const FormatMixinImplementation = superclass =>
if (typeof this.modelValue === 'undefined') {
this._syncValueUpwards();
}
+ /** @type {string} */
+ this.__prevViewValue = this.value;
+
this._reflectBackFormattedValueToUser();
if (this._inputNode) {
diff --git a/packages/form-core/src/validate/ValidateMixin.js b/packages/form-core/src/validate/ValidateMixin.js
index ea1883990..e768e9c8c 100644
--- a/packages/form-core/src/validate/ValidateMixin.js
+++ b/packages/form-core/src/validate/ValidateMixin.js
@@ -17,6 +17,10 @@ import { FormControlMixin } from '../FormControlMixin.js';
/**
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType
+ * @typedef {import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHost
+ * @typedef {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHostConstructor
+ * @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
+ * @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
*/
/**
@@ -159,7 +163,7 @@ export const ValidateMixinImplementation = superclass =>
*/
this.showsFeedbackFor = [];
- // TODO: [v1] make this fully private (preifix __)?
+ // TODO: [v1] make this fully private (prefix __)?
/**
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
* @type {ValidationType[]}
@@ -171,7 +175,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
* @readOnly
- * @type {Object.>}
+ * @type {ValidationStates}
*/
this.validationStates = {};
@@ -206,44 +210,45 @@ export const ValidateMixinImplementation = superclass =>
/**
* The amount of feedback messages that will visible in LionValidationFeedback
+ * @configurable
* @protected
*/
this._visibleMessagesAmount = 1;
/**
- * @type {Validator[]}
+ * @type {ValidationResultEntry[]}
* @private
*/
this.__syncValidationResult = [];
/**
- * @type {Validator[]}
+ * @type {ValidationResultEntry[]}
* @private
*/
this.__asyncValidationResult = [];
/**
* Aggregated result from sync Validators, async Validators and ResultValidators
- * @type {Validator[]}
+ * @type {ValidationResultEntry[]}
* @private
*/
this.__validationResult = [];
/**
- * @type {Validator[]}
+ * @type {ValidationResultEntry[]}
* @private
*/
this.__prevValidationResult = [];
/**
- * @type {Validator[]}
+ * @type {ValidationResultEntry[]}
* @private
*/
this.__prevShownValidationResult = [];
/**
* The updated children validity affects the validity of the parent. Helper to recompute
- * validatity of parent FormGroup
+ * validity of parent FormGroup
* @private
*/
this.__childModelValueChanged = false;
@@ -337,7 +342,7 @@ export const ValidateMixinImplementation = superclass =>
* Triggered by:
* - modelValue change
* - change in the 'validators' array
- * - change in the config of an individual Validator
+ * - change in the config of an individual Validator
*
* Three situations are handled:
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
@@ -384,6 +389,14 @@ export const ValidateMixinImplementation = superclass =>
* @desc step a1-3 + b (as explained in `validate()`)
*/
async __executeValidators() {
+ /**
+ * Allows Application Developer to wait for (async) validation
+ * @example
+ * ```js
+ * await el.validateComplete;
+ * ```
+ * @type {Promise}
+ */
this.validateComplete = new Promise(resolve => {
this.__validateCompleteResolve = resolve;
});
@@ -410,7 +423,7 @@ export const ValidateMixinImplementation = superclass =>
const isEmpty = this.__isEmpty(value);
if (isEmpty) {
if (requiredValidator) {
- this.__syncValidationResult = [requiredValidator];
+ this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
}
this.__finishValidation({ source: 'sync' });
return;
@@ -451,9 +464,12 @@ export const ValidateMixinImplementation = superclass =>
*/
__executeSyncValidators(syncValidators, value, { hasAsync }) {
if (syncValidators.length) {
- this.__syncValidationResult = syncValidators.filter(v =>
- v.execute(value, v.param, { node: this }),
- );
+ this.__syncValidationResult = syncValidators
+ .map(v => ({
+ validator: v,
+ outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
+ }))
+ .filter(v => Boolean(v.outcome));
}
this.__finishValidation({ source: 'sync', hasAsync });
}
@@ -468,10 +484,15 @@ export const ValidateMixinImplementation = superclass =>
if (asyncValidators.length) {
this.isPending = true;
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
- const booleanResults = await Promise.all(resultPromises);
- this.__asyncValidationResult = booleanResults
- .map((r, i) => asyncValidators[i]) // Create an array of Validators
- .filter((v, i) => booleanResults[i]); // Only leave the ones returning true
+ const asyncExecutionResults = await Promise.all(resultPromises);
+
+ this.__asyncValidationResult = asyncExecutionResults
+ .map((r, i) => ({
+ validator: asyncValidators[i],
+ outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
+ }))
+ .filter(v => Boolean(v.outcome));
+
this.__finishValidation({ source: 'async' });
this.isPending = false;
}
@@ -479,7 +500,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* step b (as explained in `validate()`), called by __finishValidation
- * @param {Validator[]} regularValidationResult result of steps 1-3
+ * @param {{validator:Validator; outcome: boolean|string;}[]} regularValidationResult result of steps 1-3
* @private
*/
__executeResultValidators(regularValidationResult) {
@@ -490,13 +511,21 @@ export const ValidateMixinImplementation = superclass =>
})
);
- return resultValidators.filter(v =>
- v.executeOnResults({
- regularValidationResult,
- prevValidationResult: this.__prevValidationResult,
- prevShownValidationResult: this.__prevShownValidationResult,
- }),
- );
+ // Map everything to Validator[] for backwards compatibility
+ return resultValidators
+ .map(v => ({
+ validator: v,
+ outcome: /** @type {boolean|string} */ (
+ v.executeOnResults({
+ regularValidationResult: regularValidationResult.map(entry => entry.validator),
+ prevValidationResult: this.__prevValidationResult.map(entry => entry.validator),
+ prevShownValidationResult: this.__prevShownValidationResult.map(
+ entry => entry.validator,
+ ),
+ })
+ ),
+ }))
+ .filter(v => Boolean(v.outcome));
}
/**
@@ -512,35 +541,32 @@ export const ValidateMixinImplementation = superclass =>
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
- // this._storeResultsOnInstance(this.__validationResult);
- const ctor =
- /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
- this.constructor
- );
+ const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
- /** @type {Object.>} */
+ /** @type {ValidationStates} */
const validationStates = ctor.validationTypes.reduce(
(acc, type) => ({ ...acc, [type]: {} }),
{},
);
- this.__validationResult.forEach(v => {
- if (!validationStates[v.type]) {
- validationStates[v.type] = {};
+ this.__validationResult.forEach(({ validator, outcome }) => {
+ if (!validationStates[validator.type]) {
+ validationStates[validator.type] = {};
}
- const vCtor = /** @type {typeof Validator} */ (v.constructor);
- validationStates[v.type][vCtor.validatorName] = true;
+ const vCtor = /** @type {typeof Validator} */ (validator.constructor);
+ validationStates[validator.type][vCtor.validatorName] = outcome;
});
this.validationStates = validationStates;
- this.hasFeedbackFor = [...new Set(this.__validationResult.map(v => v.type))];
+ this.hasFeedbackFor = [
+ ...new Set(this.__validationResult.map(({ validator }) => validator.type)),
+ ];
/** private event that should be listened to by LionFieldSet */
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
if (source === 'async' || !hasAsync) {
if (this.__validateCompleteResolve) {
- // @ts-ignore [allow-private]
- this.__validateCompleteResolve();
+ this.__validateCompleteResolve(true);
}
}
}
@@ -587,10 +613,7 @@ export const ValidateMixinImplementation = superclass =>
console.error(errorMessage, this);
throw new Error(errorMessage);
}
- const ctor =
- /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
- this.constructor
- );
+ const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
if (ctor.validationTypes.indexOf(v.type) === -1) {
const vCtor = /** @type {typeof Validator} */ (v.constructor);
// throws in constructor are not visible to end user so we do both
@@ -640,14 +663,14 @@ export const ValidateMixinImplementation = superclass =>
*/
/**
- * @param {Validator[]} validators list of objects having a .getMessage method
+ * @param {ValidationResultEntry[]} validationResults list of objects having a .getMessage method
* @return {Promise.}
* @private
*/
- async __getFeedbackMessages(validators) {
+ async __getFeedbackMessages(validationResults) {
let fieldName = await this.fieldName;
return Promise.all(
- validators.map(async validator => {
+ validationResults.map(async ({ validator, outcome }) => {
if (validator.config.fieldName) {
fieldName = await validator.config.fieldName;
}
@@ -656,6 +679,7 @@ export const ValidateMixinImplementation = superclass =>
modelValue: this.modelValue,
formControl: this,
fieldName,
+ outcome,
});
return { message, type: validator.type, validator };
}),
@@ -690,10 +714,19 @@ export const ValidateMixinImplementation = superclass =>
if (this.showsFeedbackFor.length > 0) {
this.__feedbackQueue.add(async () => {
/** @type {Validator[]} */
- this.__prioritizedResult = this._prioritizeAndFilterFeedback({
- validationResult: this.__validationResult,
+ const prioritizedValidators = this._prioritizeAndFilterFeedback({
+ validationResult: this.__validationResult.map(entry => entry.validator),
});
+ this.__prioritizedResult = prioritizedValidators
+ .map(v => {
+ const found = /** @type {ValidationResultEntry} */ (
+ this.__validationResult.find(r => v === r.validator)
+ );
+ return found;
+ })
+ .filter(Boolean);
+
if (this.__prioritizedResult.length > 0) {
this.__prevShownValidationResult = this.__prioritizedResult;
}
@@ -732,12 +765,12 @@ export const ValidateMixinImplementation = superclass =>
}
/**
- * Allows the end user to specify when a feedback message should be shown
+ * Allows the Application Developer to specify when a feedback message should be shown
* @example
* ```js
* feedbackCondition(type, meta, defaultCondition) {
* if (type === 'info') {
- * return return;
+ * return true;
* } else if (type === 'prefilledOnly') {
* return meta.prefilled;
* }
@@ -775,7 +808,9 @@ export const ValidateMixinImplementation = superclass =>
);
}
- /** @param {import('@lion/core').PropertyValues} changedProperties */
+ /**
+ * @param {import('@lion/core').PropertyValues} changedProperties
+ */
updated(changedProperties) {
super.updated(changedProperties);
@@ -783,10 +818,7 @@ export const ValidateMixinImplementation = superclass =>
changedProperties.has('shouldShowFeedbackFor') ||
changedProperties.has('hasFeedbackFor')
) {
- const ctor =
- /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
- this.constructor
- );
+ const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
this.showsFeedbackFor = /** @type {string[]} */ (
ctor.validationTypes
@@ -822,10 +854,7 @@ export const ValidateMixinImplementation = superclass =>
* @protected
*/
_updateShouldShowFeedbackFor() {
- const ctor =
- /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
- this.constructor
- );
+ const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
const newShouldShowFeedbackFor = /** @type {string[]} */ (
@@ -848,18 +877,15 @@ export const ValidateMixinImplementation = superclass =>
}
/**
- * Orders all active validators in this.__validationResult. Can
- * also filter out occurrences (based on interaction states)
+ * Orders all active validators in this.__validationResult.
+ * Can also filter out occurrences (based on interaction states)
* @overridable
* @param {{ validationResult: Validator[] }} opts
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
* @protected
*/
_prioritizeAndFilterFeedback({ validationResult }) {
- const ctor =
- /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
- this.constructor
- );
+ const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
const types = ctor.validationTypes;
// Sort all validators based on the type provided.
const res = validationResult
diff --git a/packages/form-core/src/validate/Validator.js b/packages/form-core/src/validate/Validator.js
index 03a28da3b..f272a3c0e 100644
--- a/packages/form-core/src/validate/Validator.js
+++ b/packages/form-core/src/validate/Validator.js
@@ -1,44 +1,65 @@
/**
- * @typedef {object} MessageData
- * @property {*} [MessageData.modelValue]
- * @property {string} [MessageData.fieldName]
- * @property {HTMLElement} [MessageData.formControl]
- * @property {string} [MessageData.type]
- * @property {Object.} [MessageData.config]
- * @property {string} [MessageData.name]
+ * @typedef {import('./types').FeedbackMessageData} FeedbackMessageData
+ * @typedef {import('./types').ValidatorParam} ValidatorParam
+ * @typedef {import('./types').ValidatorConfig} ValidatorConfig
+ * @typedef {import('./types').ValidatorOutcome} ValidatorOutcome
+ * @typedef {import('./types').ValidatorName} ValidatorName
+ * @typedef {import('./types').ValidationType} ValidationType
+ * @typedef {import('../FormControlMixin').FormControlHost} FormControlHost
*/
-export class Validator {
+// TODO: support attribute validators like =>
+// register in a ValidateService that is read by Validator and adds these attrs in properties
+// object.
+// They would become like configurable
+// [global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)
+// for FormControls.
+
+export class Validator extends EventTarget {
/**
- *
- * @param {?} [param]
- * @param {Object.} [config]
+ * @param {ValidatorParam} [param]
+ * @param {ValidatorConfig} [config]
*/
constructor(param, config) {
- this.__fakeExtendsEventTarget();
+ super();
- /** @type {?} */
+ /** @type {ValidatorParam} */
this.__param = param;
-
- /** @type {Object.} */
+ /** @type {ValidatorConfig} */
this.__config = config || {};
- this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
- }
-
- static get validatorName() {
- return '';
- }
-
- static get async() {
- return false;
+ /** @type {ValidationType} */
+ this.type = config?.type || 'error'; // Default type supported by ValidateMixin
}
/**
- * @desc The function that returns a Boolean
- * @param {?} [modelValue]
- * @param {?} [param]
- * @param {{}} [config]
- * @returns {Boolean|Promise}
+ * The name under which validation results get registered. For convience and predictability, this
+ * should always be the same as the constructor name (since it will be obfuscated in js builds,
+ * we need to provide it separately).
+ * @type {ValidatorName}
+ */
+ static validatorName = '';
+
+ /**
+ * Whether the validator is asynchronous or not. When true., this means execute function returns
+ * a Promise. This can be handy for:
+ * - server side calls
+ * - validations that are dependent on lazy loaded resources (they can be async until the dependency
+ * is loaded)
+ * @type {boolean}
+ */
+ static async = false;
+
+ /**
+ * The function that returns a validity outcome. When we need to shpw feedback,
+ * it should return true, otherwise false. So when an error\info|warning|success message
+ * needs to be shown, return true. For async Validators, the function canretun a Promise.
+ * It's also possible to return an enum. Let's say that a phone number can have multiple
+ * states: 'invalid-country-code' | 'too-long' | 'too-short'
+ * Those states can be retrieved in the getMessage
+ * @param {any} modelValue
+ * @param {ValidatorParam} [param]
+ * @param {ValidatorConfig} [config]
+ * @returns {ValidatorOutcome|Promise}
*/
// eslint-disable-next-line no-unused-vars, class-methods-use-this
execute(modelValue, param, config) {
@@ -51,22 +72,55 @@ export class Validator {
return true;
}
+ /**
+ * The first argument of the constructor, for instance 3 in `new MinLength(3)`. Will
+ * be stored on Validator instance and passed to `execute` function
+ * @example
+ * ```js
+ * // Store reference to Validator instance
+ * const myValidatorInstance = new MyValidator(1);
+ * // Use this instance initially on a FormControl (that uses ValidateMixin)
+ * render(html``, document.body);
+ * // Based on some event, we need to change the param
+ * myValidatorInstance.param = 2;
+ * ```
+ * @property {ValidatorParam}
+ */
set param(p) {
this.__param = p;
- if (this.dispatchEvent) {
- this.dispatchEvent(new Event('param-changed'));
- }
+ /**
+ * This event is listened for by ValidateMixin. Whenever the validation parameter has
+ * changed, the FormControl will revalidate itself
+ */
+ this.dispatchEvent(new Event('param-changed'));
}
get param() {
return this.__param;
}
+ /**
+ * The second argument of the constructor, for instance
+ * `new MinLength(3, {getFeedMessage: async () => 'too long'})`.
+ * Will be stored on Validator instance and passed to `execute` function.
+ * @example
+ * ```js
+ * // Store reference to Validator instance
+ * const myValidatorInstance = new MyValidator(1, {getMessage() => 'x'});
+ * // Use this instance initially on a FormControl (that uses ValidateMixin)
+ * render(html``, document.body);
+ * // Based on some event, we need to change the param
+ * myValidatorInstance.config = {getMessage() => 'y'};
+ * ```
+ * @property {ValidatorConfig}
+ */
set config(c) {
this.__config = c;
- if (this.dispatchEvent) {
- this.dispatchEvent(new Event('config-changed'));
- }
+ /**
+ * This event is listened for by ValidateMixin. Whenever the validation config has
+ * changed, the FormControl will revalidate itself
+ */
+ this.dispatchEvent(new Event('config-changed'));
}
get config() {
@@ -74,9 +128,29 @@ export class Validator {
}
/**
- * @overridable
- * @param {MessageData} [data]
- * @returns {Promise}
+ * This is a protected method that usually should not be overridden. It is called by ValidateMixin
+ * and it gathers data to be passed to getMessage functions found:
+ * - `this.config.getMessage`, locally provided by consumers of the Validator (overrides global getMessage)
+ * - `MyValidator.getMessage`, globally provided by creators or consumers of the Validator
+ *
+ * Confusion can arise because of similarities with former mentioned methods. In that regard, a
+ * better name for this function would have been _pepareDataAndCallHighestPrioGetMessage.
+ * @example
+ * ```js
+ * class MyValidator extends Validator {
+ * // ...
+ * // 1. globally defined
+ * static async getMessage() {
+ * return 'lowest prio, defined globally by Validator author'
+ * }
+ * }
+ * // 2. globally overridden
+ * MyValidator.getMessage = async() => 'overrides already configured message';
+ * // 3. locally overridden
+ * new MyValidator(myParam, { getMessage: async() => 'locally defined, always wins' });
+ * ```
+ * @param {Partial} [data]
+ * @returns {Promise}
* @protected
*/
async _getMessage(data) {
@@ -101,9 +175,20 @@ export class Validator {
}
/**
+ * Called inside Validator.prototype._getMessage (see explanation).
+ * @example
+ * ```js
+ * class MyValidator extends Validator {
+ * static async getMessage() {
+ * return 'lowest prio, defined globally by Validator author'
+ * }
+ * }
+ * // globally overridden
+ * MyValidator.getMessage = async() => 'overrides already configured message';
+ * ```
* @overridable
- * @param {MessageData} [data]
- * @returns {Promise}
+ * @param {Partial} [data]
+ * @returns {Promise}
*/
// eslint-disable-next-line no-unused-vars
static async getMessage(data) {
@@ -111,12 +196,38 @@ export class Validator {
}
/**
- * @param {HTMLElement} formControl
+ * Validators are allowed to have knowledge about FormControls.
+ * In some cases (in case of the Required Validator) we wanted to enhance accessibility by
+ * adding [aria-required]. Also, it would be possible to write an advanced MinLength
+ * Validator that adds a .preprocessor that restricts from typing too many characters
+ * (like the native [minlength] validator).
+ * Will be called when Validator is added to FormControl.validators.
+ * @example
+ * ```js
+ * onFormControlConnect(formControl) {
+ * if(formControl.inputNode) {
+ * inputNode.setAttribute('aria-required', 'true');
+ * }
+ * }
+ *
+ * ```
+ * @configurable
+ * @param {FormControlHost} formControl
*/
onFormControlConnect(formControl) {} // eslint-disable-line
/**
- * @param {HTMLElement} formControl
+ * Also see `onFormControlConnect`.
+ * Will be called when Validator is removed from FormControl.validators.
+ * @example
+ * ```js
+ * onFormControlDisconnect(formControl) {
+ * if(formControl.inputNode) {
+ * inputNode.removeAttribute('aria-required');
+ * }
+ * }
+ * @configurable
+ * @param {FormControlHost} formControl
*/
onFormControlDisconnect(formControl) {} // eslint-disable-line
@@ -130,41 +241,6 @@ export class Validator {
* - Or, when a webworker was started, its process could be aborted and then restarted.
*/
abortExecution() {} // eslint-disable-line
-
- /**
- * @private
- */
- __fakeExtendsEventTarget() {
- const delegate = document.createDocumentFragment();
-
- /**
- *
- * @param {string} type
- * @param {EventListener} listener
- * @param {Object} [opts]
- */
- const delegatedAddEventListener = (type, listener, opts) =>
- delegate.addEventListener(type, listener, opts);
-
- /**
- * @param {string} type
- * @param {EventListener} listener
- * @param {Object} [opts]
- */
- const delegatedRemoveEventListener = (type, listener, opts) =>
- delegate.removeEventListener(type, listener, opts);
-
- /**
- * @param {Event|CustomEvent} event
- */
- const delegatedDispatchEvent = event => delegate.dispatchEvent(event);
-
- this.addEventListener = delegatedAddEventListener;
-
- this.removeEventListener = delegatedRemoveEventListener;
-
- this.dispatchEvent = delegatedDispatchEvent;
- }
}
// For simplicity, a default validator only handles one state:
diff --git a/packages/form-core/src/validate/types/index.d.ts b/packages/form-core/src/validate/types/index.d.ts
new file mode 100644
index 000000000..36741865a
--- /dev/null
+++ b/packages/form-core/src/validate/types/index.d.ts
@@ -0,0 +1,60 @@
+import { FormControlHost } from '../../FormControlMixin';
+export { ValidationType } from '../../types/ValidateMixinTypes';
+
+/**
+ * The name under which validation results get registered. For convience and predictability, this
+ * should always be the same as the constructor name (since it will be obfuscated in js builds,
+ * we need to provide it separately).
+ * @example
+ * ```js
+ * class MyValidator extends Validator {
+ * static validatorName = 'MyValidator';
+ * // etc...
+ * }
+ * ```
+ */
+export type ValidatorName = string;
+
+/**
+ * The first argument of the constructor, for instance 3 in `new MinLength(3)`. Will
+ * be stored on Validator instance and passed to `execute` function
+ */
+export type ValidatorParam = any;
+
+/**
+ * The second argument of the constructor, for instance
+ * `new MinLength(3, {getFeedbackMessage: async () => 'too long'})`.
+ * Will be stored on Validator instance and passed to `execute` function
+ */
+export type ValidatorConfig = {
+ getMessage?: (data: Partial) => Promise;
+ type?: ValidationType;
+ node?: FormControlHost;
+ fieldName?: string | Promise;
+};
+
+/**
+ * Output of the `execute` function that returns a validity outcome. When we need to shpw feedback,
+ * it should return true, otherwise false. So when an error\info|warning|success message
+ * needs to be shown, return true.
+ * It's also possible to return an enum. Let's say that a phone number can have multiple
+ * states: 'invalid-country-code' | 'too-long' | 'too-short'
+ * Those states can be retrieved in the getMessage function.
+ */
+export type ValidatorOutcome = string | boolean;
+
+/**
+ * Data object that will be provided to `getMessage()` method
+ */
+export type FeedbackMessageData = {
+ modelValue: any;
+ /** Value configured in FormControl.fieldName. Contributes to accessible error messages */
+ fieldName: string;
+ name: ValidatorName;
+ formControl: FormControlHost;
+ type: ValidationType;
+ /* Outcome of Validator.execute function. Can be a boolean (true) or enum */
+ outcome: ValidatorOutcome;
+ config: ValidatorConfig;
+ params: ValidatorParam;
+};
diff --git a/packages/form-core/test-helpers/index.js b/packages/form-core/test-helpers/index.js
index fcc2d3d72..3b21d5670 100644
--- a/packages/form-core/test-helpers/index.js
+++ b/packages/form-core/test-helpers/index.js
@@ -1,2 +1,3 @@
export * from './ExampleValidators.js';
export * from './getFormControlMembers.js';
+export * from './mimicUserInput.js';
diff --git a/packages/form-core/test-helpers/mimicUserInput.js b/packages/form-core/test-helpers/mimicUserInput.js
new file mode 100644
index 000000000..b11e532e3
--- /dev/null
+++ b/packages/form-core/test-helpers/mimicUserInput.js
@@ -0,0 +1,16 @@
+// @ts-nocheck
+/**
+ * @param {HTMLElement} formControl
+ * @param {?} newViewValue
+ * @param {{caretIndex?:number}} config
+ */
+export function mimicUserInput(formControl, newViewValue, { caretIndex } = {}) {
+ formControl.value = newViewValue; // eslint-disable-line no-param-reassign
+ if (caretIndex) {
+ // eslint-disable-next-line no-param-reassign
+ formControl._inputNode.selectionStart = caretIndex;
+ // eslint-disable-next-line no-param-reassign
+ formControl._inputNode.selectionEnd = caretIndex;
+ }
+ formControl._inputNode.dispatchEvent(new Event('input', { bubbles: true }));
+}
diff --git a/packages/form-core/test-suites/FormatMixin.suite.js b/packages/form-core/test-suites/FormatMixin.suite.js
index 7bfa1c144..c09a6ee32 100644
--- a/packages/form-core/test-suites/FormatMixin.suite.js
+++ b/packages/form-core/test-suites/FormatMixin.suite.js
@@ -4,7 +4,7 @@ import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-w
import sinon from 'sinon';
import { Unparseable, Validator } from '../index.js';
import { FormatMixin } from '../src/FormatMixin.js';
-import { getFormControlMembers } from '../test-helpers/getFormControlMembers.js';
+import { getFormControlMembers, mimicUserInput } from '../test-helpers/index.js';
/**
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
@@ -34,22 +34,6 @@ class FormatClass extends FormatMixin(LitElement) {
}
}
-/**
- * @param {FormatClass} formControl
- * @param {?} newViewValue
- * @param {{caretIndex?:number}} config
- */
-function mimicUserInput(formControl, newViewValue, { caretIndex } = {}) {
- formControl.value = newViewValue; // eslint-disable-line no-param-reassign
- if (caretIndex) {
- // eslint-disable-next-line no-param-reassign
- formControl._inputNode.selectionStart = caretIndex;
- // eslint-disable-next-line no-param-reassign
- formControl._inputNode.selectionEnd = caretIndex;
- }
- formControl._inputNode.dispatchEvent(new Event('input', { bubbles: true }));
-}
-
/**
* @param {{tagString?: string, modelValueType?: modelValueType}} [customConfig]
*/
@@ -57,6 +41,7 @@ export function runFormatMixinSuite(customConfig) {
const cfg = {
tagString: null,
childTagString: null,
+ modelValueType: String,
...customConfig,
};
@@ -633,6 +618,58 @@ export function runFormatMixinSuite(customConfig) {
_inputNode.dispatchEvent(new Event('compositionend', { bubbles: true }));
expect(preprocessorSpy.callCount).to.equal(1);
});
+
+ describe('Live Formatters', () => {
+ it('receives meta object with { prevViewValue: string; currentCaretIndex: number; }', async () => {
+ const spy = sinon.spy();
+
+ const valInitial = generateValueBasedOnType();
+ const el = /** @type {FormatClass} */ (
+ await fixture(
+ html`<${tag} .modelValue="${valInitial}" .preprocessor=${spy}>${tag}>`,
+ )
+ );
+ const viewValInitial = el.value;
+ const valToggled = generateValueBasedOnType({ toggleValue: true });
+
+ mimicUserInput(el, valToggled, { caretIndex: 1 });
+ expect(spy.args[0][0]).to.equal(el.value);
+ const formatOptions = spy.args[0][1];
+ expect(formatOptions.prevViewValue).to.equal(viewValInitial);
+ expect(formatOptions.currentCaretIndex).to.equal(1);
+ });
+
+ it('updates return viewValue and caretIndex', async () => {
+ /**
+ * @param {string} viewValue
+ * @param {{ prevViewValue: string; currentCaretIndex: number; }} meta
+ */
+ function myPreprocessor(viewValue, { currentCaretIndex }) {
+ return { viewValue: `${viewValue}q`, caretIndex: currentCaretIndex + 1 };
+ }
+ const el = /** @type {FormatClass} */ (
+ await fixture(
+ html`<${tag} .modelValue="${'xyz'}" .preprocessor=${myPreprocessor}>${tag}>`,
+ )
+ );
+ mimicUserInput(el, 'wxyz', { caretIndex: 1 });
+ expect(el._inputNode.value).to.equal('wxyzq');
+ expect(el._inputNode.selectionStart).to.equal(2);
+ });
+
+ it('does not update when undefined is returned', async () => {
+ const el = /** @type {FormatClass} */ (
+ await fixture(
+ html`<${tag} .modelValue="${'xyz'}" live-format .liveFormatter=${() =>
+ undefined}>${tag}>`,
+ )
+ );
+ mimicUserInput(el, 'wxyz', { caretIndex: 1 });
+ expect(el._inputNode.value).to.equal('wxyz');
+ // Make sure we do not put our already existing value back, because caret index would be lost
+ expect(el._inputNode.selectionStart).to.equal(1);
+ });
+ });
});
});
});
diff --git a/packages/form-core/test-suites/ValidateMixin.suite.js b/packages/form-core/test-suites/ValidateMixin.suite.js
index 8dc835d2e..8ab767288 100644
--- a/packages/form-core/test-suites/ValidateMixin.suite.js
+++ b/packages/form-core/test-suites/ValidateMixin.suite.js
@@ -70,7 +70,7 @@ export function runValidateMixinSuite(customConfig) {
*/
describe('Validation initiation', () => {
- it('throws and console.errors if adding not Validator instances to the validators array', async () => {
+ it('throws and console.errors if adding non Validator instances to the validators array', async () => {
// we throw and console error as constructor throw are not visible to the end user
const stub = sinon.stub(console, 'error');
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}>${tag}>`));
@@ -93,7 +93,7 @@ export function runValidateMixinSuite(customConfig) {
stub.restore();
});
- it('throws and console error if adding a not supported Validator type', async () => {
+ it('throws a console error if adding a non supported Validator type', async () => {
// we throw and console error to improve DX
const stub = sinon.stub(console, 'error');
const errorMessage = `This component does not support the validator type "major error" used in "MajorValidator". You may change your validators type or add it to the components "static get validationTypes() {}".`;
@@ -720,7 +720,10 @@ export function runValidateMixinSuite(customConfig) {
// @ts-ignore [allow-private] in test
const totalValidationResult = el.__validationResult;
- expect(totalValidationResult).to.eql([resultV, validator]);
+ expect(totalValidationResult).to.eql([
+ { validator: resultV, outcome: true },
+ { validator, outcome: true },
+ ]);
});
});
@@ -1049,7 +1052,7 @@ export function runValidateMixinSuite(customConfig) {
await fixture(html`
<${tag}
.modelValue=${'123'}
- .validators=${[new MinLength(3, { message: 'foo' })]}>
+ .validators=${[new MinLength(3, { getMessage: async () => 'foo' })]}>
${tag}>`)
);
@@ -1352,14 +1355,14 @@ export function runValidateMixinSuite(customConfig) {
};
}
- // @ts-ignore
+ /**
+ * @param {string} type
+ */
_showFeedbackConditionFor(type) {
switch (type) {
case 'error':
- // @ts-ignore
return ['A', 'B'].includes(this.modelValue);
default:
- // @ts-ignore
return ['B', 'C'].includes(this.modelValue);
}
}
@@ -1384,12 +1387,10 @@ export function runValidateMixinSuite(customConfig) {
['D', []],
]) {
el.modelValue = modelValue;
- // eslint-disable-next-line no-await-in-loop
await el.updateComplete;
- // eslint-disable-next-line no-await-in-loop
await el.feedbackComplete;
- // @ts-ignore
+ // @ts-expect-error [allow-protected]
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
expect(resultOrder).to.deep.equal(expected);
}
diff --git a/packages/form-core/test-suites/ValidateMixinFeedbackPart.suite.js b/packages/form-core/test-suites/ValidateMixinFeedbackPart.suite.js
index 4108a99e2..bd92897be 100644
--- a/packages/form-core/test-suites/ValidateMixinFeedbackPart.suite.js
+++ b/packages/form-core/test-suites/ValidateMixinFeedbackPart.suite.js
@@ -7,6 +7,10 @@ import sinon from 'sinon';
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
import { AlwaysInvalid } from '../test-helpers/index.js';
+/**
+ * @typedef {import('../src/validate/types').FeedbackMessageData} FeedbackMessageData
+ */
+
export function runValidateMixinFeedbackPart() {
describe('Validity Feedback', () => {
beforeEach(() => {
@@ -303,7 +307,7 @@ export function runValidateMixinFeedbackPart() {
await fixture(html`
<${tag}
.submitted=${true}
- .validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
+ .validators=${[new MinLength(3, { getMessage: async () => 'custom via config' })]}
>${lightDom}${tag}>
`)
);
@@ -397,7 +401,7 @@ export function runValidateMixinFeedbackPart() {
});
});
- describe('Meta data', () => {
+ describe('FeedbackMessageData', () => {
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
static get validationTypes() {
@@ -431,6 +435,7 @@ export function runValidateMixinFeedbackPart() {
fieldName: '',
type: 'x',
name: 'MinLength',
+ outcome: true,
});
const instanceMessageSpy = sinon.spy();
@@ -457,6 +462,7 @@ export function runValidateMixinFeedbackPart() {
fieldName: '',
type: 'error',
name: 'MinLength',
+ outcome: true,
});
});
@@ -485,41 +491,85 @@ export function runValidateMixinFeedbackPart() {
fieldName: 'myField',
type: 'error',
name: 'MinLength',
+ outcome: true,
});
});
- });
- it('".getMessage()" gets .fieldName defined on Validator config', async () => {
- const constructorValidator = new MinLength(4, {
- fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
+ it('".getMessage()" gets .fieldName defined on Validator config', async () => {
+ const constructorValidator = new MinLength(4, {
+ fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
+ });
+ const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
+ const spy = sinon.spy(ctorValidator, 'getMessage');
+
+ const el = /** @type {ValidateElement} */ (
+ await fixture(html`
+ <${tag}
+ .submitted=${true}
+ .validators=${[constructorValidator]}
+ .modelValue=${'cat'}
+ .fieldName=${new Promise(resolve => resolve('myField'))}
+ >${lightDom}${tag}>
+ `)
+ );
+ await el.updateComplete;
+ await el.feedbackComplete;
+
+ // ignore fieldName Promise as it will always be unique
+ const compare = spy.args[0][0];
+ delete compare?.config?.fieldName;
+ expect(compare).to.eql({
+ config: {},
+ params: 4,
+ modelValue: 'cat',
+ formControl: el,
+ fieldName: 'myFieldViaCfg',
+ type: 'error',
+ name: 'MinLength',
+ outcome: true,
+ });
});
- const ctorValidator = /** @type {typeof MinLength} */ (constructorValidator.constructor);
- const spy = sinon.spy(ctorValidator, 'getMessage');
- const el = /** @type {ValidateElement} */ (
- await fixture(html`
- <${tag}
- .submitted=${true}
- .validators=${[constructorValidator]}
- .modelValue=${'cat'}
- .fieldName=${new Promise(resolve => resolve('myField'))}
- >${lightDom}${tag}>
- `)
- );
- await el.updateComplete;
- await el.feedbackComplete;
+ it('".getMessage()" gets .outcome, which can be "true" or an enum', async () => {
+ class EnumOutComeValidator extends Validator {
+ static validatorName = 'EnumOutCome';
- // ignore fieldName Promise as it will always be unique
- const compare = spy.args[0][0];
- delete compare?.config?.fieldName;
- expect(compare).to.eql({
- config: {},
- params: 4,
- modelValue: 'cat',
- formControl: el,
- fieldName: 'myFieldViaCfg',
- type: 'error',
- name: 'MinLength',
+ execute() {
+ return 'a-string-instead-of-bool';
+ }
+
+ /**
+ * @param {FeedbackMessageData} meta
+ * @returns
+ */
+ static async getMessage({ outcome }) {
+ const results = {
+ 'a-string-instead-of-bool': 'Msg based on enum output',
+ };
+ return results[/** @type {string} */ (outcome)];
+ }
+ }
+
+ const enumOutComeValidator = new EnumOutComeValidator();
+ const spy = sinon.spy(
+ /** @type {typeof EnumOutComeValidator} */ (enumOutComeValidator.constructor),
+ 'getMessage',
+ );
+
+ const el = /** @type {ValidateElement} */ (
+ await fixture(html`
+ <${tag}
+ .submitted=${true}
+ .validators=${[enumOutComeValidator]}
+ .modelValue=${'cat'}
+ >${lightDom}${tag}>
+ `)
+ );
+ await el.updateComplete;
+ await el.feedbackComplete;
+
+ const getMessageArs = spy.args[0][0];
+ expect(getMessageArs.outcome).to.equal('a-string-instead-of-bool');
});
});
diff --git a/packages/form-core/test/FormControlMixin.test.js b/packages/form-core/test/FormControlMixin.test.js
index bbc5e373b..df18d36b8 100644
--- a/packages/form-core/test/FormControlMixin.test.js
+++ b/packages/form-core/test/FormControlMixin.test.js
@@ -80,6 +80,43 @@ describe('FormControlMixin', () => {
expect(el.label).to.equal('');
});
+ /**
+ * N.B. For platform controls, the same would be achieved with
+ * However, since FormControl is usually not the activeElement (_inputNode is), this
+ * will not have the desired effect on for instance lion-input
+ */
+ it('supports "label-sr-only" to make label visually hidden, but accessible for screen reader users', async () => {
+ const el = /** @type {FormControlMixinClass} */ (
+ await fixture(html`
+ <${tag} label-sr-only>
+
+ ${inputSlot}
+ ${tag}>`)
+ );
+
+ const expectedValues = {
+ position: 'absolute',
+ top: '0px',
+ width: '1px',
+ height: '1px',
+ overflow: 'hidden',
+ clipPath: 'inset(100%)',
+ clip: 'rect(1px, 1px, 1px, 1px)',
+ whiteSpace: 'nowrap',
+ borderWidth: '0px',
+ margin: '0px',
+ padding: '0px',
+ };
+
+ const labelStyle = window.getComputedStyle(
+ // @ts-ignore
+ el.shadowRoot?.querySelector('.form-field__label'),
+ );
+ Object.entries(expectedValues).forEach(([key, val]) => {
+ expect(labelStyle[key]).to.equal(val);
+ });
+ });
+
it('can have a help-text', async () => {
const elAttr = /** @type {FormControlMixinClass} */ (
await fixture(html`
diff --git a/packages/form-core/test/validate/Validator.test.js b/packages/form-core/test/validate/Validator.test.js
index b24abc89c..48019065b 100644
--- a/packages/form-core/test/validate/Validator.test.js
+++ b/packages/form-core/test/validate/Validator.test.js
@@ -48,7 +48,7 @@ describe('Validator', () => {
it('throws when executing a Validator without a name', async () => {
class MyValidator extends Validator {}
expect(() => {
- new MyValidator().execute();
+ new MyValidator().execute(undefined);
}).to.throw(
'A validator needs to have a name! Please set it via "static get validatorName() { return \'IsCat\'; }"',
);
@@ -61,6 +61,7 @@ describe('Validator', () => {
}
}
+ // @ts-ignore needed for test
const vali = new MyValidator({}, { getMessage: 'This is the custom error message' });
const { getMessage } = getProtectedMembers(vali);
@@ -76,8 +77,8 @@ describe('Validator', () => {
});
it('receives a config object (optionally) as a second argument on instantiation', async () => {
- const vali = new Validator('myParam', { my: 'config' });
- expect(vali.config).to.eql({ my: 'config' });
+ const vali = new Validator('myParam', { fieldName: 'X' });
+ expect(vali.config).to.eql({ fieldName: 'X' });
});
it('has access to name, type, params, config in getMessage provided by config', () => {
@@ -87,7 +88,7 @@ describe('Validator', () => {
return 'MyValidator';
}
}
- const vali = new MyValidator('myParam', { my: 'config', getMessage: configSpy });
+ const vali = new MyValidator('myParam', { fieldName: 'X', getMessage: configSpy });
const { getMessage } = getProtectedMembers(vali);
getMessage();
@@ -95,7 +96,7 @@ describe('Validator', () => {
name: 'MyValidator',
type: 'error',
params: 'myParam',
- config: { my: 'config', getMessage: configSpy },
+ config: { fieldName: 'X', getMessage: configSpy },
});
});
@@ -114,7 +115,7 @@ describe('Validator', () => {
return '';
}
}
- const vali = new MyValidator('myParam', { my: 'config' });
+ const vali = new MyValidator('myParam', { fieldName: 'X' });
const { getMessage } = getProtectedMembers(vali);
getMessage();
@@ -122,7 +123,7 @@ describe('Validator', () => {
name: 'MyValidator',
type: 'error',
params: 'myParam',
- config: { my: 'config' },
+ config: { fieldName: 'X' },
});
});
@@ -137,12 +138,12 @@ describe('Validator', () => {
});
it('fires "config-changed" event on config change', async () => {
- const vali = new Validator('foo', { foo: 'bar' });
+ const vali = new Validator('foo', { fieldName: 'X' });
const cb = sinon.spy(() => {});
if (vali.addEventListener) {
vali.addEventListener('config-changed', cb);
}
- vali.config = { bar: 'foo' };
+ vali.config = { fieldName: 'Y' };
expect(cb.callCount).to.equal(1);
});
diff --git a/packages/form-core/types/FormControlMixinTypes.d.ts b/packages/form-core/types/FormControlMixinTypes.d.ts
index 156bb9f1a..3249c3930 100644
--- a/packages/form-core/types/FormControlMixinTypes.d.ts
+++ b/packages/form-core/types/FormControlMixinTypes.d.ts
@@ -36,6 +36,8 @@ export type ModelValueEventDetails = {
declare interface HTMLElementWithValue extends HTMLElement {
value: string;
+ selectionStart?: number;
+ selectionEnd?: number;
}
export declare class FormControlHost {
diff --git a/packages/form-core/types/FormatMixinTypes.d.ts b/packages/form-core/types/FormatMixinTypes.d.ts
index 1e0c668bf..f678648f7 100644
--- a/packages/form-core/types/FormatMixinTypes.d.ts
+++ b/packages/form-core/types/FormatMixinTypes.d.ts
@@ -4,6 +4,7 @@ import { FormatNumberOptions } from '@lion/localize/types/LocalizeMixinTypes';
import { ValidateHost } from './validate/ValidateMixinTypes';
import { FormControlHost } from './FormControlMixinTypes';
+export type FormatOptions = { mode: 'pasted' | 'auto' } & object;
export declare class FormatHost {
/**
* Converts viewValue to modelValue
@@ -12,7 +13,7 @@ export declare class FormatHost {
* @param {FormatOptions} opts
* @returns {*} modelValue
*/
- parser(v: string, opts: FormatNumberOptions): unknown;
+ parser(v: string, opts: FormatOptions): unknown;
/**
* Converts modelValue to formattedValue (formattedValue will be synced with
@@ -23,7 +24,7 @@ export declare class FormatHost {
* @param {FormatOptions} opts
* @returns {string} formattedValue
*/
- formatter(v: unknown, opts?: FormatNumberOptions): string;
+ formatter(v: unknown, opts?: FormatOptions): string;
/**
* Converts `.modelValue` to `.serializedValue`
@@ -44,19 +45,27 @@ export declare class FormatHost {
deserializer(v: string): unknown;
/**
- * Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter
- * invalid input amongst others.
+ * Preprocessors could be considered 'live formatters'. Their result is shown to the user
+ * on keyup instead of after blurring the field. The biggest difference between preprocessors
+ * and formatters is their moment of execution: preprocessors are run before modelValue is
+ * computed (and work based on view value), whereas formatters are run after the parser (and
+ * are based on modelValue)
+ * Automatically formats code while typing. It depends on a preprocessro that smartly
+ * updates the viewValue and caret position for best UX.
* @example
* ```js
* preprocessor(viewValue) {
* // only use digits
* return viewValue.replace(/\D/g, '');
* }
- * ```
* @param {string} v - the raw value from the after keyUp/Down event
- * @returns {string} preprocessedValue: the result of preprocessing for invalid input
+ * @param {FormatOptions & { prevViewValue: string; currentCaretIndex: number }} opts - the raw value from the after keyUp/Down event
+ * @returns {{ viewValue:string; caretIndex:number; }|string|undefined} preprocessedValue: the result of preprocessing for invalid input
*/
- preprocessor(v: string): string;
+ preprocessor(
+ v: string,
+ options: FormatOptions & { prevViewValue: string; currentCaretIndex: number },
+ ): { viewValue: string; caretIndex: number } | string | undefined;
/**
* The view value is the result of the formatter function (when available).
@@ -99,7 +108,7 @@ export declare class FormatHost {
/**
* Configuration object that will be available inside the formatter function
*/
- formatOptions: FormatNumberOptions;
+ formatOptions: FormatOptions;
/**
* The view value. Will be delegated to `._inputNode.value`
diff --git a/packages/form-core/types/validate/ValidateMixinTypes.d.ts b/packages/form-core/types/validate/ValidateMixinTypes.d.ts
index 26737c9cb..667855124 100644
--- a/packages/form-core/types/validate/ValidateMixinTypes.d.ts
+++ b/packages/form-core/types/validate/ValidateMixinTypes.d.ts
@@ -104,7 +104,7 @@ export declare class ValidateHost {
* Triggered by:
* - modelValue change
* - change in the 'validators' array
- * - change in the config of an individual Validator
+ * - change in the config of an individual Validator
*
* Three situations are handled:
* - a1) the FormControl is empty: further execution is halted. When the Required Validator
diff --git a/packages/form-integrations/test/dialog-integrations.test.js b/packages/form-integrations/test/dialog-integrations.test.js
index 09c363b3c..47b98b3d4 100644
--- a/packages/form-integrations/test/dialog-integrations.test.js
+++ b/packages/form-integrations/test/dialog-integrations.test.js
@@ -40,6 +40,7 @@ describe('Form inside dialog Integrations', () => {
'lion-input-amount',
'lion-input-iban',
'lion-input-email',
+ 'lion-input-tel',
'lion-checkbox-group',
' lion-checkbox',
' lion-checkbox',
diff --git a/packages/form-integrations/test/form-group-methods.test.js b/packages/form-integrations/test/form-group-methods.test.js
index 72ba719c5..8b42c5643 100644
--- a/packages/form-integrations/test/form-group-methods.test.js
+++ b/packages/form-integrations/test/form-group-methods.test.js
@@ -159,6 +159,7 @@ describe(`Submitting/Resetting/Clearing Form`, async () => {
},
range: '',
rsvp: '',
+ tel: '',
terms: [],
comments: '',
});
diff --git a/packages/form-integrations/test/form-integrations.test.js b/packages/form-integrations/test/form-integrations.test.js
index 8737532a4..de94eb44b 100644
--- a/packages/form-integrations/test/form-integrations.test.js
+++ b/packages/form-integrations/test/form-integrations.test.js
@@ -32,6 +32,7 @@ describe('Form Integrations', () => {
terms: [],
notifications: { value: '', checked: false },
rsvp: '',
+ tel: '',
comments: '',
});
});
@@ -59,6 +60,7 @@ describe('Form Integrations', () => {
terms: [],
notifications: '',
rsvp: '',
+ tel: '',
comments: '',
});
});
@@ -112,6 +114,7 @@ describe('Form Integrations', () => {
'lion-input-amount',
'lion-input-iban',
'lion-input-email',
+ 'lion-input-tel',
'lion-checkbox-group',
' lion-checkbox',
' lion-checkbox',
diff --git a/packages/form-integrations/test/form-validation-integrations.test.js b/packages/form-integrations/test/form-validation-integrations.test.js
index 614523032..b9d2d6c27 100644
--- a/packages/form-integrations/test/form-validation-integrations.test.js
+++ b/packages/form-integrations/test/form-validation-integrations.test.js
@@ -46,7 +46,7 @@ describe('Form Validation Integrations', () => {
await fixture(html`
<${elTag}
.validators=${[
- new Required(null, { getMessage: () => 'error' }),
+ new Required(null, { getMessage: async () => 'error' }),
new WarnValidator(null, { getMessage: () => 'warning' }),
new DefaultSuccess(),
]}
diff --git a/packages/form-integrations/test/helpers/umbrella-form.js b/packages/form-integrations/test/helpers/umbrella-form.js
index f2629e23b..d09903c3d 100644
--- a/packages/form-integrations/test/helpers/umbrella-form.js
+++ b/packages/form-integrations/test/helpers/umbrella-form.js
@@ -8,6 +8,7 @@ import '@lion/input-datepicker/define';
import '@lion/input-amount/define';
import '@lion/input-iban/define';
import '@lion/input-email/define';
+import '@lion/input-tel/define';
import '@lion/checkbox-group/define';
import '@lion/radio-group/define';
import '@lion/select/define';
@@ -71,6 +72,7 @@ export class UmbrellaForm extends LitElement {
+
`).
*
@@ -87,9 +92,10 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
if (changedProperties.has('locale') && this.locale !== changedProperties.get('locale')) {
if (this.locale) {
- this.formatOptions.locale = this.locale;
+ /** @type {AmountFormatOptions} */
+ (this.formatOptions).locale = this.locale;
} else {
- delete this.formatOptions.locale;
+ delete (/** @type {AmountFormatOptions} */ (this.formatOptions).locale);
}
this.__reformat();
}
@@ -141,7 +147,8 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
return;
}
- this.formatOptions.currency = currency || undefined;
+ /** @type {AmountFormatOptions} */
+ (this.formatOptions).currency = currency || undefined;
if (currency) {
if (!this.__currencyDisplayNodeIsConnected) {
this.appendChild(this.__currencyDisplayNode);
diff --git a/packages/input-iban/src/validators.js b/packages/input-iban/src/validators.js
index be9b09c73..5f45617d6 100644
--- a/packages/input-iban/src/validators.js
+++ b/packages/input-iban/src/validators.js
@@ -110,7 +110,7 @@ export class IsIBAN extends Validator {
* @param {string} [data.type]
* @param {Object.} [data.config]
* @param {string} [data.name]
- * @returns {Promise}
+ * @returns {Promise}
*/
static async getMessage(data) {
await loadTranslations();
@@ -153,7 +153,7 @@ export class IsCountryIBAN extends IsIBAN {
* @param {string} [data.type]
* @param {Object.} [data.config]
* @param {string} [data.name]
- * @returns {Promise}
+ * @returns {Promise}
*/
static async getMessage(data) {
await loadTranslations();
@@ -200,7 +200,7 @@ export class IsNotCountryIBAN extends IsIBAN {
* @param {string} [data.type]
* @param {Object.} [data.config]
* @param {string} [data.name]
- * @returns {Promise}
+ * @returns {Promise}
*/
static async getMessage(data) {
await loadTranslations();
diff --git a/packages/input-tel-dropdown/README.md b/packages/input-tel-dropdown/README.md
new file mode 100644
index 000000000..15477ff9c
--- /dev/null
+++ b/packages/input-tel-dropdown/README.md
@@ -0,0 +1,3 @@
+# Lion Input Tel
+
+[=> See Source <=](../../docs/components/inputs/input-tel/overview.md)
diff --git a/packages/input-tel-dropdown/define.js b/packages/input-tel-dropdown/define.js
new file mode 100644
index 000000000..f7d4be162
--- /dev/null
+++ b/packages/input-tel-dropdown/define.js
@@ -0,0 +1 @@
+import './lion-input-tel-dropdown.js';
diff --git a/packages/input-tel-dropdown/docs/features.md b/packages/input-tel-dropdown/docs/features.md
new file mode 100644
index 000000000..f6da18052
--- /dev/null
+++ b/packages/input-tel-dropdown/docs/features.md
@@ -0,0 +1,3 @@
+# Lion Input Tel Dropdown Features
+
+[=> See Source <=](../../../docs/components/inputs/input-tel-dropdown/features.md)
diff --git a/packages/input-tel-dropdown/docs/overview.md b/packages/input-tel-dropdown/docs/overview.md
new file mode 100644
index 000000000..b823a3e67
--- /dev/null
+++ b/packages/input-tel-dropdown/docs/overview.md
@@ -0,0 +1,3 @@
+# Lion Input Tel Dropdown Overview
+
+[=> See Source <=](../../../docs/components/inputs/input-tel-dropdown/overview.md)
diff --git a/packages/input-tel-dropdown/index.js b/packages/input-tel-dropdown/index.js
new file mode 100644
index 000000000..bcce24fb2
--- /dev/null
+++ b/packages/input-tel-dropdown/index.js
@@ -0,0 +1 @@
+export { LionInputTelDropdown } from './src/LionInputTelDropdown.js';
diff --git a/packages/input-tel-dropdown/lion-input-tel-dropdown.js b/packages/input-tel-dropdown/lion-input-tel-dropdown.js
new file mode 100644
index 000000000..d696e2c09
--- /dev/null
+++ b/packages/input-tel-dropdown/lion-input-tel-dropdown.js
@@ -0,0 +1,3 @@
+import { LionInputTelDropdown } from './src/LionInputTelDropdown.js';
+
+customElements.define('lion-input-tel-dropdown', LionInputTelDropdown);
diff --git a/packages/input-tel-dropdown/package.json b/packages/input-tel-dropdown/package.json
new file mode 100644
index 000000000..1a46eb423
--- /dev/null
+++ b/packages/input-tel-dropdown/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@lion/input-tel-dropdown",
+ "version": "0.0.0",
+ "description": "Input field for entering phone numbers with the help of a dropdown region list",
+ "license": "MIT",
+ "author": "ing-bank",
+ "homepage": "https://github.com/ing-bank/lion/",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ing-bank/lion.git",
+ "directory": "packages/input-tel-dropdown"
+ },
+ "main": "index.js",
+ "module": "index.js",
+ "files": [
+ "*.d.ts",
+ "*.js",
+ "custom-elements.json",
+ "docs",
+ "src",
+ "test",
+ "types"
+ ],
+ "scripts": {
+ "custom-elements-manifest": "custom-elements-manifest analyze --litelement --exclude \"docs/**/*\" \"test-helpers/**/*\"",
+ "debug": "cd ../../ && npm run debug -- --group input-tel-dropdown",
+ "debug:firefox": "cd ../../ && npm run debug:firefox -- --group input-tel-dropdown",
+ "debug:webkit": "cd ../../ && npm run debug:webkit -- --group input-tel-dropdown",
+ "publish-docs": "node ../../packages-node/publish-docs/src/cli.js --github-url https://github.com/ing-bank/lion/ --git-root-dir ../../",
+ "prepublishOnly": "npm run publish-docs && npm run custom-elements-manifest",
+ "test": "cd ../../ && npm run test:browser -- --group input-tel-dropdown"
+ },
+ "sideEffects": [
+ "lion-input-tel-dropdown.js"
+ ],
+ "dependencies": {
+ "@lion/core": "0.21.1",
+ "@lion/input-tel": "0.0.0",
+ "@lion/localize": "0.23.0"
+ },
+ "keywords": [
+ "input",
+ "input-tel-dropdown",
+ "lion",
+ "web-components"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "customElements": "custom-elements.json",
+ "exports": {
+ ".": "./index.js",
+ "./define": "./define.js",
+ "./test-suites": "./test-suites/index.js",
+ "./docs/*": "./docs/*"
+ }
+}
diff --git a/packages/input-tel-dropdown/src/LionInputTelDropdown.js b/packages/input-tel-dropdown/src/LionInputTelDropdown.js
new file mode 100644
index 000000000..de9ec97d9
--- /dev/null
+++ b/packages/input-tel-dropdown/src/LionInputTelDropdown.js
@@ -0,0 +1,375 @@
+// @ts-expect-error ref, createRef are exported (?)
+import { render, html, css, ref, createRef } from '@lion/core';
+import { LionInputTel } from '@lion/input-tel';
+
+/**
+ * Note: one could consider to implement LionInputTelDropdown as a
+ * [combobox](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox).
+ * However, the country dropdown does not directly set the textbox value, it only determines
+ * its region code. Therefore it does not comply to this criterium:
+ * "A combobox is an input widget with an associated popup that enables users to select a value for
+ * the combobox from a collection of possible values. In some implementations,
+ * the popup presents allowed values, while in other implementations, the popup presents suggested
+ * values, and users may either select one of the suggestions or type a value".
+ * We therefore decided to consider the dropdown a helper mechanism that does not set, but
+ * contributes to and helps format and validate the actual value.
+ */
+
+/**
+ * @typedef {import('lit/directives/ref.js').Ref} Ref
+ * @typedef {import('@lion/core').RenderOptions} RenderOptions
+ * @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatHost} FormatHost
+ * @typedef {import('@lion/input-tel/types').FormatStrategy} FormatStrategy
+ * @typedef {import('@lion/input-tel/types').RegionCode} RegionCode
+ * @typedef {import('../types').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
+ * @typedef {import('../types').OnDropdownChangeEvent} OnDropdownChangeEvent
+ * @typedef {import('../types').DropdownRef} DropdownRef
+ * @typedef {import('../types').RegionMeta} RegionMeta
+ * @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
+ * @typedef {import('@lion/select-rich').LionSelectRich} LionSelectRich
+ * @typedef {import('@lion/overlays').OverlayController} OverlayController
+ * @typedef {TemplateDataForDropdownInputTel & {data: {regionMetaList:RegionMeta[]}}} TemplateDataForIntlInputTel
+ */
+
+// eslint-disable-next-line prefer-destructuring
+/**
+ * @param {string} char
+ */
+function getRegionalIndicatorSymbol(char) {
+ return String.fromCodePoint(0x1f1e6 - 65 + char.toUpperCase().charCodeAt(0));
+}
+
+/**
+ * LionInputTelDropdown renders a dropdown like element next to the text field, inside the
+ * prefix slot. This could be a LionSelect, a LionSelectRich or a native select.
+ * By default, the native `