refactor(ui): convert Icon and Badge components to JSX syntax
- Convert Icon component from createElement to JSX (Icon.tsx) - Convert Icon tests to JSX (24 tests passing) - Convert Icon stories to JSX (Icon.stories.tsx) - Convert Badge component from createElement to JSX (Badge.tsx) - Convert Badge tests to JSX (34 tests passing) - Rename files from .ts to .tsx - Remove createElement imports, use native JSX syntax - All tests passing (58/58) Part of broader effort to convert entire codebase from createElement to JSX. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9a8c0beb85
commit
b17349c071
5 changed files with 254 additions and 392 deletions
62
packages/storybook/stories/Icon.stories.tsx
Normal file
62
packages/storybook/stories/Icon.stories.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Icon } from "@tpmjs/ui/Icon/Icon";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Icon",
|
||||
component: Icon,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Icon>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const AllIcons: Story = {
|
||||
render: () => (
|
||||
<div className="grid grid-cols-4 gap-8 p-8">
|
||||
{(
|
||||
[
|
||||
"copy",
|
||||
"externalLink",
|
||||
"github",
|
||||
"check",
|
||||
"x",
|
||||
"chevronDown",
|
||||
"sun",
|
||||
"moon",
|
||||
] as const
|
||||
).map((iconName) => (
|
||||
<div key={iconName} className="flex flex-col items-center gap-2">
|
||||
<Icon icon={iconName} size="lg" />
|
||||
<span className="text-sm text-foreground-secondary">{iconName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-6">
|
||||
{(["sm", "md", "lg"] as const).map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Icon icon="github" size={size} />
|
||||
<span className="text-sm text-foreground-secondary">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithColors: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-6">
|
||||
<Icon icon="check" size="lg" className="text-success" />
|
||||
<Icon icon="x" size="lg" className="text-error" />
|
||||
<Icon icon="github" size="lg" className="text-info" />
|
||||
<Icon icon="sun" size="lg" className="text-warning" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { createElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
describe("Badge", () => {
|
||||
describe("Rendering", () => {
|
||||
it("renders a badge element", () => {
|
||||
render(createElement(Badge, { "data-testid": "badge" }, "Badge"));
|
||||
render(<Badge data-testid="badge">Badge</Badge>);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
it("renders children text", () => {
|
||||
render(createElement(Badge, null, "Active"));
|
||||
render(<Badge>Active</Badge>);
|
||||
expect(screen.getByText("Active")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -21,14 +20,9 @@ describe("Badge", () => {
|
|||
describe("Variants", () => {
|
||||
it("applies default variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "default",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Default",
|
||||
),
|
||||
<Badge variant="default" data-testid="badge">
|
||||
Default
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-primary");
|
||||
|
|
@ -38,14 +32,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies secondary variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "secondary",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Secondary",
|
||||
),
|
||||
<Badge variant="secondary" data-testid="badge">
|
||||
Secondary
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-secondary");
|
||||
|
|
@ -54,14 +43,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies outline variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "outline",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Outline",
|
||||
),
|
||||
<Badge variant="outline" data-testid="badge">
|
||||
Outline
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-transparent");
|
||||
|
|
@ -71,14 +55,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies success variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "success",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Success",
|
||||
),
|
||||
<Badge variant="success" data-testid="badge">
|
||||
Success
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-success");
|
||||
|
|
@ -87,14 +66,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies error variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "error",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Error",
|
||||
),
|
||||
<Badge variant="error" data-testid="badge">
|
||||
Error
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-error");
|
||||
|
|
@ -103,14 +77,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies warning variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "warning",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Warning",
|
||||
),
|
||||
<Badge variant="warning" data-testid="badge">
|
||||
Warning
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-warning");
|
||||
|
|
@ -119,14 +88,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies info variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "info",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Info",
|
||||
),
|
||||
<Badge variant="info" data-testid="badge">
|
||||
Info
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-info");
|
||||
|
|
@ -137,14 +101,9 @@ describe("Badge", () => {
|
|||
describe("Sizes", () => {
|
||||
it("applies small size classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
size: "sm",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Small",
|
||||
),
|
||||
<Badge size="sm" data-testid="badge">
|
||||
Small
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("px-2");
|
||||
|
|
@ -154,14 +113,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies medium size classes (default)", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
size: "md",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Medium",
|
||||
),
|
||||
<Badge size="md" data-testid="badge">
|
||||
Medium
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("px-2.5");
|
||||
|
|
@ -171,14 +125,9 @@ describe("Badge", () => {
|
|||
|
||||
it("applies large size classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
size: "lg",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Large",
|
||||
),
|
||||
<Badge size="lg" data-testid="badge">
|
||||
Large
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("px-3");
|
||||
|
|
@ -187,15 +136,7 @@ describe("Badge", () => {
|
|||
});
|
||||
|
||||
it("uses medium size by default", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Default",
|
||||
),
|
||||
);
|
||||
render(<Badge data-testid="badge">Default</Badge>);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("text-sm");
|
||||
});
|
||||
|
|
@ -204,14 +145,9 @@ describe("Badge", () => {
|
|||
describe("HTML Attributes", () => {
|
||||
it("passes through id attribute", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
id: "badge-id",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
<Badge id="badge-id" data-testid="badge">
|
||||
Badge
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("id", "badge-id");
|
||||
|
|
@ -219,14 +155,9 @@ describe("Badge", () => {
|
|||
|
||||
it("passes through title attribute", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
title: "Badge tooltip",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
<Badge title="Badge tooltip" data-testid="badge">
|
||||
Badge
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("title", "Badge tooltip");
|
||||
|
|
@ -235,16 +166,14 @@ describe("Badge", () => {
|
|||
it("passes through onClick handler", () => {
|
||||
let clicked = false;
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
onClick: () => {
|
||||
clicked = true;
|
||||
},
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Clickable",
|
||||
),
|
||||
<Badge
|
||||
onClick={() => {
|
||||
clicked = true;
|
||||
}}
|
||||
data-testid="badge"
|
||||
>
|
||||
Clickable
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
badge.click();
|
||||
|
|
@ -253,14 +182,9 @@ describe("Badge", () => {
|
|||
|
||||
it("passes through role attribute", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
role: "status",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Status",
|
||||
),
|
||||
<Badge role="status" data-testid="badge">
|
||||
Status
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("role", "status");
|
||||
|
|
@ -270,14 +194,9 @@ describe("Badge", () => {
|
|||
describe("ARIA Attributes", () => {
|
||||
it("passes through aria-label", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"aria-label": "Status badge",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Active",
|
||||
),
|
||||
<Badge aria-label="Status badge" data-testid="badge">
|
||||
Active
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("aria-label", "Status badge");
|
||||
|
|
@ -285,14 +204,9 @@ describe("Badge", () => {
|
|||
|
||||
it("passes through aria-describedby", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"aria-describedby": "description-id",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
<Badge aria-describedby="description-id" data-testid="badge">
|
||||
Badge
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("aria-describedby", "description-id");
|
||||
|
|
@ -300,14 +214,9 @@ describe("Badge", () => {
|
|||
|
||||
it("passes through aria-live for dynamic status", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"aria-live": "polite",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Updating",
|
||||
),
|
||||
<Badge aria-live="polite" data-testid="badge">
|
||||
Updating
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("aria-live", "polite");
|
||||
|
|
@ -317,14 +226,9 @@ describe("Badge", () => {
|
|||
describe("Custom className", () => {
|
||||
it("merges custom className with variant classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
className: "custom-badge",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Custom",
|
||||
),
|
||||
<Badge className="custom-badge" data-testid="badge">
|
||||
Custom
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("custom-badge");
|
||||
|
|
@ -337,15 +241,13 @@ describe("Badge", () => {
|
|||
it("forwards ref to badge element", () => {
|
||||
let ref: HTMLDivElement | null = null;
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
ref: (el: HTMLDivElement | null) => {
|
||||
ref = el;
|
||||
},
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
<Badge
|
||||
ref={(el: HTMLDivElement | null) => {
|
||||
ref = el;
|
||||
}}
|
||||
>
|
||||
Badge
|
||||
</Badge>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(HTMLDivElement);
|
||||
expect(ref?.tagName).toBe("DIV");
|
||||
|
|
@ -354,15 +256,7 @@ describe("Badge", () => {
|
|||
|
||||
describe("Base Classes", () => {
|
||||
it("always includes base classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
);
|
||||
render(<Badge data-testid="badge">Badge</Badge>);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("inline-flex");
|
||||
expect(badge.className).toContain("items-center");
|
||||
|
|
@ -372,15 +266,7 @@ describe("Badge", () => {
|
|||
});
|
||||
|
||||
it("includes transition classes", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Badge",
|
||||
),
|
||||
);
|
||||
render(<Badge data-testid="badge">Badge</Badge>);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("transition-base");
|
||||
});
|
||||
|
|
@ -389,15 +275,9 @@ describe("Badge", () => {
|
|||
describe("Compound Scenarios", () => {
|
||||
it("works correctly with success variant and small size", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "success",
|
||||
size: "sm",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Success",
|
||||
),
|
||||
<Badge variant="success" size="sm" data-testid="badge">
|
||||
Success
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-success");
|
||||
|
|
@ -406,15 +286,9 @@ describe("Badge", () => {
|
|||
|
||||
it("works correctly with error variant and large size", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "error",
|
||||
size: "lg",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Error",
|
||||
),
|
||||
<Badge variant="error" size="lg" data-testid="badge">
|
||||
Error
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-error");
|
||||
|
|
@ -423,15 +297,13 @@ describe("Badge", () => {
|
|||
|
||||
it("works correctly with outline variant and custom className", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "outline",
|
||||
className: "hover:bg-accent",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Outlined",
|
||||
),
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="hover:bg-accent"
|
||||
data-testid="badge"
|
||||
>
|
||||
Outlined
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-transparent");
|
||||
|
|
@ -441,17 +313,15 @@ describe("Badge", () => {
|
|||
it("works correctly with warning variant and onClick", () => {
|
||||
let clicked = false;
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "warning",
|
||||
onClick: () => {
|
||||
clicked = true;
|
||||
},
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Warning",
|
||||
),
|
||||
<Badge
|
||||
variant="warning"
|
||||
onClick={() => {
|
||||
clicked = true;
|
||||
}}
|
||||
data-testid="badge"
|
||||
>
|
||||
Warning
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-warning");
|
||||
|
|
@ -462,23 +332,20 @@ describe("Badge", () => {
|
|||
|
||||
describe("Content Types", () => {
|
||||
it("renders with text content", () => {
|
||||
render(createElement(Badge, null, "Text"));
|
||||
render(<Badge>Text</Badge>);
|
||||
expect(screen.getByText("Text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with numeric content", () => {
|
||||
render(createElement(Badge, null, "42"));
|
||||
render(<Badge>42</Badge>);
|
||||
expect(screen.getByText("42")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with multiple children", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
null,
|
||||
"Count: ",
|
||||
createElement("strong", null, "5"),
|
||||
),
|
||||
<Badge>
|
||||
Count: <strong>5</strong>
|
||||
</Badge>,
|
||||
);
|
||||
expect(screen.getByText("Count:")).toBeInTheDocument();
|
||||
expect(screen.getByText("5")).toBeInTheDocument();
|
||||
|
|
@ -488,16 +355,14 @@ describe("Badge", () => {
|
|||
describe("Semantic Usage", () => {
|
||||
it("can be used as status indicator with role", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "success",
|
||||
role: "status",
|
||||
"aria-label": "Online status",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"Online",
|
||||
),
|
||||
<Badge
|
||||
variant="success"
|
||||
role="status"
|
||||
aria-label="Online status"
|
||||
data-testid="badge"
|
||||
>
|
||||
Online
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("role", "status");
|
||||
|
|
@ -507,16 +372,14 @@ describe("Badge", () => {
|
|||
|
||||
it("can be used as notification count", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "error",
|
||||
size: "sm",
|
||||
"aria-label": "3 unread messages",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"3",
|
||||
),
|
||||
<Badge
|
||||
variant="error"
|
||||
size="sm"
|
||||
aria-label="3 unread messages"
|
||||
data-testid="badge"
|
||||
>
|
||||
3
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge).toHaveAttribute("aria-label", "3 unread messages");
|
||||
|
|
@ -525,14 +388,9 @@ describe("Badge", () => {
|
|||
|
||||
it("can be used as tag/label", () => {
|
||||
render(
|
||||
createElement(
|
||||
Badge,
|
||||
{
|
||||
variant: "outline",
|
||||
"data-testid": "badge",
|
||||
},
|
||||
"TypeScript",
|
||||
),
|
||||
<Badge variant="outline" data-testid="badge">
|
||||
TypeScript
|
||||
</Badge>,
|
||||
);
|
||||
const badge = screen.getByTestId("badge");
|
||||
expect(badge.className).toContain("bg-transparent");
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@tpmjs/utils/cn";
|
||||
import { createElement, forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import type { BadgeProps } from "./types";
|
||||
import { badgeVariants } from "./variants";
|
||||
|
||||
|
|
@ -7,34 +7,31 @@ import { badgeVariants } from "./variants";
|
|||
* Badge component
|
||||
*
|
||||
* A versatile badge component for status indicators, tags, and labels.
|
||||
* Built with .ts-only React using createElement.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ```tsx
|
||||
* import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
* import { createElement } from 'react';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* return createElement(Badge, {
|
||||
* variant: 'success',
|
||||
* children: 'Active',
|
||||
* });
|
||||
* return <Badge variant="success">Active</Badge>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ className, variant = "default", size = "md", ...props }, ref) => {
|
||||
return createElement("div", {
|
||||
className: cn(
|
||||
badgeVariants({
|
||||
variant,
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
),
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
badgeVariants({
|
||||
variant,
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1,38 +1,37 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { createElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
describe("Icon", () => {
|
||||
describe("Rendering", () => {
|
||||
it("renders an SVG element", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon.tagName).toBe("svg");
|
||||
});
|
||||
|
||||
it("renders with correct viewBox", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("viewBox", "0 0 24 24");
|
||||
});
|
||||
|
||||
it("renders with currentColor fill", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("fill", "currentColor");
|
||||
});
|
||||
|
||||
it("includes path element", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("is aria-hidden by default", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
|
@ -40,46 +39,28 @@ describe("Icon", () => {
|
|||
|
||||
describe("Size Variants", () => {
|
||||
it("applies small size", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
size: "sm",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" size="sm" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-4");
|
||||
expect(icon.className).toContain("h-4");
|
||||
});
|
||||
|
||||
it("applies medium size", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
size: "md",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" size="md" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-5");
|
||||
expect(icon.className).toContain("h-5");
|
||||
});
|
||||
|
||||
it("applies large size", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
size: "lg",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" size="lg" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-6");
|
||||
expect(icon.className).toContain("h-6");
|
||||
});
|
||||
|
||||
it("uses md size by default", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-5");
|
||||
expect(icon.className).toContain("h-5");
|
||||
|
|
@ -88,7 +69,7 @@ describe("Icon", () => {
|
|||
|
||||
describe("Icon Variants", () => {
|
||||
it("renders copy icon", () => {
|
||||
render(createElement(Icon, { icon: "copy", "data-testid": "icon" }));
|
||||
render(<Icon icon="copy" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toHaveAttribute(
|
||||
|
|
@ -98,9 +79,7 @@ describe("Icon", () => {
|
|||
});
|
||||
|
||||
it("renders externalLink icon", () => {
|
||||
render(
|
||||
createElement(Icon, { icon: "externalLink", "data-testid": "icon" }),
|
||||
);
|
||||
render(<Icon icon="externalLink" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toHaveAttribute(
|
||||
|
|
@ -110,14 +89,14 @@ describe("Icon", () => {
|
|||
});
|
||||
|
||||
it("renders github icon", () => {
|
||||
render(createElement(Icon, { icon: "github", "data-testid": "icon" }));
|
||||
render(<Icon icon="github" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path?.getAttribute("d")).toContain("M12 2C6.477 2 2 6.477 2 12");
|
||||
});
|
||||
|
||||
it("renders check icon", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toHaveAttribute(
|
||||
|
|
@ -127,7 +106,7 @@ describe("Icon", () => {
|
|||
});
|
||||
|
||||
it("renders x icon", () => {
|
||||
render(createElement(Icon, { icon: "x", "data-testid": "icon" }));
|
||||
render(<Icon icon="x" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toHaveAttribute(
|
||||
|
|
@ -137,9 +116,7 @@ describe("Icon", () => {
|
|||
});
|
||||
|
||||
it("renders chevronDown icon", () => {
|
||||
render(
|
||||
createElement(Icon, { icon: "chevronDown", "data-testid": "icon" }),
|
||||
);
|
||||
render(<Icon icon="chevronDown" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
const path = icon.querySelector("path");
|
||||
expect(path).toHaveAttribute(
|
||||
|
|
@ -151,25 +128,13 @@ describe("Icon", () => {
|
|||
|
||||
describe("SVG Attributes", () => {
|
||||
it("passes through className", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
className: "custom-class",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" className="custom-class" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("custom-class");
|
||||
});
|
||||
|
||||
it("merges custom className with variant classes", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
className: "text-red-500",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" className="text-red-500" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("text-red-500");
|
||||
expect(icon.className).toContain("inline-block");
|
||||
|
|
@ -177,38 +142,26 @@ describe("Icon", () => {
|
|||
});
|
||||
|
||||
it("allows custom aria-hidden", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
"aria-hidden": false,
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" aria-hidden={false} data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("aria-hidden", "false");
|
||||
});
|
||||
|
||||
it("passes through aria-label", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
"aria-label": "Success",
|
||||
"aria-hidden": false,
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
<Icon
|
||||
icon="check"
|
||||
aria-label="Success"
|
||||
aria-hidden={false}
|
||||
data-testid="icon"
|
||||
/>,
|
||||
);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("aria-label", "Success");
|
||||
});
|
||||
|
||||
it("passes through id attribute", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
id: "icon-id",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
);
|
||||
render(<Icon icon="check" id="icon-id" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon).toHaveAttribute("id", "icon-id");
|
||||
});
|
||||
|
|
@ -218,12 +171,12 @@ describe("Icon", () => {
|
|||
it("forwards ref to SVG element", () => {
|
||||
let ref: SVGSVGElement | null = null;
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "check",
|
||||
ref: (el: SVGSVGElement | null) => {
|
||||
<Icon
|
||||
icon="check"
|
||||
ref={(el: SVGSVGElement | null) => {
|
||||
ref = el;
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(ref).toBeInstanceOf(SVGSVGElement);
|
||||
expect(ref?.tagName).toBe("svg");
|
||||
|
|
@ -232,7 +185,7 @@ describe("Icon", () => {
|
|||
|
||||
describe("Base Classes", () => {
|
||||
it("always includes base classes", () => {
|
||||
render(createElement(Icon, { icon: "check", "data-testid": "icon" }));
|
||||
render(<Icon icon="check" data-testid="icon" />);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("inline-block");
|
||||
expect(icon.className).toContain("fill-current");
|
||||
|
|
@ -242,12 +195,12 @@ describe("Icon", () => {
|
|||
describe("Compound Scenarios", () => {
|
||||
it("works correctly with custom size and className", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "github",
|
||||
size: "lg",
|
||||
className: "text-zinc-400 hover:text-zinc-100",
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
<Icon
|
||||
icon="github"
|
||||
size="lg"
|
||||
className="text-zinc-400 hover:text-zinc-100"
|
||||
data-testid="icon"
|
||||
/>,
|
||||
);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-6");
|
||||
|
|
@ -258,13 +211,13 @@ describe("Icon", () => {
|
|||
|
||||
it("works correctly with aria attributes and custom size", () => {
|
||||
render(
|
||||
createElement(Icon, {
|
||||
icon: "externalLink",
|
||||
size: "sm",
|
||||
"aria-label": "Opens in new tab",
|
||||
"aria-hidden": false,
|
||||
"data-testid": "icon",
|
||||
}),
|
||||
<Icon
|
||||
icon="externalLink"
|
||||
size="sm"
|
||||
aria-label="Opens in new tab"
|
||||
aria-hidden={false}
|
||||
data-testid="icon"
|
||||
/>,
|
||||
);
|
||||
const icon = screen.getByTestId("icon");
|
||||
expect(icon.className).toContain("w-4");
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@tpmjs/utils/cn";
|
||||
import { createElement, forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { type IconName, icons } from "./icons";
|
||||
import type { IconProps } from "./types";
|
||||
import { iconVariants } from "./variants";
|
||||
|
|
@ -11,19 +11,13 @@ export type { IconName };
|
|||
*
|
||||
* Renders SVG icons with consistent sizing and styling.
|
||||
* Icons inherit text color via currentColor.
|
||||
* Built with .ts-only React using createElement.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ```tsx
|
||||
* import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
* import { createElement } from 'react';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* return createElement(Icon, {
|
||||
* icon: 'github',
|
||||
* size: 'md',
|
||||
* className: 'text-zinc-400',
|
||||
* });
|
||||
* return <Icon icon="github" size="md" className="text-zinc-400" />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
|
@ -31,24 +25,22 @@ export const Icon = forwardRef<SVGSVGElement, IconProps>(
|
|||
({ className, icon, size = "md", ...props }, ref) => {
|
||||
const iconData = icons[icon];
|
||||
|
||||
return createElement(
|
||||
"svg",
|
||||
{
|
||||
className: cn(
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
className={cn(
|
||||
iconVariants({
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
),
|
||||
viewBox: iconData.viewBox,
|
||||
fill: "currentColor",
|
||||
"aria-hidden": props["aria-hidden"] ?? true,
|
||||
ref,
|
||||
...props,
|
||||
},
|
||||
createElement("path", {
|
||||
d: iconData.path,
|
||||
}),
|
||||
)}
|
||||
viewBox={iconData.viewBox}
|
||||
fill="currentColor"
|
||||
aria-hidden={props["aria-hidden"] ?? true}
|
||||
{...props}
|
||||
>
|
||||
<path d={iconData.path} />
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue