From 103a304270bf10ae5265069044d30d2129b84eb6 Mon Sep 17 00:00:00 2001 From: siloneco Date: Wed, 8 Nov 2023 22:49:18 +0900 Subject: [PATCH] feat: add button ui #22 (#71) * feat: add button ui * fix: import style and class name set --- src/components/ui/Button/index.stories.tsx | 91 ++++++++++++++++++++++ src/components/ui/Button/index.test.tsx | 74 ++++++++++++++++++ src/components/ui/Button/index.tsx | 85 ++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 src/components/ui/Button/index.stories.tsx create mode 100644 src/components/ui/Button/index.test.tsx create mode 100644 src/components/ui/Button/index.tsx diff --git a/src/components/ui/Button/index.stories.tsx b/src/components/ui/Button/index.stories.tsx new file mode 100644 index 0000000..72e18d3 --- /dev/null +++ b/src/components/ui/Button/index.stories.tsx @@ -0,0 +1,91 @@ +import { Button } from '.'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Button, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +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: ๐Ÿ‘, + children: 'Start Icon', + }, +}; + +export const EndIcon: Story = { + args: { + endIcon: ๐Ÿ‘, + children: 'End Icon', + }, +}; diff --git a/src/components/ui/Button/index.test.tsx b/src/components/ui/Button/index.test.tsx new file mode 100644 index 0000000..7789996 --- /dev/null +++ b/src/components/ui/Button/index.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + + expect(firstChild).toHaveClass('text-primary underline-offset-4'); + }); + + it('renders children correctly', () => { + const text = 'Test'; + const screen = render(); + + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + it('renders start-icon correctly', () => { + const icon = '๐Ÿ‘'; + const screen = render(); + + expect(screen.getByText(icon)).toBeInTheDocument(); + }); + + it('renders end-icon correctly', () => { + const icon = '๐Ÿ‘'; + const screen = render(); + + expect(screen.getByText(icon)).toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/Button/index.tsx b/src/components/ui/Button/index.tsx new file mode 100644 index 0000000..4c37b43 --- /dev/null +++ b/src/components/ui/Button/index.tsx @@ -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, + VariantProps { + asChild?: boolean; +} + +type Props = { + startIcon?: ReactNode; + endIcon?: ReactNode; + children: ReactNode; +}; + +const Button = forwardRef( + ( + { + className, + variant, + size, + asChild = false, + startIcon, + endIcon, + children, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : 'button'; + + return ( + + <> + {startIcon && {startIcon}} + {children} + {endIcon && {endIcon}} + + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants };