diff --git a/src/shared/images/eye-off.svg b/src/shared/images/eye-off.svg new file mode 100644 index 00000000..faa01f29 --- /dev/null +++ b/src/shared/images/eye-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/images/eye.svg b/src/shared/images/eye.svg new file mode 100644 index 00000000..b2d650c8 --- /dev/null +++ b/src/shared/images/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/ui/TextField/TextField.stories.ts b/src/shared/ui/TextField/TextField.stories.ts index b7f7304c..3391941c 100644 --- a/src/shared/ui/TextField/TextField.stories.ts +++ b/src/shared/ui/TextField/TextField.stories.ts @@ -2,45 +2,116 @@ import type { Meta, StoryObj } from "@storybook/react"; import { TextField } from "./TextField"; const meta: Meta = { - title: "Components/TextField", + title: "COMPONENTS/TextField", component: TextField, tags: ["autodocs"], argTypes: { - width: { control: "text" }, - height: { control: "text" }, - placeholder: { control: "text" }, - error: { control: "boolean" }, + fieldSize: { + control: { type: "radio" }, + options: ["sm", "md", "lg"], + description: "Input height, text size, and icon size", + defaultValue: "lg", + }, + widthSize: { + control: { type: "radio" }, + options: ["long", "short"], + description: "Input width type", + defaultValue: "long", + }, + isError: { + control: { type: "boolean" }, + description: "Show error border (red)", + defaultValue: false, + }, + isHiddenable: { + control: { type: "boolean" }, + description: "Enable eye toggle button for password fields", + defaultValue: false, + }, + placeholder: { + control: { type: "text" }, + description: "Placeholder text", + }, + value: { + control: { type: "text" }, + description: "Controlled value (if provided)", + }, }, }; - export default meta; + type Story = StoryObj; +// 기본 (Uncontrolled) export const Default: Story = { args: { - width: "400px", - height: "70px", - placeholder: "Enter your text", + placeholder: "내용을 입력해주세요.", + fieldSize: "lg", + widthSize: "long", + }, +}; + +// 에러 상태 +export const Error: Story = { + args: { + placeholder: "에러 상태", + fieldSize: "md", + widthSize: "long", + isError: true, }, }; -export const Focused: Story = { +// 비밀번호 입력 (숨김 가능) +export const PasswordHiddenable: Story = { args: { - width: "400px", - height: "70px", - placeholder: "Type something...", + placeholder: "비밀번호 입력", + fieldSize: "md", + widthSize: "long", + isHiddenable: true, }, - play: async ({ canvasElement }) => { - const input = canvasElement.querySelector("input") as HTMLInputElement; - input?.focus(); +}; + +//Long size +export const Small: Story = { + args: { + placeholder: "Small", + fieldSize: "sm", + widthSize: "long", }, }; -export const Error: Story = { +//Medium Size +export const Medium: Story = { + args: { + placeholder: "Medium", + fieldSize: "md", + widthSize: "long", + }, +}; + +//Large Size +export const Large: Story = { + args: { + placeholder: "Large", + fieldSize: "lg", + widthSize: "long", + }, +}; + +// Long Width +export const Long: Story = { + args: { + placeholder: "long width", + fieldSize: "lg", + widthSize: "long", + }, +}; + +// Short width +export const Short: Story = { args: { - placeholder: "This field has an error", - error: true, - width: "400px", - height: "70px", + placeholder: "short width", + fieldSize: "lg", + widthSize: "short", }, }; \ No newline at end of file diff --git a/src/shared/ui/TextField/TextField.tsx b/src/shared/ui/TextField/TextField.tsx index 03ec9931..0509bec9 100644 --- a/src/shared/ui/TextField/TextField.tsx +++ b/src/shared/ui/TextField/TextField.tsx @@ -1,49 +1,112 @@ -"use client" +"use client"; import React, { InputHTMLAttributes, useState } from "react"; +import cn from "@/shared/lib/cn"; +import Image from "next/image"; +import eyeIcon from "@/shared/images/eye.svg" +import eyeOffIcon from "@/shared/images/eye-off.svg" -interface TextFieldProps extends InputHTMLAttributes { - width?: string | number; - height?: string | number; - error?: boolean; +type TextFieldSize = "sm" | "md" | "lg"; +type TextFieldWidthSize = "long" | "short"; + +interface TextFieldProps extends Omit, "size"> { + fieldSize?: TextFieldSize; + widthSize?: TextFieldWidthSize; + isError?: boolean; + isHiddenable?: boolean; } -export const TextField = (props: TextFieldProps) => { - const { width, height, error, className, ...rest} = props; +const styleMap: Record< + TextFieldSize, + { + widthMap: Record; + height: number; + textSize: string; + iconSize: { w: number; h: number }; + } +> = { + lg: { + widthMap: { long: 640, short: 400 }, + height: 70, + textSize: "text-[16px] placeholder:text-[16px]", + iconSize: { w: 24, h: 24 }, + }, + md: { + widthMap: { long: 440, short: 360 }, + height: 55, + textSize: "text-[14px] placeholder:text-[14px]", + iconSize: { w: 22, h: 22 }, + }, + sm: { + widthMap: { long: 335, short: 335 }, + height: 55, + textSize: "text-[14px] placeholder:text-[14px]", + iconSize: { w: 22, h: 22 }, + }, +}; + +export const TextField = ({ + fieldSize = "lg", + widthSize = "long", + isError = false, + isHiddenable = false, + className, + ...rest +} : TextFieldProps) => { + const [text, setText] = useState(""); + const [focused, setFocused] = useState(false); + const [hidden, setHidden] = useState(isHiddenable); //비밀번호 시 숨겨진 상태로 시작 - const [text, setText] = useState(""); - const [focused, setFocused] = useState(false); + const { widthMap, height, textSize, iconSize} = styleMap[fieldSize]; + const width = widthMap[widthSize]; - return ( + return (
{ setText(e.target.value); - props.onChange?.(e); + rest.onChange?.(e); //text 수정 시 handling }} - className={`flex-1 bg-transparent outline-none text-[20px] ${ - "text-white" - }`} + className={cn( + textSize, + "flex-1 bg-transparent outline-none text-white", + className + )} onFocus={(e) => { setFocused(true); - props.onFocus?.(e); }} onBlur={(e) => { setFocused(false); - props.onBlur?.(e); }} /> + + {isHiddenable && ( + + )}
); }; \ No newline at end of file