Skip to content

Commit

Permalink
Fixed Button component with size and disabled properties, and update …
Browse files Browse the repository at this point in the history
…tests and stories
  • Loading branch information
nhistory committed Feb 4, 2025
1 parent 37cd145 commit ea04e01
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 38 deletions.
14 changes: 0 additions & 14 deletions src/components/Button/Button.stories.ts

This file was deleted.

118 changes: 118 additions & 0 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
import { vstack } from "../../../styled-system/patterns";

export default {
component: Button,
parameters: {
layout: "centered",
},
args: {
children: "시작하기",
variant: "solid",
},
} satisfies Meta<typeof Button>;

export const Basic: StoryObj<typeof Button> = {};

export const Variants: StoryObj<typeof Button> = {
render: (args) => {
return (
<div className={vstack({ gap: "4" })}>
<Button {...args} variant="solid">
솔리드 버튼
</Button>
<Button {...args} variant="outline">
아웃라인 버튼
</Button>
</div>
);
},
argTypes: {
children: {
control: false,
},
variant: {
control: "radio",
options: ["solid", "outline"],
},
},
};

export const Tones: StoryObj<typeof Button> = {
render: (args) => {
return (
<div className={vstack({ gap: "4" })}>
<Button {...args} tone="neutral">
중립 색조
</Button>
<Button {...args} tone="accent">
강조 색조
</Button>
<Button {...args} tone="danger">
위험 색조
</Button>
<Button {...args} tone="warning">
경고 색조
</Button>
</div>
);
},
argTypes: {
children: {
control: false,
},
tone: {
control: "radio",
options: ["neutral", "accent", "danger", "warning"],
},
},
};

export const Sizes: StoryObj<typeof Button> = {
render: (args) => {
return (
<div className={vstack({ gap: "4" })}>
<Button {...args} size="small">
작은 버튼
</Button>
<Button {...args} size="medium">
중간 버튼
</Button>
<Button {...args} size="large">
큰 버튼
</Button>
</div>
);
},
argTypes: {
children: {
control: false,
},
size: {
control: "radio",
options: ["small", "medium", "large"],
},
},
};

export const Disabled: StoryObj<typeof Button> = {
render: (args) => {
return (
<div className={vstack({ gap: "4" })}>
<Button {...args} disabled>
비활성화 버튼
</Button>
<Button {...args}>활성화 버튼</Button>
</div>
);
},
argTypes: {
children: {
control: false,
},
disabled: {
control: "boolean",
},
},
};
115 changes: 100 additions & 15 deletions src/components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,108 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import { composeStories } from "@storybook/react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import * as stories from "./Button.stories";
import { Button } from "./Button";

describe("<Button>", () => {
it("renders a button with text", () => {
render(<Button>Click</Button>);
const { Basic, Variants, Tones, Sizes, Disabled } = composeStories(stories);

expect(screen.getByRole("button")).toHaveTextContent("Click");
});
test("renders the button with the correct text content", () => {
render(<Basic>테스트</Basic>);

it("calls onClick handler when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
expect(screen.getByText("테스트")).toBeInTheDocument();
});

test("applies the correct variant styles", () => {
render(<Variants />);

expect(screen.getByText("솔리드 버튼")).toHaveClass("bg_bg");
expect(screen.getByText("아웃라인 버튼")).toHaveClass("bd_3px_solid");
});

test("applies the correct tone styles", () => {
render(<Tones />);

expect(screen.getByText("중립 색조")).toHaveClass("bg_bg");
expect(screen.getByText("강조 색조")).toHaveClass("bg_bg.accent");
expect(screen.getByText("위험 색조")).toHaveClass("bg_bg.danger");
expect(screen.getByText("경고 색조")).toHaveClass("bg_bg.warning");
});

test("applies the correct font size based on the size prop", () => {
render(<Sizes />);

expect(screen.getByText("작은 버튼")).toHaveClass("fs_sm");
expect(screen.getByText("중간 버튼")).toHaveClass("fs_md");
expect(screen.getByText("큰 버튼")).toHaveClass("fs_lg");
});

test("applies the correct disabled styles", () => {
render(<Disabled />);

render(<Button onClick={handleClick}>Click</Button>);
expect(screen.getByText("비활성화 버튼")).toBeDisabled();
expect(screen.getByText("활성화 버튼")).toBeEnabled();
expect(screen.getByText("비활성화 버튼")).toHaveClass("[&:disabled]:op_0.5");
});

test("renders a button with type='button' by default", () => {
render(<Basic>Default Button</Basic>);
const button = screen.getByText("Default Button");
expect(button).toHaveAttribute("type", "button");
});

await user.click(screen.getByRole("button"));
test("renders a button with type='button' by default", () => {
render(<Basic variant="solid">Default Button</Basic>);
const button = screen.getByText("Default Button");
expect(button).toHaveAttribute("type", "button");
});

test("renders a button with type='button' when specified", () => {
render(
<Button type="button" variant="solid">
Button Type Button
</Button>
);
const button = screen.getByText("Button Type Button");
expect(button).toHaveAttribute("type", "button");
});

test("renders a button with type='submit' when specified", () => {
render(
<form>
<Button type="submit" variant="solid">
Submit Type Button
</Button>
</form>
);
const button = screen.getByText("Submit Type Button");
expect(button).toHaveAttribute("type", "submit");
});

test("submits the form when type='submit' button is clicked", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Button type="submit" variant="solid">
Submit Button
</Button>
</form>
);

const submitButton = screen.getByText("Submit Button");
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});

expect(handleClick).toHaveBeenCalledTimes(1);
});
test("does not submit the form when type='button' button is clicked", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Button type="button" variant="solid">
Button Type Button
</Button>
</form>
);
const buttonTypeButton = screen.getByText("Button Type Button");
fireEvent.click(buttonTypeButton);
expect(handleSubmit).toHaveBeenCalledTimes(0);
});
60 changes: 51 additions & 9 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,57 @@ import { css, cva } from "../../../styled-system/css";
import type { SystemStyleObject } from "@pandacss/types";

