feat(ui-components): implement basic Button component (#45421)

* Move to its own folder

* file rename

* implement basic Button component
This commit is contained in:
Huyen Nguyen
2022-03-16 16:00:53 +07:00
committed by GitHub
parent 7b9bc8bb99
commit 3fc687a583
11 changed files with 152 additions and 77 deletions

View File

@ -1,25 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Button } from './button';
const onClick = jest.fn();
describe('Button', () => {
it("should have the role 'button' and the correct text", () => {
render(<Button label='Hello world' onClick={onClick} />);
expect(
screen.getByRole('button', { name: /hello world/i })
).toBeInTheDocument();
});
it('should trigger the onClick prop on click', () => {
render(<Button label='Hello world' onClick={onClick} />);
const button = screen.getByRole('button', { name: /hello world/i });
userEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,32 +0,0 @@
import React from 'react';
import { ButtonProps } from './button.types';
import './button.css';
/**
* Primary UI component for user interaction
*/
export const Button: React.FC<ButtonProps> = ({
primary,
size = 'medium',
label,
...props
}: ButtonProps) => {
const mode = primary
? 'storybook-button--primary'
: 'storybook-button--secondary';
return (
<button
className={[
'storybook-button',
`storybook-button--${size}`,
mode,
'button-default-style'
].join(' ')}
type='button'
{...props}
>
{label}
</button>
);
};

View File

@ -1,9 +0,0 @@
type ButtonSize = 'small' | 'medium' | 'large';
export interface ButtonProps {
primary?: boolean;
size?: ButtonSize;
label: string;
customKey?: string;
onClick: () => void;
}

View File

@ -1,7 +1,7 @@
import { Story } from '@storybook/react';
import React from 'react';
import { Button } from './button';
import { ButtonProps } from './button.types';
import { Button, ButtonProps } from '.';
const story = {
title: 'Example/Button',
@ -12,27 +12,27 @@ const Template: Story<ButtonProps> = args => {
return <Button {...args} />;
};
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button'
export const Default = Template.bind({});
Default.args = {
children: 'Button'
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button'
export const Danger = Template.bind({});
Danger.args = {
variant: 'danger',
children: 'Button'
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
label: 'Button'
children: 'Button'
};
export const Small = Template.bind({});
Small.args = {
size: 'small',
label: 'Button'
children: 'Button'
};
export default story;

View File

@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Button } from './button';
describe('Button', () => {
it("should have the role 'button' and render the correct text", () => {
render(<Button>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toBeInTheDocument();
});
it("should have the type 'button' by default", () => {
render(<Button>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toHaveAttribute('type', 'button');
});
it("should have the type 'submit' if it is specified", () => {
render(<Button type='submit'>Hello world</Button>);
expect(
screen.getByRole('button', { name: /hello world/i })
).toHaveAttribute('type', 'submit');
});
it('should trigger the onClick prop on click', () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Hello world</Button>);
const button = screen.getByRole('button', { name: /hello world/i });
userEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,65 @@
import React, { useMemo } from 'react';
import { ButtonProps, ButtonSize, ButtonVariant } from './types';
const defaultClassNames = ['cursor-pointer', 'inline-block', 'border-3'];
const computeClassNames = ({
size,
variant
}: {
size: ButtonSize;
variant: ButtonVariant;
}) => {
const classNames = [...defaultClassNames];
// TODO: support 'link' variant
switch (variant) {
case 'danger':
classNames.push(
'border-default-foreground-danger',
'bg-default-background-danger',
'text-default-foreground-danger'
);
break;
// default variant is 'primary'
default:
classNames.push(
'border-default-foreground-secondary',
'bg-default-background-quaternary',
'text-default-foreground-secondary'
);
}
switch (size) {
case 'large':
classNames.push('px-4 py-2.5 text-lg');
break;
case 'small':
classNames.push('px-2.5 py-1 text-sm');
break;
// default size is 'medium'
default:
classNames.push('px-3 py-1.5 text-md');
}
return classNames.join(' ');
};
export const Button = ({
variant = 'primary',
size = 'medium',
type = 'button',
onClick,
children
}: ButtonProps) => {
const classes = useMemo(
() => computeClassNames({ size, variant }),
[size, variant]
);
return (
<button className={classes} type={type} onClick={onClick}>
{children}
</button>
);
};

View File

@ -0,0 +1,2 @@
export { Button } from './button';
export type { ButtonProps } from './types';

View File

@ -0,0 +1,13 @@
import { MouseEventHandler } from 'react';
export type ButtonVariant = 'primary' | 'danger';
export type ButtonSize = 'small' | 'medium' | 'large';
export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
onClick?: MouseEventHandler<HTMLButtonElement>;
type?: 'submit' | 'button';
}

View File

@ -95,10 +95,13 @@ div.light {
--default-foreground-secondary: var(--gray85);
--default-foreground-tertiary: var(--gray80);
--default-foreground-quaternary: var(--gray75);
--default-foreground-danger: var(--red15);
--default-background-primary: var(--gray00);
--default-background-secondary: var(--gray05);
--default-background-tertiary: var(--gray10);
--default-background-quaternary: var(--gray15);
--default-background-danger: var(--red90);
}
html.dark,
@ -106,8 +109,11 @@ div.dark {
--default-foreground-primary: var(--gray00);
--default-foreground-secondary: var(--gray05);
--default-foreground-quaternary: var(--gray15);
--default-foreground-danger: var(--red90);
--default-background-primary: var(--gray90);
--default-background-secondary: var(--gray85);
--default-background-tertiary: var(--gray80);
--default-background-quaternary: var(--gray75);
--default-background-danger: var(--red15);
}

View File

@ -15,10 +15,12 @@ module.exports = {
'default-foreground-secondary': 'var(--default-foreground-secondary)',
'default-foreground-tertiary': 'var(--default-foreground-tertiary)',
'default-foreground-quaternary': 'var(--default-foreground-quaternary)',
'default-foreground-danger': 'var(--default-foreground-danger)',
'default-background-primary': 'var(--default-background-primary)',
'default-background-secondary': 'var(--default-background-secondary)',
'default-background-tertiary': 'var(--default-background-tertiary)',
'default-background-quaternary': 'var(--default-background-quaternary)',
'default-background-danger': 'var(--default-background-danger)',
green: {
50: 'var(--green05)',
100: 'var(--green10)',
@ -52,6 +54,16 @@ module.exports = {
800: 'var(--red80)',
900: 'var(--red90)'
}
},
borderWidth: {
3: '3px'
},
fontSize: {
// https://tailwindcss.com/docs/font-size#providing-a-default-line-height
// [fontSize, lineHeight]
sm: ['16px', '1.5'],
md: ['18px', '1.42857143'],
lg: ['24px', '1.3333333']
}
},
plugins: []