diff --git a/src/app/page.tsx b/src/app/page.tsx
index 37bfb2c..14e1e2a 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,17 +1,21 @@
-import { Button } from "@/components/Button/Default/Button";
-import { ArrowLeft } from "@phosphor-icons/react";
+"use client";
+
+import { IconPhosphor } from "@/components/Icon/Icon";
+import { Input } from "@/components/Input/Default/Input";
export default function Home() {
return (
- }
- iconPosition="start"
- onlyIcon
+ }
+ status="success"
+ rightComponent={
+
+ }
/>
);
diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx
index 82eada0..116f18d 100644
--- a/src/components/Icon/Icon.tsx
+++ b/src/components/Icon/Icon.tsx
@@ -4,16 +4,20 @@ import { IconProps } from "@phosphor-icons/react";
export type IconPhosphorName = keyof typeof PhosphorIcons;
-type IconPhosphorProps = {
+export type IconPhosphorProps = {
name: IconPhosphorName;
size?: number;
color?: string;
+ className?: string;
+ weight?: "fill" | "light" | "thin" | "regular" | "bold";
};
export const IconPhosphor: React.FC = ({
name,
size = 24,
color = "currentColor",
+ className,
+ weight,
}) => {
const SpecificIcon = PhosphorIcons[name] as React.ElementType;
@@ -21,5 +25,12 @@ export const IconPhosphor: React.FC = ({
return null;
}
- return ;
+ return (
+
+ );
};
diff --git a/src/components/Input/Default/Input.stories.tsx b/src/components/Input/Default/Input.stories.tsx
new file mode 100644
index 0000000..2e9c1a1
--- /dev/null
+++ b/src/components/Input/Default/Input.stories.tsx
@@ -0,0 +1,34 @@
+import { Meta, StoryObj } from "@storybook/react";
+import { Input } from "./Input";
+import { IconPhosphor } from "@/components/Icon/Icon";
+
+const meta: Meta = {
+ title: "Components/Input",
+ component: Input,
+ argTypes: {
+ type: {
+ control: "select",
+ options: ["text", "password", "number"],
+ },
+ status: {
+ control: "select",
+ options: ["success", "error", undefined],
+ },
+
+ placeholder: {
+ control: "text",
+ },
+ },
+ args: {
+ type: "text",
+ status: undefined,
+
+ icon: ,
+ rightComponent: ,
+
+ placeholder: "Placeholder",
+ },
+};
+export default meta;
+type Story = StoryObj;
+export const Default: Story = {};
diff --git a/src/components/Input/Default/Input.test.tsx b/src/components/Input/Default/Input.test.tsx
new file mode 100644
index 0000000..385fa11
--- /dev/null
+++ b/src/components/Input/Default/Input.test.tsx
@@ -0,0 +1,62 @@
+import { cleanup, render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, afterEach } from "vitest";
+import "@testing-library/jest-dom/vitest";
+import { Input } from "./Input";
+
+const makeSut = (props: React.ComponentProps) => {
+ render();
+};
+
+describe("Input", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders the input with default props", () => {
+ makeSut({ placeholder: "Type something..." });
+ const input = screen.getByPlaceholderText("Type something...");
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveAttribute("type", "text");
+ });
+
+ it("renders the input with an icon passed via prop", () => {
+ const TestIcon = () => Icon;
+ makeSut({ placeholder: "Type something...", icon: });
+ const icon = screen.getByText("Icon");
+ expect(icon).toBeInTheDocument();
+ });
+
+ it("renders the 'success' status icon when status is set to 'success'", () => {
+ makeSut({ placeholder: "Type something...", status: "success" });
+ const container =
+ screen.getByPlaceholderText("Type something...").parentElement;
+ expect(container?.querySelector(".text-success-500")).toBeInTheDocument();
+ });
+
+ it("renders the 'error' status icon when status is set to 'error'", () => {
+ makeSut({ placeholder: "Type something...", status: "error" });
+ const container =
+ screen.getByPlaceholderText("Type something...").parentElement;
+ expect(container?.querySelector(".text-danger-500")).toBeInTheDocument();
+ });
+
+ it("displays the 'rigthComponent' when the input is focused", () => {
+ const RightComponent = () => Right;
+ makeSut({
+ placeholder: "Type something...",
+ rightComponent: ,
+ });
+ const input = screen.getByPlaceholderText("Type something...");
+ expect(screen.queryByText("Right")).not.toBeInTheDocument();
+ fireEvent.focus(input);
+ expect(screen.getByText("Right")).toBeInTheDocument();
+ });
+
+ it("focuses the input when clicking on the container", () => {
+ makeSut({ placeholder: "Type something..." });
+ const input = screen.getByPlaceholderText("Type something...");
+ const container = input.parentElement;
+ fireEvent.click(container!);
+ expect(input).toHaveFocus();
+ });
+});
diff --git a/src/components/Input/Default/Input.tsx b/src/components/Input/Default/Input.tsx
new file mode 100644
index 0000000..bad45cb
--- /dev/null
+++ b/src/components/Input/Default/Input.tsx
@@ -0,0 +1,72 @@
+import { IconPhosphor } from "@/components/Icon/Icon";
+import { cn } from "@/lib/utils";
+import * as React from "react";
+import { InputProps } from "./Input.types";
+
+const Input = React.forwardRef(
+ (
+ { className, rightComponent, type = "text", icon, status, ...props },
+ ref
+ ) => {
+ const [focused, setFocused] = React.useState(false);
+
+ const renderStatusIcon = () => {
+ if (status === "success") {
+ return (
+
+ );
+ }
+ if (status === "error") {
+ return (
+
+ );
+ }
+ return null;
+ };
+
+ return (
+ {
+ const input = e.currentTarget.querySelector("input");
+ input?.focus();
+ }}
+ >
+ {icon}
+
+ setFocused(true)}
+ onBlur={() => setFocused(false)}
+ className={cn(
+ "flex-1 outline-none bg-transparent text-gray-500 placeholder:text-body-lg-400",
+ className
+ )}
+ {...props}
+ />
+ {focused && rightComponent ? rightComponent : renderStatusIcon()}
+
+ );
+ }
+);
+
+Input.displayName = "Input";
+
+export { Input };
diff --git a/src/components/Input/Default/Input.types.ts b/src/components/Input/Default/Input.types.ts
new file mode 100644
index 0000000..50f51a4
--- /dev/null
+++ b/src/components/Input/Default/Input.types.ts
@@ -0,0 +1,6 @@
+export type Status = "success" | "error" | undefined;
+export interface InputProps extends React.ComponentProps<"input"> {
+ icon?: React.ReactNode;
+ rightComponent?: React.ReactNode;
+ status?: Status;
+}