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