Skip to content

Commit

Permalink
Merge pull request #99 from goveo/feat/use-e164-format
Browse files Browse the repository at this point in the history
Switch to E.164 format
  • Loading branch information
ybrusentsov authored Sep 13, 2023
2 parents 82ebbac + dd46b0f commit c80bb62
Show file tree
Hide file tree
Showing 21 changed files with 592 additions and 417 deletions.
6 changes: 3 additions & 3 deletions packages/docs/docs/02-Usage/01-PhoneInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ import {PhoneInput} from 'react-international-phone';

## Events

| Event | Type | Description |
| -------- | ----------------------------------------------- | ----------------------------------- |
| onChange | `(phone: string, country: CountryIso2) => void` | Callback that calls on phone change |
| Event | Type | Description |
| -------- | ------------------------------------------------------------------------------------ | ----------------------------------- |
| onChange | `(phone: string, country: { country: ParsedCountry, displayValue: string }) => void` | Callback that calls on phone change |

Input events like **`onFocus`** and **`onBlur`** can be passed to the `inputProps`

Expand Down
180 changes: 126 additions & 54 deletions src/components/PhoneInput/PhoneInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,93 +52,105 @@ describe('PhoneInput', () => {
mockScrollIntoView();
});

test('should set phone value', () => {
render(<PhoneInput value="+38099109" defaultCountry="ua" />);
expect(getInput().value).toBe('+380 (99) 109 ');
describe('render value', () => {
test('should set phone value', () => {
render(<PhoneInput value="+38099109" defaultCountry="ua" />);
expect(getInput().value).toBe('+380 (99) 109 ');
});

test('should call update input value on state change', () => {
const { rerender } = render(<PhoneInput value="+12345" />);
expect(getInput().value).toBe('+1 (234) 5');

rerender(<PhoneInput value="+123456" />);
expect(getInput().value).toBe('+1 (234) 56');
});
});

describe('onChange', () => {
test('should call onChange when input value is updated', async () => {
const onChange = jest.fn();
render(
<PhoneInput value="+1 " defaultCountry="us" onChange={onChange} />,
);
render(<PhoneInput value="+1" defaultCountry="us" onChange={onChange} />);
expect(onChange.mock.calls.length).toBe(0);

fireEvent.change(getInput(), { target: { value: '38099' } });
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+380 (99) ');
expect(onChange.mock.calls[0][0]).toBe('+38099');
expect(onChange.mock.calls[0][1].displayValue).toBe('+380 (99) ');

fireEvent.change(getInput(), { target: { value: '+380 (99) 999' } });
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls[1][0]).toBe('+380 (99) 999 ');
expect(onChange.mock.calls[1][0]).toBe('+38099999');
expect(onChange.mock.calls[1][1].displayValue).toBe('+380 (99) 999 ');

fireEvent.change(getInput(), { target: { value: '' } });
expect(onChange.mock.calls.length).toBe(3);
expect(onChange.mock.calls[2][0]).toBe('');
expect(onChange.mock.calls[2][1].displayValue).toBe('');

fireEvent.change(getInput(), { target: { value: '+1 403 555-6666' } });
expect(onChange.mock.calls.length).toBe(4);
expect(onChange.mock.calls[3][0]).toBe('+1 (403) 555-6666');
expect(onChange.mock.calls[3][0]).toBe('+14035556666');
expect(onChange.mock.calls[3][1].displayValue).toBe('+1 (403) 555-6666');
});

test('should call onChange on initialization (value is not formatted)', () => {
test('should call onChange on initialization (value is not in e164 format)', () => {
const onChange = jest.fn();
render(
<PhoneInput
value="+19999999999"
value="+1 (999) 999 9999"
defaultCountry="us"
onChange={onChange}
/>,
);

expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1 (999) 999-9999');
expect(onChange.mock.calls[0][0]).toBe('+19999999999');
});

test('should call onChange on initialization (value is empty string)', () => {
const onChange = jest.fn();
render(<PhoneInput value="" defaultCountry="us" onChange={onChange} />);

expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1 ');
expect(onChange.mock.calls[0][0]).toBe('+1');
});

test('should call onChange on initialization (value is not provided)', () => {
const onChange = jest.fn();
render(<PhoneInput defaultCountry="us" onChange={onChange} />);

expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1 ');
expect(onChange.mock.calls[0][0]).toBe('+1');
});

test('should call onChange on country change', () => {
const onChange = jest.fn();
render(<PhoneInput defaultCountry="us" onChange={onChange} />);
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1 ');
expect(onChange.mock.calls[0][0]).toBe('+1');

fireEvent.click(getCountrySelector());
fireEvent.click(getDropdownOption('ua'));

expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls[1][0]).toBe('+380 ');
expect(onChange.mock.calls[1][0]).toBe('+380');

// set Canada
fireEvent.change(getInput(), { target: { value: '+1 (204) ' } });
expect(onChange.mock.calls.length).toBe(3);
expect(onChange.mock.calls[2][0]).toBe('+1 (204) ');
expect(onChange.mock.calls[2][0]).toBe('+1204');

fireEvent.click(getCountrySelector());
fireEvent.click(getDropdownOption('ca'));
expect(onChange.mock.calls.length).toBe(4);
expect(onChange.mock.calls[3][0]).toBe('+1 ');
expect(onChange.mock.calls[3][0]).toBe('+1');

// should fire change even if phone is not changed
fireEvent.click(getCountrySelector());
fireEvent.click(getDropdownOption('us'));
expect(onChange.mock.calls.length).toBe(5);
expect(onChange.mock.calls[4][0]).toBe('+1 ');
expect(onChange.mock.calls[4][0]).toBe('+1');
});

test('should call onChange on undo/redo', () => {
Expand All @@ -152,15 +164,21 @@ describe('PhoneInput', () => {
/>,
);
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1 ');
expect(onChange.mock.calls[0][0]).toBe('+1');
expect(onChange.mock.calls[0][1].displayValue).toBe('+1 ');
expect(onChange.mock.calls[0][1].displayValue).toBe(getInput().value);

fireEvent.change(getInput(), { target: { value: '+38099' } });
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls[1][0]).toBe('+380 (99) ');
expect(onChange.mock.calls[1][0]).toBe('+38099');
expect(onChange.mock.calls[1][1].displayValue).toBe('+380 (99) ');
expect(onChange.mock.calls[1][1].displayValue).toBe(getInput().value);

fireEvent.change(getInput(), { target: { value: '+38099 99' } });
expect(onChange.mock.calls.length).toBe(3);
expect(onChange.mock.calls[2][0]).toBe('+380 (99) 99');
expect(onChange.mock.calls[2][0]).toBe('+3809999');
expect(onChange.mock.calls[2][1].displayValue).toBe('+380 (99) 99');
expect(onChange.mock.calls[2][1].displayValue).toBe(getInput().value);

// undo
fireEvent.keyDown(getInput(), {
Expand All @@ -170,7 +188,9 @@ describe('PhoneInput', () => {
shiftKey: false,
});
expect(onChange.mock.calls.length).toBe(4);
expect(onChange.mock.calls[3][0]).toBe('+380 (99) ');
expect(onChange.mock.calls[3][0]).toBe('+38099');
expect(onChange.mock.calls[3][1].displayValue).toBe('+380 (99) ');
expect(onChange.mock.calls[3][1].displayValue).toBe(getInput().value);

// redo
fireEvent.keyDown(getInput(), {
Expand All @@ -180,7 +200,36 @@ describe('PhoneInput', () => {
shiftKey: true,
});
expect(onChange.mock.calls.length).toBe(5);
expect(onChange.mock.calls[4][0]).toBe('+380 (99) 99');
expect(onChange.mock.calls[4][0]).toBe('+3809999');
expect(onChange.mock.calls[4][1].displayValue).toBe('+380 (99) 99');
expect(onChange.mock.calls[4][1].displayValue).toBe(getInput().value);
});

test('should return data object as second argument', () => {
const onChange = jest.fn();
render(<PhoneInput defaultCountry="us" onChange={onChange} />);
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1');
expect(onChange.mock.calls[0][1]).toMatchObject({
country: getCountry({ field: 'iso2', value: 'us' }),
displayValue: '+1 ',
});

fireEvent.change(getInput(), { target: { value: '+1234' } });
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls[1][0]).toBe('+1234');
expect(onChange.mock.calls[1][1]).toMatchObject({
country: getCountry({ field: 'iso2', value: 'us' }),
displayValue: '+1 (234) ',
});

fireEvent.change(getInput(), { target: { value: '+380991234567' } });
expect(onChange.mock.calls.length).toBe(3);
expect(onChange.mock.calls[2][0]).toBe('+380991234567');
expect(onChange.mock.calls[2][1]).toMatchObject({
country: getCountry({ field: 'iso2', value: 'ua' }),
displayValue: '+380 (99) 123 45 67',
});
});
});

Expand Down Expand Up @@ -353,21 +402,11 @@ describe('PhoneInput', () => {

test('should render placeholder', () => {
const { rerender } = render(
<PhoneInput
defaultCountry="us"
disableDialCodePrefill
placeholder="Phone input"
/>,
<PhoneInput defaultCountry="us" placeholder="Phone input" />,
);
expect(getInput()).toHaveProperty('placeholder', 'Phone input');

rerender(
<PhoneInput
defaultCountry="us"
disableDialCodePrefill
placeholder="Test placeholder"
/>,
);
rerender(<PhoneInput defaultCountry="us" placeholder="Test placeholder" />);
expect(getInput()).toHaveProperty('placeholder', 'Test placeholder');
});

Expand Down Expand Up @@ -447,7 +486,7 @@ describe('PhoneInput', () => {
setCursorPosition(0, getInput().value.length);
getInput().focus();
await user.paste('38099');
expect(getInput().value).toBe('+1 ');
expect(getInput().value).toBe('+380 (99) ');

setCursorPosition(0, getInput().value.length);
getInput().focus();
Expand All @@ -463,45 +502,78 @@ describe('PhoneInput', () => {
});

describe('disableDialCodeAndPrefix', () => {
test('should return valid phone in onChange callback', async () => {
const onChange = jest.fn();

render(
<PhoneInput
defaultCountry="us"
disableDialCodeAndPrefix
onChange={onChange}
/>,
);

expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('+1');
expect(getInput().value).toBe('');

fireEvent.change(getInput(), { target: { value: '2345' } });
expect(onChange.mock.calls.length).toBe(2);
expect(onChange.mock.calls[1][0]).toBe('+12345');
expect(getInput().value).toBe('(234) 5');

fireEvent.change(getInput(), { target: { value: '' } });
expect(onChange.mock.calls.length).toBe(3);
expect(onChange.mock.calls[2][0]).toBe('+1');
expect(getInput().value).toBe('');

fireEvent.click(getCountrySelector());
fireEvent.click(getDropdownOption('ua'));

expect(onChange.mock.calls.length).toBe(4);
expect(onChange.mock.calls[3][0]).toBe('+380');
expect(getInput().value).toBe('');
});

test('should not include dial code inside input', () => {
render(<PhoneInput defaultCountry="us" disableDialCodeAndPrefix />);
fireEvent.change(getInput(), { target: { value: '1234567890' } });
fireEvent.change(getInput(), { target: { value: '+11234567890' } });
expect(getInput().value).toBe('(123) 456-7890');

fireEvent.change(getInput(), { target: { value: '' } });
expect(getInput().value).toBe('');

fireEvent.change(getInput(), { target: { value: '+123' } });
expect(getInput().value).toBe('(123) ');
expect(getInput().value).toBe('(23');
});

test('should ignore disableCountryGuess and forceDialCode', () => {
const { rerender } = render(
test('should ignore forceDialCode', () => {
render(
<PhoneInput
defaultCountry="us"
disableDialCodeAndPrefix
disableCountryGuess
forceDialCode
/>,
);
fireEvent.change(getInput(), { target: { value: '1234567890' } });
expect(getInput().value).toBe('(123) 456-7890');
fireEvent.change(getInput(), { target: { value: '' } });
expect(getInput().value).toBe('');
fireEvent.change(getInput(), { target: { value: '+38099' } });
fireEvent.change(getInput(), { target: { value: '38099' } });
expect(getInput().value).toBe('(380) 99');
});

rerender(
<PhoneInput
defaultCountry="us"
disableDialCodeAndPrefix
forceDialCode
/>,
);
fireEvent.change(getInput(), { target: { value: '1234567890' } });
test('should not guess country if typed value with prefix', async () => {
const user = userEvent.setup();

render(<PhoneInput defaultCountry="us" disableDialCodeAndPrefix />);
await user.type(getInput(), '1234567890');
expect(getInput().value).toBe('(123) 456-7890');
fireEvent.change(getInput(), { target: { value: '' } });

await user.clear(getInput());
expect(getInput().value).toBe('');
fireEvent.change(getInput(), { target: { value: '+38099' } });

await user.type(getInput(), '+38099');
expect(getInput().value).toBe('(380) 99');
});
});
Expand Down Expand Up @@ -841,7 +913,7 @@ describe('PhoneInput', () => {
initialSelectionStart: '+'.length,
});
expect(getInput().value).toBe('+1 (111) 11');
expect(getCursorPosition()).toBe(''.length);
expect(getCursorPosition()).toBe('+'.length);
});

test('should handle delete key', async () => {
Expand Down
15 changes: 12 additions & 3 deletions src/components/PhoneInput/PhoneInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useMemo } from 'react';
import { defaultCountries } from '../../data/countryData';
import { usePhoneInput, UsePhoneInputConfig } from '../../hooks/usePhoneInput';
import { buildClassNames } from '../../style/buildClassNames';
import { CountryIso2 } from '../../types';
import { ParsedCountry } from '../../types';
import { getCountry } from '../../utils';
import {
CountrySelector,
Expand Down Expand Up @@ -74,7 +74,13 @@ export interface PhoneInputProps
* @params `phone` - new phone value, `country` - country iso2 value
* @default undefined
*/
onChange?: (phone: string, country: CountryIso2) => void;
onChange?: (
e164Phone: string,
meta: {
country: ParsedCountry | undefined;
displayValue: string;
},
) => void;
}

export const PhoneInput: React.FC<PhoneInputProps> = ({
Expand Down Expand Up @@ -103,7 +109,10 @@ export const PhoneInput: React.FC<PhoneInputProps> = ({
countries,
...usePhoneInputConfig,
onChange: (data) => {
onChange?.(data.phone, data.country);
onChange?.(data.e164Phone, {
country: getCountry({ field: 'iso2', value: data.country }),
displayValue: data.phone,
});
},
});

Expand Down
Loading

0 comments on commit c80bb62

Please sign in to comment.