diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index ebb85c4..3f7ef4d 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,4 +1,5 @@
import { type ButtonHTMLAttributes } from "react";
+import { Spinner } from "./Spinner";
type Variant = "primary" | "secondary" | "danger";
@@ -23,13 +24,21 @@ export function Button({
variant = "primary",
loading = false,
className = "",
+ disabled,
+ children,
...rest
}: ButtonProps) {
+ const isDisabled = disabled || loading;
+
return (
+ className={`inline-flex items-center justify-center gap-2 rounded-full px-5 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed ${variants[variant]} ${ring} ${className}`}
+ >
+ {loading && }
+ {children}
+
);
}
diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx
new file mode 100644
index 0000000..261aa9c
--- /dev/null
+++ b/src/components/__tests__/Button.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from "@testing-library/react";
+import { Button } from "../Button";
+
+describe("Button", () => {
+ it("renders children without busy state by default", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: "Save" });
+ expect(button).toBeEnabled();
+ expect(button).not.toHaveAttribute("aria-busy");
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
+ });
+
+ it("disables the button and exposes busy state while loading", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: /save/i });
+ expect(button).toBeDisabled();
+ expect(button).toHaveAttribute("aria-busy", "true");
+ expect(screen.getByRole("status")).toHaveTextContent("Loading");
+ });
+
+ it("preserves explicit disabled state when not loading", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: "Delete" });
+ expect(button).toBeDisabled();
+ expect(button).not.toHaveAttribute("aria-busy");
+ });
+});