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>,
|
||||
);
|
||||
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", () => {
|
||||
render(
|
||||
<Badge variant="outline" className="hover:bg-accent" data-testid="badge">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="hover:bg-accent"
|
||||
data-testid="badge"
|
||||
>
|
||||
Outlined
|
||||
</Badge>,
|
||||
);
|
||||
|
|
@ -351,7 +355,12 @@ describe("Badge", () => {
|
|||
describe("Semantic Usage", () => {
|
||||
it("can be used as status indicator with role", () => {
|
||||
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
|
||||
</Badge>,
|
||||
);
|
||||
|
|
@ -363,7 +372,12 @@ describe("Badge", () => {
|
|||
|
||||
it("can be used as notification count", () => {
|
||||
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
|
||||
</Badge>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
|||
* Visual variant of the badge
|
||||
* @default 'default'
|
||||
*/
|
||||
variant?: "default" | "secondary" | "outline" | "success" | "error" | "warning" | "info";
|
||||
variant?:
|
||||
| "default"
|
||||
| "secondary"
|
||||
| "outline"
|
||||
| "success"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "info";
|
||||
|
||||
/**
|
||||
* Size of the badge
|
||||
|
|
|
|||
|
|
@ -19,17 +19,33 @@ export const badgeVariants = createVariants({
|
|||
|
||||
variants: {
|
||||
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(" "),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ describe("Button", () => {
|
|||
});
|
||||
|
||||
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");
|
||||
expect(button.className).toContain("h-10");
|
||||
expect(button.className).toContain("w-10");
|
||||
|
|
@ -239,14 +241,19 @@ describe("Button", () => {
|
|||
</Button>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLButtonElement);
|
||||
expect(ref?.tagName).toBe("BUTTON");
|
||||
expect(ref!.tagName).toBe("BUTTON");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML Attributes", () => {
|
||||
it("passes through HTML button attributes", () => {
|
||||
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
|
||||
</Button>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -65,9 +65,11 @@ export const buttonVariants = createVariants({
|
|||
"active:bg-accent/80",
|
||||
].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: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
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("Rendering", () => {
|
||||
|
|
@ -16,7 +23,9 @@ describe("Card", () => {
|
|||
<Card data-testid="card">
|
||||
<CardHeader data-testid="header">
|
||||
<CardTitle data-testid="title">Title</CardTitle>
|
||||
<CardDescription data-testid="description">Description</CardDescription>
|
||||
<CardDescription data-testid="description">
|
||||
Description
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent data-testid="content">Content</CardContent>
|
||||
<CardFooter data-testid="footer">Footer</CardFooter>
|
||||
|
|
@ -227,7 +236,9 @@ describe("Card", () => {
|
|||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription data-testid="description">Description text</CardDescription>
|
||||
<CardDescription data-testid="description">
|
||||
Description text
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>,
|
||||
);
|
||||
|
|
@ -239,7 +250,9 @@ describe("Card", () => {
|
|||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription data-testid="description">Description</CardDescription>
|
||||
<CardDescription data-testid="description">
|
||||
Description
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>,
|
||||
);
|
||||
|
|
@ -404,7 +417,7 @@ describe("Card", () => {
|
|||
</Card>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||
expect(ref?.tagName).toBe("DIV");
|
||||
expect(ref!.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
it("forwards ref to CardHeader element", () => {
|
||||
|
|
@ -439,7 +452,7 @@ describe("Card", () => {
|
|||
</Card>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLHeadingElement);
|
||||
expect(ref?.tagName).toBe("H3");
|
||||
expect(ref!.tagName).toBe("H3");
|
||||
});
|
||||
|
||||
it("forwards ref to CardDescription element", () => {
|
||||
|
|
@ -561,7 +574,9 @@ describe("Card", () => {
|
|||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription data-testid="description">Description</CardDescription>
|
||||
<CardDescription data-testid="description">
|
||||
Description
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>,
|
||||
);
|
||||
|
|
@ -591,7 +606,9 @@ describe("Card", () => {
|
|||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>This is a card description</CardDescription>
|
||||
</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>
|
||||
</Card>,
|
||||
);
|
||||
|
|
@ -599,7 +616,9 @@ describe("Card", () => {
|
|||
const card = screen.getByTestId("card");
|
||||
expect(card).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("Footer actions")).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,7 +93,13 @@ CardHeader.displayName = "CardHeader";
|
|||
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
|
||||
({ className, as = "h3", ...props }, ref) => {
|
||||
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.
|
||||
*/
|
||||
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return <p ref={ref} className={cn(cardDescriptionVariants(), className)} {...props} />;
|
||||
},
|
||||
);
|
||||
export const CardDescription = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
CardDescriptionProps
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn(cardDescriptionVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ export const cardVariants = createVariants({
|
|||
"shadow-md",
|
||||
].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: [
|
||||
"border border-dotted border-border",
|
||||
|
|
@ -82,7 +85,10 @@ export const cardHeaderVariants = createVariants({
|
|||
* CardTitle variant definitions
|
||||
*/
|
||||
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: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,13 @@ describe("CodeBlock", () => {
|
|||
|
||||
describe("Language", () => {
|
||||
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 code = codeblock.querySelector("code");
|
||||
expect(code).toHaveAttribute("data-language", "javascript");
|
||||
|
|
@ -71,7 +77,9 @@ describe("CodeBlock", () => {
|
|||
let code = codeblock.querySelector("code");
|
||||
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");
|
||||
code = codeblock.querySelector("code");
|
||||
expect(code).toHaveAttribute("data-language", "bash");
|
||||
|
|
@ -215,8 +223,12 @@ describe("CodeBlock", () => {
|
|||
});
|
||||
|
||||
it("handles clipboard API errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
mockClipboard.writeText.mockRejectedValueOnce(new Error("Clipboard not available"));
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
mockClipboard.writeText.mockRejectedValueOnce(
|
||||
new Error("Clipboard not available"),
|
||||
);
|
||||
|
||||
try {
|
||||
render(<CodeBlock code="test code" />);
|
||||
|
|
@ -237,13 +249,17 @@ describe("CodeBlock", () => {
|
|||
|
||||
describe("HTML Attributes", () => {
|
||||
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");
|
||||
expect(codeblock).toHaveAttribute("id", "codeblock-id");
|
||||
});
|
||||
|
||||
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");
|
||||
expect(codeblock).toHaveAttribute("data-custom", "test");
|
||||
});
|
||||
|
|
@ -251,7 +267,13 @@ describe("CodeBlock", () => {
|
|||
|
||||
describe("Custom className", () => {
|
||||
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");
|
||||
expect(codeblock.className).toContain("custom-class");
|
||||
expect(codeblock.className).toContain("bg-background");
|
||||
|
|
@ -271,7 +293,7 @@ describe("CodeBlock", () => {
|
|||
/>,
|
||||
);
|
||||
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>(
|
||||
({ 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 handleCopy = async () => {
|
||||
|
|
@ -47,7 +57,11 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
|||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(codeBlockContainerVariants(), className)}
|
||||
{...props}
|
||||
>
|
||||
<code
|
||||
className={codeBlockCodeVariants({
|
||||
size,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
|
|||
/**
|
||||
* CodeBlock component props
|
||||
*/
|
||||
export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
export interface CodeBlockProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
/**
|
||||
* Code content to display
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ describe("Container", () => {
|
|||
</Container>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||
expect(ref?.tagName).toBe("DIV");
|
||||
expect(ref!.tagName).toBe("DIV");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -281,7 +281,13 @@ describe("GridContainer", () => {
|
|||
describe("Compound Scenarios", () => {
|
||||
it("works with multiple variants combined", () => {
|
||||
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
|
||||
</GridContainer>,
|
||||
);
|
||||
|
|
@ -294,7 +300,12 @@ describe("GridContainer", () => {
|
|||
|
||||
it("works with fixed columns and custom className", () => {
|
||||
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
|
||||
</GridContainer>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,11 @@ describe("Header", () => {
|
|||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -251,7 +255,7 @@ describe("Header", () => {
|
|||
/>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLElement);
|
||||
expect(ref?.tagName).toBe("HEADER");
|
||||
expect(ref!.tagName).toBe("HEADER");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -271,7 +275,15 @@ describe("Header", () => {
|
|||
|
||||
describe("Compound Scenarios", () => {
|
||||
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");
|
||||
expect(header.className).toContain("h-20");
|
||||
expect(header.className).toContain("px-8");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { cn } from "@tpmjs/utils/cn";
|
||||
import { forwardRef } from "react";
|
||||
import type { HeaderProps } from "./types";
|
||||
import { headerActionsVariants, headerTitleVariants, headerVariants } from "./variants";
|
||||
import {
|
||||
headerActionsVariants,
|
||||
headerTitleVariants,
|
||||
headerVariants,
|
||||
} from "./variants";
|
||||
|
||||
/**
|
||||
* Header component
|
||||
|
|
@ -27,7 +31,18 @@ import { headerActionsVariants, headerTitleVariants, headerVariants } from "./va
|
|||
* ```
|
||||
*/
|
||||
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 (
|
||||
<header
|
||||
className={cn(
|
||||
|
|
@ -51,7 +66,10 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
|
|||
</div>
|
||||
)}
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes, ReactNode } from "react";
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ describe("Icon", () => {
|
|||
render(<Icon icon="chevronDown" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
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", () => {
|
||||
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");
|
||||
expect(icon).toHaveAttribute("aria-label", "Success");
|
||||
});
|
||||
|
|
@ -169,7 +179,7 @@ describe("Icon", () => {
|
|||
/>,
|
||||
);
|
||||
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
|
||||
*/
|
||||
export interface IconProps extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
|
||||
export interface IconProps
|
||||
extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
|
||||
/**
|
||||
* Icon to display
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -196,7 +196,9 @@ describe("Input", () => {
|
|||
|
||||
describe("Value and onChange", () => {
|
||||
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;
|
||||
expect(input.value).toBe("test value");
|
||||
});
|
||||
|
|
@ -315,7 +317,7 @@ describe("Input", () => {
|
|||
/>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLInputElement);
|
||||
expect(ref?.tagName).toBe("INPUT");
|
||||
expect(ref!.tagName).toBe("INPUT");
|
||||
});
|
||||
|
||||
it("can focus input through ref", () => {
|
||||
|
|
@ -328,7 +330,7 @@ describe("Input", () => {
|
|||
data-testid="input"
|
||||
/>,
|
||||
);
|
||||
ref?.focus();
|
||||
ref!.focus();
|
||||
expect(screen.getByTestId("input")).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type { InputHTMLAttributes } from "react";
|
|||
/**
|
||||
* Input component props
|
||||
*/
|
||||
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||
export interface InputProps
|
||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||
/**
|
||||
* Visual state of the input
|
||||
* @default 'default'
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ describe("Label", () => {
|
|||
</Label>,
|
||||
);
|
||||
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>(
|
||||
({ className, size = "md", required = false, disabled = false, children, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
size = "md",
|
||||
required = false,
|
||||
disabled = false,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
// biome-ignore lint/a11y/noLabelWithoutControl: This is a generic label component that can be used with htmlFor or wrap inputs
|
||||
<label
|
||||
|
|
|
|||
|
|
@ -103,28 +103,36 @@ describe("ProgressBar", () => {
|
|||
|
||||
describe("Color Variants", () => {
|
||||
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 fill = progress.querySelector("div");
|
||||
expect(fill?.className).toContain("bg-primary");
|
||||
});
|
||||
|
||||
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 fill = progress.querySelector("div");
|
||||
expect(fill?.className).toContain("bg-success");
|
||||
});
|
||||
|
||||
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 fill = progress.querySelector("div");
|
||||
expect(fill?.className).toContain("bg-warning");
|
||||
});
|
||||
|
||||
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 fill = progress.querySelector("div");
|
||||
expect(fill?.className).toContain("bg-error");
|
||||
|
|
@ -175,13 +183,17 @@ describe("ProgressBar", () => {
|
|||
|
||||
describe("HTML Attributes", () => {
|
||||
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");
|
||||
expect(progress).toHaveAttribute("id", "progress-id");
|
||||
});
|
||||
|
||||
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");
|
||||
expect(progress).toHaveAttribute("data-custom", "test");
|
||||
});
|
||||
|
|
@ -189,7 +201,13 @@ describe("ProgressBar", () => {
|
|||
|
||||
describe("Custom className", () => {
|
||||
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");
|
||||
expect(progress.className).toContain("custom-class");
|
||||
expect(progress.className).toContain("bg-surface");
|
||||
|
|
@ -235,7 +253,14 @@ describe("ProgressBar", () => {
|
|||
|
||||
describe("Compound Scenarios", () => {
|
||||
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");
|
||||
expect(progress.className).toContain("h-3");
|
||||
const fill = progress.querySelector("div");
|
||||
|
|
|
|||
|
|
@ -26,7 +26,17 @@ import { progressBarFillVariants, progressBarTrackVariants } from "./variants";
|
|||
* ```
|
||||
*/
|
||||
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
|
||||
const clampedValue = Math.min(Math.max(value, 0), 100);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
|
|||
/**
|
||||
* ProgressBar component props
|
||||
*/
|
||||
export interface ProgressBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
export interface ProgressBarProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||
/**
|
||||
* Progress value (0-100)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ describe("Section", () => {
|
|||
</Section>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLElement);
|
||||
expect(ref?.tagName).toBe("SECTION");
|
||||
expect(ref!.tagName).toBe("SECTION");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -308,7 +308,13 @@ describe("Section", () => {
|
|||
describe("Compound Scenarios", () => {
|
||||
it("works with multiple variants combined", () => {
|
||||
render(
|
||||
<Section spacing="lg" background="blueprint" container="xl" centered data-testid="section">
|
||||
<Section
|
||||
spacing="lg"
|
||||
background="blueprint"
|
||||
container="xl"
|
||||
centered
|
||||
data-testid="section"
|
||||
>
|
||||
Content
|
||||
</Section>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ describe("Tabs", () => {
|
|||
it("renders tabs container", () => {
|
||||
const handleChange = vi.fn();
|
||||
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");
|
||||
expect(tabs).toBeInTheDocument();
|
||||
|
|
@ -24,7 +29,12 @@ describe("Tabs", () => {
|
|||
it("has tablist role", () => {
|
||||
const handleChange = vi.fn();
|
||||
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");
|
||||
expect(tabs).toHaveAttribute("role", "tablist");
|
||||
|
|
@ -32,7 +42,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("renders all tabs", () => {
|
||||
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-featured")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tab-recent")).toBeInTheDocument();
|
||||
|
|
@ -40,7 +52,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("renders tab labels", () => {
|
||||
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("Featured")).toBeInTheDocument();
|
||||
expect(screen.getByText("Recent")).toBeInTheDocument();
|
||||
|
|
@ -48,7 +62,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("renders tabs as buttons", () => {
|
||||
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");
|
||||
expect(tab.tagName).toBe("BUTTON");
|
||||
expect(tab).toHaveAttribute("type", "button");
|
||||
|
|
@ -58,15 +74,32 @@ describe("Tabs", () => {
|
|||
describe("Active State", () => {
|
||||
it("marks active tab with aria-selected", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Tabs tabs={mockTabs} 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");
|
||||
render(
|
||||
<Tabs
|
||||
tabs={mockTabs}
|
||||
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", () => {
|
||||
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");
|
||||
expect(activeTab.className).toContain("text-foreground");
|
||||
expect(activeTab.className).toContain("border-primary");
|
||||
|
|
@ -74,7 +107,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("applies inactive styling to inactive tabs", () => {
|
||||
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");
|
||||
expect(inactiveTab.className).toContain("text-foreground-secondary");
|
||||
expect(inactiveTab.className).toContain("border-transparent");
|
||||
|
|
@ -84,14 +119,18 @@ describe("Tabs", () => {
|
|||
describe("Click Handling", () => {
|
||||
it("calls onTabChange when tab is clicked", () => {
|
||||
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();
|
||||
expect(handleChange).toHaveBeenCalledWith("featured");
|
||||
});
|
||||
|
||||
it("calls onTabChange with correct tab ID", () => {
|
||||
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();
|
||||
expect(handleChange).toHaveBeenCalledWith("recent");
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -99,7 +138,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("allows clicking already active tab", () => {
|
||||
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();
|
||||
expect(handleChange).toHaveBeenCalledWith("all");
|
||||
});
|
||||
|
|
@ -108,14 +149,18 @@ describe("Tabs", () => {
|
|||
describe("Count Badges", () => {
|
||||
it("renders count badge when count is provided", () => {
|
||||
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("42")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render count badge when count is undefined", () => {
|
||||
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 badge = recentTab.querySelector("span[aria-label]");
|
||||
expect(badge).not.toBeInTheDocument();
|
||||
|
|
@ -123,21 +168,27 @@ describe("Tabs", () => {
|
|||
|
||||
it("count badge has aria-label", () => {
|
||||
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");
|
||||
expect(badge).toHaveAttribute("aria-label", "1234 items");
|
||||
});
|
||||
|
||||
it("applies active styling to count badge on active tab", () => {
|
||||
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");
|
||||
expect(activeBadge.className).toContain("text-foreground");
|
||||
});
|
||||
|
||||
it("applies inactive styling to count badge on inactive tab", () => {
|
||||
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");
|
||||
expect(inactiveBadge.className).toContain("text-foreground-tertiary");
|
||||
});
|
||||
|
|
@ -145,7 +196,13 @@ describe("Tabs", () => {
|
|||
it("handles zero count", () => {
|
||||
const handleChange = vi.fn();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -153,7 +210,14 @@ describe("Tabs", () => {
|
|||
describe("Size Variants", () => {
|
||||
it("applies small size", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("text-sm");
|
||||
expect(tab.className).toContain("px-3");
|
||||
|
|
@ -162,7 +226,14 @@ describe("Tabs", () => {
|
|||
|
||||
it("applies medium size", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("text-base");
|
||||
expect(tab.className).toContain("px-4");
|
||||
|
|
@ -171,7 +242,14 @@ describe("Tabs", () => {
|
|||
|
||||
it("applies large size", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("text-lg");
|
||||
expect(tab.className).toContain("px-6");
|
||||
|
|
@ -180,7 +258,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("uses md size by default", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("text-base");
|
||||
expect(tab.className).toContain("px-4");
|
||||
|
|
@ -191,23 +271,38 @@ describe("Tabs", () => {
|
|||
describe("Accessibility", () => {
|
||||
it('tabs have role="tab"', () => {
|
||||
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");
|
||||
expect(tab).toHaveAttribute("role", "tab");
|
||||
});
|
||||
|
||||
it("tabs have unique IDs", () => {
|
||||
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-featured")).toHaveAttribute("id", "tab-featured");
|
||||
expect(screen.getByTestId("tab-recent")).toHaveAttribute("id", "tab-recent");
|
||||
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
||||
"id",
|
||||
"tab-featured",
|
||||
);
|
||||
expect(screen.getByTestId("tab-recent")).toHaveAttribute(
|
||||
"id",
|
||||
"tab-recent",
|
||||
);
|
||||
});
|
||||
|
||||
it("tabs have aria-controls attribute", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
|
||||
expect(screen.getByTestId("tab-all")).toHaveAttribute("aria-controls", "tabpanel-all");
|
||||
render(
|
||||
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
|
||||
);
|
||||
expect(screen.getByTestId("tab-all")).toHaveAttribute(
|
||||
"aria-controls",
|
||||
"tabpanel-all",
|
||||
);
|
||||
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
|
||||
"aria-controls",
|
||||
"tabpanel-featured",
|
||||
|
|
@ -289,7 +384,12 @@ describe("Tabs", () => {
|
|||
it("container includes base classes", () => {
|
||||
const handleChange = vi.fn();
|
||||
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");
|
||||
expect(tabs.className).toContain("flex");
|
||||
|
|
@ -300,7 +400,9 @@ describe("Tabs", () => {
|
|||
|
||||
it("tab buttons include base classes", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("inline-flex");
|
||||
expect(tab.className).toContain("items-center");
|
||||
|
|
@ -314,7 +416,14 @@ describe("Tabs", () => {
|
|||
describe("Compound Scenarios", () => {
|
||||
it("works correctly with large size and active tab with count", () => {
|
||||
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");
|
||||
expect(tab.className).toContain("text-lg");
|
||||
expect(tab.className).toContain("px-6");
|
||||
|
|
@ -328,14 +437,18 @@ describe("Tabs", () => {
|
|||
it("handles single tab", () => {
|
||||
const handleChange = vi.fn();
|
||||
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.getByText("Only Tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles empty tabs array", () => {
|
||||
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");
|
||||
expect(buttons.length).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { cn } from "@tpmjs/utils/cn";
|
||||
import { forwardRef } from "react";
|
||||
import type { Tab, TabsProps } from "./types";
|
||||
import { tabButtonVariants, tabCountVariants, tabsContainerVariants } from "./variants";
|
||||
import {
|
||||
tabButtonVariants,
|
||||
tabCountVariants,
|
||||
tabsContainerVariants,
|
||||
} from "./variants";
|
||||
|
||||
// Re-export types for consumers
|
||||
export type { Tab, TabsProps };
|
||||
|
|
@ -36,7 +40,15 @@ export type { Tab, 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,
|
||||
) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export interface Tab {
|
|||
/**
|
||||
* Tabs component props
|
||||
*/
|
||||
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||
export interface TabsProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export type VariantProps<T extends Record<string, Record<string, string>>> = Partial<{
|
||||
[K in keyof T]: keyof T[K];
|
||||
}>;
|
||||
export type VariantProps<T extends Record<string, Record<string, string>>> =
|
||||
Partial<{
|
||||
[K in keyof T]: keyof T[K];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns Function that accepts variant props and returns composed className string
|
||||
*/
|
||||
export function createVariants<T extends Record<string, Record<string, string>>>(
|
||||
config: VariantConfig<T>,
|
||||
) {
|
||||
export function createVariants<
|
||||
T extends Record<string, Record<string, string>>,
|
||||
>(config: VariantConfig<T>) {
|
||||
return function getVariantClasses(props?: VariantProps<T>): string {
|
||||
const classes: string[] = [config.base];
|
||||
|
||||
|
|
@ -82,7 +83,11 @@ export function createVariants<T extends Record<string, Record<string, string>>>
|
|||
// Apply variant classes
|
||||
for (const [key, value] of Object.entries(mergedProps)) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +96,8 @@ export function createVariants<T extends Record<string, Record<string, string>>>
|
|||
if (config.compoundVariants) {
|
||||
for (const { conditions, className } of config.compoundVariants) {
|
||||
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) {
|
||||
classes.push(className);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue