From 2e72efc05e4f4af8e982e6687a6e0f5aa9c49ef4 Mon Sep 17 00:00:00 2001
From: vladimirpotekhin
Date: Wed, 22 Nov 2023 14:30:21 +0300
Subject: [PATCH] feat(phone): add ability to configure the separator
---
.../tests/addons/phone/phone-separator.cy.ts | 161 ++++++++++++++++++
.../src/pages/phone/phone-doc.component.ts | 4 +
.../src/pages/phone/phone-doc.template.html | 14 ++
.../lib/masks/phone/phone-mask-non-strict.ts | 4 +-
.../src/lib/masks/phone/phone-mask-strict.ts | 4 +-
.../phone/src/lib/masks/phone/phone-mask.ts | 5 +-
.../masks/phone/utils/get-phone-template.ts | 8 +-
7 files changed, 195 insertions(+), 5 deletions(-)
create mode 100644 projects/demo-integrations/cypress/tests/addons/phone/phone-separator.cy.ts
diff --git a/projects/demo-integrations/cypress/tests/addons/phone/phone-separator.cy.ts b/projects/demo-integrations/cypress/tests/addons/phone/phone-separator.cy.ts
new file mode 100644
index 000000000..f9ce496b2
--- /dev/null
+++ b/projects/demo-integrations/cypress/tests/addons/phone/phone-separator.cy.ts
@@ -0,0 +1,161 @@
+import {DemoPath} from '@demo/constants';
+
+import {BROWSER_SUPPORTS_REAL_EVENTS} from '../../../support/constants';
+
+describe('Phone', () => {
+ describe('Separator', () => {
+ beforeEach(() => {
+ cy.visit(`/${DemoPath.PhonePackage}/API?separator$=1`);
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .as('input');
+ });
+
+ describe('basic typing (1 character per keydown)', () => {
+ const tests = [
+ // [Typed value, Masked value, caretIndex]
+ ['920', '+7 920', '+7 920'.length],
+ ['920341', '+7 920 341', '+7 920 341'.length],
+ ['92034156', '+7 920 341 56', '+7 920 341 56'.length],
+ ['9203415627', '+7 920 341 56 27', '+7 920 341 56 27'.length],
+ ['92034156274234123', '+7 920 341 56 27', '+7 920 341 56 27'.length],
+ ['9 nd4 e', '+7 94', '+7 94'.length],
+ ] as const;
+
+ tests.forEach(([typedValue, maskedValue, caretIndex]) => {
+ it(`Type "${typedValue}" => "${maskedValue}"`, () => {
+ cy.get('@input')
+ .type(typedValue)
+ .should('have.value', maskedValue)
+ .should('have.prop', 'selectionStart', caretIndex)
+ .should('have.prop', 'selectionEnd', caretIndex);
+ });
+ });
+ });
+
+ describe('basic erasing (value = "+7 920 424 11 32"', () => {
+ beforeEach(() => {
+ cy.get('@input').type('9204241132');
+ });
+
+ const tests = [
+ // [How many times "Backspace"-key was pressed, caretPosition, Masked value]
+ [1, '+7 920 424 11 3'.length, '+7 920 424 11 3'],
+ [2, '+7 920 424 11'.length, '+7 920 424 11'],
+ [3, '+7 920 424 1'.length, '+7 920 424 1'],
+ [4, '+7 920 424'.length, '+7 920 424'],
+ [13, '+7 '.length, '+7 '],
+ ] as const;
+
+ tests.forEach(([n, caretIndex, maskedValue]) => {
+ it(`Backspace x${n} => "${maskedValue}"`, () => {
+ cy.get('@input')
+ .type('{backspace}'.repeat(n))
+ .should('have.value', maskedValue)
+ .should('have.prop', 'selectionStart', caretIndex)
+ .should('have.prop', 'selectionEnd', caretIndex);
+ });
+ });
+ });
+
+ describe('Editing somewhere in the middle of a value (NOT the last character)', () => {
+ beforeEach(() => {
+ cy.get('@input').type('920 424 11 32');
+ });
+
+ it('+7 9|20 424 11 32 => Backspace => +7 2|04241132', () => {
+ cy.get('@input')
+ .type('{leftArrow}'.repeat('20 424 11 32'.length))
+ .type('{backspace}')
+ .should('have.value', '+7 204241132')
+ .should('have.prop', 'selectionStart', '+7 '.length)
+ .should('have.prop', 'selectionEnd', '+7 '.length);
+ });
+ });
+
+ describe('Text selection', () => {
+ beforeEach(() => {
+ cy.get('@input').type('920 424 11 32');
+ });
+
+ describe(
+ 'Select range and press Backspace',
+ BROWSER_SUPPORTS_REAL_EVENTS,
+ () => {
+ it('+7 920 424-11-32 => Select "+7 920 424 |11| 32" => Backspace => +7 920 424 |32', () => {
+ cy.get('@input')
+ .type('{leftArrow}'.repeat(' 32'.length))
+ .realPress([
+ 'Shift',
+ ...new Array('11'.length).fill('ArrowLeft'),
+ ]);
+
+ cy.get('@input')
+ .type('{backspace}')
+ .should('have.value', '+7 920 424 32')
+ .should('have.prop', 'selectionStart', '+7 920 424 '.length)
+ .should('have.prop', 'selectionEnd', '+7 920 424 '.length);
+ });
+ },
+ );
+
+ describe(
+ 'Select range and type a digit',
+ BROWSER_SUPPORTS_REAL_EVENTS,
+ () => {
+ it('+7 920 424-11-32 => Select "+7 920 424 |11| 32" => Type "5" => +7 920 424 5|3 2', () => {
+ cy.get('@input')
+ .type('{leftArrow}'.repeat(' 32'.length))
+ .realPress([
+ 'Shift',
+ ...new Array('11'.length).fill('ArrowLeft'),
+ ]);
+
+ cy.get('@input')
+ .type('5')
+ .should('have.value', '+7 920 424 53 2')
+ .should('have.prop', 'selectionStart', '+7 920 424 5'.length)
+ .should('have.prop', 'selectionEnd', '+7 920 424 5'.length);
+ });
+ },
+ );
+ });
+ });
+
+ describe('Some countries', () => {
+ it('US: +1 212 343 3355', () => {
+ openCountry('US');
+
+ cy.get('@input').type('2123433355');
+ cy.get('@input').should('have.value', '+1 212 343 3355');
+ });
+
+ it('KZ: +7 771 931 1111', () => {
+ openCountry('KZ');
+
+ cy.get('@input').type('7719311111');
+ cy.get('@input').should('have.value', '+7 771 931 1111');
+ });
+
+ it('BY: +375 44 748 82 69', () => {
+ openCountry('BY');
+
+ cy.get('@input').type('447488269');
+ cy.get('@input').should('have.value', '+375 44 748 82 69');
+ });
+
+ it('TR: +90 539 377 07 43', () => {
+ openCountry('TR');
+
+ cy.get('@input').type('5393770743');
+ cy.get('@input').should('have.value', '+90 539 377 07 43');
+ });
+ });
+});
+
+function openCountry(code: string): void {
+ cy.visit(`/${DemoPath.PhonePackage}/API?countryIsoCode=${code}&separator$=1`);
+ cy.get('#demo-content input').should('be.visible').first().focus().as('input');
+}
diff --git a/projects/demo/src/pages/phone/phone-doc.component.ts b/projects/demo/src/pages/phone/phone-doc.component.ts
index a451121b7..315bab1bb 100644
--- a/projects/demo/src/pages/phone/phone-doc.component.ts
+++ b/projects/demo/src/pages/phone/phone-doc.component.ts
@@ -57,6 +57,10 @@ export class PhoneDocComponent implements GeneratorOptions {
countryIsoCode: CountryCode = 'RU';
+ separatorVariants = ['-', ' '];
+
+ separator = '-';
+
maskitoOptions = maskitoPhoneOptionsGenerator(this);
constructor(@Inject(TUI_IS_APPLE) private readonly isApple: boolean) {}
diff --git a/projects/demo/src/pages/phone/phone-doc.template.html b/projects/demo/src/pages/phone/phone-doc.template.html
index d0d33070f..3c417ea3e 100644
--- a/projects/demo/src/pages/phone/phone-doc.template.html
+++ b/projects/demo/src/pages/phone/phone-doc.template.html
@@ -159,6 +159,20 @@
true
+
+ Separator between groups of numbers in a phone number (excluding country code and area code).
+
+ Default:
+ -
+
+
diff --git a/projects/phone/src/lib/masks/phone/phone-mask-non-strict.ts b/projects/phone/src/lib/masks/phone/phone-mask-non-strict.ts
index 4208bc60e..10e67b661 100644
--- a/projects/phone/src/lib/masks/phone/phone-mask-non-strict.ts
+++ b/projects/phone/src/lib/masks/phone/phone-mask-non-strict.ts
@@ -10,9 +10,11 @@ import {generatePhoneMask, getPhoneTemplate, selectTemplate} from './utils';
export function maskitoPhoneNonStrictOptionsGenerator({
defaultIsoCode,
metadata,
+ separator = '-',
}: {
defaultIsoCode?: CountryCode;
metadata: MetadataJson;
+ separator?: string;
}): Required {
const formatter = new AsYouType(defaultIsoCode, metadata);
const prefix = '+';
@@ -22,7 +24,7 @@ export function maskitoPhoneNonStrictOptionsGenerator({
return {
...MASKITO_DEFAULT_OPTIONS,
mask: ({value}) => {
- const newTemplate = getPhoneTemplate(formatter, value);
+ const newTemplate = getPhoneTemplate(formatter, value, separator);
const newPhoneLength = value.replace(/\D/g, '').length;
currentTemplate = selectTemplate({
diff --git a/projects/phone/src/lib/masks/phone/phone-mask-strict.ts b/projects/phone/src/lib/masks/phone/phone-mask-strict.ts
index 743b9f865..42e6547a5 100644
--- a/projects/phone/src/lib/masks/phone/phone-mask-strict.ts
+++ b/projects/phone/src/lib/masks/phone/phone-mask-strict.ts
@@ -22,9 +22,11 @@ import {generatePhoneMask, getPhoneTemplate, selectTemplate} from './utils';
export function maskitoPhoneStrictOptionsGenerator({
countryIsoCode,
metadata,
+ separator = '-',
}: {
countryIsoCode: CountryCode;
metadata: MetadataJson;
+ separator?: string;
}): Required {
const code = getCountryCallingCode(countryIsoCode, metadata);
const formatter = new AsYouType(countryIsoCode, metadata);
@@ -36,7 +38,7 @@ export function maskitoPhoneStrictOptionsGenerator({
return {
...MASKITO_DEFAULT_OPTIONS,
mask: ({value}) => {
- const newTemplate = getPhoneTemplate(formatter, value);
+ const newTemplate = getPhoneTemplate(formatter, value, separator);
const newPhoneLength = value.replace(/\D/g, '').length;
currentTemplate = selectTemplate({
diff --git a/projects/phone/src/lib/masks/phone/phone-mask.ts b/projects/phone/src/lib/masks/phone/phone-mask.ts
index 040f7d2d9..4de3f263f 100644
--- a/projects/phone/src/lib/masks/phone/phone-mask.ts
+++ b/projects/phone/src/lib/masks/phone/phone-mask.ts
@@ -8,15 +8,18 @@ export function maskitoPhoneOptionsGenerator({
countryIsoCode,
metadata,
strict = true,
+ separator = '-',
}: {
countryIsoCode?: CountryCode;
metadata: MetadataJson;
strict?: boolean;
+ separator?: string;
}): Required {
return strict && countryIsoCode
- ? maskitoPhoneStrictOptionsGenerator({countryIsoCode, metadata})
+ ? maskitoPhoneStrictOptionsGenerator({countryIsoCode, metadata, separator})
: maskitoPhoneNonStrictOptionsGenerator({
defaultIsoCode: countryIsoCode,
metadata,
+ separator,
});
}
diff --git a/projects/phone/src/lib/masks/phone/utils/get-phone-template.ts b/projects/phone/src/lib/masks/phone/utils/get-phone-template.ts
index 90e06a56f..da6faa0ba 100644
--- a/projects/phone/src/lib/masks/phone/utils/get-phone-template.ts
+++ b/projects/phone/src/lib/masks/phone/utils/get-phone-template.ts
@@ -1,13 +1,17 @@
import {AsYouType} from 'libphonenumber-js/core';
-export function getPhoneTemplate(formatter: AsYouType, value: string): string {
+export function getPhoneTemplate(
+ formatter: AsYouType,
+ value: string,
+ separator: string,
+): string {
formatter.input(value.replace(/[^\d+]/g, ''));
const initialTemplate = formatter.getTemplate();
const split = initialTemplate.split(' ');
const template =
split.length > 1
- ? `${split.slice(0, 2).join(' ')} ${split.slice(2).join('-')}`
+ ? `${split.slice(0, 2).join(' ')} ${split.slice(2).join(separator)}`
: initialTemplate;
formatter.reset();