Skip to content

Commit

Permalink
feat: add button ui #22 (#71)
Browse files Browse the repository at this point in the history
* feat: add button ui

* fix: import style and class name set
  • Loading branch information
siloneco authored Nov 8, 2023
1 parent ecc23d0 commit 103a304
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 0 deletions.
91 changes: 91 additions & 0 deletions src/components/ui/Button/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Button } from '.';

import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof Button> = {
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
args: {
children: 'Default',
},
};

export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary',
},
};

export const Destructive: Story = {
args: {
variant: 'destructive',
children: 'Destructive',
},
};

export const Outline: Story = {
args: {
variant: 'outline',
children: 'Outline',
},
};

export const Link: Story = {
args: {
variant: 'link',
children: 'Link',
},
};

export const Ghost: Story = {
args: {
variant: 'ghost',
children: 'Ghost',
},
};

export const Small: Story = {
args: {
size: 'sm',
children: 'Small',
},
};

export const Large: Story = {
args: {
size: 'lg',
children: 'Large',
},
};

export const Icon: Story = {
args: {
size: 'icon',
children: 'Icon',
},
};

export const StartIcon: Story = {
args: {
startIcon: <span>👍</span>,
children: 'Start Icon',
},
};

export const EndIcon: Story = {
args: {
endIcon: <span>👍</span>,
children: 'End Icon',
},
};
74 changes: 74 additions & 0 deletions src/components/ui/Button/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { render } from '@testing-library/react';

import '@testing-library/jest-dom';
import { Button } from '.';

describe('ui/Buttonのテスト', () => {
// Test rendering with 'default' variant
it('renders the default variant of the button correctly', () => {
const {
container: { firstChild },
} = render(<Button>test</Button>);

expect(firstChild).toHaveClass('bg-primary text-primary-foreground');
});

// Test rendering with 'secondary' variant
it('renders the secondary variant of the button correctly', () => {
const {
container: { firstChild },
} = render(<Button variant="secondary">test</Button>);

expect(firstChild).toHaveClass('bg-secondary text-secondary-foreground');
});

// Test rendering with 'destructive' variant
it('renders the destructive variant of the button correctly', () => {
const {
container: { firstChild },
} = render(<Button variant="destructive">test</Button>);

expect(firstChild).toHaveClass(
'bg-destructive text-destructive-foreground'
);
});

// Test rendering with 'outline' variant
it('renders the outline variant of the button correctly', () => {
const {
container: { firstChild },
} = render(<Button variant="outline">test</Button>);

expect(firstChild).toHaveClass('border border-input bg-background');
});

// Test rendering with 'link' variant
it('renders the link variant of the button correctly', () => {
const {
container: { firstChild },
} = render(<Button variant="link">test</Button>);

expect(firstChild).toHaveClass('text-primary underline-offset-4');
});

it('renders children correctly', () => {
const text = 'Test';
const screen = render(<Button>{text}</Button>);

expect(screen.getByText(text)).toBeInTheDocument();
});

it('renders start-icon correctly', () => {
const icon = '👍';
const screen = render(<Button startIcon={icon}>test</Button>);

expect(screen.getByText(icon)).toBeInTheDocument();
});

it('renders end-icon correctly', () => {
const icon = '👍';
const screen = render(<Button endIcon={icon}>test</Button>);

expect(screen.getByText(icon)).toBeInTheDocument();
});
});
85 changes: 85 additions & 0 deletions src/components/ui/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { ReactNode } from 'react';
import { forwardRef } from 'react';

import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';

import type { VariantProps } from 'class-variance-authority';

import { cn } from '@/libs/utils';

const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}

type Props = {
startIcon?: ReactNode;
endIcon?: ReactNode;
children: ReactNode;
};

const Button = forwardRef<HTMLButtonElement, ButtonProps & Props>(
(
{
className,
variant,
size,
asChild = false,
startIcon,
endIcon,
children,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';

return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
<>
{startIcon && <span className="mr-2">{startIcon}</span>}
{children}
{endIcon && <span className="ml-2">{endIcon}</span>}
</>
</Comp>
);
}
);
Button.displayName = 'Button';

export { Button, buttonVariants };

0 comments on commit 103a304

Please sign in to comment.