Skip to content

Commit

Permalink
feat(custom-button): add custom-button component
Browse files Browse the repository at this point in the history
  • Loading branch information
EGNKupava committed Dec 5, 2021
1 parent 43754f9 commit 983bcd7
Show file tree
Hide file tree
Showing 31 changed files with 1,809 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/custom-button/CHANGELOG.md
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.
28 changes: 28 additions & 0 deletions packages/custom-button/package.json
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"
]
}
228 changes: 228 additions & 0 deletions packages/custom-button/src/Component.test.tsx
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();
});
});
124 changes: 124 additions & 0 deletions packages/custom-button/src/Component.tsx
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,
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 983bcd7

Please sign in to comment.