type ButtonVariant = "solid" | "outline";

type ButtonTone = "neutral" | "accent" | "danger" | "warning";
type ButtonSize = "small" | "medium" | "large";

export interface ButtonProps
extends Omit<HTMLAttributes<HTMLElement>, "style"> {
/** 텍스트 */
children: React.ReactNode;
/** 타입 */
type?: "button" | "submit";
/** 클릭 시 실행함수 */
onClick?: () => void;
/** 종류 */
variant: ButtonVariant;
/** 색조 */
tone?: ButtonTone;
/** 버튼의 크기 */
size?: ButtonSize;
/** 추가 스타일 */
style?: SystemStyleObject; // Add style prop for custom inline styles
style?: SystemStyleObject;
/** 버튼 비활성화 여부 */
disabled?: boolean;
}

/**
* 버튼 컴포넌트입니다.
* - `variant` 속성으로 버튼의 스타일 종류를 지정할 수 있습니다. (solid, outline)
* - `tone` 속성으로 버튼의 색상 강조를 지정할 수 있습니다. (neutral, accent, danger, warning)
* - `size` 속성으로 버튼의 크기를 지정할 수 있습니다. (small, medium, large)
* - `type` 속성으로 버튼의 타입을 지정할 수 있습니다. (button, submit)
* - `disabled` 속성을 사용하여 버튼을 비활성화할 수 있습니다.
*/
export const Button = ({
children,
type = "button",
onClick,
variant = "solid",
tone = "neutral",
style, // destructure the style prop
style,
size = "medium",
disabled,
...rest
}: ButtonProps) => {
return (
<button
className={css(
styles.raw({ tone, variant }),
styles.raw({ tone, variant, size }),
baseStyles,
...(Array.isArray(style) ? style : [style]) // Ensure style is an array
...(Array.isArray(style) ? style : [style])
)}
type={type}
onClick={onClick}
disabled={disabled}
{...rest}
>
{children}
Expand All @@ -52,8 +64,6 @@ export const Button = ({
const baseStyles = {
appearance: "none",
margin: "0",
padding: "0.7rem 3rem",
fontSize: "1.5rem",
fontWeight: 500,
textAlign: "center",
textDecoration: "none",
Expand All @@ -66,10 +76,42 @@ const baseStyles = {
transition: "0.2s",
lineHeight: "1",
outline: "0",
"&:disabled": { opacity: 0.5 },
"&:disabled": {
opacity: 0.5,
cursor: "not-allowed",
},
};

const styles = cva({
base: {
padding: "0.7rem 3rem",
},
variants: {
size: {
small: {
padding: "0.5rem 1.5rem",
fontSize: "sm",
},
medium: {
padding: "0.7rem 2rem",
fontSize: "md",
},
large: {
padding: "1rem 2.5rem",
fontSize: "lg",
},
},
variant: {
solid: {},
outline: {},
},
tone: {
neutral: {},
accent: {},
danger: {},
warning: {},
},
},
compoundVariants: [
{
variant: "solid",
Expand Down

0 comments on commit ea04e01

Please sign in to comment.