feat(input-tel): new component LionInputTel
This commit is contained in:
parent
8314b4b3ab
commit
a882c94f11
62 changed files with 17096 additions and 0 deletions
5
.changeset/rare-panthers-crash.md
Normal file
5
.changeset/rare-panthers-crash.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/input-tel': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
New component "LionInputTel"
|
||||||
254
docs/components/inputs/input-tel/features.md
Normal file
254
docs/components/inputs/input-tel/features.md
Normal file
|
|
@ -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`<h-region-code-table></h-region-code-table>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
<select
|
||||||
|
aria-label="Set scenario"
|
||||||
|
@change="${({ target }) => setDerivedActiveRegionScenario(target.value)}"
|
||||||
|
>
|
||||||
|
<option value="">--- select scenario ---</option>
|
||||||
|
<option value="only-allowed-region">1. only allowed region</option>
|
||||||
|
<option value="user-input">2. user input</option>
|
||||||
|
<option value="locale">3. locale</option>
|
||||||
|
</select>
|
||||||
|
<output style="display:block; min-height: 1.5em;" id="myOutput" ${ref(outputRef)}></output>
|
||||||
|
<lion-input-tel
|
||||||
|
${ref(inputTelRef)}
|
||||||
|
@model-value-changed="${({ detail }) => {
|
||||||
|
if (detail.isTriggeredByUser && selectRef.value) {
|
||||||
|
selectRef.value.value = '';
|
||||||
|
}
|
||||||
|
}}"
|
||||||
|
name="phoneNumber"
|
||||||
|
label="Active region"
|
||||||
|
.allowedRegions="${initialAllowedRegions}"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${[
|
||||||
|
'activeRegion',
|
||||||
|
{
|
||||||
|
name: 'all or allowed regions',
|
||||||
|
processor: el => JSON.stringify(el._allowedOrAllRegions),
|
||||||
|
},
|
||||||
|
'modelValue',
|
||||||
|
]}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
<lion-input-tel
|
||||||
|
label="Allowed regions 'NL', 'BE', 'DE'"
|
||||||
|
help-text="Type '+31'(NL), '+32'(BE) or '+49'(DE) and see how activeRegion changes"
|
||||||
|
.allowedRegions="${['NL', 'BE', 'DE']}"
|
||||||
|
.modelValue="${'+31612345678'}"
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${['modelValue', 'activeRegion']}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
<lion-input-tel
|
||||||
|
label="Only allowed region 'DE'"
|
||||||
|
help-text="Restricts validation / formatting to one region"
|
||||||
|
.allowedRegions="${['DE']}"
|
||||||
|
.modelValue="${'+31612345678'}"
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${['modelValue', 'activeRegion', 'validationStates']}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
<select @change="${({ target }) => (inputTel.value.formatStrategy = target.value)}">
|
||||||
|
<option value="e164">e164</option>
|
||||||
|
<option value="international">international</option>
|
||||||
|
<option value="national">national</option>
|
||||||
|
<option value="significant">significant</option>
|
||||||
|
<option value="rfc3966">rfc3966</option>
|
||||||
|
</select>
|
||||||
|
<lion-input-tel
|
||||||
|
${ref(inputTel)}
|
||||||
|
label="Format strategy"
|
||||||
|
help-text="Choose a strategy above"
|
||||||
|
.modelValue=${'+46707123456'}
|
||||||
|
format-strategy="national"
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${['modelValue', 'formatStrategy']}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
<lion-input-tel
|
||||||
|
label="Realtime format on user input"
|
||||||
|
help-text="Partial numbers are also formatted"
|
||||||
|
.modelValue=${new Unparseable('+31')}
|
||||||
|
format-strategy="international"
|
||||||
|
live-format
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
<lion-input-tel
|
||||||
|
label="Active phone number type"
|
||||||
|
.modelValue="${'+31612345678'}"
|
||||||
|
format-strategy="international"
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${['activePhoneNumberType']}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
```
|
||||||
3
docs/components/inputs/input-tel/index.md
Normal file
3
docs/components/inputs/input-tel/index.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Inputs >> Input Tel ||20
|
||||||
|
|
||||||
|
-> go to Overview
|
||||||
59
docs/components/inputs/input-tel/overview.md
Normal file
59
docs/components/inputs/input-tel/overview.md
Normal file
|
|
@ -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`
|
||||||
|
<lion-input-tel
|
||||||
|
.modelValue="${'+639921343959'}"
|
||||||
|
live-format
|
||||||
|
label="Telephone number"
|
||||||
|
name="phoneNumber"
|
||||||
|
></lion-input-tel>
|
||||||
|
<h-output
|
||||||
|
.show="${[
|
||||||
|
'activeRegion',
|
||||||
|
{
|
||||||
|
name: 'all or allowed regions',
|
||||||
|
processor: el => JSON.stringify(el._allowedOrAllRegions),
|
||||||
|
},
|
||||||
|
'modelValue',
|
||||||
|
]}" 'modelValue']}"
|
||||||
|
.readyPromise="${PhoneUtilManager.loadComplete}"
|
||||||
|
></h-output>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Extends our [input](../input/overview.md)
|
||||||
|
- Shows a mobile telephone keypad on mobile (by having a native `<input inputmode="tel">` 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';
|
||||||
|
```
|
||||||
107
docs/components/inputs/input-tel/src/h-region-code-table.js
Normal file
107
docs/components/inputs/input-tel/src/h-region-code-table.js
Normal file
|
|
@ -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`
|
||||||
|
<table role="table">
|
||||||
|
<caption class="sr-only">
|
||||||
|
Region codes
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left">
|
||||||
|
<span class="backdrop"></span><span class="content">country name</span>
|
||||||
|
</th>
|
||||||
|
<th align="right">
|
||||||
|
<span class="backdrop"></span><span class="content">region code</span>
|
||||||
|
</th>
|
||||||
|
<th align="right">
|
||||||
|
<span class="backdrop"></span><span class="content">country code</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${repeat(
|
||||||
|
finalRegionMetaList,
|
||||||
|
regionMeta => regionMeta.regionCode,
|
||||||
|
({ regionCode, countryCode, flagSymbol, nameForLocale }) =>
|
||||||
|
html` <tr>
|
||||||
|
<td align="left"><span aria-hidden="true">${flagSymbol}</span> ${nameForLocale}</td>
|
||||||
|
<td align="right">${regionCode}</td>
|
||||||
|
<td align="right">${countryCode}</td>
|
||||||
|
</tr>`,
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('h-region-code-table', HRegionCodeTable);
|
||||||
3
packages/input-tel/README.md
Normal file
3
packages/input-tel/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lion Input Tel
|
||||||
|
|
||||||
|
[=> See Source <=](../../docs/components/inputs/input-tel/overview.md)
|
||||||
1
packages/input-tel/define.js
Normal file
1
packages/input-tel/define.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import './lion-input-tel.js';
|
||||||
3
packages/input-tel/docs/features.md
Normal file
3
packages/input-tel/docs/features.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lion Input Tel Features
|
||||||
|
|
||||||
|
[=> See Source <=](../../../docs/components/inputs/input-tel/features.md)
|
||||||
3
packages/input-tel/docs/overview.md
Normal file
3
packages/input-tel/docs/overview.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lion Input Tel Overview
|
||||||
|
|
||||||
|
[=> See Source <=](../../../docs/components/inputs/input-tel/overview.md)
|
||||||
3
packages/input-tel/index.js
Normal file
3
packages/input-tel/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { LionInputTel } from './src/LionInputTel.js';
|
||||||
|
export { PhoneUtilManager } from './src/PhoneUtilManager.js';
|
||||||
|
export { liveFormatPhoneNumber } from './src/preprocessors.js';
|
||||||
14999
packages/input-tel/lib/awesome-phonenumber-esm.js
Normal file
14999
packages/input-tel/lib/awesome-phonenumber-esm.js
Normal file
File diff suppressed because it is too large
Load diff
3
packages/input-tel/lion-input-tel.js
Normal file
3
packages/input-tel/lion-input-tel.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionInputTel } from './src/LionInputTel.js';
|
||||||
|
|
||||||
|
customElements.define('lion-input-tel', LionInputTel);
|
||||||
63
packages/input-tel/package.json
Normal file
63
packages/input-tel/package.json
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/input-tel",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Input field for entering phone numbers, including validation, formatting and mobile keyboard support.",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"*.d.ts",
|
||||||
|
"*.js",
|
||||||
|
"custom-elements.json",
|
||||||
|
"docs",
|
||||||
|
"src",
|
||||||
|
"test",
|
||||||
|
"test-helpers",
|
||||||
|
"test-suites",
|
||||||
|
"translations",
|
||||||
|
"types"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"custom-elements-manifest": "custom-elements-manifest analyze --litelement --exclude \"docs/**/*\" \"test-helpers/**/*\"",
|
||||||
|
"debug": "cd ../../ && npm run debug -- --group input-tel",
|
||||||
|
"debug:firefox": "cd ../../ && npm run debug:firefox -- --group input-tel",
|
||||||
|
"debug:webkit": "cd ../../ && npm run debug:webkit -- --group input-tel",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"sideEffects": [
|
||||||
|
"lion-input-tel.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "0.21.1",
|
||||||
|
"@lion/form-core": "0.16.0",
|
||||||
|
"@lion/input": "0.16.0",
|
||||||
|
"@lion/localize": "0.23.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"input",
|
||||||
|
"input-tel",
|
||||||
|
"lion",
|
||||||
|
"web-components"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"customElements": "custom-elements.json",
|
||||||
|
"customElementsManifest": "custom-elements.json",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./define": "./define.js",
|
||||||
|
"./test-suites": "./test-suites/index.js",
|
||||||
|
"./test-helpers": "./test-helpers/index.js",
|
||||||
|
"./docs/*": "./docs/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
317
packages/input-tel/src/LionInputTel.js
Normal file
317
packages/input-tel/src/LionInputTel.js
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
import { Unparseable } from '@lion/form-core';
|
||||||
|
import { LocalizeMixin, localize } from '@lion/localize';
|
||||||
|
import { LionInput } from '@lion/input';
|
||||||
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
import { liveFormatPhoneNumber } from './preprocessors.js';
|
||||||
|
import { formatPhoneNumber } from './formatters.js';
|
||||||
|
import { parsePhoneNumber } from './parsers.js';
|
||||||
|
import { IsPhoneNumber } from './validators.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
* @typedef {import('../types').PhoneNumberType} PhoneNumberType
|
||||||
|
* @typedef {import('@lion/form-core/types/FormatMixinTypes').FormatOptions} FormatOptions
|
||||||
|
* @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
|
||||||
|
* @typedef {FormatOptions & {regionCode: RegionCode; formatStrategy: FormatStrategy}} FormatOptionsTel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class LionInputTel extends LocalizeMixin(LionInput) {
|
||||||
|
/**
|
||||||
|
* @configure LitElement
|
||||||
|
*/
|
||||||
|
static properties = {
|
||||||
|
allowedRegions: { type: Array },
|
||||||
|
formatStrategy: { type: String, attribute: 'format-strategy' },
|
||||||
|
activeRegion: { type: String },
|
||||||
|
_phoneUtil: { type: Object, state: true },
|
||||||
|
_needsLightDomRender: { type: Number, state: true },
|
||||||
|
_derivedRegionCode: { type: String, state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently active region based on:
|
||||||
|
* 1. allowed regions: get the region from configured allowed regions (if one entry)
|
||||||
|
* 2. user input: try to derive active region from user input
|
||||||
|
* 3. locale: try to get the region from locale (`html[lang]` attribute)
|
||||||
|
* @readonly
|
||||||
|
* @property {RegionCode|undefined}activeRegion
|
||||||
|
*/
|
||||||
|
get activeRegion() {
|
||||||
|
return this.__activeRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore read only
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-empty-function
|
||||||
|
set activeRegion(v) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of phone number, derived from textbox value. Enum with values:
|
||||||
|
* -'fixed-line'
|
||||||
|
* -'fixed-line-or-mobile'
|
||||||
|
* -'mobile'
|
||||||
|
* -'pager'
|
||||||
|
* -'personal-number'
|
||||||
|
* -'premium-rate'
|
||||||
|
* -'shared-cost'
|
||||||
|
* -'toll-free'
|
||||||
|
* -'uan'
|
||||||
|
* -'voip'
|
||||||
|
* -'unknown'
|
||||||
|
* See https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
* @readonly
|
||||||
|
* @property {PhoneNumberType|undefined} activePhoneNumberType
|
||||||
|
*/
|
||||||
|
get activePhoneNumberType() {
|
||||||
|
let pn;
|
||||||
|
try {
|
||||||
|
pn = this._phoneUtil && this._phoneUtil(this.modelValue, this.activeRegion);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (_) {}
|
||||||
|
return pn?.g?.type || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore read only
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-empty-function
|
||||||
|
set activePhoneNumberType(v) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected setter for activeRegion, only meant for subclassers
|
||||||
|
* @protected
|
||||||
|
* @param {RegionCode|undefined} newValue
|
||||||
|
*/
|
||||||
|
_setActiveRegion(newValue) {
|
||||||
|
const oldValue = this.activeRegion;
|
||||||
|
this.__activeRegion = newValue;
|
||||||
|
this.requestUpdate('activeRegion', oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for rendering the region/country list
|
||||||
|
* @property _allowedOrAllRegions
|
||||||
|
* @type {RegionCode[]}
|
||||||
|
*/
|
||||||
|
get _allowedOrAllRegions() {
|
||||||
|
return (
|
||||||
|
(this.allowedRegions?.length
|
||||||
|
? this.allowedRegions
|
||||||
|
: this._phoneUtil?.getSupportedRegionCodes()) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property _phoneUtilLoadComplete
|
||||||
|
* @protected
|
||||||
|
* @type {Promise<PhoneNumber>}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get _phoneUtilLoadComplete() {
|
||||||
|
return PhoneUtilManager.loadComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lifecycle platform
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines what the formatter output should look like.
|
||||||
|
* Formatting strategies as provided by google-libphonenumber
|
||||||
|
* See: https://www.npmjs.com/package/google-libphonenumber
|
||||||
|
* @type {FormatStrategy}
|
||||||
|
*/
|
||||||
|
this.formatStrategy = 'international';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regions that should be considered when international phone numbers are detected.
|
||||||
|
* (when not configured, all regions worldwide will be considered)
|
||||||
|
* @type {RegionCode[]}
|
||||||
|
*/
|
||||||
|
this.allowedRegions = [];
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
this.__isPhoneNumberValidatorInstance = new IsPhoneNumber();
|
||||||
|
/** @configures ValidateMixin */
|
||||||
|
this.defaultValidators.push(this.__isPhoneNumberValidatorInstance);
|
||||||
|
|
||||||
|
// Expose awesome-phonenumber lib for Subclassers
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {PhoneNumber|null}
|
||||||
|
*/
|
||||||
|
this._phoneUtil = PhoneUtilManager.isLoaded
|
||||||
|
? /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that triggers a light dom render aligned with update loop.
|
||||||
|
* TODO: combine with render fn of SlotMixin
|
||||||
|
* @protected
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this._needsLightDomRender = 0;
|
||||||
|
|
||||||
|
if (!PhoneUtilManager.isLoaded) {
|
||||||
|
PhoneUtilManager.loadComplete.then(() => {
|
||||||
|
this._onPhoneNumberUtilReady();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
|
||||||
|
// This will trigger the right keyboard on mobile
|
||||||
|
this._inputNode.inputMode = 'tel';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has('activeRegion')) {
|
||||||
|
// Make sure new modelValue is computed, but prevent formattedValue from being set when focused
|
||||||
|
this.__isUpdatingRegionWhileFocused = this.focused;
|
||||||
|
this._calculateValues({ source: null });
|
||||||
|
this.__isUpdatingRegionWhileFocused = false;
|
||||||
|
|
||||||
|
this.__isPhoneNumberValidatorInstance.param = this.activeRegion;
|
||||||
|
/** @type {FormatOptionsTel} */
|
||||||
|
(this.formatOptions).regionCode = /** @type {RegionCode} */ (this.activeRegion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('formatStrategy')) {
|
||||||
|
this._calculateValues({ source: null });
|
||||||
|
/** @type {FormatOptionsTel} */
|
||||||
|
(this.formatOptions).formatStrategy = this.formatStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('modelValue') || changedProperties.has('allowedRegions')) {
|
||||||
|
this.__calculateActiveRegion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure LocalizeMixin
|
||||||
|
*/
|
||||||
|
onLocaleUpdated() {
|
||||||
|
super.onLocaleUpdated();
|
||||||
|
|
||||||
|
const localeSplitted = localize.locale.split('-');
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {RegionCode}
|
||||||
|
*/
|
||||||
|
this._langIso = /** @type {RegionCode} */ (
|
||||||
|
localeSplitted[localeSplitted.length - 1].toUpperCase()
|
||||||
|
);
|
||||||
|
this.__calculateActiveRegion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure FormatMixin
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatter(modelValue) {
|
||||||
|
return formatPhoneNumber(modelValue, {
|
||||||
|
regionCode: /** @type {RegionCode} */ (this.activeRegion),
|
||||||
|
formatStrategy: this.formatStrategy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure FormatMixin
|
||||||
|
* @param {string} viewValue a phone number without (or with) country code, like '06 12345678'
|
||||||
|
* @returns {string} a trimmed phone number with country code, like '+31612345678'
|
||||||
|
*/
|
||||||
|
parser(viewValue) {
|
||||||
|
return parsePhoneNumber(viewValue, {
|
||||||
|
regionCode: /** @type {RegionCode} */ (this.activeRegion),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @configure FormatMixin
|
||||||
|
* @param {string} viewValue
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} options.prevViewValue
|
||||||
|
* @param {number} options.currentCaretIndex
|
||||||
|
* @returns {{ viewValue: string; caretIndex: number; } | undefined }
|
||||||
|
*/
|
||||||
|
preprocessor(viewValue, { currentCaretIndex, prevViewValue }) {
|
||||||
|
return liveFormatPhoneNumber(viewValue, {
|
||||||
|
regionCode: /** @type {RegionCode} */ (this.activeRegion),
|
||||||
|
formatStrategy: this.formatStrategy,
|
||||||
|
currentCaretIndex,
|
||||||
|
prevViewValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not reflect back .formattedValue during typing (this normally wouldn't happen when
|
||||||
|
* FormatMixin calls _calculateValues based on user input, but for LionInputTel we need to
|
||||||
|
* call it on .activeRegion change)
|
||||||
|
* @enhance FormatMixin
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_reflectBackOn() {
|
||||||
|
return !this.__isUpdatingRegionWhileFocused && super._reflectBackOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_onPhoneNumberUtilReady() {
|
||||||
|
// This should trigger a rerender in shadow dom
|
||||||
|
this._phoneUtil = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber);
|
||||||
|
// This should trigger a rerender in light dom
|
||||||
|
this._scheduleLightDomRender();
|
||||||
|
// Format when libPhoneNumber is loaded
|
||||||
|
this._calculateValues({ source: null });
|
||||||
|
this.__calculateActiveRegion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This allows to hook into the update hook
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_scheduleLightDomRender() {
|
||||||
|
this._needsLightDomRender += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__calculateActiveRegion() {
|
||||||
|
// 1. Get the region from preconfigured allowed region (if one entry)
|
||||||
|
if (this.allowedRegions?.length === 1) {
|
||||||
|
this._setActiveRegion(this.allowedRegions[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try to derive action region from user value
|
||||||
|
const value = !(this.modelValue instanceof Unparseable) ? this.modelValue : this.value;
|
||||||
|
const regionDerivedFromValue = value && this._phoneUtil && this._phoneUtil(value).g?.regionCode;
|
||||||
|
|
||||||
|
if (regionDerivedFromValue && this._allowedOrAllRegions.includes(regionDerivedFromValue)) {
|
||||||
|
this._setActiveRegion(regionDerivedFromValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to get the region from locale
|
||||||
|
if (this._langIso && this._allowedOrAllRegions.includes(this._langIso)) {
|
||||||
|
this._setActiveRegion(this._langIso);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Not derivable
|
||||||
|
this._setActiveRegion(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/input-tel/src/PhoneUtilManager.js
Normal file
38
packages/input-tel/src/PhoneUtilManager.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/** @type {(value: any) => void} */
|
||||||
|
let resolveLoaded;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Handles lazy loading of the (relatively large) google-libphonenumber library, allowing
|
||||||
|
* for quick first paints
|
||||||
|
* - Maintains one instance of phoneNumberUtil that can be shared across multiple places
|
||||||
|
* - Allows for easy mocking in unit tests
|
||||||
|
*/
|
||||||
|
export class PhoneUtilManager {
|
||||||
|
static async loadLibPhoneNumber() {
|
||||||
|
const PhoneNumber = (await import('../lib/awesome-phonenumber-esm.js')).default;
|
||||||
|
this.PhoneNumber = PhoneNumber;
|
||||||
|
resolveLoaded(undefined);
|
||||||
|
return PhoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if google-libphonenumber has been loaded
|
||||||
|
*/
|
||||||
|
static get isLoaded() {
|
||||||
|
return Boolean(this.PhoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait till google-libphonenumber has been loaded
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* await PhoneUtilManager.loadComplete;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
PhoneUtilManager.loadComplete = new Promise(resolve => {
|
||||||
|
resolveLoaded = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
PhoneUtilManager.loadLibPhoneNumber();
|
||||||
57
packages/input-tel/src/formatters.js
Normal file
57
packages/input-tel/src/formatters.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
* @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @param {object} options
|
||||||
|
* @param {RegionCode} options.regionCode
|
||||||
|
* @param {FormatStrategy} [options.formatStrategy='international']
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatPhoneNumber(modelValue, { regionCode, formatStrategy = 'international' }) {
|
||||||
|
// Do not format when not loaded
|
||||||
|
if (!PhoneUtilManager.isLoaded) {
|
||||||
|
return modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const PhoneNumber = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber);
|
||||||
|
|
||||||
|
let pn;
|
||||||
|
try {
|
||||||
|
pn = new PhoneNumber(modelValue, regionCode); // phoneNumberUtil.parse(modelValue, regionCode);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (modelValue?.length >= 4 && modelValue?.length <= 16 && pn?.isValid()) {
|
||||||
|
let formattedValue;
|
||||||
|
|
||||||
|
switch (formatStrategy) {
|
||||||
|
case 'e164':
|
||||||
|
formattedValue = pn.getNumber('e164'); // -> '+46707123456' (default)
|
||||||
|
break;
|
||||||
|
case 'international':
|
||||||
|
formattedValue = pn.getNumber('international'); // -> '+46 70 712 34 56'
|
||||||
|
break;
|
||||||
|
case 'national':
|
||||||
|
formattedValue = pn.getNumber('national'); // -> '070-712 34 56'
|
||||||
|
break;
|
||||||
|
case 'rfc3966':
|
||||||
|
formattedValue = pn.getNumber('rfc3966'); // -> 'tel:+46-70-712-34-56'
|
||||||
|
break;
|
||||||
|
case 'significant':
|
||||||
|
formattedValue = pn.getNumber('significant'); // -> '707123456'
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return formattedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelValue;
|
||||||
|
}
|
||||||
33
packages/input-tel/src/parsers.js
Normal file
33
packages/input-tel/src/parsers.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
* @typedef {* & import('awesome-phonenumber').default} PhoneNumber
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} viewValue
|
||||||
|
* @param {{regionCode:RegionCode;}} options
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function parsePhoneNumber(viewValue, { regionCode }) {
|
||||||
|
// Do not format when not loaded
|
||||||
|
if (!PhoneUtilManager.isLoaded) {
|
||||||
|
return viewValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const PhoneNumber = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber);
|
||||||
|
|
||||||
|
let pn;
|
||||||
|
try {
|
||||||
|
pn = PhoneNumber(viewValue, regionCode);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (pn) {
|
||||||
|
return pn.getNumber('e164');
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewValue;
|
||||||
|
}
|
||||||
51
packages/input-tel/src/preprocessors.js
Normal file
51
packages/input-tel/src/preprocessors.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
import { formatPhoneNumber } from './formatters.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types').FormatStrategy} FormatStrategy
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
* @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} viewValue
|
||||||
|
* @param {object} options
|
||||||
|
* @param {RegionCode} options.regionCode
|
||||||
|
* @param {string} options.prevViewValue
|
||||||
|
* @param {number} options.currentCaretIndex
|
||||||
|
* @param {FormatStrategy} options.formatStrategy
|
||||||
|
* @returns {{viewValue:string; caretIndex:number;}|undefined}
|
||||||
|
*/
|
||||||
|
export function liveFormatPhoneNumber(
|
||||||
|
viewValue,
|
||||||
|
{ regionCode, formatStrategy, prevViewValue, currentCaretIndex },
|
||||||
|
) {
|
||||||
|
const diff = viewValue.length - prevViewValue.length;
|
||||||
|
// Do not format when not loaded
|
||||||
|
if (diff <= 0 || !PhoneUtilManager.isLoaded) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const PhoneNumber = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber);
|
||||||
|
const ayt = PhoneNumber.getAsYouType(regionCode);
|
||||||
|
|
||||||
|
for (const char of viewValue) {
|
||||||
|
if (char !== '') {
|
||||||
|
ayt.addChar(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newViewValue = formatPhoneNumber(ayt.number(), { regionCode, formatStrategy });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given following situation:
|
||||||
|
* - viewValue: `+316123`
|
||||||
|
* - currentCaretIndex: 2 (inbetween 3 and 1)
|
||||||
|
* - prevViewValue `+36123` (we inserted '1' at position 2)
|
||||||
|
* => we should get `+31 6123`, and new caretIndex should be 3, and not newViewValue.length
|
||||||
|
*/
|
||||||
|
const diffBetweenNewAndCurrent = newViewValue.length - viewValue.length;
|
||||||
|
const newCaretIndex = currentCaretIndex + diffBetweenNewAndCurrent;
|
||||||
|
return newViewValue ? { viewValue: newViewValue, caretIndex: newCaretIndex } : undefined;
|
||||||
|
}
|
||||||
70
packages/input-tel/src/validators.js
Normal file
70
packages/input-tel/src/validators.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Validator } from '@lion/form-core';
|
||||||
|
import { PhoneUtilManager } from './PhoneUtilManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
* @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} modelValue
|
||||||
|
* @param {RegionCode} regionCode
|
||||||
|
* @returns {false|'invalid-country-code'|'unknown'|'too-long'|'too-short'}
|
||||||
|
*/
|
||||||
|
function hasFeedback(modelValue, regionCode) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const PhoneNumber = /** @type {PhoneNumber} */ (PhoneUtilManager.PhoneNumber);
|
||||||
|
let invalidCountryCode = false;
|
||||||
|
|
||||||
|
if (regionCode && modelValue?.length >= 4 && modelValue?.length <= 16) {
|
||||||
|
let pn;
|
||||||
|
try {
|
||||||
|
pn = PhoneNumber(modelValue, regionCode);
|
||||||
|
invalidCountryCode = pn.g.regionCode !== regionCode;
|
||||||
|
if (invalidCountryCode) {
|
||||||
|
return 'invalid-country-code';
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (_) {}
|
||||||
|
// too-short/too-long info seems to be not there (we get 'is-possible'?)
|
||||||
|
const enumValue = !pn.isValid() ? pn.g.possibility : false;
|
||||||
|
if (enumValue === 'is-possible') {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
return enumValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsPhoneNumber extends Validator {
|
||||||
|
static validatorName = 'IsPhoneNumber';
|
||||||
|
|
||||||
|
static get async() {
|
||||||
|
// Will be run as async the first time if PhoneUtilManager hasn't loaded yet, sync afterwards
|
||||||
|
return !PhoneUtilManager.isLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} modelValue telephone number without country prefix
|
||||||
|
* @param {RegionCode} regionCode
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
execute(modelValue, regionCode) {
|
||||||
|
if (!PhoneUtilManager.isLoaded) {
|
||||||
|
// Return a Promise once not loaded yet. Since async Validators are meant for things like
|
||||||
|
// loading server side data (in this case a lib), we continue as a sync Validator once loaded
|
||||||
|
return new Promise(resolve => {
|
||||||
|
PhoneUtilManager.loadComplete.then(() => {
|
||||||
|
resolve(hasFeedback(modelValue, regionCode));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hasFeedback(modelValue, regionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add a file for loadDefaultMessages
|
||||||
|
static async getMessage() {
|
||||||
|
return 'Not a valid phone number';
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/input-tel/test-helpers/index.js
Normal file
1
packages/input-tel/test-helpers/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { mockPhoneUtilManager, restorePhoneUtilManager } from './mockPhoneUtilManager.js';
|
||||||
25
packages/input-tel/test-helpers/mockPhoneUtilManager.js
Normal file
25
packages/input-tel/test-helpers/mockPhoneUtilManager.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
|
||||||
|
const originalLoadComplete = PhoneUtilManager.loadComplete;
|
||||||
|
const originalIsLoaded = PhoneUtilManager.isLoaded;
|
||||||
|
|
||||||
|
export function mockPhoneUtilManager() {
|
||||||
|
/** @type {(value: any) => void} */
|
||||||
|
let resolveLoaded;
|
||||||
|
let isLoaded = false;
|
||||||
|
PhoneUtilManager.loadComplete = new Promise(resolve => {
|
||||||
|
resolveLoaded = () => {
|
||||||
|
isLoaded = true;
|
||||||
|
resolve(undefined);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
Object.defineProperty(PhoneUtilManager, 'isLoaded', { get: () => isLoaded });
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return { resolveLoaded };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restorePhoneUtilManager() {
|
||||||
|
PhoneUtilManager.loadComplete = originalLoadComplete;
|
||||||
|
Object.defineProperty(PhoneUtilManager, 'isLoaded', { get: () => originalIsLoaded });
|
||||||
|
}
|
||||||
384
packages/input-tel/test-suites/LionInputTel.suite.js
Normal file
384
packages/input-tel/test-suites/LionInputTel.suite.js
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
import {
|
||||||
|
expect,
|
||||||
|
fixture as _fixture,
|
||||||
|
fixtureSync as _fixtureSync,
|
||||||
|
html,
|
||||||
|
defineCE,
|
||||||
|
unsafeStatic,
|
||||||
|
aTimeout,
|
||||||
|
} from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { mimicUserInput } from '@lion/form-core/test-helpers';
|
||||||
|
import { localize } from '@lion/localize';
|
||||||
|
import { LionInputTel } from '../src/LionInputTel.js';
|
||||||
|
import { IsPhoneNumber } from '../src/validators.js';
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
import {
|
||||||
|
mockPhoneUtilManager,
|
||||||
|
restorePhoneUtilManager,
|
||||||
|
} from '../test-helpers/mockPhoneUtilManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
* @typedef {import('../types').RegionCode} RegionCode
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fixture = /** @type {(arg: string | TemplateResult) => Promise<LionInputTel>} */ (_fixture);
|
||||||
|
const fixtureSync = /** @type {(arg: string | TemplateResult) => LionInputTel} */ (_fixtureSync);
|
||||||
|
|
||||||
|
// const isPhoneNumberUtilLoadComplete = el => el._phoneUtilLoadComplete;
|
||||||
|
|
||||||
|
const getRegionCodeBasedOnLocale = () => {
|
||||||
|
const localeSplitted = localize.locale.split('-');
|
||||||
|
return /** @type {RegionCode} */ (localeSplitted[localeSplitted.length - 1]).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ klass:LionInputTel }} config
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
export function runInputTelSuite({ klass = LionInputTel } = {}) {
|
||||||
|
// @ts-ignore
|
||||||
|
const tagName = defineCE(/** @type {* & HTMLElement} */ (class extends klass {}));
|
||||||
|
const tag = unsafeStatic(tagName);
|
||||||
|
|
||||||
|
describe('LionInputTel', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Wait till PhoneUtilManager has been loaded
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Region codes', () => {
|
||||||
|
describe('Readonly accessor `.activeRegion`', () => {
|
||||||
|
// 1. **allowed regions**: try to get the region from preconfigured allowed region (first entry)
|
||||||
|
it('takes .allowedRegions[0] when only one allowed region configured', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedRegions="${['DE']}" .modelValue="${'+31612345678'}" ></${tag}> `,
|
||||||
|
);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.activeRegion).to.equal('DE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when multiple .allowedRegions, but no modelValue match', async () => {
|
||||||
|
// involve locale, so we are sure it does not fall back on locale
|
||||||
|
const currentCode = getRegionCodeBasedOnLocale();
|
||||||
|
const allowedRegions = ['BE', 'DE', 'CN'];
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${'+31612345678'}" .allowedRegions="${allowedRegions.filter(
|
||||||
|
ar => ar !== currentCode,
|
||||||
|
)}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.activeRegion).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. **user input**: try to derive active region from user input
|
||||||
|
it('deducts it from modelValue when provided', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .modelValue="${'+31612345678'}"></${tag}> `);
|
||||||
|
// Region code for country code '31' is 'NL'
|
||||||
|
expect(el.activeRegion).to.equal('NL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.modelValue takes precedence over .allowedRegions when both preconfigured and .modelValue updated', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedRegions="${[
|
||||||
|
'DE',
|
||||||
|
'BE',
|
||||||
|
'NL',
|
||||||
|
]}" .modelValue="${'+31612345678'}" ></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.activeRegion).to.equal('NL');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. **locale**: try to get the region from locale (`html[lang]` attribute)
|
||||||
|
it('automatically bases it on current locale when nothing preconfigured', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
const currentCode = getRegionCodeBasedOnLocale();
|
||||||
|
expect(el.activeRegion).to.equal(currentCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when locale not within allowed regions', async () => {
|
||||||
|
const currentCode = getRegionCodeBasedOnLocale();
|
||||||
|
const allowedRegions = ['NL', 'BE', 'DE'];
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedRegions="${allowedRegions.filter(
|
||||||
|
ar => ar !== currentCode,
|
||||||
|
)}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.activeRegion).to.equal(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can preconfigure the region code via prop', async () => {
|
||||||
|
const currentCode = getRegionCodeBasedOnLocale();
|
||||||
|
const newCode = currentCode === 'DE' ? 'NL' : 'DE';
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${[newCode]}"></${tag}> `);
|
||||||
|
expect(el.activeRegion).to.equal(newCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('reformats when region code is changed on the fly', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedRegions="${['NL']}" .modelValue="${'+31612345678'}"></${tag}> `,
|
||||||
|
);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
el.allowedRegions = ['NL'];
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.formattedValue).to.equal('612345678');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Readonly accessor `.activePhoneNumberType`', () => {
|
||||||
|
const types = [
|
||||||
|
{ type: 'fixed-line', number: '030 1234567', allowedRegions: ['NL'] },
|
||||||
|
{ type: 'mobile', number: '06 12345678', allowedRegions: ['NL'] },
|
||||||
|
// { type: 'fixed-line-or-mobile', number: '030 1234567' },
|
||||||
|
// { type: 'pager', number: '06 12345678' },
|
||||||
|
// { type: 'personal-number', number: '06 12345678' },
|
||||||
|
// { type: 'premium-rate', number: '06 12345678' },
|
||||||
|
// { type: 'shared-cost', : '06 12345678' },
|
||||||
|
// { type: 'toll-free', number: '06 12345678' },
|
||||||
|
// { type: 'uan', number: '06 12345678' },
|
||||||
|
// { type: 'voip', number: '06 12345678' },
|
||||||
|
// { type: 'unknown', number: '06 12345678' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { type, number, allowedRegions } of types) {
|
||||||
|
it(`returns "${type}" for ${type} numbers`, async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${allowedRegions}"></${tag}> `);
|
||||||
|
mimicUserInput(el, number);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.activePhoneNumberType).to.equal(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User interaction', () => {
|
||||||
|
it('sets inputmode to "tel" for mobile keyboard', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
// @ts-expect-error [allow-protected] inside tests
|
||||||
|
expect(el._inputNode.inputMode).to.equal('tel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats according to locale', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${'+31612345678'}" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reflect back formattedValue after activeRegion change when input still focused', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .modelValue="${'+639608920056'}"></${tag}> `);
|
||||||
|
expect(el.activeRegion).to.equal('PH');
|
||||||
|
el.focus();
|
||||||
|
mimicUserInput(el, '+31612345678');
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.activeRegion).to.equal('NL');
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
expect(el.value).to.equal('+31612345678');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.npmjs.com/package/google-libphonenumber
|
||||||
|
// https://en.wikipedia.org/wiki/E.164
|
||||||
|
describe('Values', () => {
|
||||||
|
it('stores a modelValue in E164 format', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${['NL']}"></${tag}> `);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.modelValue).to.equal('+31612345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores a serializedValue in E164 format', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${['NL']}"></${tag}> `);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.serializedValue).to.equal('+31612345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores a formattedValue according to format strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="national" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('06 12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Format strategies', () => {
|
||||||
|
it('supports "national" strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="national" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('06 12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "international" strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="international" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "e164" strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="e164" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('+31612345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "rfc3966" strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="rfc3966" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('tel:+31-6-12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "significant" strategy', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} format-strategy="significant" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
mimicUserInput(el, '612345678');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('612345678');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: this should be allowed for in FormatMixin =>
|
||||||
|
// in _onModelValueChanged we can add a hook '_checkModelValueFormat'. This needs to be
|
||||||
|
// called whenever .modelValue is supplied by devleloper (not when being internal result
|
||||||
|
// of parser call).
|
||||||
|
// Alternatively, we could be forgiving by attempting to treat it as a view value and
|
||||||
|
// correct the format (although strictness will be preferred...)
|
||||||
|
it.skip('does not allow modelValues in non E164 format', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${'612345678'}" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.modelValue).to.equal(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation', () => {
|
||||||
|
it('applies IsPhoneNumber as default validator', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
expect(el.defaultValidators.find(v => v instanceof IsPhoneNumber)).to.be.not.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('configures IsPhoneNumber with regionCode before first validation', async () => {
|
||||||
|
const el = fixtureSync(
|
||||||
|
html` <${tag} .allowedRegions="${['NL']}" .modelValue="${'612345678'}"></${tag}> `,
|
||||||
|
);
|
||||||
|
const spy = sinon.spy(el, 'validate');
|
||||||
|
const validatorInstance = /** @type {IsPhoneNumber} */ (
|
||||||
|
el.defaultValidators.find(v => v instanceof IsPhoneNumber)
|
||||||
|
);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(validatorInstance.param).to.equal('NL');
|
||||||
|
expect(spy).to.have.been.called;
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates IsPhoneNumber param on regionCode change', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .allowedRegions="${['NL']}" .modelValue="${'612345678'}"></${tag}> `,
|
||||||
|
);
|
||||||
|
const validatorInstance = /** @type {IsPhoneNumber} */ (
|
||||||
|
el.defaultValidators.find(v => v instanceof IsPhoneNumber)
|
||||||
|
);
|
||||||
|
// @ts-expect-error allow protected in tests
|
||||||
|
el._setActiveRegion('DE');
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(validatorInstance.param).to.equal('DE');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User interaction', () => {
|
||||||
|
it('sets inputmode to "tel" for mobile keyboard', async () => {
|
||||||
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
|
// @ts-expect-error [allow-protected] inside tests
|
||||||
|
expect(el._inputNode.inputMode).to.equal('tel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats according to locale', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${['NL']}"></${tag}> `);
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
await el.updateComplete;
|
||||||
|
el.modelValue = '612345678';
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Live format', () => {
|
||||||
|
it('calls .preprocessor on keyup', async () => {
|
||||||
|
const el = await fixture(html` <${tag} .allowedRegions="${['NL']}"></${tag}> `);
|
||||||
|
mimicUserInput(el, '+316');
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.value).to.equal('+31 6');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
describe('Audit', () => {
|
||||||
|
it('passes a11y audit', async () => {
|
||||||
|
const el = await fixture(html`<${tag} label="tel" .modelValue=${'0123456789'}></${tag}>`);
|
||||||
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes a11y audit when readonly', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html`<${tag} label="tel" readonly .modelValue=${'0123456789'}></${tag}>`,
|
||||||
|
);
|
||||||
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes a11y audit when disabled', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html`<${tag} label="tel" disabled .modelValue=${'0123456789'}></${tag}>`,
|
||||||
|
);
|
||||||
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Lazy loading awesome-phonenumber', () => {
|
||||||
|
/** @type {(value:any) => void} */
|
||||||
|
let resolveLoaded;
|
||||||
|
beforeEach(() => {
|
||||||
|
({ resolveLoaded } = mockPhoneUtilManager());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restorePhoneUtilManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reformats once lib has been loaded', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${'612345678'}" .allowedRegions="${['NL']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.formattedValue).to.equal('612345678');
|
||||||
|
resolveLoaded(undefined);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.formattedValue).to.equal('+31 6 12345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates once lib has been loaded', async () => {
|
||||||
|
const el = await fixture(
|
||||||
|
html` <${tag} .modelValue="${'+31612345678'}" .allowedRegions="${['DE']}"></${tag}> `,
|
||||||
|
);
|
||||||
|
expect(el.hasFeedbackFor).to.eql([]);
|
||||||
|
resolveLoaded(undefined);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(el.hasFeedbackFor).to.eql(['error']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
1
packages/input-tel/test-suites/index.js
Normal file
1
packages/input-tel/test-suites/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { runInputTelSuite } from './LionInputTel.suite.js';
|
||||||
3
packages/input-tel/test/LionInputTel.test.js
Normal file
3
packages/input-tel/test/LionInputTel.test.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { runInputTelSuite } from '../test-suites/LionInputTel.suite.js';
|
||||||
|
|
||||||
|
runInputTelSuite();
|
||||||
28
packages/input-tel/test/formatters.test.js
Normal file
28
packages/input-tel/test/formatters.test.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
import { formatPhoneNumber } from '../src/formatters.js';
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
|
||||||
|
describe('formatPhoneNumber', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Wait till PhoneUtilManager has been loaded
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats a phone number according to provided formatStrategy', () => {
|
||||||
|
expect(formatPhoneNumber('0707123456', { regionCode: 'SE', formatStrategy: 'e164' })).to.equal(
|
||||||
|
'+46707123456',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
formatPhoneNumber('+46707123456', { regionCode: 'SE', formatStrategy: 'international' }),
|
||||||
|
).to.equal('+46 70 712 34 56');
|
||||||
|
expect(
|
||||||
|
formatPhoneNumber('+46707123456', { regionCode: 'SE', formatStrategy: 'national' }),
|
||||||
|
).to.equal('070-712 34 56');
|
||||||
|
expect(
|
||||||
|
formatPhoneNumber('+46707123456', { regionCode: 'SE', formatStrategy: 'rfc3966' }),
|
||||||
|
).to.equal('tel:+46-70-712-34-56');
|
||||||
|
expect(
|
||||||
|
formatPhoneNumber('+46707123456', { regionCode: 'SE', formatStrategy: 'significant' }),
|
||||||
|
).to.equal('707123456');
|
||||||
|
});
|
||||||
|
});
|
||||||
16
packages/input-tel/test/parsers.test.js
Normal file
16
packages/input-tel/test/parsers.test.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
import { parsePhoneNumber } from '../src/parsers.js';
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
|
||||||
|
describe('parsePhoneNumber', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Wait till PhoneUtilManager has been loaded
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a a view value to e164 standard', () => {
|
||||||
|
expect(parsePhoneNumber('0707123456', { regionCode: 'SE' })).to.equal('+46707123456');
|
||||||
|
expect(parsePhoneNumber('0707123456', { regionCode: 'NL' })).to.equal('+31707123456');
|
||||||
|
expect(parsePhoneNumber('0707123456', { regionCode: 'DE' })).to.equal('+49707123456');
|
||||||
|
});
|
||||||
|
});
|
||||||
32
packages/input-tel/test/preprocessors.test.js
Normal file
32
packages/input-tel/test/preprocessors.test.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
import { liveFormatPhoneNumber } from '../src/preprocessors.js';
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
|
||||||
|
describe('liveFormatPhoneNumber', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Wait till PhoneUtilManager has been loaded
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('live formats an incomplete view value', () => {
|
||||||
|
expect(
|
||||||
|
liveFormatPhoneNumber('+316123', {
|
||||||
|
regionCode: 'NL',
|
||||||
|
formatStrategy: 'international',
|
||||||
|
prevViewValue: '+36123',
|
||||||
|
currentCaretIndex: 2,
|
||||||
|
}),
|
||||||
|
).to.eql({ viewValue: '+31 6 123', caretIndex: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('live formats a complete view value', () => {
|
||||||
|
expect(
|
||||||
|
liveFormatPhoneNumber('+31612345678', {
|
||||||
|
regionCode: 'NL',
|
||||||
|
formatStrategy: 'international',
|
||||||
|
prevViewValue: '+3161234578',
|
||||||
|
currentCaretIndex: 10,
|
||||||
|
}),
|
||||||
|
).to.eql({ caretIndex: 12, viewValue: '+31 6 12345678' });
|
||||||
|
});
|
||||||
|
});
|
||||||
94
packages/input-tel/test/validators.test.js
Normal file
94
packages/input-tel/test/validators.test.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { expect, aTimeout } from '@open-wc/testing';
|
||||||
|
import { IsPhoneNumber } from '../src/validators.js';
|
||||||
|
import { PhoneUtilManager } from '../src/PhoneUtilManager.js';
|
||||||
|
import {
|
||||||
|
mockPhoneUtilManager,
|
||||||
|
restorePhoneUtilManager,
|
||||||
|
} from '../test-helpers/mockPhoneUtilManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {* & import('@lion/input-tel/lib/awesome-phonenumber-esm').default} PhoneNumber
|
||||||
|
*/
|
||||||
|
|
||||||
|
// For enum output, see: https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
describe('IsPhoneNumber validation', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Wait till PhoneUtilManager has been loaded
|
||||||
|
await PhoneUtilManager.loadComplete;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is invalid when no input is provided', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
expect(validator.execute('', 'NL')).to.equal('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is invalid when non digits are entered, returns "unknown"', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
expect(validator.execute('foo', 'NL')).to.equal('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is invalid when wrong country code is entered, returns "invalid-country-code"', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
// 32 is BE region code
|
||||||
|
expect(validator.execute('+32612345678', 'NL')).to.equal('invalid-country-code');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: find out why awesome-phonenumber does not detect too-short/too-long
|
||||||
|
it.skip('is invalid when number is too short, returns "too-short"', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
expect(validator.execute('+3161234567', 'NL')).to.equal('too-short');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: find out why awesome-phonenumber does not detect too-short/too-long
|
||||||
|
it.skip('is invalid when number is too long, returns "too-long"', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
expect(validator.execute('+316123456789', 'NL')).to.equal('too-long');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is valid when a phone number is entered', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
expect(validator.execute('+31612345678', 'NL')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles validation via awesome-phonenumber', () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
const spy = sinon.spy(PhoneUtilManager, 'PhoneNumber');
|
||||||
|
validator.execute('0123456789', 'NL');
|
||||||
|
expect(spy).to.have.been.calledOnce;
|
||||||
|
expect(spy.lastCall.args[1]).to.equal('NL');
|
||||||
|
validator.execute('0123456789', 'DE');
|
||||||
|
expect(spy.lastCall.args[1]).to.equal('DE');
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Lazy loading PhoneUtilManager', () => {
|
||||||
|
/** @type {(value:any) => void} */
|
||||||
|
let resolveLoaded;
|
||||||
|
beforeEach(() => {
|
||||||
|
({ resolveLoaded } = mockPhoneUtilManager());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restorePhoneUtilManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('behaves asynchronously when lib is still loading', () => {
|
||||||
|
expect(IsPhoneNumber.async).to.be.true;
|
||||||
|
resolveLoaded(undefined);
|
||||||
|
expect(IsPhoneNumber.async).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('waits for the lib to be loaded before execution completes when still in async mode', async () => {
|
||||||
|
const validator = new IsPhoneNumber();
|
||||||
|
const spy = sinon.spy(PhoneUtilManager, 'PhoneNumber');
|
||||||
|
const validationResult = validator.execute('061234', 'NL');
|
||||||
|
expect(validationResult).to.be.instanceOf(Promise);
|
||||||
|
expect(spy).to.not.have.been.called;
|
||||||
|
resolveLoaded(undefined);
|
||||||
|
await aTimeout(0);
|
||||||
|
expect(spy).to.have.been.calledOnce;
|
||||||
|
spy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
5
packages/input-tel/translations/bg-BG.js
Normal file
5
packages/input-tel/translations/bg-BG.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import bg from './bg.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...bg,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/bg.js
Normal file
4
packages/input-tel/translations/bg.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Избор на държава',
|
||||||
|
phoneNumber: 'Телефонен номер',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/cs-CZ.js
Normal file
5
packages/input-tel/translations/cs-CZ.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import cs from './cs.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...cs,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/cs.js
Normal file
4
packages/input-tel/translations/cs.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Vybrat zemi',
|
||||||
|
phoneNumber: 'Telefonní číslo',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/de-DE.js
Normal file
5
packages/input-tel/translations/de-DE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import de from './de.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...de,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/de.js
Normal file
4
packages/input-tel/translations/de.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Land auswählen',
|
||||||
|
phoneNumber: 'Telefonnummer',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/en-AU.js
Normal file
5
packages/input-tel/translations/en-AU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/en-GB.js
Normal file
5
packages/input-tel/translations/en-GB.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/en-US.js
Normal file
5
packages/input-tel/translations/en-US.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/en.js
Normal file
4
packages/input-tel/translations/en.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Select country',
|
||||||
|
phoneNumber: 'Phone number',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/es-ES.js
Normal file
5
packages/input-tel/translations/es-ES.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import es from './es.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...es,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/es.js
Normal file
4
packages/input-tel/translations/es.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Seleccione país',
|
||||||
|
phoneNumber: 'Número de teléfono',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/fr-BE.js
Normal file
5
packages/input-tel/translations/fr-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import fr from './fr.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...fr,
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/fr-FR.js
Normal file
5
packages/input-tel/translations/fr-FR.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import fr from './fr.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...fr,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/fr.js
Normal file
4
packages/input-tel/translations/fr.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Sélectionnez un pays',
|
||||||
|
phoneNumber: 'Numéro de téléphone',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/hu-HU.js
Normal file
5
packages/input-tel/translations/hu-HU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import hu from './hu.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...hu,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/hu.js
Normal file
4
packages/input-tel/translations/hu.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Ország kiválasztása',
|
||||||
|
phoneNumber: 'Telefonszám',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/it-IT.js
Normal file
5
packages/input-tel/translations/it-IT.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import it from './it.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...it,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/it.js
Normal file
4
packages/input-tel/translations/it.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Selezionare il paese',
|
||||||
|
phoneNumber: 'Numero di telefono',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/nl-BE.js
Normal file
5
packages/input-tel/translations/nl-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import nl from './nl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...nl,
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/nl-NL.js
Normal file
5
packages/input-tel/translations/nl-NL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import nl from './nl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...nl,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/nl.js
Normal file
4
packages/input-tel/translations/nl.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Selecteer land',
|
||||||
|
phoneNumber: 'Telefoonnummer',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/pl-PL.js
Normal file
5
packages/input-tel/translations/pl-PL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import pl from './pl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...pl,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/pl.js
Normal file
4
packages/input-tel/translations/pl.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Wybierz kraj',
|
||||||
|
phoneNumber: 'Numer telefonu',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/ro-RO.js
Normal file
5
packages/input-tel/translations/ro-RO.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ro from './ro.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...ro,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/ro.js
Normal file
4
packages/input-tel/translations/ro.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Selectare țară',
|
||||||
|
phoneNumber: 'Număr de telefon',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/ru-RU.js
Normal file
5
packages/input-tel/translations/ru-RU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ru from './ru.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...ru,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/ru.js
Normal file
4
packages/input-tel/translations/ru.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Выбрать страну',
|
||||||
|
phoneNumber: 'Номер телефона',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/sk-SK.js
Normal file
5
packages/input-tel/translations/sk-SK.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import sk from './sk.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...sk,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/sk.js
Normal file
4
packages/input-tel/translations/sk.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Zvoliť krajinu',
|
||||||
|
phoneNumber: 'Telefónne číslo',
|
||||||
|
};
|
||||||
5
packages/input-tel/translations/uk-UA.js
Normal file
5
packages/input-tel/translations/uk-UA.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import uk from './uk.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...uk,
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/uk.js
Normal file
4
packages/input-tel/translations/uk.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: 'Вибрати країну',
|
||||||
|
phoneNumber: 'Номер телефону',
|
||||||
|
};
|
||||||
4
packages/input-tel/translations/zh.js
Normal file
4
packages/input-tel/translations/zh.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
selectCountry: '选择国家/地区',
|
||||||
|
phoneNumber: '电话号码',
|
||||||
|
};
|
||||||
290
packages/input-tel/types/index.d.ts
vendored
Normal file
290
packages/input-tel/types/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
* Phone number types as provided by google-libphonenumber
|
||||||
|
* See:
|
||||||
|
* - https://www.npmjs.com/package/google-libphonenumber
|
||||||
|
* - https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
*/
|
||||||
|
export type PhoneNumberType =
|
||||||
|
| 'fixed-line'
|
||||||
|
| 'fixed-line-or-mobile'
|
||||||
|
| 'mobile'
|
||||||
|
| 'pager'
|
||||||
|
| 'personal-number'
|
||||||
|
| 'premium-rate'
|
||||||
|
| 'shared-cost'
|
||||||
|
| 'toll-free'
|
||||||
|
| 'uan'
|
||||||
|
| 'voip'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Phone number possibilities as provided by google-libphonenumber
|
||||||
|
* See:
|
||||||
|
* - https://www.npmjs.com/package/google-libphonenumber
|
||||||
|
* - https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
*/
|
||||||
|
export type PhoneNumberPossibility =
|
||||||
|
| 'is-possible'
|
||||||
|
| 'invalid-country-code'
|
||||||
|
| 'too-long'
|
||||||
|
| 'too-short'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Phone number formats / formatting strategies as provided by google-libphonenumber
|
||||||
|
* See:
|
||||||
|
* - https://www.npmjs.com/package/google-libphonenumber
|
||||||
|
* - https://www.npmjs.com/package/awesome-phonenumber
|
||||||
|
*/
|
||||||
|
export type FormatStrategy = 'e164' | 'international' | 'national' | 'rfc3966' | 'significant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported countries/regions as provided via
|
||||||
|
* `libphonenumber.PhoneNumberUtil.getInstance().getSupportedRegions()`
|
||||||
|
*/
|
||||||
|
export type RegionCode =
|
||||||
|
| 'AC'
|
||||||
|
| 'AD'
|
||||||
|
| 'AE'
|
||||||
|
| 'AF'
|
||||||
|
| 'AG'
|
||||||
|
| 'AI'
|
||||||
|
| 'AL'
|
||||||
|
| 'AM'
|
||||||
|
| 'AO'
|
||||||
|
| 'AR'
|
||||||
|
| 'AS'
|
||||||
|
| 'AT'
|
||||||
|
| 'AU'
|
||||||
|
| 'AW'
|
||||||
|
| 'AX'
|
||||||
|
| 'AZ'
|
||||||
|
| 'BA'
|
||||||
|
| 'BB'
|
||||||
|
| 'BD'
|
||||||
|
| 'BE'
|
||||||
|
| 'BF'
|
||||||
|
| 'BG'
|
||||||
|
| 'BH'
|
||||||
|
| 'BI'
|
||||||
|
| 'BJ'
|
||||||
|
| 'BL'
|
||||||
|
| 'BM'
|
||||||
|
| 'BN'
|
||||||
|
| 'BO'
|
||||||
|
| 'BQ'
|
||||||
|
| 'BR'
|
||||||
|
| 'BS'
|
||||||
|
| 'BT'
|
||||||
|
| 'BW'
|
||||||
|
| 'BY'
|
||||||
|
| 'BZ'
|
||||||
|
| 'CA'
|
||||||
|
| 'CC'
|
||||||
|
| 'CD'
|
||||||
|
| 'CF'
|
||||||
|
| 'CG'
|
||||||
|
| 'CH'
|
||||||
|
| 'CI'
|
||||||
|
| 'CK'
|
||||||
|
| 'CL'
|
||||||
|
| 'CM'
|
||||||
|
| 'CN'
|
||||||
|
| 'CO'
|
||||||
|
| 'CR'
|
||||||
|
| 'CU'
|
||||||
|
| 'CV'
|
||||||
|
| 'CW'
|
||||||
|
| 'CX'
|
||||||
|
| 'CY'
|
||||||
|
| 'CZ'
|
||||||
|
| 'DE'
|
||||||
|
| 'DJ'
|
||||||
|
| 'DK'
|
||||||
|
| 'DM'
|
||||||
|
| 'DO'
|
||||||
|
| 'DZ'
|
||||||
|
| 'EC'
|
||||||
|
| 'EE'
|
||||||
|
| 'EG'
|
||||||
|
| 'EH'
|
||||||
|
| 'ER'
|
||||||
|
| 'ES'
|
||||||
|
| 'ET'
|
||||||
|
| 'FI'
|
||||||
|
| 'FJ'
|
||||||
|
| 'FK'
|
||||||
|
| 'FM'
|
||||||
|
| 'FO'
|
||||||
|
| 'FR'
|
||||||
|
| 'GA'
|
||||||
|
| 'GB'
|
||||||
|
| 'GD'
|
||||||
|
| 'GE'
|
||||||
|
| 'GF'
|
||||||
|
| 'GG'
|
||||||
|
| 'GH'
|
||||||
|
| 'GI'
|
||||||
|
| 'GL'
|
||||||
|
| 'GM'
|
||||||
|
| 'GN'
|
||||||
|
| 'GP'
|
||||||
|
| 'GQ'
|
||||||
|
| 'GR'
|
||||||
|
| 'GT'
|
||||||
|
| 'GU'
|
||||||
|
| 'GW'
|
||||||
|
| 'GY'
|
||||||
|
| 'HK'
|
||||||
|
| 'HN'
|
||||||
|
| 'HR'
|
||||||
|
| 'HT'
|
||||||
|
| 'HU'
|
||||||
|
| 'ID'
|
||||||
|
| 'IE'
|
||||||
|
| 'IL'
|
||||||
|
| 'IM'
|
||||||
|
| 'IN'
|
||||||
|
| 'IO'
|
||||||
|
| 'IQ'
|
||||||
|
| 'IR'
|
||||||
|
| 'IS'
|
||||||
|
| 'IT'
|
||||||
|
| 'JE'
|
||||||
|
| 'JM'
|
||||||
|
| 'JO'
|
||||||
|
| 'JP'
|
||||||
|
| 'KE'
|
||||||
|
| 'KG'
|
||||||
|
| 'KH'
|
||||||
|
| 'KI'
|
||||||
|
| 'KM'
|
||||||
|
| 'KN'
|
||||||
|
| 'KP'
|
||||||
|
| 'KR'
|
||||||
|
| 'KW'
|
||||||
|
| 'KY'
|
||||||
|
| 'KZ'
|
||||||
|
| 'LA'
|
||||||
|
| 'LB'
|
||||||
|
| 'LC'
|
||||||
|
| 'LI'
|
||||||
|
| 'LK'
|
||||||
|
| 'LR'
|
||||||
|
| 'LS'
|
||||||
|
| 'LT'
|
||||||
|
| 'LU'
|
||||||
|
| 'LV'
|
||||||
|
| 'LY'
|
||||||
|
| 'MA'
|
||||||
|
| 'MC'
|
||||||
|
| 'MD'
|
||||||
|
| 'ME'
|
||||||
|
| 'MF'
|
||||||
|
| 'MG'
|
||||||
|
| 'MH'
|
||||||
|
| 'MK'
|
||||||
|
| 'ML'
|
||||||
|
| 'MM'
|
||||||
|
| 'MN'
|
||||||
|
| 'MO'
|
||||||
|
| 'MP'
|
||||||
|
| 'MQ'
|
||||||
|
| 'MR'
|
||||||
|
| 'MS'
|
||||||
|
| 'MT'
|
||||||
|
| 'MU'
|
||||||
|
| 'MV'
|
||||||
|
| 'MW'
|
||||||
|
| 'MX'
|
||||||
|
| 'MY'
|
||||||
|
| 'MZ'
|
||||||
|
| 'NA'
|
||||||
|
| 'NC'
|
||||||
|
| 'NE'
|
||||||
|
| 'NF'
|
||||||
|
| 'NG'
|
||||||
|
| 'NI'
|
||||||
|
| 'NL'
|
||||||
|
| 'NO'
|
||||||
|
| 'NP'
|
||||||
|
| 'NR'
|
||||||
|
| 'NU'
|
||||||
|
| 'NZ'
|
||||||
|
| 'OM'
|
||||||
|
| 'PA'
|
||||||
|
| 'PE'
|
||||||
|
| 'PF'
|
||||||
|
| 'PG'
|
||||||
|
| 'PH'
|
||||||
|
| 'PK'
|
||||||
|
| 'PL'
|
||||||
|
| 'PM'
|
||||||
|
| 'PR'
|
||||||
|
| 'PS'
|
||||||
|
| 'PT'
|
||||||
|
| 'PW'
|
||||||
|
| 'PY'
|
||||||
|
| 'QA'
|
||||||
|
| 'RE'
|
||||||
|
| 'RO'
|
||||||
|
| 'RS'
|
||||||
|
| 'RU'
|
||||||
|
| 'RW'
|
||||||
|
| 'SA'
|
||||||
|
| 'SB'
|
||||||
|
| 'SC'
|
||||||
|
| 'SD'
|
||||||
|
| 'SE'
|
||||||
|
| 'SG'
|
||||||
|
| 'SH'
|
||||||
|
| 'SI'
|
||||||
|
| 'SJ'
|
||||||
|
| 'SK'
|
||||||
|
| 'SL'
|
||||||
|
| 'SM'
|
||||||
|
| 'SN'
|
||||||
|
| 'SO'
|
||||||
|
| 'SR'
|
||||||
|
| 'SS'
|
||||||
|
| 'ST'
|
||||||
|
| 'SV'
|
||||||
|
| 'SX'
|
||||||
|
| 'SY'
|
||||||
|
| 'SZ'
|
||||||
|
| 'TA'
|
||||||
|
| 'TC'
|
||||||
|
| 'TD'
|
||||||
|
| 'TG'
|
||||||
|
| 'TH'
|
||||||
|
| 'TJ'
|
||||||
|
| 'TK'
|
||||||
|
| 'TL'
|
||||||
|
| 'TM'
|
||||||
|
| 'TN'
|
||||||
|
| 'TO'
|
||||||
|
| 'TR'
|
||||||
|
| 'TT'
|
||||||
|
| 'TV'
|
||||||
|
| 'TW'
|
||||||
|
| 'TZ'
|
||||||
|
| 'UA'
|
||||||
|
| 'UG'
|
||||||
|
| 'US'
|
||||||
|
| 'UY'
|
||||||
|
| 'UZ'
|
||||||
|
| 'VA'
|
||||||
|
| 'VC'
|
||||||
|
| 'VE'
|
||||||
|
| 'VG'
|
||||||
|
| 'VI'
|
||||||
|
| 'VN'
|
||||||
|
| 'VU'
|
||||||
|
| 'WF'
|
||||||
|
| 'WS'
|
||||||
|
| 'XK'
|
||||||
|
| 'YE'
|
||||||
|
| 'YT'
|
||||||
|
| 'ZA'
|
||||||
|
| 'ZM'
|
||||||
|
| 'ZW';
|
||||||
Loading…
Reference in a new issue