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 (
-
); 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; +}