fix: correct Biome formatting and restore non-null assertions in tests
- Remove root biome.json (conflicted with packages/config/biome.json) - Format all files with correct config (spaces, not tabs) - Restore non-null assertions (ref!) in test files where refs are guaranteed - Biome's optional chaining conversion broke TypeScript inference in tests Fixes type-check and format-check CI failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e904e8d319
commit
990ba6f050
31 changed files with 484 additions and 152 deletions
29
biome.json
29
biome.json
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false,
|
|
||||||
"ignore": [".next", "dist", ".turbo", "node_modules", "storybook-static"]
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"formatWithErrors": false,
|
|
||||||
"indentStyle": "tab",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100,
|
|
||||||
"lineEnding": "lf"
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -250,7 +250,7 @@ describe("Badge", () => {
|
||||||
</Badge>,
|
</Badge>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(ref?.tagName).toBe("DIV");
|
expect(ref!.tagName).toBe("DIV");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -297,7 +297,11 @@ describe("Badge", () => {
|
||||||
|
|
||||||
it("works correctly with outline variant and custom className", () => {
|
it("works correctly with outline variant and custom className", () => {
|
||||||
render(
|
render(
|
||||||
<Badge variant="outline" className="hover:bg-accent" data-testid="badge">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="hover:bg-accent"
|
||||||
|
data-testid="badge"
|
||||||
|
>
|
||||||
Outlined
|
Outlined
|
||||||
</Badge>,
|
</Badge>,
|
||||||
);
|
);
|
||||||
|
|
@ -351,7 +355,12 @@ describe("Badge", () => {
|
||||||
describe("Semantic Usage", () => {
|
describe("Semantic Usage", () => {
|
||||||
it("can be used as status indicator with role", () => {
|
it("can be used as status indicator with role", () => {
|
||||||
render(
|
render(
|
||||||
<Badge variant="success" role="status" aria-label="Online status" data-testid="badge">
|
<Badge
|
||||||
|
variant="success"
|
||||||
|
role="status"
|
||||||
|
aria-label="Online status"
|
||||||
|
data-testid="badge"
|
||||||
|
>
|
||||||
Online
|
Online
|
||||||
</Badge>,
|
</Badge>,
|
||||||
);
|
);
|
||||||
|
|
@ -363,7 +372,12 @@ describe("Badge", () => {
|
||||||
|
|
||||||
it("can be used as notification count", () => {
|
it("can be used as notification count", () => {
|
||||||
render(
|
render(
|
||||||
<Badge variant="error" size="sm" aria-label="3 unread messages" data-testid="badge">
|
<Badge
|
||||||
|
variant="error"
|
||||||
|
size="sm"
|
||||||
|
aria-label="3 unread messages"
|
||||||
|
data-testid="badge"
|
||||||
|
>
|
||||||
3
|
3
|
||||||
</Badge>,
|
</Badge>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,14 @@ export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
* Visual variant of the badge
|
* Visual variant of the badge
|
||||||
* @default 'default'
|
* @default 'default'
|
||||||
*/
|
*/
|
||||||
variant?: "default" | "secondary" | "outline" | "success" | "error" | "warning" | "info";
|
variant?:
|
||||||
|
| "default"
|
||||||
|
| "secondary"
|
||||||
|
| "outline"
|
||||||
|
| "success"
|
||||||
|
| "error"
|
||||||
|
| "warning"
|
||||||
|
| "info";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Size of the badge
|
* Size of the badge
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,33 @@ export const badgeVariants = createVariants({
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: ["bg-primary text-primary-foreground", "border border-primary"].join(" "),
|
default: [
|
||||||
|
"bg-primary text-primary-foreground",
|
||||||
|
"border border-primary",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
secondary: ["bg-secondary text-secondary-foreground", "border border-secondary"].join(" "),
|
secondary: [
|
||||||
|
"bg-secondary text-secondary-foreground",
|
||||||
|
"border border-secondary",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
outline: ["bg-transparent text-foreground", "border border-border"].join(" "),
|
outline: ["bg-transparent text-foreground", "border border-border"].join(
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
|
||||||
success: ["bg-success text-success-foreground", "border border-success"].join(" "),
|
success: [
|
||||||
|
"bg-success text-success-foreground",
|
||||||
|
"border border-success",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
error: ["bg-error text-error-foreground", "border border-error"].join(" "),
|
error: ["bg-error text-error-foreground", "border border-error"].join(
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
|
||||||
warning: ["bg-warning text-warning-foreground", "border border-warning"].join(" "),
|
warning: [
|
||||||
|
"bg-warning text-warning-foreground",
|
||||||
|
"border border-warning",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
info: ["bg-info text-info-foreground", "border border-info"].join(" "),
|
info: ["bg-info text-info-foreground", "border border-info"].join(" "),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,9 @@ describe("Button", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies icon size classes", () => {
|
it("applies icon size classes", () => {
|
||||||
render(<Button size="icon" data-testid="button" aria-label="Icon button" />);
|
render(
|
||||||
|
<Button size="icon" data-testid="button" aria-label="Icon button" />,
|
||||||
|
);
|
||||||
const button = screen.getByTestId("button");
|
const button = screen.getByTestId("button");
|
||||||
expect(button.className).toContain("h-10");
|
expect(button.className).toContain("h-10");
|
||||||
expect(button.className).toContain("w-10");
|
expect(button.className).toContain("w-10");
|
||||||
|
|
@ -239,14 +241,19 @@ describe("Button", () => {
|
||||||
</Button>,
|
</Button>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLButtonElement);
|
expect(ref).toBeInstanceOf(HTMLButtonElement);
|
||||||
expect(ref?.tagName).toBe("BUTTON");
|
expect(ref!.tagName).toBe("BUTTON");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("HTML Attributes", () => {
|
describe("HTML Attributes", () => {
|
||||||
it("passes through HTML button attributes", () => {
|
it("passes through HTML button attributes", () => {
|
||||||
render(
|
render(
|
||||||
<Button type="submit" name="test-button" value="test-value" data-testid="button">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
name="test-button"
|
||||||
|
value="test-value"
|
||||||
|
data-testid="button"
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>,
|
</Button>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,11 @@ export const buttonVariants = createVariants({
|
||||||
"active:bg-accent/80",
|
"active:bg-accent/80",
|
||||||
].join(" "),
|
].join(" "),
|
||||||
|
|
||||||
link: ["text-primary underline-offset-4", "hover:underline", "active:text-primary/80"].join(
|
link: [
|
||||||
" ",
|
"text-primary underline-offset-4",
|
||||||
),
|
"hover:underline",
|
||||||
|
"active:text-primary/80",
|
||||||
|
].join(" "),
|
||||||
},
|
},
|
||||||
|
|
||||||
size: {
|
size: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./Card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "./Card";
|
||||||
|
|
||||||
describe("Card", () => {
|
describe("Card", () => {
|
||||||
describe("Rendering", () => {
|
describe("Rendering", () => {
|
||||||
|
|
@ -16,7 +23,9 @@ describe("Card", () => {
|
||||||
<Card data-testid="card">
|
<Card data-testid="card">
|
||||||
<CardHeader data-testid="header">
|
<CardHeader data-testid="header">
|
||||||
<CardTitle data-testid="title">Title</CardTitle>
|
<CardTitle data-testid="title">Title</CardTitle>
|
||||||
<CardDescription data-testid="description">Description</CardDescription>
|
<CardDescription data-testid="description">
|
||||||
|
Description
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent data-testid="content">Content</CardContent>
|
<CardContent data-testid="content">Content</CardContent>
|
||||||
<CardFooter data-testid="footer">Footer</CardFooter>
|
<CardFooter data-testid="footer">Footer</CardFooter>
|
||||||
|
|
@ -227,7 +236,9 @@ describe("Card", () => {
|
||||||
render(
|
render(
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardDescription data-testid="description">Description text</CardDescription>
|
<CardDescription data-testid="description">
|
||||||
|
Description text
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
|
|
@ -239,7 +250,9 @@ describe("Card", () => {
|
||||||
render(
|
render(
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardDescription data-testid="description">Description</CardDescription>
|
<CardDescription data-testid="description">
|
||||||
|
Description
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
|
|
@ -404,7 +417,7 @@ describe("Card", () => {
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(ref?.tagName).toBe("DIV");
|
expect(ref!.tagName).toBe("DIV");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards ref to CardHeader element", () => {
|
it("forwards ref to CardHeader element", () => {
|
||||||
|
|
@ -439,7 +452,7 @@ describe("Card", () => {
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLHeadingElement);
|
expect(ref).toBeInstanceOf(HTMLHeadingElement);
|
||||||
expect(ref?.tagName).toBe("H3");
|
expect(ref!.tagName).toBe("H3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards ref to CardDescription element", () => {
|
it("forwards ref to CardDescription element", () => {
|
||||||
|
|
@ -561,7 +574,9 @@ describe("Card", () => {
|
||||||
render(
|
render(
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardDescription data-testid="description">Description</CardDescription>
|
<CardDescription data-testid="description">
|
||||||
|
Description
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
|
|
@ -591,7 +606,9 @@ describe("Card", () => {
|
||||||
<CardTitle>Card Title</CardTitle>
|
<CardTitle>Card Title</CardTitle>
|
||||||
<CardDescription>This is a card description</CardDescription>
|
<CardDescription>This is a card description</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent data-testid="content">Card content goes here</CardContent>
|
<CardContent data-testid="content">
|
||||||
|
Card content goes here
|
||||||
|
</CardContent>
|
||||||
<CardFooter data-testid="footer">Footer actions</CardFooter>
|
<CardFooter data-testid="footer">Footer actions</CardFooter>
|
||||||
</Card>,
|
</Card>,
|
||||||
);
|
);
|
||||||
|
|
@ -599,7 +616,9 @@ describe("Card", () => {
|
||||||
const card = screen.getByTestId("card");
|
const card = screen.getByTestId("card");
|
||||||
expect(card).toBeInTheDocument();
|
expect(card).toBeInTheDocument();
|
||||||
expect(screen.getByText("Card Title")).toBeInTheDocument();
|
expect(screen.getByText("Card Title")).toBeInTheDocument();
|
||||||
expect(screen.getByText("This is a card description")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText("This is a card description"),
|
||||||
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText("Card content goes here")).toBeInTheDocument();
|
expect(screen.getByText("Card content goes here")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Footer actions")).toBeInTheDocument();
|
expect(screen.getByText("Footer actions")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,13 @@ CardHeader.displayName = "CardHeader";
|
||||||
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
|
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
|
||||||
({ className, as = "h3", ...props }, ref) => {
|
({ className, as = "h3", ...props }, ref) => {
|
||||||
const Component = as;
|
const Component = as;
|
||||||
return <Component ref={ref} className={cn(cardTitleVariants(), className)} {...props} />;
|
return (
|
||||||
|
<Component
|
||||||
|
ref={ref}
|
||||||
|
className={cn(cardTitleVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -104,11 +110,18 @@ CardTitle.displayName = "CardTitle";
|
||||||
*
|
*
|
||||||
* Description text for a card, typically placed below the title.
|
* Description text for a card, typically placed below the title.
|
||||||
*/
|
*/
|
||||||
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
|
export const CardDescription = forwardRef<
|
||||||
({ className, ...props }, ref) => {
|
HTMLParagraphElement,
|
||||||
return <p ref={ref} className={cn(cardDescriptionVariants(), className)} {...props} />;
|
CardDescriptionProps
|
||||||
},
|
>(({ className, ...props }, ref) => {
|
||||||
);
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn(cardDescriptionVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
CardDescription.displayName = "CardDescription";
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ export const cardVariants = createVariants({
|
||||||
"shadow-md",
|
"shadow-md",
|
||||||
].join(" "),
|
].join(" "),
|
||||||
|
|
||||||
outline: ["border-2 border-dotted border-border", "bg-transparent text-foreground"].join(" "),
|
outline: [
|
||||||
|
"border-2 border-dotted border-border",
|
||||||
|
"bg-transparent text-foreground",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
blueprint: [
|
blueprint: [
|
||||||
"border border-dotted border-border",
|
"border border-dotted border-border",
|
||||||
|
|
@ -82,7 +85,10 @@ export const cardHeaderVariants = createVariants({
|
||||||
* CardTitle variant definitions
|
* CardTitle variant definitions
|
||||||
*/
|
*/
|
||||||
export const cardTitleVariants = createVariants({
|
export const cardTitleVariants = createVariants({
|
||||||
base: ["text-2xl font-semibold leading-none tracking-tight", "text-foreground"].join(" "),
|
base: [
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
"text-foreground",
|
||||||
|
].join(" "),
|
||||||
|
|
||||||
variants: {},
|
variants: {},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,13 @@ describe("CodeBlock", () => {
|
||||||
|
|
||||||
describe("Language", () => {
|
describe("Language", () => {
|
||||||
it("sets data-language attribute", () => {
|
it("sets data-language attribute", () => {
|
||||||
render(<CodeBlock code="const x = 5" language="javascript" data-testid="codeblock" />);
|
render(
|
||||||
|
<CodeBlock
|
||||||
|
code="const x = 5"
|
||||||
|
language="javascript"
|
||||||
|
data-testid="codeblock"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const codeblock = screen.getByTestId("codeblock");
|
const codeblock = screen.getByTestId("codeblock");
|
||||||
const code = codeblock.querySelector("code");
|
const code = codeblock.querySelector("code");
|
||||||
expect(code).toHaveAttribute("data-language", "javascript");
|
expect(code).toHaveAttribute("data-language", "javascript");
|
||||||
|
|
@ -71,7 +77,9 @@ describe("CodeBlock", () => {
|
||||||
let code = codeblock.querySelector("code");
|
let code = codeblock.querySelector("code");
|
||||||
expect(code).toHaveAttribute("data-language", "python");
|
expect(code).toHaveAttribute("data-language", "python");
|
||||||
|
|
||||||
rerender(<CodeBlock code="code" language="bash" data-testid="codeblock" />);
|
rerender(
|
||||||
|
<CodeBlock code="code" language="bash" data-testid="codeblock" />,
|
||||||
|
);
|
||||||
codeblock = screen.getByTestId("codeblock");
|
codeblock = screen.getByTestId("codeblock");
|
||||||
code = codeblock.querySelector("code");
|
code = codeblock.querySelector("code");
|
||||||
expect(code).toHaveAttribute("data-language", "bash");
|
expect(code).toHaveAttribute("data-language", "bash");
|
||||||
|
|
@ -215,8 +223,12 @@ describe("CodeBlock", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles clipboard API errors gracefully", async () => {
|
it("handles clipboard API errors gracefully", async () => {
|
||||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
const consoleErrorSpy = vi
|
||||||
mockClipboard.writeText.mockRejectedValueOnce(new Error("Clipboard not available"));
|
.spyOn(console, "error")
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
mockClipboard.writeText.mockRejectedValueOnce(
|
||||||
|
new Error("Clipboard not available"),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
render(<CodeBlock code="test code" />);
|
render(<CodeBlock code="test code" />);
|
||||||
|
|
@ -237,13 +249,17 @@ describe("CodeBlock", () => {
|
||||||
|
|
||||||
describe("HTML Attributes", () => {
|
describe("HTML Attributes", () => {
|
||||||
it("passes through id attribute", () => {
|
it("passes through id attribute", () => {
|
||||||
render(<CodeBlock code="code" id="codeblock-id" data-testid="codeblock" />);
|
render(
|
||||||
|
<CodeBlock code="code" id="codeblock-id" data-testid="codeblock" />,
|
||||||
|
);
|
||||||
const codeblock = screen.getByTestId("codeblock");
|
const codeblock = screen.getByTestId("codeblock");
|
||||||
expect(codeblock).toHaveAttribute("id", "codeblock-id");
|
expect(codeblock).toHaveAttribute("id", "codeblock-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through data attributes", () => {
|
it("passes through data attributes", () => {
|
||||||
render(<CodeBlock code="code" data-custom="test" data-testid="codeblock" />);
|
render(
|
||||||
|
<CodeBlock code="code" data-custom="test" data-testid="codeblock" />,
|
||||||
|
);
|
||||||
const codeblock = screen.getByTestId("codeblock");
|
const codeblock = screen.getByTestId("codeblock");
|
||||||
expect(codeblock).toHaveAttribute("data-custom", "test");
|
expect(codeblock).toHaveAttribute("data-custom", "test");
|
||||||
});
|
});
|
||||||
|
|
@ -251,7 +267,13 @@ describe("CodeBlock", () => {
|
||||||
|
|
||||||
describe("Custom className", () => {
|
describe("Custom className", () => {
|
||||||
it("merges custom className with variant classes", () => {
|
it("merges custom className with variant classes", () => {
|
||||||
render(<CodeBlock code="code" className="custom-class" data-testid="codeblock" />);
|
render(
|
||||||
|
<CodeBlock
|
||||||
|
code="code"
|
||||||
|
className="custom-class"
|
||||||
|
data-testid="codeblock"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const codeblock = screen.getByTestId("codeblock");
|
const codeblock = screen.getByTestId("codeblock");
|
||||||
expect(codeblock.className).toContain("custom-class");
|
expect(codeblock.className).toContain("custom-class");
|
||||||
expect(codeblock.className).toContain("bg-background");
|
expect(codeblock.className).toContain("bg-background");
|
||||||
|
|
@ -271,7 +293,7 @@ describe("CodeBlock", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(ref?.querySelector("code")).toBeInTheDocument();
|
expect(ref!.querySelector("code")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,17 @@ import {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||||
({ className, code, language = "text", size = "md", showCopy = true, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
code,
|
||||||
|
language = "text",
|
||||||
|
size = "md",
|
||||||
|
showCopy = true,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
|
|
@ -47,7 +57,11 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(codeBlockContainerVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<code
|
<code
|
||||||
className={codeBlockCodeVariants({
|
className={codeBlockCodeVariants({
|
||||||
size,
|
size,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
|
||||||
/**
|
/**
|
||||||
* CodeBlock component props
|
* CodeBlock component props
|
||||||
*/
|
*/
|
||||||
export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
export interface CodeBlockProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||||
/**
|
/**
|
||||||
* Code content to display
|
* Code content to display
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ describe("Container", () => {
|
||||||
</Container>,
|
</Container>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(ref?.tagName).toBe("DIV");
|
expect(ref!.tagName).toBe("DIV");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,13 @@ describe("GridContainer", () => {
|
||||||
describe("Compound Scenarios", () => {
|
describe("Compound Scenarios", () => {
|
||||||
it("works with multiple variants combined", () => {
|
it("works with multiple variants combined", () => {
|
||||||
render(
|
render(
|
||||||
<GridContainer columns={4} gap="lg" align="center" justify="center" data-testid="grid">
|
<GridContainer
|
||||||
|
columns={4}
|
||||||
|
gap="lg"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
data-testid="grid"
|
||||||
|
>
|
||||||
Content
|
Content
|
||||||
</GridContainer>,
|
</GridContainer>,
|
||||||
);
|
);
|
||||||
|
|
@ -294,7 +300,12 @@ describe("GridContainer", () => {
|
||||||
|
|
||||||
it("works with fixed columns and custom className", () => {
|
it("works with fixed columns and custom className", () => {
|
||||||
render(
|
render(
|
||||||
<GridContainer columns={3} responsive="fixed" className="my-custom-grid" data-testid="grid">
|
<GridContainer
|
||||||
|
columns={3}
|
||||||
|
responsive="fixed"
|
||||||
|
className="my-custom-grid"
|
||||||
|
data-testid="grid"
|
||||||
|
>
|
||||||
Content
|
Content
|
||||||
</GridContainer>,
|
</GridContainer>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,11 @@ describe("Header", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders ReactNode as actions", () => {
|
it("renders ReactNode as actions", () => {
|
||||||
render(<Header actions={<div data-testid="custom-actions">Custom Actions</div>} />);
|
render(
|
||||||
|
<Header
|
||||||
|
actions={<div data-testid="custom-actions">Custom Actions</div>}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(screen.getByTestId("custom-actions")).toBeInTheDocument();
|
expect(screen.getByTestId("custom-actions")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -251,7 +255,7 @@ describe("Header", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLElement);
|
expect(ref).toBeInstanceOf(HTMLElement);
|
||||||
expect(ref?.tagName).toBe("HEADER");
|
expect(ref!.tagName).toBe("HEADER");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -271,7 +275,15 @@ describe("Header", () => {
|
||||||
|
|
||||||
describe("Compound Scenarios", () => {
|
describe("Compound Scenarios", () => {
|
||||||
it("works correctly with large size and sticky", () => {
|
it("works correctly with large size and sticky", () => {
|
||||||
render(<Header title="App" actions="Sign In" size="lg" sticky={true} data-testid="header" />);
|
render(
|
||||||
|
<Header
|
||||||
|
title="App"
|
||||||
|
actions="Sign In"
|
||||||
|
size="lg"
|
||||||
|
sticky={true}
|
||||||
|
data-testid="header"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const header = screen.getByTestId("header");
|
const header = screen.getByTestId("header");
|
||||||
expect(header.className).toContain("h-20");
|
expect(header.className).toContain("h-20");
|
||||||
expect(header.className).toContain("px-8");
|
expect(header.className).toContain("px-8");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { cn } from "@tpmjs/utils/cn";
|
import { cn } from "@tpmjs/utils/cn";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import type { HeaderProps } from "./types";
|
import type { HeaderProps } from "./types";
|
||||||
import { headerActionsVariants, headerTitleVariants, headerVariants } from "./variants";
|
import {
|
||||||
|
headerActionsVariants,
|
||||||
|
headerTitleVariants,
|
||||||
|
headerVariants,
|
||||||
|
} from "./variants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header component
|
* Header component
|
||||||
|
|
@ -27,7 +31,18 @@ import { headerActionsVariants, headerTitleVariants, headerVariants } from "./va
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const Header = forwardRef<HTMLElement, HeaderProps>(
|
export const Header = forwardRef<HTMLElement, HeaderProps>(
|
||||||
({ className, title, actions, size = "md", sticky = false, children, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
actions,
|
||||||
|
size = "md",
|
||||||
|
sticky = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -51,7 +66,10 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{children && (
|
{children && (
|
||||||
<div className="flex-1 flex items-center justify-center" data-testid="header-children">
|
<div
|
||||||
|
className="flex-1 flex items-center justify-center"
|
||||||
|
data-testid="header-children"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes, ReactNode } from "react";
|
||||||
/**
|
/**
|
||||||
* Header component props
|
* Header component props
|
||||||
*/
|
*/
|
||||||
export interface HeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
export interface HeaderProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
||||||
/**
|
/**
|
||||||
* Title/logo content for the left side
|
* Title/logo content for the left side
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,10 @@ describe("Icon", () => {
|
||||||
render(<Icon icon="chevronDown" data-testid="icon" />);
|
render(<Icon icon="chevronDown" data-testid="icon" />);
|
||||||
const icon = screen.getByTestId("icon");
|
const icon = screen.getByTestId("icon");
|
||||||
const path = icon.querySelector("path");
|
const path = icon.querySelector("path");
|
||||||
expect(path).toHaveAttribute("d", "M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z");
|
expect(path).toHaveAttribute(
|
||||||
|
"d",
|
||||||
|
"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +148,14 @@ describe("Icon", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through aria-label", () => {
|
it("passes through aria-label", () => {
|
||||||
render(<Icon icon="check" aria-label="Success" aria-hidden={false} data-testid="icon" />);
|
render(
|
||||||
|
<Icon
|
||||||
|
icon="check"
|
||||||
|
aria-label="Success"
|
||||||
|
aria-hidden={false}
|
||||||
|
data-testid="icon"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const icon = screen.getByTestId("icon");
|
const icon = screen.getByTestId("icon");
|
||||||
expect(icon).toHaveAttribute("aria-label", "Success");
|
expect(icon).toHaveAttribute("aria-label", "Success");
|
||||||
});
|
});
|
||||||
|
|
@ -169,7 +179,7 @@ describe("Icon", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(SVGSVGElement);
|
expect(ref).toBeInstanceOf(SVGSVGElement);
|
||||||
expect(ref?.tagName).toBe("svg");
|
expect(ref!.tagName).toBe("svg");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import type { IconName } from "./icons";
|
||||||
/**
|
/**
|
||||||
* Icon component props
|
* Icon component props
|
||||||
*/
|
*/
|
||||||
export interface IconProps extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
|
export interface IconProps
|
||||||
|
extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
|
||||||
/**
|
/**
|
||||||
* Icon to display
|
* Icon to display
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,9 @@ describe("Input", () => {
|
||||||
|
|
||||||
describe("Value and onChange", () => {
|
describe("Value and onChange", () => {
|
||||||
it("renders with initial value", () => {
|
it("renders with initial value", () => {
|
||||||
render(<Input value="test value" onChange={() => {}} data-testid="input" />);
|
render(
|
||||||
|
<Input value="test value" onChange={() => {}} data-testid="input" />,
|
||||||
|
);
|
||||||
const input = screen.getByTestId("input") as HTMLInputElement;
|
const input = screen.getByTestId("input") as HTMLInputElement;
|
||||||
expect(input.value).toBe("test value");
|
expect(input.value).toBe("test value");
|
||||||
});
|
});
|
||||||
|
|
@ -315,7 +317,7 @@ describe("Input", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLInputElement);
|
expect(ref).toBeInstanceOf(HTMLInputElement);
|
||||||
expect(ref?.tagName).toBe("INPUT");
|
expect(ref!.tagName).toBe("INPUT");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can focus input through ref", () => {
|
it("can focus input through ref", () => {
|
||||||
|
|
@ -328,7 +330,7 @@ describe("Input", () => {
|
||||||
data-testid="input"
|
data-testid="input"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
ref?.focus();
|
ref!.focus();
|
||||||
expect(screen.getByTestId("input")).toHaveFocus();
|
expect(screen.getByTestId("input")).toHaveFocus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { InputHTMLAttributes } from "react";
|
||||||
/**
|
/**
|
||||||
* Input component props
|
* Input component props
|
||||||
*/
|
*/
|
||||||
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
export interface InputProps
|
||||||
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||||
/**
|
/**
|
||||||
* Visual state of the input
|
* Visual state of the input
|
||||||
* @default 'default'
|
* @default 'default'
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ describe("Label", () => {
|
||||||
</Label>,
|
</Label>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLLabelElement);
|
expect(ref).toBeInstanceOf(HTMLLabelElement);
|
||||||
expect(ref?.tagName).toBe("LABEL");
|
expect(ref!.tagName).toBe("LABEL");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,17 @@ import { labelVariants } from "./variants";
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
||||||
({ className, size = "md", required = false, disabled = false, children, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
size = "md",
|
||||||
|
required = false,
|
||||||
|
disabled = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/noLabelWithoutControl: This is a generic label component that can be used with htmlFor or wrap inputs
|
// biome-ignore lint/a11y/noLabelWithoutControl: This is a generic label component that can be used with htmlFor or wrap inputs
|
||||||
<label
|
<label
|
||||||
|
|
|
||||||
|
|
@ -103,28 +103,36 @@ describe("ProgressBar", () => {
|
||||||
|
|
||||||
describe("Color Variants", () => {
|
describe("Color Variants", () => {
|
||||||
it("applies primary variant", () => {
|
it("applies primary variant", () => {
|
||||||
render(<ProgressBar value={50} variant="primary" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} variant="primary" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
const fill = progress.querySelector("div");
|
const fill = progress.querySelector("div");
|
||||||
expect(fill?.className).toContain("bg-primary");
|
expect(fill?.className).toContain("bg-primary");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies success variant", () => {
|
it("applies success variant", () => {
|
||||||
render(<ProgressBar value={50} variant="success" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} variant="success" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
const fill = progress.querySelector("div");
|
const fill = progress.querySelector("div");
|
||||||
expect(fill?.className).toContain("bg-success");
|
expect(fill?.className).toContain("bg-success");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies warning variant", () => {
|
it("applies warning variant", () => {
|
||||||
render(<ProgressBar value={50} variant="warning" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} variant="warning" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
const fill = progress.querySelector("div");
|
const fill = progress.querySelector("div");
|
||||||
expect(fill?.className).toContain("bg-warning");
|
expect(fill?.className).toContain("bg-warning");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies danger variant", () => {
|
it("applies danger variant", () => {
|
||||||
render(<ProgressBar value={50} variant="danger" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} variant="danger" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
const fill = progress.querySelector("div");
|
const fill = progress.querySelector("div");
|
||||||
expect(fill?.className).toContain("bg-error");
|
expect(fill?.className).toContain("bg-error");
|
||||||
|
|
@ -175,13 +183,17 @@ describe("ProgressBar", () => {
|
||||||
|
|
||||||
describe("HTML Attributes", () => {
|
describe("HTML Attributes", () => {
|
||||||
it("passes through id attribute", () => {
|
it("passes through id attribute", () => {
|
||||||
render(<ProgressBar value={50} id="progress-id" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} id="progress-id" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
expect(progress).toHaveAttribute("id", "progress-id");
|
expect(progress).toHaveAttribute("id", "progress-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through data attributes", () => {
|
it("passes through data attributes", () => {
|
||||||
render(<ProgressBar value={50} data-custom="test" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar value={50} data-custom="test" data-testid="progress" />,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
expect(progress).toHaveAttribute("data-custom", "test");
|
expect(progress).toHaveAttribute("data-custom", "test");
|
||||||
});
|
});
|
||||||
|
|
@ -189,7 +201,13 @@ describe("ProgressBar", () => {
|
||||||
|
|
||||||
describe("Custom className", () => {
|
describe("Custom className", () => {
|
||||||
it("merges custom className with variant classes", () => {
|
it("merges custom className with variant classes", () => {
|
||||||
render(<ProgressBar value={50} className="custom-class" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar
|
||||||
|
value={50}
|
||||||
|
className="custom-class"
|
||||||
|
data-testid="progress"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
expect(progress.className).toContain("custom-class");
|
expect(progress.className).toContain("custom-class");
|
||||||
expect(progress.className).toContain("bg-surface");
|
expect(progress.className).toContain("bg-surface");
|
||||||
|
|
@ -235,7 +253,14 @@ describe("ProgressBar", () => {
|
||||||
|
|
||||||
describe("Compound Scenarios", () => {
|
describe("Compound Scenarios", () => {
|
||||||
it("works correctly with large size and success variant", () => {
|
it("works correctly with large size and success variant", () => {
|
||||||
render(<ProgressBar value={80} size="lg" variant="success" data-testid="progress" />);
|
render(
|
||||||
|
<ProgressBar
|
||||||
|
value={80}
|
||||||
|
size="lg"
|
||||||
|
variant="success"
|
||||||
|
data-testid="progress"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const progress = screen.getByTestId("progress");
|
const progress = screen.getByTestId("progress");
|
||||||
expect(progress.className).toContain("h-3");
|
expect(progress.className).toContain("h-3");
|
||||||
const fill = progress.querySelector("div");
|
const fill = progress.querySelector("div");
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,17 @@ import { progressBarFillVariants, progressBarTrackVariants } from "./variants";
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
||||||
({ className, value, size = "md", variant = "primary", showLabel = false, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
size = "md",
|
||||||
|
variant = "primary",
|
||||||
|
showLabel = false,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
// Clamp value between 0 and 100
|
// Clamp value between 0 and 100
|
||||||
const clampedValue = Math.min(Math.max(value, 0), 100);
|
const clampedValue = Math.min(Math.max(value, 0), 100);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
|
||||||
/**
|
/**
|
||||||
* ProgressBar component props
|
* ProgressBar component props
|
||||||
*/
|
*/
|
||||||
export interface ProgressBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
export interface ProgressBarProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||||
/**
|
/**
|
||||||
* Progress value (0-100)
|
* Progress value (0-100)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ describe("Section", () => {
|
||||||
</Section>,
|
</Section>,
|
||||||
);
|
);
|
||||||
expect(ref).toBeInstanceOf(HTMLElement);
|
expect(ref).toBeInstanceOf(HTMLElement);
|
||||||
expect(ref?.tagName).toBe("SECTION");
|
expect(ref!.tagName).toBe("SECTION");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -308,7 +308,13 @@ describe("Section", () => {
|
||||||
describe("Compound Scenarios", () => {
|
describe("Compound Scenarios", () => {
|
||||||
it("works with multiple variants combined", () => {
|
it("works with multiple variants combined", () => {
|
||||||
render(
|
render(
|
||||||
<Section spacing="lg" background="blueprint" container="xl" centered data-testid="section">
|
<Section
|
||||||
|
spacing="lg"
|
||||||
|
background="blueprint"
|
||||||
|
container="xl"
|
||||||
|
centered
|
||||||
|
data-testid="section"
|
||||||
|
>
|
||||||
Content
|
Content
|
||||||
</Section>,
|
</Section>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ describe("Tabs", () => {
|
||||||
it("renders tabs container", () => {
|
it("renders tabs container", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(
|
render(
|
||||||
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
data-testid="tabs"
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
const tabs = screen.getByTestId("tabs");
|
const tabs = screen.getByTestId("tabs");
|
||||||
expect(tabs).toBeInTheDocument();
|
expect(tabs).toBeInTheDocument();
|
||||||
|
|
@ -24,7 +29,12 @@ describe("Tabs", () => {
|
||||||
it("has tablist role", () => {
|
it("has tablist role", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(
|
render(
|
||||||
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
data-testid="tabs"
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
const tabs = screen.getByTestId("tabs");
|
const tabs = screen.getByTestId("tabs");
|
||||||
expect(tabs).toHaveAttribute("role", "tablist");
|
expect(tabs).toHaveAttribute("role", "tablist");
|
||||||
|
|
@ -32,7 +42,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("renders all tabs", () => {
|
it("renders all tabs", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
expect(screen.getByTestId("tab-all")).toBeInTheDocument();
|
expect(screen.getByTestId("tab-all")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("tab-featured")).toBeInTheDocument();
|
expect(screen.getByTestId("tab-featured")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("tab-recent")).toBeInTheDocument();
|
expect(screen.getByTestId("tab-recent")).toBeInTheDocument();
|
||||||
|
|
@ -40,7 +52,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("renders tab labels", () => {
|
it("renders tab labels", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
expect(screen.getByText("All Tools")).toBeInTheDocument();
|
expect(screen.getByText("All Tools")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Featured")).toBeInTheDocument();
|
expect(screen.getByText("Featured")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Recent")).toBeInTheDocument();
|
expect(screen.getByText("Recent")).toBeInTheDocument();
|
||||||
|
|
@ -48,7 +62,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("renders tabs as buttons", () => {
|
it("renders tabs as buttons", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.tagName).toBe("BUTTON");
|
expect(tab.tagName).toBe("BUTTON");
|
||||||
expect(tab).toHaveAttribute("type", "button");
|
expect(tab).toHaveAttribute("type", "button");
|
||||||
|
|
@ -58,15 +74,32 @@ describe("Tabs", () => {
|
||||||
describe("Active State", () => {
|
describe("Active State", () => {
|
||||||
it("marks active tab with aria-selected", () => {
|
it("marks active tab with aria-selected", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="featured" onTabChange={handleChange} />);
|
render(
|
||||||
expect(screen.getByTestId("tab-featured")).toHaveAttribute("aria-selected", "true");
|
<Tabs
|
||||||
expect(screen.getByTestId("tab-all")).toHaveAttribute("aria-selected", "false");
|
tabs={mockTabs}
|
||||||
expect(screen.getByTestId("tab-recent")).toHaveAttribute("aria-selected", "false");
|
activeTab="featured"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("tab-all")).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("tab-recent")).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies active styling to active tab", () => {
|
it("applies active styling to active tab", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const activeTab = screen.getByTestId("tab-all");
|
const activeTab = screen.getByTestId("tab-all");
|
||||||
expect(activeTab.className).toContain("text-foreground");
|
expect(activeTab.className).toContain("text-foreground");
|
||||||
expect(activeTab.className).toContain("border-primary");
|
expect(activeTab.className).toContain("border-primary");
|
||||||
|
|
@ -74,7 +107,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("applies inactive styling to inactive tabs", () => {
|
it("applies inactive styling to inactive tabs", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const inactiveTab = screen.getByTestId("tab-featured");
|
const inactiveTab = screen.getByTestId("tab-featured");
|
||||||
expect(inactiveTab.className).toContain("text-foreground-secondary");
|
expect(inactiveTab.className).toContain("text-foreground-secondary");
|
||||||
expect(inactiveTab.className).toContain("border-transparent");
|
expect(inactiveTab.className).toContain("border-transparent");
|
||||||
|
|
@ -84,14 +119,18 @@ describe("Tabs", () => {
|
||||||
describe("Click Handling", () => {
|
describe("Click Handling", () => {
|
||||||
it("calls onTabChange when tab is clicked", () => {
|
it("calls onTabChange when tab is clicked", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
screen.getByTestId("tab-featured").click();
|
screen.getByTestId("tab-featured").click();
|
||||||
expect(handleChange).toHaveBeenCalledWith("featured");
|
expect(handleChange).toHaveBeenCalledWith("featured");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onTabChange with correct tab ID", () => {
|
it("calls onTabChange with correct tab ID", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
screen.getByTestId("tab-recent").click();
|
screen.getByTestId("tab-recent").click();
|
||||||
expect(handleChange).toHaveBeenCalledWith("recent");
|
expect(handleChange).toHaveBeenCalledWith("recent");
|
||||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -99,7 +138,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("allows clicking already active tab", () => {
|
it("allows clicking already active tab", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
screen.getByTestId("tab-all").click();
|
screen.getByTestId("tab-all").click();
|
||||||
expect(handleChange).toHaveBeenCalledWith("all");
|
expect(handleChange).toHaveBeenCalledWith("all");
|
||||||
});
|
});
|
||||||
|
|
@ -108,14 +149,18 @@ describe("Tabs", () => {
|
||||||
describe("Count Badges", () => {
|
describe("Count Badges", () => {
|
||||||
it("renders count badge when count is provided", () => {
|
it("renders count badge when count is provided", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
expect(screen.getByText("1234")).toBeInTheDocument();
|
expect(screen.getByText("1234")).toBeInTheDocument();
|
||||||
expect(screen.getByText("42")).toBeInTheDocument();
|
expect(screen.getByText("42")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render count badge when count is undefined", () => {
|
it("does not render count badge when count is undefined", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const recentTab = screen.getByTestId("tab-recent");
|
const recentTab = screen.getByTestId("tab-recent");
|
||||||
const badge = recentTab.querySelector("span[aria-label]");
|
const badge = recentTab.querySelector("span[aria-label]");
|
||||||
expect(badge).not.toBeInTheDocument();
|
expect(badge).not.toBeInTheDocument();
|
||||||
|
|
@ -123,21 +168,27 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("count badge has aria-label", () => {
|
it("count badge has aria-label", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const badge = screen.getByText("1234");
|
const badge = screen.getByText("1234");
|
||||||
expect(badge).toHaveAttribute("aria-label", "1234 items");
|
expect(badge).toHaveAttribute("aria-label", "1234 items");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies active styling to count badge on active tab", () => {
|
it("applies active styling to count badge on active tab", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const activeBadge = screen.getByText("1234");
|
const activeBadge = screen.getByText("1234");
|
||||||
expect(activeBadge.className).toContain("text-foreground");
|
expect(activeBadge.className).toContain("text-foreground");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies inactive styling to count badge on inactive tab", () => {
|
it("applies inactive styling to count badge on inactive tab", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const inactiveBadge = screen.getByText("42");
|
const inactiveBadge = screen.getByText("42");
|
||||||
expect(inactiveBadge.className).toContain("text-foreground-tertiary");
|
expect(inactiveBadge.className).toContain("text-foreground-tertiary");
|
||||||
});
|
});
|
||||||
|
|
@ -145,7 +196,13 @@ describe("Tabs", () => {
|
||||||
it("handles zero count", () => {
|
it("handles zero count", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
const tabsWithZero: Tab[] = [{ id: "empty", label: "Empty", count: 0 }];
|
const tabsWithZero: Tab[] = [{ id: "empty", label: "Empty", count: 0 }];
|
||||||
render(<Tabs tabs={tabsWithZero} activeTab="empty" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs
|
||||||
|
tabs={tabsWithZero}
|
||||||
|
activeTab="empty"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(screen.getByText("0")).toBeInTheDocument();
|
expect(screen.getByText("0")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -153,7 +210,14 @@ describe("Tabs", () => {
|
||||||
describe("Size Variants", () => {
|
describe("Size Variants", () => {
|
||||||
it("applies small size", () => {
|
it("applies small size", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="sm" />);
|
render(
|
||||||
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
size="sm"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.className).toContain("text-sm");
|
expect(tab.className).toContain("text-sm");
|
||||||
expect(tab.className).toContain("px-3");
|
expect(tab.className).toContain("px-3");
|
||||||
|
|
@ -162,7 +226,14 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("applies medium size", () => {
|
it("applies medium size", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="md" />);
|
render(
|
||||||
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
size="md"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.className).toContain("text-base");
|
expect(tab.className).toContain("text-base");
|
||||||
expect(tab.className).toContain("px-4");
|
expect(tab.className).toContain("px-4");
|
||||||
|
|
@ -171,7 +242,14 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("applies large size", () => {
|
it("applies large size", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="lg" />);
|
render(
|
||||||
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
size="lg"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.className).toContain("text-lg");
|
expect(tab.className).toContain("text-lg");
|
||||||
expect(tab.className).toContain("px-6");
|
expect(tab.className).toContain("px-6");
|
||||||
|
|
@ -180,7 +258,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("uses md size by default", () => {
|
it("uses md size by default", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.className).toContain("text-base");
|
expect(tab.className).toContain("text-base");
|
||||||
expect(tab.className).toContain("px-4");
|
expect(tab.className).toContain("px-4");
|
||||||
|
|
@ -191,23 +271,38 @@ describe("Tabs", () => {
|
||||||
describe("Accessibility", () => {
|
describe("Accessibility", () => {
|
||||||
it('tabs have role="tab"', () => {
|
it('tabs have role="tab"', () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab).toHaveAttribute("role", "tab");
|
expect(tab).toHaveAttribute("role", "tab");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tabs have unique IDs", () => {
|
it("tabs have unique IDs", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
expect(screen.getByTestId("tab-all")).toHaveAttribute("id", "tab-all");
|
expect(screen.getByTestId("tab-all")).toHaveAttribute("id", "tab-all");
|
||||||
expect(screen.getByTestId("tab-featured")).toHaveAttribute("id", "tab-featured");
|
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
||||||
expect(screen.getByTestId("tab-recent")).toHaveAttribute("id", "tab-recent");
|
"id",
|
||||||
|
"tab-featured",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("tab-recent")).toHaveAttribute(
|
||||||
|
"id",
|
||||||
|
"tab-recent",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tabs have aria-controls attribute", () => {
|
it("tabs have aria-controls attribute", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
expect(screen.getByTestId("tab-all")).toHaveAttribute("aria-controls", "tabpanel-all");
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("tab-all")).toHaveAttribute(
|
||||||
|
"aria-controls",
|
||||||
|
"tabpanel-all",
|
||||||
|
);
|
||||||
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
||||||
"aria-controls",
|
"aria-controls",
|
||||||
"tabpanel-featured",
|
"tabpanel-featured",
|
||||||
|
|
@ -289,7 +384,12 @@ describe("Tabs", () => {
|
||||||
it("container includes base classes", () => {
|
it("container includes base classes", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(
|
render(
|
||||||
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="all"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
data-testid="tabs"
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
const tabs = screen.getByTestId("tabs");
|
const tabs = screen.getByTestId("tabs");
|
||||||
expect(tabs.className).toContain("flex");
|
expect(tabs.className).toContain("flex");
|
||||||
|
|
@ -300,7 +400,9 @@ describe("Tabs", () => {
|
||||||
|
|
||||||
it("tab buttons include base classes", () => {
|
it("tab buttons include base classes", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-all");
|
const tab = screen.getByTestId("tab-all");
|
||||||
expect(tab.className).toContain("inline-flex");
|
expect(tab.className).toContain("inline-flex");
|
||||||
expect(tab.className).toContain("items-center");
|
expect(tab.className).toContain("items-center");
|
||||||
|
|
@ -314,7 +416,14 @@ describe("Tabs", () => {
|
||||||
describe("Compound Scenarios", () => {
|
describe("Compound Scenarios", () => {
|
||||||
it("works correctly with large size and active tab with count", () => {
|
it("works correctly with large size and active tab with count", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
render(<Tabs tabs={mockTabs} activeTab="featured" onTabChange={handleChange} size="lg" />);
|
render(
|
||||||
|
<Tabs
|
||||||
|
tabs={mockTabs}
|
||||||
|
activeTab="featured"
|
||||||
|
onTabChange={handleChange}
|
||||||
|
size="lg"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const tab = screen.getByTestId("tab-featured");
|
const tab = screen.getByTestId("tab-featured");
|
||||||
expect(tab.className).toContain("text-lg");
|
expect(tab.className).toContain("text-lg");
|
||||||
expect(tab.className).toContain("px-6");
|
expect(tab.className).toContain("px-6");
|
||||||
|
|
@ -328,14 +437,18 @@ describe("Tabs", () => {
|
||||||
it("handles single tab", () => {
|
it("handles single tab", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
const singleTab: Tab[] = [{ id: "only", label: "Only Tab" }];
|
const singleTab: Tab[] = [{ id: "only", label: "Only Tab" }];
|
||||||
render(<Tabs tabs={singleTab} activeTab="only" onTabChange={handleChange} />);
|
render(
|
||||||
|
<Tabs tabs={singleTab} activeTab="only" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
expect(screen.getByTestId("tab-only")).toBeInTheDocument();
|
expect(screen.getByTestId("tab-only")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Only Tab")).toBeInTheDocument();
|
expect(screen.getByText("Only Tab")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles empty tabs array", () => {
|
it("handles empty tabs array", () => {
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
const { container } = render(<Tabs tabs={[]} activeTab="" onTabChange={handleChange} />);
|
const { container } = render(
|
||||||
|
<Tabs tabs={[]} activeTab="" onTabChange={handleChange} />,
|
||||||
|
);
|
||||||
const buttons = container.querySelectorAll("button");
|
const buttons = container.querySelectorAll("button");
|
||||||
expect(buttons.length).toBe(0);
|
expect(buttons.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { cn } from "@tpmjs/utils/cn";
|
import { cn } from "@tpmjs/utils/cn";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import type { Tab, TabsProps } from "./types";
|
import type { Tab, TabsProps } from "./types";
|
||||||
import { tabButtonVariants, tabCountVariants, tabsContainerVariants } from "./variants";
|
import {
|
||||||
|
tabButtonVariants,
|
||||||
|
tabCountVariants,
|
||||||
|
tabsContainerVariants,
|
||||||
|
} from "./variants";
|
||||||
|
|
||||||
// Re-export types for consumers
|
// Re-export types for consumers
|
||||||
export type { Tab, TabsProps };
|
export type { Tab, TabsProps };
|
||||||
|
|
@ -36,7 +40,15 @@ export type { Tab, TabsProps };
|
||||||
*/
|
*/
|
||||||
export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
|
export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
|
||||||
(
|
(
|
||||||
{ className, tabs, activeTab, onTabChange, size = "md", variant = "default", ...props },
|
{
|
||||||
|
className,
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
size = "md",
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ export interface Tab {
|
||||||
/**
|
/**
|
||||||
* Tabs component props
|
* Tabs component props
|
||||||
*/
|
*/
|
||||||
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
export interface TabsProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||||
/**
|
/**
|
||||||
* Array of tabs to display
|
* Array of tabs to display
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,10 @@ export type VariantConfig<T extends Record<string, Record<string, string>>> = {
|
||||||
/**
|
/**
|
||||||
* Props type for consuming variant functions
|
* Props type for consuming variant functions
|
||||||
*/
|
*/
|
||||||
export type VariantProps<T extends Record<string, Record<string, string>>> = Partial<{
|
export type VariantProps<T extends Record<string, Record<string, string>>> =
|
||||||
[K in keyof T]: keyof T[K];
|
Partial<{
|
||||||
}>;
|
[K in keyof T]: keyof T[K];
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a type-safe variant composition function
|
* Creates a type-safe variant composition function
|
||||||
|
|
@ -67,9 +68,9 @@ export type VariantProps<T extends Record<string, Record<string, string>>> = Par
|
||||||
* @param config - Variant configuration with base, variants, compound variants, and defaults
|
* @param config - Variant configuration with base, variants, compound variants, and defaults
|
||||||
* @returns Function that accepts variant props and returns composed className string
|
* @returns Function that accepts variant props and returns composed className string
|
||||||
*/
|
*/
|
||||||
export function createVariants<T extends Record<string, Record<string, string>>>(
|
export function createVariants<
|
||||||
config: VariantConfig<T>,
|
T extends Record<string, Record<string, string>>,
|
||||||
) {
|
>(config: VariantConfig<T>) {
|
||||||
return function getVariantClasses(props?: VariantProps<T>): string {
|
return function getVariantClasses(props?: VariantProps<T>): string {
|
||||||
const classes: string[] = [config.base];
|
const classes: string[] = [config.base];
|
||||||
|
|
||||||
|
|
@ -82,7 +83,11 @@ export function createVariants<T extends Record<string, Record<string, string>>>
|
||||||
// Apply variant classes
|
// Apply variant classes
|
||||||
for (const [key, value] of Object.entries(mergedProps)) {
|
for (const [key, value] of Object.entries(mergedProps)) {
|
||||||
const valueStr = value as string;
|
const valueStr = value as string;
|
||||||
if (value !== undefined && value !== null && config.variants[key]?.[valueStr]) {
|
if (
|
||||||
|
value !== undefined &&
|
||||||
|
value !== null &&
|
||||||
|
config.variants[key]?.[valueStr]
|
||||||
|
) {
|
||||||
classes.push(config.variants[key][valueStr]);
|
classes.push(config.variants[key][valueStr]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +96,8 @@ export function createVariants<T extends Record<string, Record<string, string>>>
|
||||||
if (config.compoundVariants) {
|
if (config.compoundVariants) {
|
||||||
for (const { conditions, className } of config.compoundVariants) {
|
for (const { conditions, className } of config.compoundVariants) {
|
||||||
const matches = Object.entries(conditions).every(
|
const matches = Object.entries(conditions).every(
|
||||||
([key, value]) => mergedProps[key as keyof typeof mergedProps] === value,
|
([key, value]) =>
|
||||||
|
mergedProps[key as keyof typeof mergedProps] === value,
|
||||||
);
|
);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
classes.push(className);
|
classes.push(className);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue