-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core-components-bank-card): add component draft (#208)
* 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
Showing
13 changed files
with
986 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: 'Номер карты или счёта', | ||
}; |
Oops, something went wrong.