Skip to content

Commit c164167

Browse files
committed
feat: TextField 컴포넌트 구현
1 parent 50a5c3b commit c164167

File tree

3 files changed

+124
-55
lines changed

3 files changed

+124
-55
lines changed

src/components/Input.tsx

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/components/TextField.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {
2+
forwardRef,
3+
ReactNode,
4+
ElementType,
5+
ComponentPropsWithRef,
6+
ComponentPropsWithoutRef,
7+
} from "react";
8+
9+
import { cn } from "@/utils/className";
10+
11+
/* ---------- Util Type ---------- */
12+
type ExclusiveKeys<A, B> = Exclude<keyof A, keyof B>;
13+
type Strict<A, K extends PropertyKey> = Omit<A, K> & { [P in K]?: never };
14+
15+
/* ---------- Own Type ---------- */
16+
interface CustomProps {
17+
label?: string;
18+
prefix?: ReactNode;
19+
postfix?: ReactNode;
20+
size?: keyof typeof sizeMap;
21+
fullWidth?: boolean;
22+
disabled?: boolean;
23+
validateMessage?: string;
24+
className?: string;
25+
wrapperClassName?: string;
26+
}
27+
28+
/* ---------- Native Attrs ---------- */
29+
type InputAttrs = ComponentPropsWithoutRef<"input">;
30+
type TextareaAttrs = ComponentPropsWithoutRef<"textarea">;
31+
32+
type InputOnly = ExclusiveKeys<InputAttrs, TextareaAttrs>;
33+
type TextareaOnly = ExclusiveKeys<TextareaAttrs, InputAttrs>;
34+
35+
type StrictInputAttrs = Strict<InputAttrs, TextareaOnly>;
36+
type StrictTextareaAttrs = Strict<TextareaAttrs, InputOnly>;
37+
38+
type InputProps = Omit<StrictInputAttrs, keyof CustomProps> &
39+
CustomProps & { as?: "input" };
40+
41+
type TextareaProps = Omit<StrictTextareaAttrs, keyof CustomProps> &
42+
CustomProps & { as: "textarea" };
43+
44+
type TextFieldProps = InputProps | TextareaProps;
45+
type TextFieldRef<P extends TextFieldProps> = P extends { as: "textarea" }
46+
? ComponentPropsWithRef<"textarea">["ref"]
47+
: ComponentPropsWithRef<"input">["ref"];
48+
49+
const sizeMap = {
50+
lg: "py-4 px-5 text-[1rem]",
51+
sm: "p-2.5 text-sm",
52+
} as const;
53+
54+
function TextFieldInner<T extends TextFieldProps>(
55+
{
56+
as,
57+
id,
58+
label,
59+
prefix,
60+
postfix,
61+
fullWidth,
62+
disabled,
63+
wrapperClassName,
64+
className,
65+
size = "lg",
66+
validateMessage,
67+
...rest
68+
}: T,
69+
ref: TextFieldRef<T>,
70+
) {
71+
const Component = (as ?? "input") as ElementType;
72+
73+
const wrapperClassNames = cn(
74+
"flex w-fit gap-1.5 border rounded-[0.375rem] border-gray-30 focus-within:border-blue-20 placeholder:text-gray-40 ",
75+
sizeMap[size],
76+
fullWidth && "w-full",
77+
disabled && "bg-gray-20 text-gray-40",
78+
validateMessage && "focus-within:border-red-40",
79+
wrapperClassName,
80+
);
81+
82+
const textElementClassNames = cn(
83+
"disabled:placeholder:text-gray-40 resize-none outline-none",
84+
fullWidth && "flex-1",
85+
className,
86+
);
87+
88+
return (
89+
<div>
90+
{label && (
91+
<label htmlFor={id} className="inline-block mb-2 leading-[1.625rem]">
92+
{label}
93+
</label>
94+
)}
95+
96+
<div className={wrapperClassNames}>
97+
{prefix}
98+
<Component
99+
id={id}
100+
ref={ref}
101+
disabled={disabled}
102+
className={textElementClassNames}
103+
{...rest}
104+
/>
105+
{postfix}
106+
</div>
107+
108+
{validateMessage && (
109+
<p className="m-2 text-sm text-red-40">{validateMessage}</p>
110+
)}
111+
</div>
112+
);
113+
}
114+
115+
TextFieldInner.displayName = "TextField";
116+
117+
export const TextField = forwardRef(TextFieldInner) as <
118+
P extends TextFieldProps,
119+
>(
120+
props: P & { ref?: TextFieldRef<P> },
121+
) => ReactNode;
122+
123+
export default TextField;

src/utils/className.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const cn = (...input: unknown[]) => input.filter(Boolean).join(" ");

0 commit comments

Comments
 (0)