@uniai-fe/uds-primitives 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/styles.css +20 -20
- package/package.json +2 -2
- package/src/components/input/markup/text/Base.tsx +143 -105
- package/src/components/input/markup/text/Identification.tsx +13 -12
- package/src/components/input/markup/text/Password.tsx +9 -38
- package/src/components/input/markup/text/Phone.tsx +9 -40
- package/src/components/input/markup/text/Search.tsx +9 -40
- package/src/components/input/markup/text/index.ts +1 -1
- package/src/components/input/styles/index.scss +11 -11
- package/src/components/input/types/index.ts +30 -21
- package/src/components/input/utils/index.ts +4 -4
- package/src/index.tsx +1 -0
- package/src/types/form-field.ts +80 -0
- package/src/types/index.ts +1 -1
package/README.md
CHANGED
|
@@ -194,4 +194,6 @@ src/components/{category}/
|
|
|
194
194
|
- `CONTEXT.md` 및 `CONTEXT-*.md`: 각 컴포넌트의 상태/진행/디자인 근거
|
|
195
195
|
- `RADIX-SIZE-GUIDE.md`: primitives 사이즈 체계와 Radix 매핑 규칙
|
|
196
196
|
|
|
197
|
+
* **컨벤션**: 모든 컴포넌트/스토리/문서는 slot/prefix/suffix 용어를 사용하지 않고, 레이아웃 기준(`header/body/footer`, 2단 구조는 `upper/lower`)과 `util*` 키워드를 사용한다. 인터랙션 함수는 `on*` 접두사를 사용하고, JSDoc `@param`은 depth 전체를 풀어 쓴다.
|
|
198
|
+
|
|
197
199
|
필요한 컨텍스트를 확인한 뒤 컴포넌트를 import해 사용하면 됩니다.
|
package/dist/styles.css
CHANGED
|
@@ -2501,7 +2501,7 @@ figure.chip {
|
|
|
2501
2501
|
.input-field[data-size=large] {
|
|
2502
2502
|
min-height: var(--theme-input-height-large);
|
|
2503
2503
|
}
|
|
2504
|
-
.input-field[data-
|
|
2504
|
+
.input-field[data-priority=secondary] {
|
|
2505
2505
|
border: none;
|
|
2506
2506
|
border-bottom: var(--theme-input-border-width-default) solid var(--theme-input-border-color);
|
|
2507
2507
|
border-radius: 0;
|
|
@@ -2509,7 +2509,7 @@ figure.chip {
|
|
|
2509
2509
|
padding-block: var(--spacing-padding-4);
|
|
2510
2510
|
background-color: transparent;
|
|
2511
2511
|
}
|
|
2512
|
-
.input-field[data-
|
|
2512
|
+
.input-field[data-priority=tertiary] {
|
|
2513
2513
|
border-radius: var(--theme-input-radius-tertiary);
|
|
2514
2514
|
background-color: var(--theme-input-surface);
|
|
2515
2515
|
min-height: var(--theme-input-height-tertiary);
|
|
@@ -2517,30 +2517,30 @@ figure.chip {
|
|
|
2517
2517
|
row-gap: var(--spacing-gap-1);
|
|
2518
2518
|
column-gap: var(--theme-input-gap);
|
|
2519
2519
|
}
|
|
2520
|
-
.input-field[data-
|
|
2520
|
+
.input-field[data-priority=tertiary] .input-inline-label {
|
|
2521
2521
|
flex-basis: 100%;
|
|
2522
2522
|
}
|
|
2523
|
-
.input-field[data-
|
|
2523
|
+
.input-field[data-priority=tertiary] .input-element {
|
|
2524
2524
|
min-height: var(--theme-size-medium-2);
|
|
2525
2525
|
width: auto;
|
|
2526
2526
|
flex: 1 1 auto;
|
|
2527
2527
|
}
|
|
2528
|
-
.input-field[data-
|
|
2528
|
+
.input-field[data-priority=tertiary] .input-element + .input-affix {
|
|
2529
2529
|
margin-left: auto;
|
|
2530
2530
|
}
|
|
2531
|
-
.input-field:not([data-
|
|
2531
|
+
.input-field:not([data-priority=secondary])[data-state=active], .input-field:not([data-priority=secondary])[data-state=focused] {
|
|
2532
2532
|
border-color: var(--theme-input-border-active);
|
|
2533
2533
|
border-width: var(--theme-input-border-width-emphasis);
|
|
2534
2534
|
}
|
|
2535
|
-
.input-field:not([data-
|
|
2535
|
+
.input-field:not([data-priority=secondary])[data-state=success] {
|
|
2536
2536
|
border-color: var(--theme-input-border-success);
|
|
2537
2537
|
border-width: var(--theme-input-border-width-emphasis);
|
|
2538
2538
|
}
|
|
2539
|
-
.input-field:not([data-
|
|
2539
|
+
.input-field:not([data-priority=secondary])[data-state=error] {
|
|
2540
2540
|
border-color: var(--theme-input-border-error);
|
|
2541
2541
|
border-width: var(--theme-input-border-width-emphasis);
|
|
2542
2542
|
}
|
|
2543
|
-
.input-field:not([data-
|
|
2543
|
+
.input-field:not([data-priority=secondary])[data-state=disabled] {
|
|
2544
2544
|
border-color: var(--theme-input-border-disabled);
|
|
2545
2545
|
border-width: var(--theme-input-border-width-default);
|
|
2546
2546
|
background-color: var(--theme-input-surface-disabled);
|
|
@@ -2580,11 +2580,11 @@ figure.chip {
|
|
|
2580
2580
|
color: var(--theme-input-label-color);
|
|
2581
2581
|
}
|
|
2582
2582
|
|
|
2583
|
-
.input-field[data-
|
|
2583
|
+
.input-field[data-priority=secondary] .input-element {
|
|
2584
2584
|
padding-inline: 0;
|
|
2585
2585
|
}
|
|
2586
2586
|
|
|
2587
|
-
.input-field[data-
|
|
2587
|
+
.input-field[data-priority=tertiary] .input-element {
|
|
2588
2588
|
min-height: var(--theme-size-medium-2);
|
|
2589
2589
|
}
|
|
2590
2590
|
|
|
@@ -2607,14 +2607,14 @@ figure.chip {
|
|
|
2607
2607
|
min-width: 20px;
|
|
2608
2608
|
color: var(--theme-input-helper-color);
|
|
2609
2609
|
}
|
|
2610
|
-
.input-affix--
|
|
2610
|
+
.input-affix--left {
|
|
2611
2611
|
order: -1;
|
|
2612
2612
|
margin-right: var(--spacing-gap-3);
|
|
2613
2613
|
}
|
|
2614
|
-
.input-affix--
|
|
2614
|
+
.input-affix--right, .input-affix--clear, .input-affix--status {
|
|
2615
2615
|
margin-left: var(--spacing-gap-3);
|
|
2616
2616
|
}
|
|
2617
|
-
.input-affix--
|
|
2617
|
+
.input-affix--clear, .input-affix--status {
|
|
2618
2618
|
color: var(--theme-input-text-color);
|
|
2619
2619
|
}
|
|
2620
2620
|
.input-affix--status[data-state=error] {
|
|
@@ -2624,22 +2624,22 @@ figure.chip {
|
|
|
2624
2624
|
color: var(--color-primary-default);
|
|
2625
2625
|
}
|
|
2626
2626
|
|
|
2627
|
-
.input-field[data-
|
|
2627
|
+
.input-field[data-priority=secondary] {
|
|
2628
2628
|
border-bottom-width: var(--theme-input-border-width-default);
|
|
2629
2629
|
}
|
|
2630
|
-
.input-field[data-
|
|
2630
|
+
.input-field[data-priority=secondary][data-state=active], .input-field[data-priority=secondary][data-state=focused] {
|
|
2631
2631
|
border-bottom-color: var(--theme-input-border-active);
|
|
2632
2632
|
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2633
2633
|
}
|
|
2634
|
-
.input-field[data-
|
|
2634
|
+
.input-field[data-priority=secondary][data-state=success] {
|
|
2635
2635
|
border-bottom-color: var(--theme-input-border-success);
|
|
2636
2636
|
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2637
2637
|
}
|
|
2638
|
-
.input-field[data-
|
|
2638
|
+
.input-field[data-priority=secondary][data-state=error] {
|
|
2639
2639
|
border-bottom-color: var(--theme-input-border-error);
|
|
2640
2640
|
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2641
2641
|
}
|
|
2642
|
-
.input-field[data-
|
|
2642
|
+
.input-field[data-priority=secondary][data-state=disabled] {
|
|
2643
2643
|
border-bottom-color: var(--theme-input-border-underline-disabled);
|
|
2644
2644
|
border-bottom-width: var(--theme-input-border-width-default);
|
|
2645
2645
|
}
|
|
@@ -2671,7 +2671,7 @@ figure.chip {
|
|
|
2671
2671
|
border-color: var(--theme-input-border-color);
|
|
2672
2672
|
background-color: var(--theme-input-surface-disabled);
|
|
2673
2673
|
}
|
|
2674
|
-
.input[data-state=disabled] .input-field[data-
|
|
2674
|
+
.input[data-state=disabled] .input-field[data-priority=secondary] {
|
|
2675
2675
|
background-color: transparent;
|
|
2676
2676
|
}
|
|
2677
2677
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-primitives",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "UNIAI Design System; Primitives Components Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
|
-
"packageManager": "pnpm@10.26.
|
|
17
|
+
"packageManager": "pnpm@10.26.1",
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=24",
|
|
20
20
|
"pnpm": ">=10"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
|
+
import type { ForwardedRef } from "react";
|
|
2
3
|
import {
|
|
3
4
|
ChangeEvent,
|
|
4
5
|
FocusEvent,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
useEffect,
|
|
8
9
|
useId,
|
|
9
10
|
useMemo,
|
|
11
|
+
useRef,
|
|
10
12
|
useState,
|
|
11
13
|
} from "react";
|
|
12
14
|
import type { InputProps } from "../../types";
|
|
@@ -18,95 +20,103 @@ import {
|
|
|
18
20
|
composeInputClassName,
|
|
19
21
|
} from "../../utils";
|
|
20
22
|
import ErrorIcon from "../../img/error.svg";
|
|
21
|
-
import SuccessIcon from "../../img/success.svg";
|
|
22
23
|
import ResetIcon from "../../img/reset.svg";
|
|
24
|
+
import SuccessIcon from "../../img/success.svg";
|
|
25
|
+
|
|
26
|
+
const setForwardedRef = <T,>(ref: ForwardedRef<T>, value: T | null): void => {
|
|
27
|
+
if (!ref) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof ref === "function") {
|
|
31
|
+
ref(value);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
ref.current = value;
|
|
35
|
+
};
|
|
23
36
|
|
|
24
37
|
/**
|
|
25
|
-
* Native `<input>` 기반 텍스트 필드.
|
|
26
|
-
* label/
|
|
38
|
+
* Native `<input>` 기반 텍스트 필드.
|
|
39
|
+
* priority/size/state 축과 left/right/clear/status 슬롯, label/helper 피드백 슬롯을 모두 제공하며
|
|
40
|
+
* react-hook-form `register` 결과를 그대로 전달받아 내부 ref/onChange/onBlur에 병합한다.
|
|
41
|
+
*
|
|
27
42
|
* @component
|
|
28
|
-
* @param {InputProps} props
|
|
29
|
-
* @param {"primary" | "secondary" | "tertiary"} [props.
|
|
30
|
-
* @param {"small" | "medium" | "large"} [props.size="medium"]
|
|
31
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.state="default"] 시각
|
|
32
|
-
* @param {boolean} [props.block=false] true면 width 100
|
|
33
|
-
* @param {React.ReactNode} [props.
|
|
34
|
-
* @param {React.ReactNode} [props.
|
|
35
|
-
* @param {React.
|
|
36
|
-
* @param {React.ReactNode} [props.successIcon] success
|
|
37
|
-
* @param {React.ReactNode} [props.errorIcon] error
|
|
38
|
-
* @param {React.ReactNode} [props.label]
|
|
39
|
-
* @param {React.ReactNode} [props.
|
|
40
|
-
* @param {boolean} [props.
|
|
41
|
-
* @param {string} [props.inputClassName] 실제 `<input>` 요소 className
|
|
42
|
-
* @param {string} [props.
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {
|
|
45
|
-
* @param {boolean} [props.disabled] native disabled
|
|
46
|
-
* @param {string} [props.id]
|
|
47
|
-
* @param {string} [props.className] root `.input` className
|
|
48
|
-
* @param {
|
|
49
|
-
*
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {
|
|
52
|
-
* @param {
|
|
53
|
-
* @param {(event:
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {
|
|
43
|
+
* @param {InputProps} props Input 컴포넌트 공통 props
|
|
44
|
+
* @param {"primary" | "secondary" | "tertiary"} [props.priority="primary"] 디자인 토큰 우선순위
|
|
45
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/타이포 세트
|
|
46
|
+
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.state="default"] 시각 상태
|
|
47
|
+
* @param {boolean} [props.block=false] true면 width 100%
|
|
48
|
+
* @param {React.ReactNode} [props.left] 입력 왼쪽 슬롯(아이콘/텍스트)
|
|
49
|
+
* @param {React.ReactNode} [props.right] 입력 오른쪽 슬롯
|
|
50
|
+
* @param {React.ReactNode} [props.clearIcon] 입력값 초기화 아이콘. 지정하지 않으면 기본 Reset 아이콘
|
|
51
|
+
* @param {React.ReactNode} [props.successIcon] success 상태 아이콘 override
|
|
52
|
+
* @param {React.ReactNode} [props.errorIcon] error 상태 아이콘 override
|
|
53
|
+
* @param {React.ReactNode} [props.label] 상단 또는 inline label 콘텐츠
|
|
54
|
+
* @param {React.ReactNode} [props.helper] helper 텍스트 콘텐츠
|
|
55
|
+
* @param {boolean} [props.hideHelper] true면 helper 영역 숨김
|
|
56
|
+
* @param {string} [props.inputClassName] 실제 `<input>` 요소 className
|
|
57
|
+
* @param {string} [props.boxClassName] `.input-box` className
|
|
58
|
+
* @param {ComponentPropsWithoutRef<"label">} [props.labelProps] label 추가 속성
|
|
59
|
+
* @param {ComponentPropsWithoutRef<"div">} [props.helperProps] helper container 속성
|
|
60
|
+
* @param {boolean} [props.disabled] native disabled
|
|
61
|
+
* @param {string} [props.id] 외부 id. label htmlFor와 공유된다
|
|
62
|
+
* @param {string} [props.className] root `.input` className
|
|
63
|
+
* @param {UseFormRegisterReturn} [props.register] react-hook-form register 반환값
|
|
64
|
+
* @param {string} [props.name] native name. register 사용 시 자동으로 병합
|
|
65
|
+
* @param {InputState} [props.data-simulated-state] Storybook 등에서 시각 상태 강제용
|
|
66
|
+
* @param {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] change 핸들러
|
|
67
|
+
* @param {(event: FocusEvent<HTMLInputElement>) => void} [props.onFocus] focus 핸들러
|
|
68
|
+
* @param {(event: FocusEvent<HTMLInputElement>) => void} [props.onBlur] blur 핸들러
|
|
69
|
+
* @param {string | number | readonly string[]} [props.value] 제어형 값
|
|
70
|
+
* @param {string | number | readonly string[]} [props.defaultValue] 비제어 초기값
|
|
71
|
+
* @param {string} [props.type="text"] native input type
|
|
56
72
|
*/
|
|
57
73
|
const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
58
74
|
(
|
|
59
75
|
{
|
|
60
|
-
|
|
76
|
+
priority = "primary",
|
|
61
77
|
size = "medium",
|
|
62
78
|
state: stateProp = "default",
|
|
63
79
|
block = false,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
left,
|
|
81
|
+
right,
|
|
82
|
+
clearIcon,
|
|
67
83
|
successIcon,
|
|
68
84
|
errorIcon,
|
|
69
85
|
label,
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
helper,
|
|
87
|
+
hideHelper,
|
|
72
88
|
inputClassName,
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
boxClassName,
|
|
90
|
+
helperProps,
|
|
75
91
|
labelProps,
|
|
76
92
|
disabled,
|
|
77
93
|
id,
|
|
78
94
|
className,
|
|
95
|
+
register,
|
|
79
96
|
"data-simulated-state": simulatedState,
|
|
80
|
-
type = "text",
|
|
81
|
-
defaultValue,
|
|
82
97
|
value,
|
|
98
|
+
defaultValue,
|
|
99
|
+
name,
|
|
83
100
|
onChange,
|
|
84
101
|
onFocus,
|
|
85
102
|
onBlur,
|
|
103
|
+
type = "text",
|
|
86
104
|
...restProps
|
|
87
105
|
},
|
|
88
106
|
forwardedRef,
|
|
89
107
|
) => {
|
|
90
108
|
const generatedId = useId();
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const registerRef = register?.ref;
|
|
110
|
+
const registerOnChange = register?.onChange;
|
|
111
|
+
const registerOnBlur = register?.onBlur;
|
|
112
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
94
113
|
const [isFocused, setIsFocused] = useState(false);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[generatedId, id, labelProps?.htmlFor],
|
|
102
|
-
);
|
|
103
|
-
const shouldShowHelper = Boolean(helperText) && !hideHelperText;
|
|
104
|
-
const helperElementId = useMemo(() => {
|
|
105
|
-
if (!shouldShowHelper) {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
return helperTextProps?.id ?? `${fieldId}-helper-text`;
|
|
109
|
-
}, [fieldId, helperTextProps?.id, shouldShowHelper]);
|
|
114
|
+
const [hasValue, setHasValue] = useState(() => {
|
|
115
|
+
const initial = value ?? defaultValue;
|
|
116
|
+
return initial !== undefined && initial !== null
|
|
117
|
+
? String(initial).length > 0
|
|
118
|
+
: false;
|
|
119
|
+
});
|
|
110
120
|
|
|
111
121
|
useEffect(() => {
|
|
112
122
|
if (stateProp === "disabled" || disabled) {
|
|
@@ -114,6 +124,16 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
114
124
|
}
|
|
115
125
|
}, [disabled, stateProp]);
|
|
116
126
|
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (value !== undefined && value !== null) {
|
|
129
|
+
setHasValue(String(value).length > 0);
|
|
130
|
+
}
|
|
131
|
+
}, [value]);
|
|
132
|
+
|
|
133
|
+
const resolvedState = useMemo(
|
|
134
|
+
() => (disabled ? "disabled" : stateProp),
|
|
135
|
+
[disabled, stateProp],
|
|
136
|
+
);
|
|
117
137
|
const visualState = useMemo(() => {
|
|
118
138
|
if (resolvedState === "disabled") {
|
|
119
139
|
return "disabled";
|
|
@@ -124,6 +144,17 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
124
144
|
return isFocused ? "active" : resolvedState;
|
|
125
145
|
}, [isFocused, resolvedState]);
|
|
126
146
|
|
|
147
|
+
const fieldId = useMemo(
|
|
148
|
+
() => id ?? labelProps?.htmlFor ?? generatedId,
|
|
149
|
+
[generatedId, id, labelProps?.htmlFor],
|
|
150
|
+
);
|
|
151
|
+
const helperElementId = useMemo(() => {
|
|
152
|
+
if (!helper || hideHelper) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return helperProps?.id ?? `${fieldId}-helper-text`;
|
|
156
|
+
}, [fieldId, helperProps?.id, helper, hideHelper]);
|
|
157
|
+
|
|
127
158
|
const defaultStatusIcon = useMemo(() => {
|
|
128
159
|
if (resolvedState === "success") {
|
|
129
160
|
return <SuccessIcon aria-hidden="true" />;
|
|
@@ -144,51 +175,46 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
144
175
|
return null;
|
|
145
176
|
}, [defaultStatusIcon, errorIcon, resolvedState, successIcon]);
|
|
146
177
|
|
|
147
|
-
const
|
|
178
|
+
const defaultClearIcon = useMemo(() => {
|
|
148
179
|
if (visualState === "active") {
|
|
149
180
|
return <ResetIcon aria-hidden="true" />;
|
|
150
181
|
}
|
|
151
182
|
return null;
|
|
152
183
|
}, [visualState]);
|
|
153
184
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
currentValue !== undefined && currentValue !== null
|
|
158
|
-
? String(currentValue).length > 0
|
|
159
|
-
: false;
|
|
160
|
-
const showResetSlot = Boolean(
|
|
161
|
-
effectiveResetSlot && hasInputValue && resolvedState !== "disabled",
|
|
185
|
+
const effectiveClearIcon = clearIcon ?? defaultClearIcon;
|
|
186
|
+
const showClearIcon = Boolean(
|
|
187
|
+
effectiveClearIcon && hasValue && resolvedState !== "disabled",
|
|
162
188
|
);
|
|
163
189
|
const isDisabled = resolvedState === "disabled";
|
|
164
190
|
const labelFor = labelProps?.htmlFor ?? fieldId;
|
|
165
191
|
const containerClassName = useMemo(
|
|
166
192
|
() =>
|
|
167
193
|
composeInputClassName({
|
|
168
|
-
|
|
194
|
+
priority,
|
|
169
195
|
size,
|
|
170
196
|
state: visualState,
|
|
171
197
|
block,
|
|
172
198
|
className,
|
|
173
199
|
}),
|
|
174
|
-
[
|
|
200
|
+
[priority, block, className, size, visualState],
|
|
175
201
|
);
|
|
176
|
-
const
|
|
202
|
+
const fieldBoxClassName = useMemo(
|
|
177
203
|
() =>
|
|
178
204
|
composeInputBoxClassName({
|
|
179
|
-
|
|
205
|
+
priority,
|
|
180
206
|
size,
|
|
181
207
|
state: visualState,
|
|
182
208
|
block,
|
|
183
|
-
className:
|
|
209
|
+
className: boxClassName,
|
|
184
210
|
}),
|
|
185
|
-
[
|
|
211
|
+
[priority, block, size, visualState, boxClassName],
|
|
186
212
|
);
|
|
187
213
|
const helperClassName = useMemo(
|
|
188
|
-
() => clsx("input-helper-text",
|
|
189
|
-
[
|
|
214
|
+
() => clsx("input-helper-text", helperProps?.className),
|
|
215
|
+
[helperProps?.className],
|
|
190
216
|
);
|
|
191
|
-
const shouldRenderInlineLabel =
|
|
217
|
+
const shouldRenderInlineLabel = priority === "tertiary" && Boolean(label);
|
|
192
218
|
const labelClassName = useMemo(
|
|
193
219
|
() =>
|
|
194
220
|
clsx(
|
|
@@ -203,6 +229,15 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
203
229
|
[inputClassName],
|
|
204
230
|
);
|
|
205
231
|
|
|
232
|
+
const mergedRef = useCallback(
|
|
233
|
+
(node: HTMLInputElement | null) => {
|
|
234
|
+
inputRef.current = node;
|
|
235
|
+
setForwardedRef(forwardedRef, node);
|
|
236
|
+
registerRef?.(node);
|
|
237
|
+
},
|
|
238
|
+
[forwardedRef, registerRef],
|
|
239
|
+
);
|
|
240
|
+
|
|
206
241
|
const handleFocus = useCallback(
|
|
207
242
|
(event: FocusEvent<HTMLInputElement>) => {
|
|
208
243
|
setIsFocused(true);
|
|
@@ -214,25 +249,27 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
214
249
|
const handleBlur = useCallback(
|
|
215
250
|
(event: FocusEvent<HTMLInputElement>) => {
|
|
216
251
|
setIsFocused(false);
|
|
252
|
+
registerOnBlur?.(event);
|
|
217
253
|
onBlur?.(event);
|
|
218
254
|
},
|
|
219
|
-
[onBlur],
|
|
255
|
+
[onBlur, registerOnBlur],
|
|
220
256
|
);
|
|
221
257
|
|
|
222
258
|
const handleChange = useCallback(
|
|
223
259
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
260
|
+
setHasValue(event.currentTarget.value.length > 0);
|
|
261
|
+
registerOnChange?.(event);
|
|
227
262
|
onChange?.(event);
|
|
228
263
|
},
|
|
229
|
-
[onChange,
|
|
264
|
+
[onChange, registerOnChange],
|
|
230
265
|
);
|
|
231
266
|
|
|
267
|
+
const inputName = register?.name ?? name;
|
|
268
|
+
|
|
232
269
|
return (
|
|
233
270
|
<div
|
|
234
271
|
className={containerClassName}
|
|
235
|
-
data-
|
|
272
|
+
data-priority={priority}
|
|
236
273
|
data-size={size}
|
|
237
274
|
data-state={visualState}
|
|
238
275
|
data-block={block ? "true" : undefined}
|
|
@@ -244,17 +281,17 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
244
281
|
className={labelClassName}
|
|
245
282
|
htmlFor={labelFor}
|
|
246
283
|
data-slot="label"
|
|
247
|
-
data-
|
|
284
|
+
data-priority={priority}
|
|
248
285
|
data-state={visualState}
|
|
249
286
|
>
|
|
250
287
|
{label}
|
|
251
288
|
</label>
|
|
252
289
|
) : null}
|
|
253
|
-
<div className={
|
|
290
|
+
<div className={fieldBoxClassName} data-slot="box">
|
|
254
291
|
<div
|
|
255
292
|
className={INPUT_FIELD_CLASSNAME}
|
|
256
293
|
data-state={visualState}
|
|
257
|
-
data-
|
|
294
|
+
data-priority={priority}
|
|
258
295
|
data-size={size}
|
|
259
296
|
data-block={block ? "true" : undefined}
|
|
260
297
|
>
|
|
@@ -267,44 +304,45 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
267
304
|
{label}
|
|
268
305
|
</div>
|
|
269
306
|
) : null}
|
|
270
|
-
{
|
|
307
|
+
{left ? (
|
|
271
308
|
<div
|
|
272
|
-
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--
|
|
273
|
-
data-slot="
|
|
309
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--left`}
|
|
310
|
+
data-slot="left"
|
|
274
311
|
>
|
|
275
|
-
{
|
|
312
|
+
{left}
|
|
276
313
|
</div>
|
|
277
314
|
) : null}
|
|
278
315
|
<input
|
|
279
316
|
{...restProps}
|
|
280
317
|
id={fieldId}
|
|
281
|
-
ref={
|
|
318
|
+
ref={mergedRef}
|
|
282
319
|
className={inputElementClassName}
|
|
283
320
|
disabled={isDisabled}
|
|
284
321
|
aria-invalid={resolvedState === "error" ? true : undefined}
|
|
285
322
|
aria-describedby={helperElementId}
|
|
286
323
|
type={type}
|
|
287
|
-
defaultValue={defaultValue}
|
|
288
324
|
value={value}
|
|
325
|
+
defaultValue={defaultValue}
|
|
326
|
+
name={inputName}
|
|
289
327
|
onChange={handleChange}
|
|
290
328
|
onFocus={handleFocus}
|
|
291
329
|
onBlur={handleBlur}
|
|
292
330
|
/>
|
|
293
|
-
{
|
|
331
|
+
{right ? (
|
|
294
332
|
<div
|
|
295
|
-
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--
|
|
296
|
-
data-slot="
|
|
333
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--right`}
|
|
334
|
+
data-slot="right"
|
|
297
335
|
>
|
|
298
|
-
{
|
|
336
|
+
{right}
|
|
299
337
|
</div>
|
|
300
338
|
) : null}
|
|
301
|
-
{
|
|
339
|
+
{showClearIcon ? (
|
|
302
340
|
<div
|
|
303
|
-
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--
|
|
304
|
-
data-slot="
|
|
341
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--clear`}
|
|
342
|
+
data-slot="clear"
|
|
305
343
|
data-visible="true"
|
|
306
344
|
>
|
|
307
|
-
{
|
|
345
|
+
{effectiveClearIcon}
|
|
308
346
|
</div>
|
|
309
347
|
) : null}
|
|
310
348
|
{statusSlot ? (
|
|
@@ -318,16 +356,16 @@ const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
|
318
356
|
) : null}
|
|
319
357
|
</div>
|
|
320
358
|
</div>
|
|
321
|
-
{
|
|
359
|
+
{helper && !hideHelper ? (
|
|
322
360
|
<div
|
|
323
|
-
{...
|
|
361
|
+
{...helperProps}
|
|
324
362
|
className={helperClassName}
|
|
325
363
|
id={helperElementId}
|
|
326
|
-
data-slot="helper
|
|
327
|
-
data-
|
|
364
|
+
data-slot="helper"
|
|
365
|
+
data-priority={priority}
|
|
328
366
|
data-state={visualState}
|
|
329
367
|
>
|
|
330
|
-
{
|
|
368
|
+
{helper}
|
|
331
369
|
</div>
|
|
332
370
|
) : null}
|
|
333
371
|
</div>
|
|
@@ -4,26 +4,27 @@ import {
|
|
|
4
4
|
forwardRef,
|
|
5
5
|
useImperativeHandle,
|
|
6
6
|
KeyboardEvent,
|
|
7
|
+
ReactNode,
|
|
7
8
|
useCallback,
|
|
8
9
|
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState,
|
|
11
12
|
} from "react";
|
|
12
|
-
import type {
|
|
13
|
+
import type { InputState } from "../../types";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* IdentificationInput props. 고정 길이 숫자 코드 입력에 필요한 label/helper/state/onComplete를 제공한다.
|
|
16
17
|
* @property {number} [length=6] 입력칸 개수(4~8 사이로 자동 보정).
|
|
17
|
-
* @property {
|
|
18
|
-
* @property {
|
|
19
|
-
* @property {
|
|
18
|
+
* @property {ReactNode} [label] 상단 라벨.
|
|
19
|
+
* @property {ReactNode} [helper] helper 텍스트.
|
|
20
|
+
* @property {InputState} [state="default"] 시각 상태.
|
|
20
21
|
* @property {(code: string) => void} [onComplete] 모든 셀이 채워졌을 때 호출.
|
|
21
22
|
*/
|
|
22
23
|
export interface IdentificationInputProps {
|
|
23
24
|
length?: number;
|
|
24
|
-
label?:
|
|
25
|
-
|
|
26
|
-
state?:
|
|
25
|
+
label?: ReactNode;
|
|
26
|
+
helper?: ReactNode;
|
|
27
|
+
state?: InputState;
|
|
27
28
|
onComplete?: (code: string) => void;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -32,15 +33,15 @@ export interface IdentificationInputProps {
|
|
|
32
33
|
* @component
|
|
33
34
|
* @param {IdentificationInputProps} props
|
|
34
35
|
* @param {number} [props.length=6] 입력 필드 길이. 4~8 범위로 자동 보정된다.
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {
|
|
37
|
-
* @param {
|
|
36
|
+
* @param {ReactNode} [props.label] 상단 label 콘텐츠.
|
|
37
|
+
* @param {ReactNode} [props.helper] helper 텍스트.
|
|
38
|
+
* @param {InputState} [props.state] 시각 상태.
|
|
38
39
|
* @param {(code: string) => void} [props.onComplete] 모든 셀이 채워졌을 때 호출되는 콜백.
|
|
39
40
|
*/
|
|
40
41
|
const IdentificationInput = forwardRef<
|
|
41
42
|
HTMLInputElement[],
|
|
42
43
|
IdentificationInputProps
|
|
43
|
-
>(({ length = 6, label,
|
|
44
|
+
>(({ length = 6, label, helper, state, onComplete }, forwardedRef) => {
|
|
44
45
|
const safeLength = Math.max(4, Math.min(8, length));
|
|
45
46
|
const [values, setValues] = useState(() =>
|
|
46
47
|
Array.from({ length: safeLength }, () => ""),
|
|
@@ -112,7 +113,7 @@ const IdentificationInput = forwardRef<
|
|
|
112
113
|
[onComplete, safeLength],
|
|
113
114
|
);
|
|
114
115
|
|
|
115
|
-
const helperNode = useMemo(() =>
|
|
116
|
+
const helperNode = useMemo(() => helper, [helper]);
|
|
116
117
|
|
|
117
118
|
// forwardRef 사용자는 각 셀 DOM 배열을 직접 제어할 수 있도록 노출한다.
|
|
118
119
|
useImperativeHandle(
|
|
@@ -5,61 +5,32 @@ import HideOffIcon from "../../img/hide-off.svg";
|
|
|
5
5
|
import HideOnIcon from "../../img/hide-on.svg";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* PasswordInput 전용 props. Text Input props에서 type
|
|
8
|
+
* PasswordInput 전용 props. Text Input props에서 type을 password 전용으로 고정하고 보기/숨김 토글 옵션을 확장한다.
|
|
9
9
|
* @property {boolean} [defaultVisible=false] 초기 렌더 시 비밀번호를 드러낼지 여부.
|
|
10
10
|
* @property {{show: string; hide: string}} [toggleLabel] 토글 버튼에 사용할 라벨 텍스트 집합.
|
|
11
|
-
* @property {string} [defaultValue] 비제어 초기값.
|
|
12
11
|
*/
|
|
13
|
-
export interface
|
|
14
|
-
InputProps,
|
|
15
|
-
"type" | "defaultValue"
|
|
16
|
-
> {
|
|
12
|
+
export interface InputPasswordProps extends Omit<InputProps, "type"> {
|
|
17
13
|
defaultVisible?: boolean;
|
|
18
14
|
toggleLabel?: {
|
|
19
15
|
show: string;
|
|
20
16
|
hide: string;
|
|
21
17
|
};
|
|
22
|
-
defaultValue?: string;
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
/**
|
|
26
|
-
* PasswordInput — 기본 Text 입력을 비밀번호 토글 UX로
|
|
21
|
+
* PasswordInput — 기본 Text 입력을 비밀번호 토글 UX로 확장한다.
|
|
27
22
|
* @component
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {"primary" | "secondary" | "tertiary"} [props.appearance="primary"] 토큰 세트.
|
|
30
|
-
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/spacing.
|
|
31
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.state="default"] 시각 상태.
|
|
32
|
-
* @param {boolean} [props.block=false] true면 width 100%.
|
|
33
|
-
* @param {React.ReactNode} [props.prefix] prefix 슬롯(Password에서는 기본 제공하지 않음).
|
|
34
|
-
* @param {React.ReactNode} [props.suffix] suffix 슬롯. 미지정 시 보기/숨김 토글 버튼 자동 배치.
|
|
35
|
-
* @param {React.ReactNode} [props.successIcon] success 상태 아이콘.
|
|
36
|
-
* @param {React.ReactNode} [props.errorIcon] error 상태 아이콘.
|
|
37
|
-
* @param {React.ReactNode} [props.label] label 영역 콘텐츠.
|
|
38
|
-
* @param {React.ReactNode} [props.helperText] helper 영역 텍스트.
|
|
39
|
-
* @param {boolean} [props.hideHelperText] helper 숨김 여부.
|
|
40
|
-
* @param {string} [props.inputClassName] `<input>` className.
|
|
41
|
-
* @param {string} [props.wrapperClassName] `.input-box` className.
|
|
42
|
-
* @param {object} [props.labelProps] label attr.
|
|
43
|
-
* @param {object} [props.helperTextProps] helper attr.
|
|
44
|
-
* @param {boolean} [props.disabled] native disabled.
|
|
45
|
-
* @param {string} [props.id] 외부 id.
|
|
46
|
-
* @param {string} [props.className] root className.
|
|
47
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.data-simulated-state]
|
|
48
|
-
* Storybook 시각 상태 강제용.
|
|
49
|
-
* @param {string} [props.defaultValue] 비제어 초기값.
|
|
50
|
-
* @param {string | number | readonly string[]} [props.value] 제어형 값.
|
|
51
|
-
* @param {(event: React.ChangeEvent<HTMLInputElement>) => void} [props.onChange] 변경 핸들러.
|
|
52
|
-
* @param {(event: React.FocusEvent<HTMLInputElement>) => void} [props.onFocus] focus 핸들러.
|
|
53
|
-
* @param {(event: React.FocusEvent<HTMLInputElement>) => void} [props.onBlur] blur 핸들러.
|
|
23
|
+
* @param {InputPasswordProps} props
|
|
54
24
|
* @param {boolean} [props.defaultVisible=false] 초기 노출 여부.
|
|
55
|
-
* @param {{show: string; hide: string}} [props.toggleLabel] 토글 버튼 라벨.
|
|
25
|
+
* @param {{show: string; hide: string}} [props.toggleLabel={show:"보기",hide:"숨김"}] 토글 버튼 라벨.
|
|
26
|
+
* 나머지 props는 Text Input과 동일하다.
|
|
56
27
|
*/
|
|
57
|
-
const PasswordInput = forwardRef<HTMLInputElement,
|
|
28
|
+
const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
58
29
|
(
|
|
59
30
|
{
|
|
60
31
|
defaultVisible = false,
|
|
61
32
|
toggleLabel = { show: "보기", hide: "숨김" },
|
|
62
|
-
|
|
33
|
+
right,
|
|
63
34
|
defaultValue,
|
|
64
35
|
...restProps
|
|
65
36
|
},
|
|
@@ -93,7 +64,7 @@ const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
93
64
|
ref={forwardedRef}
|
|
94
65
|
type={visible ? "text" : "password"}
|
|
95
66
|
defaultValue={defaultValue}
|
|
96
|
-
|
|
67
|
+
right={right ?? toggleButton}
|
|
97
68
|
/>
|
|
98
69
|
);
|
|
99
70
|
},
|
|
@@ -10,19 +10,13 @@ import { Text } from "./Base";
|
|
|
10
10
|
* @property {string} [defaultValue] 비제어 초기값(숫자/포맷 모두 허용).
|
|
11
11
|
* @property {(value: string, digits: string) => void} [onValueChange] 포맷팅 값/숫자만 값을 함께 전달.
|
|
12
12
|
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
|
|
13
|
-
* @property {() => void} [onRequestCode]
|
|
14
|
-
* @property {string} [requestButtonLabel="인증번호 요청"]
|
|
15
|
-
* @property {boolean} [requestButtonDisabled]
|
|
13
|
+
* @property {() => void} [onRequestCode] right 슬롯 버튼 클릭 시 호출.
|
|
14
|
+
* @property {string} [requestButtonLabel="인증번호 요청"] right 슬롯 버튼 라벨.
|
|
15
|
+
* @property {boolean} [requestButtonDisabled] right 슬롯 버튼 disabled 여부.
|
|
16
16
|
*/
|
|
17
17
|
export interface PhoneInputProps extends Omit<
|
|
18
18
|
InputProps,
|
|
19
|
-
| "
|
|
20
|
-
| "prefix"
|
|
21
|
-
| "inputMode"
|
|
22
|
-
| "pattern"
|
|
23
|
-
| "onChange"
|
|
24
|
-
| "value"
|
|
25
|
-
| "defaultValue"
|
|
19
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
26
20
|
> {
|
|
27
21
|
value?: string;
|
|
28
22
|
defaultValue?: string;
|
|
@@ -43,33 +37,8 @@ const formatPhoneNumber = (digits: string) => maskPhone(digits);
|
|
|
43
37
|
/**
|
|
44
38
|
* PhoneInput — 휴대폰 번호 마스킹과 인증번호 요청 버튼을 제공하는 입력.
|
|
45
39
|
* @component
|
|
46
|
-
* @param {PhoneInputProps} props
|
|
47
|
-
*
|
|
48
|
-
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/spacing.
|
|
49
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.state="default"] 시각 상태.
|
|
50
|
-
* @param {boolean} [props.block=false] true면 width 100%.
|
|
51
|
-
* @param {React.ReactNode} [props.suffix] suffix 슬롯. 미지정 시 인증 버튼 자동 배치.
|
|
52
|
-
* @param {React.ReactNode} [props.successIcon] success 상태 아이콘.
|
|
53
|
-
* @param {React.ReactNode} [props.errorIcon] error 상태 아이콘.
|
|
54
|
-
* @param {React.ReactNode} [props.label] label 콘텐츠.
|
|
55
|
-
* @param {React.ReactNode} [props.helperText] helper 텍스트.
|
|
56
|
-
* @param {boolean} [props.hideHelperText] helper 숨김 여부.
|
|
57
|
-
* @param {string} [props.inputClassName] `<input>` className.
|
|
58
|
-
* @param {string} [props.wrapperClassName] `.input-box` className.
|
|
59
|
-
* @param {object} [props.labelProps] label attr.
|
|
60
|
-
* @param {object} [props.helperTextProps] helper attr.
|
|
61
|
-
* @param {boolean} [props.disabled] native disabled.
|
|
62
|
-
* @param {string} [props.id] 외부 id.
|
|
63
|
-
* @param {string} [props.className] root className.
|
|
64
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.data-simulated-state]
|
|
65
|
-
* Storybook 시각 상태 강제용.
|
|
66
|
-
* @param {string} [props.value] 제어형 값(포맷팅).
|
|
67
|
-
* @param {string} [props.defaultValue] 비제어 초기값.
|
|
68
|
-
* @param {(value: string, digits: string) => void} [props.onValueChange] 포맷팅/숫자 값 동시 전달.
|
|
69
|
-
* @param {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override.
|
|
70
|
-
* @param {() => void} [props.onRequestCode] 인증번호 요청 버튼 핸들러.
|
|
71
|
-
* @param {string} [props.requestButtonLabel="인증번호 요청"] 버튼 라벨.
|
|
72
|
-
* @param {boolean} [props.requestButtonDisabled] 버튼 disabled.
|
|
40
|
+
* @param {PhoneInputProps} props Phone 전용 props.
|
|
41
|
+
* 나머지 Text Input 공통 props(priority/size/state/helper 등)도 동일하게 사용할 수 있다.
|
|
73
42
|
*/
|
|
74
43
|
const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
75
44
|
(
|
|
@@ -81,7 +50,7 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
81
50
|
onRequestCode,
|
|
82
51
|
requestButtonLabel = "인증번호 요청",
|
|
83
52
|
requestButtonDisabled,
|
|
84
|
-
|
|
53
|
+
right,
|
|
85
54
|
...restProps
|
|
86
55
|
},
|
|
87
56
|
forwardedRef,
|
|
@@ -119,7 +88,7 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
119
88
|
if (!onRequestCode) {
|
|
120
89
|
return null;
|
|
121
90
|
}
|
|
122
|
-
// 휴대폰 인증 입력은 우측
|
|
91
|
+
// 휴대폰 인증 입력은 우측 right 슬롯에 인증 버튼을 둔다.
|
|
123
92
|
return (
|
|
124
93
|
<button
|
|
125
94
|
type="button"
|
|
@@ -140,7 +109,7 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
140
109
|
inputMode="tel"
|
|
141
110
|
value={formattedValue}
|
|
142
111
|
onChange={handleChange}
|
|
143
|
-
|
|
112
|
+
right={right ?? actionButton}
|
|
144
113
|
/>
|
|
145
114
|
);
|
|
146
115
|
},
|
|
@@ -9,52 +9,21 @@ import SearchIcon from "../../img/search.svg";
|
|
|
9
9
|
export interface SearchInputProps extends Omit<InputProps, "type"> {}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* SearchInput — 기본 Text 입력에 검색 아이콘
|
|
13
|
-
*
|
|
14
|
-
* @param {SearchInputProps} props
|
|
15
|
-
* @param {"primary" | "secondary" | "tertiary"} [props.appearance="primary"] 토큰 세트.
|
|
16
|
-
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이/spacing.
|
|
17
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.state="default"] 시각 상태.
|
|
18
|
-
* @param {boolean} [props.block=false] true면 width 100%.
|
|
19
|
-
* @param {React.ReactNode} [props.prefix] prefix 슬롯. 미지정 시 돋보기 아이콘이 자동 배치된다.
|
|
20
|
-
* @param {React.ReactNode} [props.suffix] suffix 슬롯.
|
|
21
|
-
* @param {React.ReactNode} [props.successIcon] success 상태 아이콘.
|
|
22
|
-
* @param {React.ReactNode} [props.errorIcon] error 상태 아이콘.
|
|
23
|
-
* @param {React.ReactNode} [props.label] label 콘텐츠.
|
|
24
|
-
* @param {React.ReactNode} [props.helperText] helper 텍스트.
|
|
25
|
-
* @param {boolean} [props.hideHelperText] helper 숨김 여부.
|
|
26
|
-
* @param {string} [props.inputClassName] `<input>` className.
|
|
27
|
-
* @param {string} [props.wrapperClassName] `.input-box` className.
|
|
28
|
-
* @param {object} [props.labelProps] label attr.
|
|
29
|
-
* @param {object} [props.helperTextProps] helper attr.
|
|
30
|
-
* @param {boolean} [props.disabled] native disabled.
|
|
31
|
-
* @param {string} [props.id] 외부 id.
|
|
32
|
-
* @param {string} [props.className] root className.
|
|
33
|
-
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled"} [props.data-simulated-state]
|
|
34
|
-
* Storybook 시각 상태 강제용.
|
|
35
|
-
* @param {string | number | readonly string[]} [props.defaultValue] 비제어 초기값.
|
|
36
|
-
* @param {string | number | readonly string[]} [props.value] 제어형 값.
|
|
37
|
-
* @param {(event: React.ChangeEvent<HTMLInputElement>) => void} [props.onChange] 입력 변경 핸들러.
|
|
38
|
-
* @param {(event: React.FocusEvent<HTMLInputElement>) => void} [props.onFocus] focus 핸들러.
|
|
39
|
-
* @param {(event: React.FocusEvent<HTMLInputElement>) => void} [props.onBlur] blur 핸들러.
|
|
12
|
+
* SearchInput — 기본 Text 입력에 검색 아이콘 left 슬롯과 type="search"를 제공한다.
|
|
13
|
+
* 다른 props(priority/size/helper 등)는 Text Input과 동일하다.
|
|
40
14
|
*/
|
|
41
15
|
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
42
|
-
({
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
16
|
+
({ left, ...restProps }, forwardedRef) => {
|
|
17
|
+
const leftNode = useMemo(() => {
|
|
18
|
+
if (left) {
|
|
19
|
+
return left;
|
|
46
20
|
}
|
|
47
|
-
// 검색 input은 기본적으로 돋보기
|
|
21
|
+
// 검색 input은 기본적으로 돋보기 아이콘을 left 슬롯에 노출한다.
|
|
48
22
|
return <SearchIcon aria-hidden="true" />;
|
|
49
|
-
}, [
|
|
23
|
+
}, [left]);
|
|
50
24
|
|
|
51
25
|
return (
|
|
52
|
-
<Text
|
|
53
|
-
{...restProps}
|
|
54
|
-
ref={forwardedRef}
|
|
55
|
-
type="search"
|
|
56
|
-
prefix={prefixNode}
|
|
57
|
-
/>
|
|
26
|
+
<Text {...restProps} ref={forwardedRef} type="search" left={leftNode} />
|
|
58
27
|
);
|
|
59
28
|
},
|
|
60
29
|
);
|
|
@@ -4,7 +4,7 @@ export { PasswordInput } from "./Password";
|
|
|
4
4
|
export { PhoneInput } from "./Phone";
|
|
5
5
|
export { SearchInput } from "./Search";
|
|
6
6
|
export { IdentificationInput } from "./Identification";
|
|
7
|
-
export type {
|
|
7
|
+
export type { InputPasswordProps } from "./Password";
|
|
8
8
|
export type { PhoneInputProps } from "./Phone";
|
|
9
9
|
export type { SearchInputProps } from "./Search";
|
|
10
10
|
export type { IdentificationInputProps } from "./Identification";
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
min-height: var(--theme-input-height-large);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
&[data-
|
|
99
|
+
&[data-priority="secondary"] {
|
|
100
100
|
border: none;
|
|
101
101
|
border-bottom: var(--theme-input-border-width-default) solid
|
|
102
102
|
var(--theme-input-border-color);
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
background-color: transparent;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
&[data-
|
|
109
|
+
&[data-priority="tertiary"] {
|
|
110
110
|
border-radius: var(--theme-input-radius-tertiary);
|
|
111
111
|
background-color: var(--theme-input-surface);
|
|
112
112
|
min-height: var(--theme-input-height-tertiary);
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
&:not([data-
|
|
133
|
+
&:not([data-priority="secondary"]) {
|
|
134
134
|
&[data-state="active"],
|
|
135
135
|
&[data-state="focused"] {
|
|
136
136
|
border-color: var(--theme-input-border-active);
|
|
@@ -196,11 +196,11 @@
|
|
|
196
196
|
color: var(--theme-input-label-color);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
.input-field[data-
|
|
199
|
+
.input-field[data-priority="secondary"] .input-element {
|
|
200
200
|
padding-inline: 0;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
.input-field[data-
|
|
203
|
+
.input-field[data-priority="tertiary"] .input-element {
|
|
204
204
|
min-height: var(--theme-size-medium-2);
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -225,18 +225,18 @@
|
|
|
225
225
|
min-width: 20px;
|
|
226
226
|
color: var(--theme-input-helper-color);
|
|
227
227
|
|
|
228
|
-
&--
|
|
228
|
+
&--left {
|
|
229
229
|
order: -1;
|
|
230
230
|
margin-right: var(--spacing-gap-3);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
&--
|
|
234
|
-
&--
|
|
233
|
+
&--right,
|
|
234
|
+
&--clear,
|
|
235
235
|
&--status {
|
|
236
236
|
margin-left: var(--spacing-gap-3);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
&--
|
|
239
|
+
&--clear,
|
|
240
240
|
&--status {
|
|
241
241
|
color: var(--theme-input-text-color);
|
|
242
242
|
}
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
color: var(--color-primary-default);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
-
.input-field[data-
|
|
253
|
+
.input-field[data-priority="secondary"] {
|
|
254
254
|
border-bottom-width: var(--theme-input-border-width-default);
|
|
255
255
|
|
|
256
256
|
&[data-state="active"],
|
|
@@ -307,7 +307,7 @@
|
|
|
307
307
|
border-color: var(--theme-input-border-color);
|
|
308
308
|
background-color: var(--theme-input-surface-disabled);
|
|
309
309
|
|
|
310
|
-
&[data-
|
|
310
|
+
&[data-priority="secondary"] {
|
|
311
311
|
background-color: transparent;
|
|
312
312
|
}
|
|
313
313
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef,
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import type { UseFormRegisterReturn } from "react-hook-form";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* priority 축은 tokens 기반 테마 계층을 지정한다.
|
|
5
6
|
*/
|
|
6
|
-
export const
|
|
7
|
+
export const INPUT_PRIORITIES = ["primary", "secondary", "tertiary"] as const;
|
|
7
8
|
/**
|
|
8
9
|
* size 축은 높이/타이포/spacing을 결정한다.
|
|
9
10
|
*/
|
|
@@ -20,48 +21,56 @@ export const INPUT_STATES = [
|
|
|
20
21
|
"disabled",
|
|
21
22
|
] as const;
|
|
22
23
|
|
|
23
|
-
export type
|
|
24
|
+
export type InputPriority = (typeof INPUT_PRIORITIES)[number];
|
|
24
25
|
export type InputSize = (typeof INPUT_SIZES)[number];
|
|
25
26
|
export type InputState = (typeof INPUT_STATES)[number];
|
|
26
27
|
|
|
27
28
|
type NativeInputProps = ComponentPropsWithoutRef<"input">;
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* 좌우 슬롯과 status 아이콘 정의.
|
|
31
32
|
*/
|
|
32
|
-
export interface
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
export interface InputSlots {
|
|
34
|
+
left?: ReactNode;
|
|
35
|
+
right?: ReactNode;
|
|
36
|
+
clearIcon?: ReactNode;
|
|
36
37
|
successIcon?: ReactNode;
|
|
37
38
|
errorIcon?: ReactNode;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* label/
|
|
42
|
+
* label/helper 등 피드백 슬롯 정의.
|
|
42
43
|
*/
|
|
43
44
|
export interface InputFeedback {
|
|
44
45
|
label?: ReactNode;
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
helper?: ReactNode;
|
|
47
|
+
hideHelper?: boolean;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* 텍스트 입력의 핵심 props. native input 속성에서 size
|
|
51
|
+
* 텍스트 입력의 핵심 props. native input 속성에서 size는 제외하고 left/right 등 슬롯을 별도로 정의한다.
|
|
51
52
|
*/
|
|
52
53
|
export interface InputProps
|
|
53
|
-
extends
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
appearance?: InputAppearance;
|
|
54
|
+
extends Omit<NativeInputProps, "size">, InputSlots, InputFeedback {
|
|
55
|
+
/** semantic color/token 세트 */
|
|
56
|
+
priority?: InputPriority;
|
|
57
|
+
/** 높이/타이포 세트 */
|
|
58
58
|
size?: InputSize;
|
|
59
|
+
/** 시각 상태. disabled prop과 조합된다 */
|
|
59
60
|
state?: InputState;
|
|
61
|
+
/** true면 width:100% */
|
|
60
62
|
block?: boolean;
|
|
63
|
+
/** 실제 `<input>` className */
|
|
61
64
|
inputClassName?: string;
|
|
62
|
-
|
|
65
|
+
/** `.input-box` className */
|
|
66
|
+
boxClassName?: string;
|
|
67
|
+
/** label attr 커스터마이즈 */
|
|
63
68
|
labelProps?: ComponentPropsWithoutRef<"label">;
|
|
64
|
-
|
|
69
|
+
/** helper attr 커스터마이즈 */
|
|
70
|
+
helperProps?: ComponentPropsWithoutRef<"div">;
|
|
71
|
+
/** react-hook-form register 반환값 */
|
|
72
|
+
register?: UseFormRegisterReturn;
|
|
73
|
+
/** Storybook 등에서 강제 상태 표현용 */
|
|
65
74
|
"data-simulated-state"?: InputState;
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -69,7 +78,7 @@ export interface InputProps
|
|
|
69
78
|
* className composer helper가 필요로 하는 파라미터 집합.
|
|
70
79
|
*/
|
|
71
80
|
export interface InputClassNameOptions {
|
|
72
|
-
|
|
81
|
+
priority: InputPriority;
|
|
73
82
|
size: InputSize;
|
|
74
83
|
state: InputState;
|
|
75
84
|
block: boolean;
|
|
@@ -11,7 +11,7 @@ const INPUT_AFFIX_CLASSNAME = "input-affix";
|
|
|
11
11
|
* container `.input` element className을 조립한다.
|
|
12
12
|
*/
|
|
13
13
|
const composeInputClassName = ({
|
|
14
|
-
|
|
14
|
+
priority,
|
|
15
15
|
size,
|
|
16
16
|
state,
|
|
17
17
|
block,
|
|
@@ -19,7 +19,7 @@ const composeInputClassName = ({
|
|
|
19
19
|
}: InputClassNameOptions) =>
|
|
20
20
|
clsx(
|
|
21
21
|
INPUT_CLASSNAME,
|
|
22
|
-
`${INPUT_CLASSNAME}--
|
|
22
|
+
`${INPUT_CLASSNAME}--priority-${priority}`,
|
|
23
23
|
`${INPUT_CLASSNAME}--size-${size}`,
|
|
24
24
|
{
|
|
25
25
|
[`${INPUT_CLASSNAME}--state-${state}`]: state !== "default",
|
|
@@ -32,7 +32,7 @@ const composeInputClassName = ({
|
|
|
32
32
|
* 박스(wrapper) `.input-box` className을 조립한다.
|
|
33
33
|
*/
|
|
34
34
|
const composeInputBoxClassName = ({
|
|
35
|
-
|
|
35
|
+
priority,
|
|
36
36
|
size,
|
|
37
37
|
state,
|
|
38
38
|
block,
|
|
@@ -40,7 +40,7 @@ const composeInputBoxClassName = ({
|
|
|
40
40
|
}: InputClassNameOptions) =>
|
|
41
41
|
clsx(
|
|
42
42
|
INPUT_BOX_CLASSNAME,
|
|
43
|
-
`${INPUT_BOX_CLASSNAME}--
|
|
43
|
+
`${INPUT_BOX_CLASSNAME}--priority-${priority}`,
|
|
44
44
|
`${INPUT_BOX_CLASSNAME}--size-${size}`,
|
|
45
45
|
{
|
|
46
46
|
[`${INPUT_BOX_CLASSNAME}--state-${state}`]: state !== "default",
|
package/src/index.tsx
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import type { InputProps } from "../components/input/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input Field Attr; HTML native 속성 래핑
|
|
6
|
+
* @typedef {InputFieldAttr}
|
|
7
|
+
* @template FieldElement
|
|
8
|
+
* @desc
|
|
9
|
+
* - InputHTMLAttributes를 그대로 재사용해 네이티브 속성을 전달한다.
|
|
10
|
+
*/
|
|
11
|
+
export type InputFieldAttr<
|
|
12
|
+
FieldElement extends HTMLElement = HTMLInputElement,
|
|
13
|
+
> = InputHTMLAttributes<FieldElement>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Input Field Style; 레이아웃 래퍼 스타일
|
|
17
|
+
* @interface InputFieldStyle
|
|
18
|
+
* @desc
|
|
19
|
+
* - className/block 등 래퍼 스타일만 관리한다.
|
|
20
|
+
*/
|
|
21
|
+
export interface InputFieldStyle {
|
|
22
|
+
/**
|
|
23
|
+
* 필드 컨테이너 className
|
|
24
|
+
*/
|
|
25
|
+
className?: string;
|
|
26
|
+
/**
|
|
27
|
+
* label 컨테이너 className
|
|
28
|
+
*/
|
|
29
|
+
labelClassName?: string;
|
|
30
|
+
/**
|
|
31
|
+
* helper 컨테이너 className
|
|
32
|
+
*/
|
|
33
|
+
helperClassName?: string;
|
|
34
|
+
/**
|
|
35
|
+
* true면 block 레이아웃
|
|
36
|
+
*/
|
|
37
|
+
block?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Input Field Options; UX 텍스트/설명
|
|
42
|
+
* @interface InputFieldOptions
|
|
43
|
+
* @desc
|
|
44
|
+
* - label/helper/description 등 사용자-facing 정보를 정의한다.
|
|
45
|
+
*/
|
|
46
|
+
export interface InputFieldOptions {
|
|
47
|
+
/**
|
|
48
|
+
* 라벨 텍스트 또는 노드
|
|
49
|
+
*/
|
|
50
|
+
label?: ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* helper 텍스트
|
|
53
|
+
*/
|
|
54
|
+
helper?: ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* 접근성 안내/설명
|
|
57
|
+
*/
|
|
58
|
+
description?: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Input Field Config; attr + style + options + props
|
|
63
|
+
* @interface InputFieldConfig
|
|
64
|
+
* @template TProps
|
|
65
|
+
* @template FieldElement
|
|
66
|
+
*/
|
|
67
|
+
export interface InputFieldConfig<
|
|
68
|
+
TProps extends InputProps = InputProps,
|
|
69
|
+
FieldElement extends HTMLElement = HTMLInputElement,
|
|
70
|
+
>
|
|
71
|
+
extends InputFieldStyle, InputFieldOptions {
|
|
72
|
+
/**
|
|
73
|
+
* HTML native attr
|
|
74
|
+
*/
|
|
75
|
+
attr?: InputFieldAttr<FieldElement>;
|
|
76
|
+
/**
|
|
77
|
+
* primitives Input 계열 props
|
|
78
|
+
*/
|
|
79
|
+
props?: TProps;
|
|
80
|
+
}
|
package/src/types/index.ts
CHANGED