-
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(custom-button): add custom-button component
- Loading branch information
Showing
31 changed files
with
1,809 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Change Log | ||
|
||
All notable changes to this project will be documented in this file. | ||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. |
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,28 @@ | ||
{ | ||
"name": "@alfalab/core-components-custom-button", | ||
"version": "1.0.0", | ||
"description": "", | ||
"keywords": [], | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"module": "./dist/esm/index.js", | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"postinstall": "node ./dist/send-stats.js > /dev/null 2>&1 || exit 0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.9.0 || ^17.0.1" | ||
}, | ||
"dependencies": { | ||
"@alfalab/core-components-button": "^5.0.0", | ||
"classnames": "^2.2.6" | ||
}, | ||
"buildThemes": [ | ||
"site" | ||
] | ||
} |
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,228 @@ | ||
import React, { MouseEvent } from 'react'; | ||
import { render, fireEvent } from '@testing-library/react'; | ||
|
||
import { CustomButton } from './index'; | ||
|
||
const dataTestId = 'test-id'; | ||
|
||
describe('CustomButton', () => { | ||
describe('Snapshots tests', () => { | ||
it('should match snapshot', () => { | ||
expect(render(<CustomButton />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render custom background color', () => { | ||
expect(render(<CustomButton backgroundColor='#00ff00' />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render black color content', () => { | ||
expect(render(<CustomButton contentColor='black' />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render left addons', () => { | ||
expect(render(<CustomButton leftAddons={<div>Left addons</div>} />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render right addons', () => { | ||
expect( | ||
render(<CustomButton rightAddons={<div>Right addons</div>} />), | ||
).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render CustomButton by default', () => { | ||
expect(render(<CustomButton />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render anchor if href pass', () => { | ||
expect(render(<CustomButton href='https://some-url' />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render loader if loading pass', () => { | ||
expect(render(<CustomButton loading={true} />)).toMatchSnapshot(); | ||
}); | ||
|
||
it('should render loader if loading & href pass', () => { | ||
expect( | ||
render(<CustomButton loading={true} href='https://some-url' />), | ||
).toMatchSnapshot(); | ||
}); | ||
}); | ||
|
||
describe('Attributes tests', () => { | ||
it('should set `data-test-id` attribute', () => { | ||
const { getByTestId } = render(<CustomButton dataTestId={dataTestId} />); | ||
|
||
expect(getByTestId(dataTestId).tagName).toBe('BUTTON'); | ||
}); | ||
|
||
it('should set rel="noreferrer noopener" if "href" and target="_blank" are passed', () => { | ||
const { container } = render(<CustomButton href='#' target='_blank' />); | ||
|
||
const relAttr = container.firstElementChild?.getAttribute('rel'); | ||
|
||
expect(relAttr).toBe('noreferrer noopener'); | ||
}); | ||
|
||
it('should have `style` attribute', () => { | ||
const { container } = render(<CustomButton />); | ||
|
||
expect(container.firstElementChild).toHaveAttribute('style'); | ||
}); | ||
|
||
it('should set type="button" by default', () => { | ||
const { container } = render(<CustomButton />); | ||
expect(container.firstElementChild).toHaveAttribute('type', 'button'); | ||
}); | ||
|
||
it('should set type attribute', () => { | ||
const type = 'submit'; | ||
const { container } = render(<CustomButton type={type} />); | ||
expect(container.firstElementChild).toHaveAttribute('type', type); | ||
}); | ||
}); | ||
|
||
describe('Classes tests', () => { | ||
it('should set `className` class', () => { | ||
const className = 'test-class'; | ||
const { container } = render(<CustomButton className={className} />); | ||
|
||
expect(container.firstElementChild).toHaveClass(className); | ||
}); | ||
|
||
it('should have `customButton` class as default', () => { | ||
const { container } = render(<CustomButton />); | ||
expect(container.firstElementChild).toHaveClass('customButton'); | ||
}); | ||
|
||
it('should have `white` class as default', () => { | ||
const { container } = render(<CustomButton />); | ||
expect(container.firstElementChild).toHaveClass('customButton'); | ||
}); | ||
|
||
it('should have `black` class', () => { | ||
const { container } = render(<CustomButton contentColor='black' />); | ||
expect(container.firstElementChild).toHaveClass('black'); | ||
}); | ||
|
||
it('should set `size` class', () => { | ||
const size = 'm'; | ||
const { container } = render(<CustomButton size={size} />); | ||
|
||
expect(container.firstElementChild).toHaveClass(size); | ||
}); | ||
|
||
it('should set `block` class', () => { | ||
const { container } = render(<CustomButton block={true} />); | ||
|
||
expect(container.firstElementChild).toHaveClass('block'); | ||
}); | ||
|
||
it('should set `iconOnly` class', () => { | ||
const { container } = render(<CustomButton />); | ||
|
||
expect(container.firstElementChild).toHaveClass('iconOnly'); | ||
}); | ||
|
||
it('should set `nowrap` class', () => { | ||
const { container } = render(<CustomButton nowrap={true} />); | ||
|
||
expect(container.firstElementChild).toHaveClass('nowrap'); | ||
}); | ||
}); | ||
|
||
describe('Callbacks tests', () => { | ||
it('should call `onClick` prop', () => { | ||
const cb = jest.fn(); | ||
|
||
const { getByTestId } = render(<CustomButton onClick={cb} dataTestId={dataTestId} />); | ||
|
||
fireEvent.click(getByTestId(dataTestId)); | ||
|
||
expect(cb).toBeCalledTimes(1); | ||
}); | ||
|
||
it('should not call `onClick` prop if disabled', () => { | ||
const cb = jest.fn(); | ||
|
||
const { getByTestId } = render( | ||
<CustomButton onClick={cb} dataTestId={dataTestId} disabled={true} />, | ||
); | ||
|
||
fireEvent.click(getByTestId(dataTestId)); | ||
|
||
expect(cb).not.toBeCalled(); | ||
}); | ||
|
||
/** | ||
* Тест нужен для проверки типа eventTarget (HTMLCustomButtonElement/HTMLAnchorElement). | ||
* Если тест скомпилился - то все ок. | ||
*/ | ||
it('target should contain CustomButton element', () => { | ||
const { getByTestId } = render(<CustomButton onClick={cb} dataTestId={dataTestId} />); | ||
|
||
const CustomButtonNode = getByTestId(dataTestId); | ||
|
||
function cb(event: MouseEvent<HTMLButtonElement>) { | ||
expect(event.target).toBe(CustomButtonNode); | ||
} | ||
|
||
fireEvent.click(CustomButtonNode, { target: CustomButtonNode }); | ||
}); | ||
|
||
/** | ||
* Тест нужен для проверки типа eventTarget (HTMLCustomButtonElement/HTMLAnchorElement). | ||
* Если тест скомпилился - то все ок. | ||
*/ | ||
it('target should contain anchor element', () => { | ||
const { getByTestId } = render( | ||
<CustomButton onClick={cb} dataTestId={dataTestId} href='#' />, | ||
); | ||
|
||
const anchorNode = getByTestId(dataTestId); | ||
|
||
function cb(event: MouseEvent<HTMLAnchorElement>) { | ||
expect(event.target).toBe(anchorNode); | ||
} | ||
|
||
fireEvent.click(anchorNode, { target: anchorNode }); | ||
}); | ||
}); | ||
|
||
describe('Custom component', () => { | ||
it('should use custom component', () => { | ||
const cb = jest.fn(); | ||
cb.mockReturnValue(null); | ||
|
||
render(<CustomButton Component={cb} dataTestId={dataTestId} />); | ||
|
||
expect(cb).toBeCalled(); | ||
|
||
const props = cb.mock.calls[0][0]; | ||
expect(props['data-test-id']).toBe(dataTestId); | ||
}); | ||
|
||
it('should pass `to` instead `href` to custom component', () => { | ||
const cb = jest.fn(); | ||
cb.mockReturnValue(null); | ||
|
||
render(<CustomButton Component={cb} href='test' />); | ||
|
||
expect(cb).toBeCalled(); | ||
|
||
const props = cb.mock.calls[0][0]; | ||
|
||
expect(props.href).toBeFalsy(); | ||
expect(props.to).toBe('test'); | ||
}); | ||
}); | ||
|
||
it('should unmount without errors', () => { | ||
const { unmount } = render( | ||
<CustomButton leftAddons={<span>Left</span>} rightAddons={<span>Right</span>}> | ||
Text | ||
</CustomButton>, | ||
); | ||
|
||
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,124 @@ | ||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, ElementType } from 'react'; | ||
import cn from 'classnames'; | ||
|
||
import { Button } from '@alfalab/core-components-button'; | ||
|
||
import styles from './index.module.css'; | ||
|
||
const DEFAULT_BUTTON_COLOR = '#9A06BF'; | ||
const DEFAULT_CONTENT_COLOR = 'white'; | ||
|
||
export type ComponentProps = { | ||
/** | ||
* Слот слева | ||
*/ | ||
leftAddons?: React.ReactNode; | ||
|
||
/** | ||
* Слот справа | ||
*/ | ||
rightAddons?: React.ReactNode; | ||
|
||
/** | ||
* Размер компонента | ||
*/ | ||
size?: 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl'; | ||
|
||
/** | ||
* Растягивает компонент на ширину контейнера | ||
*/ | ||
block?: boolean; | ||
|
||
/** | ||
* Дополнительный класс | ||
*/ | ||
className?: string; | ||
|
||
/** | ||
* Выводит ссылку в виде кнопки | ||
*/ | ||
href?: string; | ||
|
||
/** | ||
* Позволяет использовать кастомный компонент для кнопки (например Link из роутера) | ||
*/ | ||
Component?: ElementType; | ||
|
||
/** | ||
* Идентификатор для систем автоматизированного тестирования | ||
*/ | ||
dataTestId?: string; | ||
|
||
/** | ||
* Показать лоадер | ||
*/ | ||
loading?: boolean; | ||
|
||
/** | ||
* Не переносить текст кнопки на новую строку | ||
*/ | ||
nowrap?: boolean; | ||
|
||
/** | ||
* Цвет кнопки | ||
*/ | ||
backgroundColor?: string; | ||
|
||
/** | ||
* Цвет контента | ||
*/ | ||
contentColor?: 'black' | 'white'; | ||
}; | ||
|
||
type AnchorButtonProps = ComponentProps & AnchorHTMLAttributes<HTMLAnchorElement>; | ||
type NativeButtonProps = ComponentProps & ButtonHTMLAttributes<HTMLButtonElement>; | ||
|
||
export type CustomButtonProps = Partial<AnchorButtonProps | NativeButtonProps>; | ||
|
||
export const CustomButton = React.forwardRef< | ||
HTMLAnchorElement | HTMLButtonElement, | ||
CustomButtonProps | ||
>( | ||
( | ||
{ | ||
children, | ||
className, | ||
loading, | ||
backgroundColor = DEFAULT_BUTTON_COLOR, | ||
contentColor = DEFAULT_CONTENT_COLOR, | ||
...restProps | ||
}, | ||
ref, | ||
) => { | ||
const buttonProps = { | ||
style: { background: backgroundColor }, | ||
...restProps, | ||
}; | ||
|
||
const buttonClassName = cn(styles.customButton, className, styles[contentColor], { | ||
[styles.customLoading]: loading, | ||
}); | ||
|
||
return ( | ||
<Button | ||
view='primary' | ||
ref={ref} | ||
className={buttonClassName} | ||
loading={loading} | ||
{...buttonProps} | ||
> | ||
{children} | ||
</Button> | ||
); | ||
}, | ||
); | ||
|
||
/** | ||
* Для отображения в сторибуке | ||
*/ | ||
CustomButton.defaultProps = { | ||
size: 'm', | ||
block: false, | ||
loading: false, | ||
nowrap: false, | ||
}; |
3 changes: 3 additions & 0 deletions
3
...n/src/__image_snapshots__/custom-button-background-colors-sizes-sprite-snap.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions
3
...m-button/src/__image_snapshots__/custom-button-colors-and-block-sprite-snap.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions
3
...__/custom-button-colors-and-loading-background-color-0-content-color-0-snap.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.