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();