diff --git a/packages/docs/docs/02-Usage/01-PhoneInput.md b/packages/docs/docs/02-Usage/01-PhoneInput.md index 80db0fcd..a7f9fbff 100644 --- a/packages/docs/docs/02-Usage/01-PhoneInput.md +++ b/packages/docs/docs/02-Usage/01-PhoneInput.md @@ -174,6 +174,14 @@ description="Disable phone value mask formatting. All formatting characters will defaultValue="false" /> +### `allowMaskOverflow` + +Allow input to exceed the mask length. When set to true, formatting mask will apply to the part that fits within the mask capacity, and overflow digits will be appended at the end unformatted.
For example: "+123456789012", will be formatted like "+1 (234) 567-89012"} +defaultValue="false" +/> + ### `flags` +### `allowMaskOverflow` + +Allow input to exceed the mask length. When set to true, formatting mask will apply to the part that fits within the mask capacity, and overflow digits will be appended at the end unformatted.
For example: "+123456789012", will be formatted like "+1 (234) 567-89012"} +defaultValue="false" +/> + ### `inputRef` { @@ -166,6 +174,7 @@ export const usePhoneInput = ({ defaultMask, countryGuessingEnabled, disableFormatting, + allowMaskOverflow, }; const ref = useRef(null); diff --git a/src/stories/PhoneInput/PhoneInput.stories.tsx b/src/stories/PhoneInput/PhoneInput.stories.tsx index f31c1d36..ab3e2c97 100644 --- a/src/stories/PhoneInput/PhoneInput.stories.tsx +++ b/src/stories/PhoneInput/PhoneInput.stories.tsx @@ -26,6 +26,7 @@ import { WithAutofocus } from './stories/WithAutofocus.story'; import { DisableFormatting } from './stories/DisableFormatting.story'; import { ControlledMode } from './stories/ControlledMode.story'; import { CustomFlags } from './stories/CustomFlags.story'; +import { AllowMaskOverflow } from './stories/AllowMaskOverflow.story'; export const _Default = Default; export const _WithInitialValue = WithInitialValue; @@ -42,3 +43,4 @@ export const _WithAutofocus = WithAutofocus; export const _DisableFormatting = DisableFormatting; export const _ControlledMode = ControlledMode; export const _CustomFlags = CustomFlags; +export const _AllowMaskOverflow = AllowMaskOverflow; diff --git a/src/stories/PhoneInput/stories/AllowMaskOverflow.story.tsx b/src/stories/PhoneInput/stories/AllowMaskOverflow.story.tsx new file mode 100644 index 00000000..272de633 --- /dev/null +++ b/src/stories/PhoneInput/stories/AllowMaskOverflow.story.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { PhoneInput } from '../../../index'; +import { PhoneInputStory } from '../PhoneInput.stories'; + +export const AllowMaskOverflow: PhoneInputStory = { + name: 'Allow Mask Overflow', + render: (args) => , + args: { + defaultCountry: 'us', + allowMaskOverflow: true, + placeholder: 'Enter your phone number', + }, +}; diff --git a/src/utils/common/__tests__/applyMask.test.ts b/src/utils/common/__tests__/applyMask.test.ts index dd21cbb1..1c08b3a9 100644 --- a/src/utils/common/__tests__/applyMask.test.ts +++ b/src/utils/common/__tests__/applyMask.test.ts @@ -114,4 +114,74 @@ describe('applyMask', () => { '(1234', ); }); + + test('should handle allowMaskOverflow option', () => { + // format the part that fits, append overflow at the end + expect( + applyMask({ + value: '1234567890123', + mask: '.. .. ....', + maskSymbol: '.', + allowMaskOverflow: true, + }), + ).toBe('12 34 567890123'); + + expect( + applyMask({ + value: '123456789', + mask: '.... ....', + maskSymbol: '.', + allowMaskOverflow: true, + }), + ).toBe('1234 56789'); + + expect( + applyMask({ + value: '23456789012', + mask: '(...) ...-....', + maskSymbol: '.', + allowMaskOverflow: true, + }), + ).toBe('(234) 567-89012'); + + // with offset + expect( + applyMask({ + value: '+1234567890123', + mask: '.. .. ....', + maskSymbol: '.', + offset: 2, + allowMaskOverflow: true, + }), + ).toBe('+123 45 67890123'); + + // apply formatting on mask not overflow (normal behavior) + expect( + applyMask({ + value: '1234567', + mask: '.. .. ....', + maskSymbol: '.', + allowMaskOverflow: true, + }), + ).toBe('12 34 567'); + + expect( + applyMask({ + value: '1234', + mask: '.... ....', + maskSymbol: '.', + allowMaskOverflow: true, + }), + ).toBe('1234 '); + + expect( + applyMask({ + value: '+1234567', + mask: '.. .. ....', + maskSymbol: '.', + offset: 2, + allowMaskOverflow: true, + }), + ).toBe('+123 45 67'); + }); }); diff --git a/src/utils/common/applyMask.ts b/src/utils/common/applyMask.ts index 18903055..b71f6b7e 100644 --- a/src/utils/common/applyMask.ts +++ b/src/utils/common/applyMask.ts @@ -22,6 +22,16 @@ interface ApplyMaskArgs { * if false -> "(1234) " */ trimNonMaskCharsLeftover?: boolean; + + /** + * @description Allow input to exceed the mask length. When set to true, formatting mask will apply to the part that fits, and overflow digits will be appended at the end. + * @example + * value: "12345678" + * mask: "(....)" + * if true -> "(1234)5678" (formatted part + overflow) + * if false -> "(1234" (formatted but truncated) + */ + allowMaskOverflow?: boolean; } export const applyMask = ({ @@ -30,14 +40,21 @@ export const applyMask = ({ maskSymbol, offset = 0, trimNonMaskCharsLeftover = false, + allowMaskOverflow = false, }: ApplyMaskArgs): string => { if (value.length < offset) return value; - const savedValuePart = value.slice(0, offset); - const valueToMask = value.slice(offset); + const prefix = value.slice(0, offset); + const valueToFormat = value.slice(offset); + + const maskLength = mask + .split('') + .filter((char) => char === maskSymbol).length; - let result = savedValuePart; + const valueToMask = valueToFormat.slice(0, maskLength); + const overflow = allowMaskOverflow ? valueToFormat.slice(maskLength) : ''; + let result = prefix; let charsPlaced = 0; for (const maskChar of mask.split('')) { @@ -56,5 +73,5 @@ export const applyMask = ({ } } - return result; + return result + overflow; }; diff --git a/src/utils/handlePhoneChange.ts b/src/utils/handlePhoneChange.ts index c8544e40..c4a7973f 100644 --- a/src/utils/handlePhoneChange.ts +++ b/src/utils/handlePhoneChange.ts @@ -15,6 +15,7 @@ export interface PhoneFormattingConfig { defaultMask: string; countryGuessingEnabled: boolean; disableFormatting: boolean; + allowMaskOverflow: boolean; } interface HandlePhoneChangeProps extends PhoneFormattingConfig { @@ -38,6 +39,7 @@ export function handlePhoneChange({ defaultMask, countryGuessingEnabled, disableFormatting, + allowMaskOverflow, }: HandlePhoneChangeProps): { phone: string; inputValue: string; @@ -77,6 +79,7 @@ export function handlePhoneChange({ forceDialCode, insertDialCodeOnEmpty, disableDialCodeAndPrefix, + allowMaskOverflow, }); const resultCountry = diff --git a/src/utils/handleUserInput.ts b/src/utils/handleUserInput.ts index f8891e12..bc2ee203 100644 --- a/src/utils/handleUserInput.ts +++ b/src/utils/handleUserInput.ts @@ -33,6 +33,7 @@ export const handleUserInput = ( defaultMask, disableFormatting, countries, + allowMaskOverflow, }: HandleUserInputOptions, ): { phone: string; @@ -131,6 +132,7 @@ export const handleUserInput = ( disableDialCodeAndPrefix, disableFormatting, defaultMask, + allowMaskOverflow, }); const newCursorPosition = getCursorPosition({ diff --git a/src/utils/phoneUtils/formatPhone.ts b/src/utils/phoneUtils/formatPhone.ts index b200a609..30f396b2 100644 --- a/src/utils/phoneUtils/formatPhone.ts +++ b/src/utils/phoneUtils/formatPhone.ts @@ -32,6 +32,13 @@ export interface FormatPhoneConfig { * Trim all non-digit values from the end of the result */ trimNonDigitsEnd?: boolean; + /** + * Allow input to exceed the mask length. When set to true, formatting mask will apply to the part that fits, and overflow digits will be appended at the end. + * @example + * phone: "+1 23456789012", mask: "(...) ...-...." + * result: "+1 (234) 567-89012" + */ + allowMaskOverflow?: boolean; } export const formatPhone = ( @@ -137,6 +144,7 @@ export const formatPhone = ( trimNonMaskCharsLeftover: config.trimNonDigitsEnd || (config.disableDialCodeAndPrefix && phoneRightSide.length === 0), + allowMaskOverflow: config.allowMaskOverflow, }); if (config.disableDialCodeAndPrefix) {