Skip to content

Commit

Permalink
feat(core-components-phone-input): add phone-input (#192)
Browse files Browse the repository at this point in the history
* feat(core-components-phone-input): add phone-input

* Update packages/phone-input/src/Component.tsx

Co-authored-by: Alexander Soldatov <inc1uder@ya.ru>

* feat(core-components-phone-input): fix if condition

* refactor(core-components-phone-input): refactor component

* fix(core-components-phone-input): fix del first num after code country

* fix(core-components-phone-input): fix delete after +7

* fix(core-components-phone-input): fix delete after +7

* fix(core-components-phone-input): fix caret position

* improvement(core-components-phone-input): improvement comment

* fix(core-components-phone-input): fix caret position

* improvement(core-components-phone-input): improvement component

* fix(core-components-phone-input): delete optional chaining

* fix(core-components-phone-input): fix caret position

Co-authored-by: Alexander Soldatov <inc1uder@ya.ru>
Co-authored-by: stepancar <stepancar@hotmail.com>
  • Loading branch information
3 people committed Jul 23, 2020
1 parent 4fb8370 commit 42cedbf
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/amount-input/src/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Icon from '@alfalab/icons-glyph/StarMIcon';
import { Meta, Props, Title } from '@storybook/addon-docs/blocks';
import { text, select, boolean, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
Expand Down
4 changes: 2 additions & 2 deletions packages/amount-input/src/Component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('AmountInput', () => {

await userEvent.type(input, ',');
/**
* TODO: проверить положение картки
* TODO: проверить положение карeтки
* expect(input.selectionStart).toBe(4);
*/
expect(input.value).toBe('123,45');
Expand All @@ -190,7 +190,7 @@ describe('AmountInput', () => {
input.setSelectionRange(4, 4);
await userEvent.type(input, '.');
/**
* TODO: проверить положение картки
* TODO: проверить положение карeтки
* expect(input.selectionStart).toBe(4);
*/
expect(input.value).toBe('123,45');
Expand Down
5 changes: 4 additions & 1 deletion packages/masked-input/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type MaskedInputProps = Omit<InputProps, 'value'> & {
onBeforeDisplay?: TextMaskConfig['pipe'];
};

// Символ плейсхолдера не может входить в маску, поэтому вместо проблела используется \u2000
// Символ плейсхолдера не может входить в маску, поэтому вместо пробела используется \u2000
export const PLACEHOLDER_CHAR = '\u2000';

export const MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(
Expand Down Expand Up @@ -49,6 +49,9 @@ export const MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(
keepCharPositions: false,
showMask: false,
placeholderChar: PLACEHOLDER_CHAR,
rawValue: '',
currentCaretPosition: 0,
previousConformedValue: '',
});
}
}, [onBeforeDisplay, mask]);
Expand Down
22 changes: 22 additions & 0 deletions packages/phone-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@alfalab/core-components-phone-input",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "ISC",
"main": "dist/index.js",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"classnames": "^2.2.6",
"react": "^16.9.0"
},
"dependencies": {
"@alfalab/core-components-masked-input": "^1.3.1",
"text-mask-core": "^5.1.2"
}
}
38 changes: 38 additions & 0 deletions packages/phone-input/src/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Icon from '@alfalab/icons-glyph/StarMIcon';
import { Meta, Props, Title } from '@storybook/addon-docs/blocks';
import { text, select, boolean, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';

import { PhoneInput } from './Component';
import { name, version } from '../package.json';

<!-- prettier-ignore -->
<Meta title='Компоненты|PhoneInput' component={PhoneInput} />

<Title>
PhoneInput ({name}@{version})
</Title>

```tsx
import { PhoneInput } from '@alfalab/core-components-phone-input';
```

## Описание

Компонент текстового поля для ввода российского номера телефона

<Story name='Песочница'>
<PhoneInput
block={boolean('block', false)}
size={select('size', ['s', 'm', 'l'], 's')}
error={text('error', '')}
hint={text('hint', '')}
label={text('label', '')}
leftAddons={boolean('leftAddons', false) && <Icon />}
rightAddons={boolean('rightAddons', false) && <Icon />}
bottomAddons={boolean('bottomAddons', false) && <span>bottom text</span>}
onChange={action('change')}
/>
</Story>

<Props of={PhoneInput} />
118 changes: 118 additions & 0 deletions packages/phone-input/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { PhoneInput } from './index';

describe('PhoneInput', () => {
const dataTestId = 'test-id';

it('should match snapshot', () => {
expect(render(<PhoneInput />)).toMatchSnapshot();
});

describe('should right delete number', () => {
it('"+7 1" -> press backspace -> "+7 "', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '+7 1' } });
userEvent.type(inputElement, '{backspace}');
expect(inputElement.value).toBe('+7 ');
});
/*
* TODO: хотелось бы добавить тестов на проверку удаления цифр в номере,
* но не нашел способа двигать каретку
*/
});

