Skip to content

Commit

Permalink
feat(core-components-bank-card): add component draft (#208)
Browse files Browse the repository at this point in the history
* feat(core-components-bank-card): add component draft

* refactor(core-components-bank-card): add aspect ratio. change styles

* refactor(core-components-bank-card): add luhn, add mir, change styles

* refactor(core-components-bank-card): replace camera icon

* feat(core-components-bank-card): add defaultProps

* refactor(core-components-bank-card): update styles and layout

* refactor(core-components-bank-card): update styles

* refactor(core-components-bank-card): replace icons

* feat(core-components-bank-card): add maestro

* fix(core-components-bank-card): review fixes

* fix(core-components-bank-card): override font-family

* fix(core-components-bank-card): release fixes

* refactor(core-components-bank-card): small fixes
  • Loading branch information
reme3d2y committed Sep 2, 2020
1 parent 8494b21 commit 93943b7
Show file tree
Hide file tree
Showing 13 changed files with 986 additions and 6 deletions.
2 changes: 1 addition & 1 deletion bin/purgecss.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { promisify } = require('util');

const writeFile = promisify(fs.writeFile);

const matches = glob.sync('dist/**/*.css', { ignore: 'dist/+(themes|vars)/**' });
const matches = glob.sync('dist/**/*.css', { ignore: 'dist/+(themes|vars|bank-card)/**' });

/**
* Purgecss вырезает селекторы по атрибуту (например .component[data-popper-placement='left'] .arrow в поповере).
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"dependencies": {
"@alfalab/hooks": "^0.8.1",
"@alfalab/icons-classic": "^1.7.0",
"@alfalab/icons-glyph": "^1.26.0",
"@alfalab/icons-logotype": "^1.4.0",
"@alfalab/utils": "^0.3.0",
"@popperjs/core": "^2.3.3",
"alfa-ui-primitives": "^2.47.0",
Expand All @@ -66,7 +68,6 @@
"text-mask-core": "^5.1.2"
},
"devDependencies": {
"@alfalab/icons-glyph": "^1.6.0",
"@alfalab/rollup-plugin-postcss": "^3.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
Expand Down
25 changes: 25 additions & 0 deletions packages/bank-card/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@alfalab/core-components-bank-card",
"version": "1.0.0",
"description": "Bank card component",
"keywords": [],
"license": "ISC",
"main": "dist/index.js",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"classnames": "^2.2.6",
"react": "^16.9.0",
"react-dom": "^16.9.0"
},
"dependencies": {
"@alfalab/core-components-masked-input": "^1.3.1",
"@alfalab/icons-classic": "^1.7.0",
"@alfalab/icons-glyph": "^1.26.0",
"@alfalab/icons-logotype": "^1.4.0"
}
}
37 changes: 37 additions & 0 deletions packages/bank-card/src/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Meta, Story, Props, Preview, Title } from '@storybook/addon-docs/blocks';
import { text, select } from '@storybook/addon-knobs';
import { Design } from 'storybook/blocks/design';
import { Status } from 'storybook/blocks/status';

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


<Meta title='Компоненты|BankCard' component={BankCard} />

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

<Status>
Draft
</Status>


```tsx
import { BankCard } from '@alfalab/core-components-bank-card';
```

<Design>
<a href="https://www.figma.com/file/XvRLVKtW2FIQa1Rl3w7ZdP/Passcode-%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D1%82%D0%B8%D0%BF?node-id=1%3A1100">Figma</a>
</Design>

## Описание

Компонент в виде карты для ввода номера карты или банковского счета

<Story name='Песочница'>
<BankCard />
</Story>

<Props of={BankCard} />
115 changes: 115 additions & 0 deletions packages/bank-card/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { LogoAlfabankMColorIcon } from '@alfalab/icons-classic';

import { BankCard } from './index';

describe('BankCard', () => {
describe('Snapshots tests', () => {
it('should match snapshots', () => {
expect(render(<BankCard value='4111 1111 1111 1111' />).container).toMatchSnapshot();
expect(render(<BankCard value='5500 0000 0000 0004' />).container).toMatchSnapshot();
expect(render(<BankCard value='2201 3820 0000 0013' />).container).toMatchSnapshot();
expect(
render(<BankCard value='1234 1234 1234 1234 1234' />).container,
).toMatchSnapshot();
});
});

it('should forward ref to input', () => {
const inputRef = jest.fn();
const dataTestId = 'test-id';
const { getByTestId } = render(<BankCard ref={inputRef} dataTestId={dataTestId} />);

expect(inputRef.mock.calls).toEqual([[getByTestId(dataTestId)]]);
});

it('should set `data-test-id` atribute to input', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<BankCard dataTestId={dataTestId} />);

expect(getByTestId(dataTestId).tagName).toBe('INPUT');
});

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

const input = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(input, { target: { value: '4444444444444444' } });

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

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

const input = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(input, { target: { value: '44444444444444444444' } });

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

it('should set custom label', () => {
const label = 'Custom label';
const { container } = render(<BankCard inputLabel={label} />);

expect(container).toHaveTextContent(label);
});

it('should set custom background', () => {
const backgroundColor = '#b000b5';
const { container } = render(<BankCard backgroundColor={backgroundColor} />);

expect(container.querySelector('.content')).toHaveStyle({
backgroundColor,
});
});

it('should set custom bank logo', () => {
const dataTestId = 'test-id';
const logo = (
<div data-test-id={dataTestId}>
<LogoAlfabankMColorIcon />
</div>
);
const { getByTestId } = render(<BankCard bankLogo={logo} />);

expect(getByTestId(dataTestId)).toBeTruthy();
});

describe('Callbacks tests', () => {
it('should call `onChange` prop', () => {
const cb = jest.fn();
const dataTestId = 'test-id';
const value = '4444 4444 4444 4444 ';
const { getByTestId } = render(<BankCard onChange={cb} dataTestId={dataTestId} />);

const input = getByTestId(dataTestId) as HTMLInputElement;

fireEvent.change(input, { target: { value } });

expect(cb).toBeCalledTimes(1);
expect(input.value).toBe(value);
});

it('should call `onUsePhoto` prop', () => {
const cb = jest.fn();
const dataTestId = 'test-id';
const { getByRole } = render(<BankCard onUsePhoto={cb} dataTestId={dataTestId} />);

fireEvent.click(getByRole('button'));

expect(cb).toBeCalledTimes(1);
});
});

it('should unmount without errors', () => {
const { unmount } = render(<BankCard value='value' onChange={jest.fn()} />);

expect(unmount).not.toThrowError();
});
});
163 changes: 163 additions & 0 deletions packages/bank-card/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useCallback, MouseEvent, ReactNode, useState, ChangeEvent } from 'react';
import cn from 'classnames';
import { MaskedInput } from '@alfalab/core-components-masked-input';
// Дождаться иконку альфы в icons-logotype
import { BankAlfaLColorIcon } from '@alfalab/icons-classic';

import { CameraMIcon } from '@alfalab/icons-glyph';

import { VisaXxlIcon, MastercardLIcon, MirXxlIcon } from '@alfalab/icons-logotype';

import styles from './index.module.css';
import { validateCardNumber } from './utils';

export type BankCardProps = {
/**
* Дополнительный класс
*/
className?: string;

/**
* Цвет фона карты
*/
backgroundColor?: string;

/**
* Иконка логотипа банка (размер L)
*/
bankLogo?: ReactNode;

/**
* Лэйбл поля ввода
*/
inputLabel?: string;

/**
* Значение поля ввода
*/
value?: string;

/**
* Обработчик ввода
*/
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;

/**
* Обработчик вызова камеры
*/
onUsePhoto?: (event: MouseEvent<HTMLButtonElement>) => void;

/**
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;
};

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

const getBrandIcon = (value = '') => {
// Показываем логотип только после ввода всех цифр карты
if (value.length === cardMask.length && validateCardNumber(value)) {
if (value.startsWith('2')) return <MirXxlIcon />;
if (value.startsWith('4')) return <VisaXxlIcon />;
if (value.startsWith('5')) return <MastercardLIcon />;
if (value.startsWith('6')) return <MastercardLIcon />;
}
return null;
};

export const BankCard = React.forwardRef<HTMLInputElement, BankCardProps>(
(
{
bankLogo = <BankAlfaLColorIcon />,
backgroundColor = '#EF3124',
inputLabel = 'Номер карты или счёта',
value,
className,
onUsePhoto,
onChange,
dataTestId,
},
ref,
) => {
const [focused, setFocused] = useState(false);
const [filled, setFilled] = useState(value !== undefined && value !== '');

const [brandIcon, setBrandIcon] = useState<ReactNode>(getBrandIcon(value));

const getMask = (newValue: string) =>
newValue.length <= cardMask.length ? cardMask : accountNumberMask;

const handleInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setFilled(event.target.value !== '');

setBrandIcon(getBrandIcon(event.target.value));

if (onChange) {
onChange(event);
}
},
[onChange],
);

const handleInputFocus = useCallback(() => {
setFocused(true);
}, []);

const handleInputBlur = useCallback(() => {
setFocused(false);
}, []);

const renderRightAddons = useCallback(() => {
return (
<button type='button' className={styles.usePhoto} onClick={onUsePhoto}>
<CameraMIcon />
</button>
);
}, [onUsePhoto]);

return (
<div
className={cn(styles.component, className, {
[styles.focused]: focused,
[styles.filled]: filled,
})}
>
<div className={styles.aspectRatioContainer}>
<div className={styles.content} style={{ backgroundColor }}>
<div className={styles.bankLogo}>{bankLogo}</div>

<MaskedInput
ref={ref}
value={value}
mask={getMask}
block={true}
label={inputLabel}
rightAddons={renderRightAddons()}
inputClassName={styles.input}
labelClassName={styles.label}
onChange={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
dataTestId={dataTestId}
inputMode='numeric'
pattern='[0-9]*'
/>

{brandIcon && <div className={styles.brandLogo}>{brandIcon}</div>}
</div>
</div>
</div>
);
},
);

BankCard.defaultProps = {
bankLogo: <BankAlfaLColorIcon />,
backgroundColor: '#EF3124',
inputLabel: 'Номер карты или счёта',
};

0 comments on commit 93943b7

Please sign in to comment.