Skip to content

Commit

Permalink
Merge pull request #29 from kodiak-packages/14-create-button-component
Browse files Browse the repository at this point in the history
Implemented the Button component.
  • Loading branch information
bramvanhoutte authored May 5, 2020
2 parents 92e482d + dc83cd1 commit baf46b1
Show file tree
Hide file tree
Showing 15 changed files with 442 additions and 78 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ npm install ventura
import { Button } from 'ventura';

const MyComponent = () => {
<Button text="Click me" />;

return <Button>Click me</Button>;
};
```

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
},
"dependencies": {
"classnames": "^2.2.6",
"modern-css-reset": "^1.1.0",
"react-feather": "^2.0.8"
},
"keywords": [
Expand Down
38 changes: 34 additions & 4 deletions src/components/Button/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,63 @@ route: /button
---

import { Playground, Props } from 'docz'
import { Activity, Trash2 } from '../../index.ts'
import Button from './Button.tsx'

# Button

Buttons make common actions more obvious and help users more easily perform them. Buttons use labels and sometimes icons to communicate the action that will occur when the user touches them.

### Best practices
## Best practices

- Group buttons logically into sets based on usage and importance.
- Ensure that button actions are clear and consistent.
- The main action of a group set can be a primary button.
- Select a single button variation and do not mix them.

## Basic usage
## Examples

### Basic usage

<Playground>
<Button>Click me</Button>
</Playground>

## Button type usage
### Button types

<Playground>
<Button type="primary">Click me</Button>
<br/>
<Button type="secondary">Click me</Button>
</Playground>

### Button with an onClick function

<Playground>
<Button onClick={(event) => console.log(`LOGGER: ${event.target}`)}>Click me</Button>
</Playground>

### Button that is disabled

<Playground>
<Button isDisabled>Submit</Button>
</Playground>

### Button with loading state

<Playground>
<Button isLoading>Submit</Button>
</Playground>

### Buttons with an icon

Button can be prefixed or suffixed with an Icon.

<Playground>
<Button suffixIcon={<Activity />}>Insights</Button>
<br/>
<Button prefixIcon={<Trash2 />}>Remove</Button>
</Playground>

## API

<Props of={Button} />
52 changes: 44 additions & 8 deletions src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
.base {
border-radius: var(--button-border-radius);
line-height: var(--button-line-height);
.button {
border-radius: var(--border-radius-small);
line-height: inherit;
font-size: 14px;
padding: 0 16px;
padding: 7.5px 14px;
border: none;
cursor: pointer;
}

.typePrimary {
background-color: var(--button-primary-background);
display: flex;
flex-direction: row;
align-items: center;
}

.typePrimary:hover {
Expand All @@ -18,3 +17,40 @@
.typeSecondary:hover {
background-color: rgba(67, 90, 111, 0.06);
}

.typePrimary:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-focus);
}

.typeSecondary:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-focus);
}

.typePrimary, .typePrimary:active {
background-color: var(--color-primary);
}

.typeSecondary, .typeSecondary:active {
background-color: var(--color-secondary);
}

.button:disabled {
color: var(--color-disabled-font);
background-color: var(--color-disabled);
cursor: default;
}

.labelWithPrefixIcon {
margin-left: 8px;
}

.labelWithSuffixIcon {
margin-right: 8px;
}

.spinner {
width: 14px;
height: 14px;
}
92 changes: 78 additions & 14 deletions src/components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,99 @@
import React, { ComponentProps } from 'react';
import React from 'react';
import { fireEvent, render } from '@testing-library/react';

import { Button } from '../../index';
import { GitHub } from '../../index';
import Button from './Button';

describe('Button', () => {
const onClickFn = jest.fn();
const defaultButtonProps: ComponentProps<typeof Button> = {
type: 'primary',
children: 'Click me',
onClick: onClickFn,
};

test('default snapshot', () => {
const component = <Button {...defaultButtonProps} />;
const component = <Button>Click me</Button>;
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('secondary button', () => {
const component = <Button {...defaultButtonProps} type="secondary" />;
const component = <Button type="secondary">Click me</Button>;
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('className prop', () => {
const className = 'center';
const component = (
<Button className={className} name="class">
Hey
</Button>
);
const { getByTestId } = render(component);
const buttonElement = getByTestId('button-class');

const renderedClassNames = buttonElement.className.split(' ');
expect(renderedClassNames).toContain(className);
// className in prop should be the last in the row
expect(renderedClassNames.indexOf(className)).toBe(renderedClassNames.length - 1);
});

test('onClick should be triggered when clicked', async () => {
const component = <Button {...defaultButtonProps} name="test" />;
const { findByTestId } = render(component);
const onClickFn = jest.fn();
const component = (
<Button onClick={onClickFn} name="click">
Click me
</Button>
);
const { getByTestId } = render(component);

const button = await findByTestId('test');
const button = await getByTestId('button-click');
fireEvent.click(button);

expect(onClickFn).toHaveBeenCalledTimes(1);
});

test('disabled button', () => {
const onClickFn = jest.fn();
const component = (
<Button isDisabled name="disabled">
Click me
</Button>
);
const { getByTestId } = render(component);
const button = getByTestId('button-disabled');

expect(button.hasAttribute('disabled')).toBe(true);
expect(button.getAttribute('disabled')).not.toBe(false);

fireEvent.click(button);
expect(onClickFn).toHaveBeenCalledTimes(0);
});

test('loading button', () => {
const onClickFn = jest.fn();
const component = (
<Button isLoading name="loading">
Click me
</Button>
);
const { asFragment, getByTestId } = render(component);

expect(asFragment()).toMatchSnapshot();

const button = getByTestId('button-loading');

expect(button.hasAttribute('disabled')).toBe(true);
expect(button.getAttribute('disabled')).not.toBe(false);

fireEvent.click(button);
expect(onClickFn).toHaveBeenCalledTimes(0);
});

test('prefix icon button', () => {
const component = <Button prefixIcon={<GitHub />}>Click me</Button>;
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('suffix icon button', () => {
const component = <Button suffixIcon={<GitHub />}>Click me</Button>;
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});
});
61 changes: 45 additions & 16 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
import React from 'react';
import React, { MouseEventHandler } from 'react';
import classNames from 'classnames';

import Spinner from '../utils/Spinner/Spinner';

import styles from './Button.module.css';

export interface Props {
children: string;
type: 'primary' | 'secondary';
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
children: React.ReactNode;
type?: 'primary' | 'secondary';
onClick?: MouseEventHandler<HTMLButtonElement>;
className?: string;
isDisabled?: boolean;
isLoading?: boolean;
prefixIcon?: React.ReactElement;
suffixIcon?: React.ReactElement;
name?: string;
}

const Button: React.FC<Props> = ({ children, type = 'primary', onClick, name }: Props) => {
const getAppearanceClass = (buttonType: typeof type) => {
if (buttonType === 'secondary') {
return styles.typeSecondary;
}
return styles.typePrimary;
};

const classNames: string[] = [styles.base];
const Button: React.FC<Props> = ({
children,
type = 'primary',
onClick,
className,
isDisabled,
isLoading,
prefixIcon,
suffixIcon,
name,
}: Props) => {
const buttonClassNames = classNames(
styles.button,
{
[styles.typePrimary]: type === 'primary',
[styles.typeSecondary]: type === 'secondary',
},
className,
);

classNames.push(getAppearanceClass(type));
const labelClassNames = classNames({
[styles.labelWithPrefixIcon]: Boolean(prefixIcon) || isLoading,
[styles.labelWithSuffixIcon]: Boolean(suffixIcon),
});

return (
<button className={classNames.join(' ')} type="button" onClick={onClick} data-testid={name}>
{children}
<button
disabled={isDisabled || isLoading}
className={buttonClassNames}
type="button"
onClick={isLoading || suffixIcon ? undefined : onClick}
data-testid={`button-${name}`}
>
{isLoading ? <Spinner className={styles.spinner} /> : prefixIcon}
<span className={labelClassNames}>{children}</span>
{isLoading || suffixIcon}
</button>
);
};
Expand Down
Loading

0 comments on commit baf46b1

Please sign in to comment.