describe('should format a phone number according default to mask', () => {
it('input "+" -> "+7 "', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '+' } });
expect(inputElement.value).toBe('+7 ');
});

it('input "7" -> "+7 "', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '7' } });
expect(inputElement.value).toBe('+7 ');
});

it('input "8" -> "+7 "', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

userEvent.type(inputElement, '8');
expect(inputElement.value).toBe('+7 ');
});

it('input "1" -> "+7 1"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

userEvent.type(inputElement, '1');
expect(inputElement.value).toBe('+7 1');
});

it('insert "+71112223344" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '+71112223344' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});

it('insert "81112223344" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '81112223344' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});

it('insert "1112223344" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '1112223344' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});

it('insert "8882223344" -> "+7 888 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '8882223344' } });
expect(inputElement.value).toBe('+7 888 222-33-44');
});

it('insert "71112223344" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '71112223344' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});

it('insert "8 (111) 222-33-44" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '8 (111) 222-33-44' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});

it('insert "111222334455" -> "+7 111 222-33-44"', () => {
const { getByTestId } = render(<PhoneInput dataTestId={dataTestId} />);
const inputElement = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(inputElement, { target: { value: '111222334455' } });
expect(inputElement.value).toBe('+7 111 222-33-44');
});
});
});
107 changes: 107 additions & 0 deletions packages/phone-input/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useImperativeHandle, useRef } from 'react';
import { conformToMask, TextMaskConfig } from 'text-mask-core';
import { MaskedInput, MaskedInputProps } from '@alfalab/core-components-masked-input';

import { deleteFormatting } from './utils';

const mask = [
'+',
'7',
' ',
/([0-6]|[8-9])/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
'-',
/\d/,
/\d/,
'-',
/\d/,
/\d/,
];

export type PhoneInputProps = Omit<MaskedInputProps, 'onBeforeDisplay' | 'type' | 'mask'>;

export const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
({ ...restProps }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);

// Оставляет возможность прокинуть ref извне
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);

const handleBeforeDisplay = (conformedValue: string, config: TextMaskConfig) => {
const { rawValue, previousConformedValue, currentCaretPosition } = config;

/*
* код ниже нужен для фикса следующих багов библиотеки text-mask:
* 1) так как код страны указан в маске жестко как "+7",
* то при удалении цифры перед ним каретку устанавливается перед кодом страны
* 2) в номере телефона есть пробелы и дефисы,
* при редактировании цифр рядом с этими символами каретка перескакивает через них,
* а не остается на том же месте, на котором была до редактирования
*/
const previousValueWithoutFormatting = previousConformedValue
? deleteFormatting(previousConformedValue)
: '';
const currentValueWithoutFormatting = deleteFormatting(conformedValue);
if (
(previousConformedValue &&
Math.abs(
previousValueWithoutFormatting.length -
currentValueWithoutFormatting?.length,
) === 1 &&
[3, 6, 11].includes(currentCaretPosition)) ||
([7, 10, 13].includes(currentCaretPosition) &&
previousConformedValue.length > currentCaretPosition)
) {
const caret = currentCaretPosition;
window.requestAnimationFrame(() => {
if (inputRef !== null && inputRef.current) {
inputRef.current.selectionStart = caret;
inputRef.current.selectionEnd = caret;
}
});
}

// Удаление цифры перед кодом страны удаляет только саму цифру, код остается ("+7 1" -> "+7 ")
if (rawValue === '+7 ') {
return rawValue;
}

// Вставка номера с 10 цифрами без кода страны
if (rawValue.length === 10 && conformedValue.length === mask.length) {
const masked = conformToMask(`+7${rawValue}`, mask, config);
return masked.conformedValue;
}

// Вставка номера, начинающегося с 8 или 7: 89990313131, 71112223344
if (
conformedValue.length === mask.length &&
(rawValue.startsWith('8') || rawValue.startsWith('7'))
) {
const masked = conformToMask(`+7${rawValue.slice(1)}`, mask, config);
return masked.conformedValue;
}

// Если ввод начат с 7 или 8 - выводит "+7 " и дает продолжить ввод со след. цифры
if (rawValue.length === 1 && ['7', '8'].includes(rawValue[0])) {
return '+7 ';
}

return conformedValue;
};

return (
<MaskedInput
{...restProps}
mask={mask}
onBeforeDisplay={handleBeforeDisplay}
type='tel'
ref={inputRef}
/>
);
},
);

0 comments on commit 42cedbf

Please sign in to comment.