From 183ca100dcb8e38cc7f549bfd91a6da4d804d594 Mon Sep 17 00:00:00 2001 From: Hafid Nur Date: Sun, 16 Mar 2025 00:56:02 +0700 Subject: [PATCH] feat: add Input component --- libs/ui/src/atoms/index.ts | 1 + libs/ui/src/atoms/input/index.ts | 1 + libs/ui/src/atoms/input/input.spec.tsx | 36 ++++++++++++ libs/ui/src/atoms/input/input.stories.tsx | 71 +++++++++++++++++++++++ libs/ui/src/atoms/input/input.tsx | 60 +++++++++++++++++++ 5 files changed, 169 insertions(+) create mode 100644 libs/ui/src/atoms/input/index.ts create mode 100644 libs/ui/src/atoms/input/input.spec.tsx create mode 100644 libs/ui/src/atoms/input/input.stories.tsx create mode 100644 libs/ui/src/atoms/input/input.tsx diff --git a/libs/ui/src/atoms/index.ts b/libs/ui/src/atoms/index.ts index eaf5eea..55d2082 100644 --- a/libs/ui/src/atoms/index.ts +++ b/libs/ui/src/atoms/index.ts @@ -1 +1,2 @@ export * from './button'; +export * from './input'; diff --git a/libs/ui/src/atoms/input/index.ts b/libs/ui/src/atoms/input/index.ts new file mode 100644 index 0000000..4ce4a88 --- /dev/null +++ b/libs/ui/src/atoms/input/index.ts @@ -0,0 +1 @@ +export * from "./input"; diff --git a/libs/ui/src/atoms/input/input.spec.tsx b/libs/ui/src/atoms/input/input.spec.tsx new file mode 100644 index 0000000..4fdf16b --- /dev/null +++ b/libs/ui/src/atoms/input/input.spec.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Input } from "./input"; + +describe("Test Input Component", () => { + it("renders the input with placeholder text", () => { + render(); + expect(screen.getByPlaceholderText("Placeholder")).toBeInTheDocument(); + }); + + // it("renders the input with different sizes", () => { + // const sizeClasses = { + // sm: "text-[10px] max-h-[28px]", + // md: "text-[12px] max-h-[30px]", + // lg: "text-[15px] max-h-[34px]", + // }; + // Object.entries(sizeClasses).forEach(([size, className]) => { + // render(); + // expect(screen.getByRole("textbox")).toHaveClass(className); + // }); + // }); + + // it("renders the input with different types", () => { + // const types = ["text", "password", "email", "number"]; + // types.forEach((type) => { + // render(); + // const input = screen.getByRole("textbox"); + // expect(input).toHaveAttribute("type", type); + // }); + // }); + + it("disables the input field when 'disabled' prop is set", () => { + render(); + expect(screen.getByRole("textbox")).toBeDisabled(); + }); +}); diff --git a/libs/ui/src/atoms/input/input.stories.tsx b/libs/ui/src/atoms/input/input.stories.tsx new file mode 100644 index 0000000..7bea8f5 --- /dev/null +++ b/libs/ui/src/atoms/input/input.stories.tsx @@ -0,0 +1,71 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Input } from './input'; + +const meta: Meta = { + title: 'Components/Input', + component: Input, + argTypes: { + type: { + control: 'select', + options: ['text', 'email'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Large: Story = { + args: { + size: 'lg', + placeholder: 'Placeholder', + }, +}; + +export const Medium: Story = { + args: { + size: 'md', + placeholder: 'Placeholder', + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + placeholder: 'Placeholder', + }, +}; + +export const TextInput: Story = { + args: { + size: 'md', + type: 'text', + }, +}; + +export const EmailInput: Story = { + args: { + size: 'md', + type: 'email', + }, +}; + +export const Disabled: Story = { + args: { + size: 'md', + type: 'text', + disabled: true, + }, +}; + +export const WithError: Story = { + args: { + size: 'md', + type: 'text', + error: 'This field is required', + }, +}; diff --git a/libs/ui/src/atoms/input/input.tsx b/libs/ui/src/atoms/input/input.tsx new file mode 100644 index 0000000..3a80d49 --- /dev/null +++ b/libs/ui/src/atoms/input/input.tsx @@ -0,0 +1,60 @@ +import { + DetailedHTMLProps, + FC, + InputHTMLAttributes, + ReactElement, +} from 'react'; +import { cn } from '@imphnen-frontend-service/utils'; + +type TInputType = 'text' | 'email'; +type TInputSize = 'sm' | 'md' | 'lg'; + +type TInputProps = Omit< + DetailedHTMLProps, HTMLInputElement>, + 'size' | 'type' +> & { + type?: TInputType; + size?: TInputSize; + error?: string; +}; + +const sizeClasses: Record = { + sm: 'text-[10px] max-h-[28px]', + md: 'text-[12px] max-h-[30px]', + lg: 'text-[15px] max-h-[34px]', +}; + +const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed'; +const errorClass = + 'border-danger-500 hover:border-danger-500 focus:outline-danger-500'; + +export const Input: FC = ({ + size = 'md', + type, + placeholder = 'Placeholder', + disabled, + error, + className, + ...rest +}): ReactElement => { + const mergedClassName = cn( + 'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree', + sizeClasses[size], + disabled && disabledClass, + error && errorClass, + className + ); + + return ( + <> + + {error &&

{error}

} + + ); +};