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:
Ajax Davis 2025-11-26 02:10:18 +10:00
parent 9a8c0beb85
commit b17349c071
5 changed files with 254 additions and 392 deletions

View 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>
),
};

View file

@ -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");

View file

@ -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}
/>
);
},
);

View file

@ -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");

View file

@ -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>
);
},
);