Skip to content

Commit

Permalink
feat(*): add masked-input (#76)
Browse files Browse the repository at this point in the history
* feat(*): add masked-input

* build demo pls

* chore(*): remove unused deps

* refactor(*): update component & stories

* test(masked-input): add tests

* refactor(masked-input): simplify props. add missing typings

* style(masked-input): small fixes according to pr
  • Loading branch information
reme3d2y committed Apr 21, 2020
1 parent 4e23acd commit d5c4ba5
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 1 deletion.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
},
"dependencies": {
"@storybook/preset-create-react-app": "^1.5.2",
"classnames": "^2.2.6"
"classnames": "^2.2.6",
"text-mask-core": "^5.1.2"
},
"devDependencies": {
"@alfalab/icons-glyph": "^1.6.0",
Expand Down
18 changes: 18 additions & 0 deletions src/masked-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@alfalab/core-components-masked-input",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "ISC",
"main": "index.js",
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^16.9.0"
},
"dependencies": {
"classnames": "^2.2.6",
"text-mask-core": "^5.1.2"
}
}
46 changes: 46 additions & 0 deletions src/masked-input/src/Component.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';

import { withKnobs } from '@storybook/addon-knobs';

import { MaskedInput } from './Component';

import styles from '../../../.storybook/styles.css';

export default {
title: 'Common|MaskedInput',
component: MaskedInput,
decorators: [withKnobs],
};

export const MaskedInputStory = () => {
// prettier-ignore
const masks = {
phone: ['+', /\d/, ' ', '(', /\d/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, '-', /\d/, /\d/],
card: [/\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/],
};

const placeholders = {
phone: '+7 (000) 000-00-00',
card: '0000 0000 0000 0000',
};

return (
<div style={{ width: '240px' }}>
<div className={styles.row}>
<div className={styles.col}>
<MaskedInput mask={masks.phone} placeholder={placeholders.phone} block={true} />
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<MaskedInput mask={masks.card} placeholder={placeholders.card} block={true} />
</div>
</div>
</div>
);
};

MaskedInputStory.story = {
name: 'MaskedInput',
parameters: {},
};
32 changes: 32 additions & 0 deletions src/masked-input/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';

import { MaskedInput } from './index';

// prettier-ignore
const cardMask = [/\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/];

describe('MaskedInput', () => {
describe('Snapshots tests', () => {
it('should match snapshot', () => {
expect(render(<MaskedInput />)).toMatchSnapshot();
});
});

it('should format value according to mask', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<MaskedInput mask={cardMask} dataTestId={dataTestId} />);

const input = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(input, { target: { value: 'before1234123412341234after±!@#$%^&*()_+' } });

expect(input.value).toBe('1234 1234 1234 1234');
});

it('should unmount without errors', () => {
const { unmount } = render(<MaskedInput />);

expect(unmount).not.toThrowError();
});
});
64 changes: 64 additions & 0 deletions src/masked-input/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useEffect, useRef, useCallback, useImperativeHandle } from 'react';
import { createTextMaskInputElement, TextMaskConfig, TextMaskInputElement } from 'text-mask-core';
import { Input, InputProps } from '../../input/src/Component';

export type MaskedInputProps = Omit<InputProps, 'value'> & {
/**
* Значение поля
*/
value?: string;

/**
* Маска для поля ввода
* https://github.com/text-mask/text-mask/blob/master/componentDocumentation.md#mask-array
*/
mask?: TextMaskConfig['mask'];

/**
* Дает возможность изменить значение поля перед рендером
*/
onBeforeDisplay?: TextMaskConfig['pipe'];
};

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

export const MaskedInput = React.forwardRef<HTMLInputElement | null, MaskedInputProps>(
({ mask, value, onBeforeDisplay, onChange, ...restProps }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const textMask = useRef<TextMaskInputElement | null>(null);

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

const handleInputChange = useCallback(
event => {
if (textMask.current) textMask.current.update();
if (onChange) onChange(event);
},
[onChange, textMask],
);

useEffect(() => {
if (inputRef.current) {
textMask.current = createTextMaskInputElement({
mask,
inputElement: inputRef.current,
pipe: onBeforeDisplay,
guide: false,
keepCharPositions: false,
showMask: false,
placeholderChar: PLACEHOLDER_CHAR,
});
}
}, [onBeforeDisplay, mask]);

useEffect(() => {
if (textMask.current) {
textMask.current.update(value);
}
}, [textMask, value]);

return <Input {...restProps} value={value} onChange={handleInputChange} ref={inputRef} />;
},
);
104 changes: 104 additions & 0 deletions src/masked-input/src/__snapshots__/Component.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MaskedInput Snapshots tests should match snapshot 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="component s"
>
<div
class="inner"
>
<div
class="inputWrapper"
>
<input
class="input"
type="text"
value=""
/>
</div>
<div
class="addons"
/>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="component s"
>
<div
class="inner"
>
<div
class="inputWrapper"
>
<input
class="input"
type="text"
value=""
/>
</div>
<div
class="addons"
/>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
1 change: 1 addition & 0 deletions src/masked-input/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Component';
13 changes: 13 additions & 0 deletions src/masked-input/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"composite": false,
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "dist",
"types": ["../../typings/module", "../../typings/text-mask-core"]
},
"exclude": ["src/Component.stories.tsx", "src/Component.test.tsx"],
"include": ["src"]
}
40 changes: 40 additions & 0 deletions typings/text-mask-core.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
declare module 'text-mask-core' {
export type Mask = Array<string | RegExp> | boolean;

export type TextMaskConfig = {
mask?: Mask | ((rawValue: string) => Mask);
guide?: boolean;
showMask?: boolean;
placeholderChar?: string;
keepCharPositions?: boolean;
pipe?: (
conformedValue: string,
config: TextMaskConfig,
) => false | string | { value: string; indexesOfPipedChars: number[] };
};

export type TextMaskInputElement = {
state: { previousConformedValue: string; previousPlaceholder: string };
update: (
rawValue?: string,
textMaskConfig?: TextMaskConfig & { inputElement: HTMLInputElement },
) => void;
};

export function createTextMaskInputElement(
config: TextMaskConfig & { inputElement: HTMLInputElement },
): TextMaskInputElement;

export function conformToMask(
text: string,
mask: Mask,
config?: TextMaskConfig,
): conformToMaskResult;

export type conformToMaskResult = {
conformedValue: string;
meta: {
someCharsRejected: boolean;
};
};
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17292,6 +17292,11 @@ text-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==

text-mask-core@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/text-mask-core/-/text-mask-core-5.1.2.tgz#80dd5ebe04825757e46619e691407a9f8b3c1b6f"
integrity sha1-gN1evgSCV1fkZhnmkUB6n4s8G28=

text-table@0.2.0, text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
Expand Down

0 comments on commit d5c4ba5

Please sign in to comment.