@uniai-fe/uds-primitives 0.3.21 → 0.3.22
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 +7 -0
- package/dist/styles.css +67 -1
- package/package.json +1 -1
- package/src/components/input/markup/foundation/TextArea.tsx +236 -0
- package/src/components/input/markup/foundation/index.tsx +2 -0
- package/src/components/input/styles/foundation.scss +51 -1
- package/src/components/input/styles/variables.scss +21 -0
- package/src/components/input/types/index.ts +1 -0
- package/src/components/input/types/textarea.ts +73 -0
package/README.md
CHANGED
|
@@ -114,6 +114,13 @@ function Templates() {
|
|
|
114
114
|
|
|
115
115
|
- TextInput Base 컴포넌트의 `clear` 버튼 로직을 pointer 이벤트 기반으로 재작성해 모바일 터치 환경에서도 안정적으로 입력값이 초기화되도록 했다.
|
|
116
116
|
- react-hook-form `register`와 연계된 값도 동일하게 초기화되며, focus가 빠지면 clear 버튼이 자동으로 숨겨진다.
|
|
117
|
+
- `Input.TextArea`를 추가했다.
|
|
118
|
+
- `height?: number | string`으로 높이를 직접 제어한다. (기본 `128px`)
|
|
119
|
+
- `length?: number`을 주면 `0 / n자` 카운터가 우측 하단에 노출된다.
|
|
120
|
+
- 기본 `resize`는 비활성(`none`)이며, `size` 축은 Input 기본 size token(`--input-default-*`)을 재사용한다.
|
|
121
|
+
- Select/Dropdown 계약을 `items` 중심으로 정렬했다.
|
|
122
|
+
- Select: `items`, `onSelectChange`, `dropdownOptions`, `open/defaultOpen/onOpen`
|
|
123
|
+
- Dropdown.Template: `items[].selected + onChange(payload)`
|
|
117
124
|
|
|
118
125
|
## 스타일 내보내기
|
|
119
126
|
|
package/dist/styles.css
CHANGED
|
@@ -361,6 +361,27 @@
|
|
|
361
361
|
--input-tertiary-radius-base: var(--theme-radius-large-2);
|
|
362
362
|
--input-table-radius-base: 0;
|
|
363
363
|
--input-tertiary-element-min-height: var(--theme-size-medium-2);
|
|
364
|
+
--input-textarea-min-height: calc(var(--input-default-height-medium) * 2);
|
|
365
|
+
--input-textarea-height: 128px;
|
|
366
|
+
/* TextArea size token은 Input 기본 size token을 재할당해 축을 일치시킨다. */
|
|
367
|
+
--input-textarea-radius-small: var(--input-default-radius-base);
|
|
368
|
+
--input-textarea-radius-medium: var(--input-default-radius-base);
|
|
369
|
+
--input-textarea-radius-large: var(--input-default-radius-base);
|
|
370
|
+
--input-textarea-padding-inline-small: var(--spacing-padding-6);
|
|
371
|
+
--input-textarea-padding-inline-medium: var(--spacing-padding-6);
|
|
372
|
+
--input-textarea-padding-inline-large: var(--spacing-padding-6);
|
|
373
|
+
--input-textarea-padding-block-small: var(--spacing-padding-5);
|
|
374
|
+
--input-textarea-padding-block-medium: var(--spacing-padding-5);
|
|
375
|
+
--input-textarea-padding-block-large: var(--spacing-padding-6);
|
|
376
|
+
--input-textarea-gap-small: var(--input-default-gap);
|
|
377
|
+
--input-textarea-gap-medium: var(--input-default-gap);
|
|
378
|
+
--input-textarea-gap-large: var(--input-default-gap);
|
|
379
|
+
--input-textarea-counter-font-size: var(--font-label-medium-size);
|
|
380
|
+
--input-textarea-counter-line-height: var(--font-label-medium-line-height);
|
|
381
|
+
--input-textarea-counter-font-weight: var(--font-label-medium-weight);
|
|
382
|
+
--input-textarea-counter-letter-spacing: var(
|
|
383
|
+
--font-label-medium-letter-spacing
|
|
384
|
+
);
|
|
364
385
|
--input-table-text-small-size: var(--font-body-xxsmall-size);
|
|
365
386
|
--input-table-text-small-line-height: var(--font-body-xxsmall-line-height);
|
|
366
387
|
--input-table-text-small-weight: var(--font-body-xxsmall-weight);
|
|
@@ -2730,6 +2751,47 @@ figure.chip {
|
|
|
2730
2751
|
min-width: 0;
|
|
2731
2752
|
}
|
|
2732
2753
|
|
|
2754
|
+
.input[data-input-type=textarea] .input-field {
|
|
2755
|
+
--input-textarea-radius: var(--input-textarea-radius-medium);
|
|
2756
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-medium);
|
|
2757
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-medium);
|
|
2758
|
+
--input-textarea-gap: var(--input-textarea-gap-medium);
|
|
2759
|
+
align-items: stretch;
|
|
2760
|
+
flex-direction: column;
|
|
2761
|
+
gap: var(--input-textarea-gap);
|
|
2762
|
+
border-radius: var(--input-textarea-radius);
|
|
2763
|
+
padding: var(--input-textarea-padding-block) var(--input-textarea-padding-inline);
|
|
2764
|
+
}
|
|
2765
|
+
.input[data-input-type=textarea] .input-field[data-size=small] {
|
|
2766
|
+
--input-textarea-radius: var(--input-textarea-radius-small);
|
|
2767
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-small);
|
|
2768
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-small);
|
|
2769
|
+
--input-textarea-gap: var(--input-textarea-gap-small);
|
|
2770
|
+
}
|
|
2771
|
+
.input[data-input-type=textarea] .input-field[data-size=large] {
|
|
2772
|
+
--input-textarea-radius: var(--input-textarea-radius-large);
|
|
2773
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-large);
|
|
2774
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-large);
|
|
2775
|
+
--input-textarea-gap: var(--input-textarea-gap-large);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
.input-textarea-element {
|
|
2779
|
+
min-height: var(--input-textarea-min-height);
|
|
2780
|
+
height: var(--input-textarea-height);
|
|
2781
|
+
resize: none;
|
|
2782
|
+
margin: 0;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
.input-textarea-length {
|
|
2786
|
+
margin: 0;
|
|
2787
|
+
align-self: flex-end;
|
|
2788
|
+
color: var(--input-placeholder-color);
|
|
2789
|
+
font-size: var(--input-textarea-counter-font-size);
|
|
2790
|
+
line-height: var(--input-textarea-counter-line-height);
|
|
2791
|
+
font-weight: var(--input-textarea-counter-font-weight);
|
|
2792
|
+
letter-spacing: var(--input-textarea-counter-letter-spacing);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2733
2795
|
.input-field-utilities {
|
|
2734
2796
|
display: flex;
|
|
2735
2797
|
align-items: center;
|
|
@@ -2851,11 +2913,15 @@ figure.chip {
|
|
|
2851
2913
|
.input[data-state=error] .input-helper-text {
|
|
2852
2914
|
color: var(--input-label-error-color);
|
|
2853
2915
|
}
|
|
2916
|
+
.input[data-state=error] .input-textarea-length {
|
|
2917
|
+
color: var(--input-label-error-color);
|
|
2918
|
+
}
|
|
2854
2919
|
|
|
2855
2920
|
.input[data-state=disabled] .input-label,
|
|
2856
2921
|
.input[data-state=disabled] .input-inline-label,
|
|
2857
2922
|
.input[data-state=disabled] .input-helper-text,
|
|
2858
|
-
.input[data-state=disabled] .input-affix
|
|
2923
|
+
.input[data-state=disabled] .input-affix,
|
|
2924
|
+
.input[data-state=disabled] .input-textarea-length {
|
|
2859
2925
|
color: var(--input-helper-disabled-color);
|
|
2860
2926
|
}
|
|
2861
2927
|
.input[data-state=disabled] .input-field {
|
package/package.json
CHANGED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import type { CSSProperties, ChangeEvent, FocusEvent } from "react";
|
|
5
|
+
import {
|
|
6
|
+
forwardRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useMemo,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
import type { InputTextAreaProps } from "../../types";
|
|
14
|
+
import {
|
|
15
|
+
getFormFieldWidthAttr,
|
|
16
|
+
getFormFieldWidthValue,
|
|
17
|
+
} from "../../../form/utils/form-field";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Native `<textarea>` 기반 텍스트 영역.
|
|
21
|
+
* Input.Base와 동일한 priority/size/state/width/register 계약을 사용한다.
|
|
22
|
+
*
|
|
23
|
+
* @component
|
|
24
|
+
* @param {InputTextAreaProps} props TextArea 컴포넌트 공통 props
|
|
25
|
+
*/
|
|
26
|
+
const InputTextArea = forwardRef<HTMLTextAreaElement, InputTextAreaProps>(
|
|
27
|
+
(
|
|
28
|
+
{
|
|
29
|
+
priority = "primary",
|
|
30
|
+
size = "medium",
|
|
31
|
+
state: stateProp = "default",
|
|
32
|
+
block = false,
|
|
33
|
+
width,
|
|
34
|
+
inlineLabel,
|
|
35
|
+
inputClassName,
|
|
36
|
+
boxClassName: boxClassNameProp,
|
|
37
|
+
disabled,
|
|
38
|
+
id,
|
|
39
|
+
className,
|
|
40
|
+
register,
|
|
41
|
+
"data-simulated-state": simulatedState,
|
|
42
|
+
height,
|
|
43
|
+
length,
|
|
44
|
+
maxLength,
|
|
45
|
+
value,
|
|
46
|
+
defaultValue,
|
|
47
|
+
name,
|
|
48
|
+
onChange,
|
|
49
|
+
onFocus,
|
|
50
|
+
onBlur,
|
|
51
|
+
...restProps
|
|
52
|
+
},
|
|
53
|
+
ref,
|
|
54
|
+
) => {
|
|
55
|
+
const isReadOnly = restProps.readOnly === true;
|
|
56
|
+
// table priority는 width 지정이 없으면 셀 가로폭을 기본으로 채운다.
|
|
57
|
+
const isTablePriority = priority === "table";
|
|
58
|
+
const resolvedBlock = block || (isTablePriority && width === undefined);
|
|
59
|
+
// tertiary는 디자인 계약상 small 단일 사이즈만 사용한다.
|
|
60
|
+
const resolvedSize = priority === "tertiary" ? "small" : size;
|
|
61
|
+
const generatedId = useId();
|
|
62
|
+
const registerRef = register?.ref;
|
|
63
|
+
const registerOnChange = register?.onChange;
|
|
64
|
+
const registerOnBlur = register?.onBlur;
|
|
65
|
+
const registerName = register?.name;
|
|
66
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
67
|
+
const [currentLength, setCurrentLength] = useState(() => {
|
|
68
|
+
const initial = value ?? defaultValue;
|
|
69
|
+
return initial !== undefined && initial !== null
|
|
70
|
+
? String(initial).length
|
|
71
|
+
: 0;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (disabled || stateProp === "disabled" || isReadOnly) {
|
|
76
|
+
setIsFocused(false);
|
|
77
|
+
}
|
|
78
|
+
}, [disabled, isReadOnly, stateProp]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (value !== undefined && value !== null) {
|
|
82
|
+
setCurrentLength(String(value).length);
|
|
83
|
+
}
|
|
84
|
+
}, [value]);
|
|
85
|
+
|
|
86
|
+
const currentState = disabled ? "disabled" : stateProp;
|
|
87
|
+
const isDisabled =
|
|
88
|
+
currentState === "disabled" || currentState === "loading";
|
|
89
|
+
// tertiary는 status feedback(success/error/active/focused) 컬러를 적용하지 않는다.
|
|
90
|
+
const visualState =
|
|
91
|
+
priority === "tertiary"
|
|
92
|
+
? isDisabled
|
|
93
|
+
? "disabled"
|
|
94
|
+
: "default"
|
|
95
|
+
: !isDisabled && isFocused
|
|
96
|
+
? "active"
|
|
97
|
+
: currentState;
|
|
98
|
+
|
|
99
|
+
const setTextAreaRef = useCallback(
|
|
100
|
+
(node: HTMLTextAreaElement | null) => {
|
|
101
|
+
if (typeof ref === "function") {
|
|
102
|
+
ref(node);
|
|
103
|
+
} else if (ref) {
|
|
104
|
+
ref.current = node;
|
|
105
|
+
}
|
|
106
|
+
registerRef?.(node);
|
|
107
|
+
},
|
|
108
|
+
[ref, registerRef],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
112
|
+
if (!isReadOnly) {
|
|
113
|
+
setIsFocused(true);
|
|
114
|
+
}
|
|
115
|
+
onFocus?.(event);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
119
|
+
setIsFocused(false);
|
|
120
|
+
registerOnBlur?.(event);
|
|
121
|
+
onBlur?.(event);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
125
|
+
setCurrentLength(event.currentTarget.value.length);
|
|
126
|
+
registerOnChange?.(event);
|
|
127
|
+
onChange?.(event);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const textAreaName = registerName ?? name;
|
|
131
|
+
const widthAttr =
|
|
132
|
+
width !== undefined
|
|
133
|
+
? getFormFieldWidthAttr(width)
|
|
134
|
+
: resolvedBlock
|
|
135
|
+
? "full"
|
|
136
|
+
: undefined;
|
|
137
|
+
const widthValue =
|
|
138
|
+
width !== undefined ? getFormFieldWidthValue(width) : undefined;
|
|
139
|
+
// height는 runtime 값이므로 CSS variable을 인라인 주입한다.
|
|
140
|
+
const resolvedHeight = useMemo(() => {
|
|
141
|
+
if (typeof height === "number") {
|
|
142
|
+
return `${height}px`;
|
|
143
|
+
}
|
|
144
|
+
if (typeof height === "string" && height.length > 0) {
|
|
145
|
+
return height;
|
|
146
|
+
}
|
|
147
|
+
return "128px";
|
|
148
|
+
}, [height]);
|
|
149
|
+
const containerStyle = {
|
|
150
|
+
...(widthValue !== undefined
|
|
151
|
+
? ({ ["--input-width" as const]: widthValue } as CSSProperties)
|
|
152
|
+
: {}),
|
|
153
|
+
["--input-textarea-height" as const]: resolvedHeight,
|
|
154
|
+
} as CSSProperties;
|
|
155
|
+
const showLength = typeof length === "number";
|
|
156
|
+
const resolvedMaxLength = maxLength ?? length;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
className={clsx(
|
|
161
|
+
"input",
|
|
162
|
+
"input-type-textarea",
|
|
163
|
+
`input-priority-${priority}`,
|
|
164
|
+
`input-size-${resolvedSize}`,
|
|
165
|
+
`input-state-${visualState}`,
|
|
166
|
+
resolvedBlock && "input-block",
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
data-input-type="textarea"
|
|
170
|
+
data-priority={priority}
|
|
171
|
+
data-size={resolvedSize}
|
|
172
|
+
data-state={visualState}
|
|
173
|
+
data-readonly={isReadOnly ? "true" : undefined}
|
|
174
|
+
data-block={resolvedBlock ? "true" : undefined}
|
|
175
|
+
{...(simulatedState ? { "data-simulated-state": simulatedState } : {})}
|
|
176
|
+
data-width={widthAttr}
|
|
177
|
+
style={containerStyle}
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
className={clsx(
|
|
181
|
+
"input-box",
|
|
182
|
+
`input-box-priority-${priority}`,
|
|
183
|
+
`input-box-size-${resolvedSize}`,
|
|
184
|
+
`input-box-state-${visualState}`,
|
|
185
|
+
resolvedBlock && "input-box-block",
|
|
186
|
+
boxClassNameProp,
|
|
187
|
+
)}
|
|
188
|
+
data-slot="box"
|
|
189
|
+
>
|
|
190
|
+
<div
|
|
191
|
+
className="input-field"
|
|
192
|
+
data-state={visualState}
|
|
193
|
+
data-priority={priority}
|
|
194
|
+
data-size={resolvedSize}
|
|
195
|
+
data-readonly={isReadOnly ? "true" : undefined}
|
|
196
|
+
data-block={resolvedBlock ? "true" : undefined}
|
|
197
|
+
>
|
|
198
|
+
<div className="input-field-control">
|
|
199
|
+
{priority === "tertiary" && inlineLabel ? (
|
|
200
|
+
// tertiary 라벨은 FormField header가 아니라 input border 내부에 노출한다.
|
|
201
|
+
<span className="input-inline-label">{inlineLabel}</span>
|
|
202
|
+
) : null}
|
|
203
|
+
<textarea
|
|
204
|
+
{...restProps}
|
|
205
|
+
id={id ?? generatedId}
|
|
206
|
+
ref={setTextAreaRef}
|
|
207
|
+
className={clsx(
|
|
208
|
+
"input-element",
|
|
209
|
+
"input-textarea-element",
|
|
210
|
+
inputClassName,
|
|
211
|
+
)}
|
|
212
|
+
disabled={isDisabled}
|
|
213
|
+
aria-invalid={currentState === "error" ? true : undefined}
|
|
214
|
+
name={textAreaName}
|
|
215
|
+
maxLength={resolvedMaxLength}
|
|
216
|
+
value={value}
|
|
217
|
+
defaultValue={defaultValue}
|
|
218
|
+
onChange={handleChange}
|
|
219
|
+
onFocus={handleFocus}
|
|
220
|
+
onBlur={handleBlur}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
{showLength ? (
|
|
224
|
+
// length prop이 있을 때만 카운터를 노출한다.
|
|
225
|
+
<p className="input-textarea-length">{`${currentLength} / ${length}자`}</p>
|
|
226
|
+
) : null}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
InputTextArea.displayName = "TextArea";
|
|
235
|
+
|
|
236
|
+
export default InputTextArea;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import InputBase from "./Input";
|
|
2
|
+
import InputTextArea from "./TextArea";
|
|
2
3
|
import InputBaseSideSlot from "./SideSlot";
|
|
3
4
|
import InputBaseUtil from "./Utility";
|
|
4
5
|
import InputBaseUtilityButton from "./Button";
|
|
@@ -6,6 +7,7 @@ import { InputStatusIcon } from "./StatusIcon";
|
|
|
6
7
|
|
|
7
8
|
export const InputFoundation = {
|
|
8
9
|
Base: InputBase,
|
|
10
|
+
TextArea: InputTextArea,
|
|
9
11
|
Util: InputBaseUtil,
|
|
10
12
|
SideSlot: InputBaseSideSlot,
|
|
11
13
|
Icon: InputStatusIcon,
|
|
@@ -277,6 +277,51 @@
|
|
|
277
277
|
min-width: 0;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
.input[data-input-type="textarea"] .input-field {
|
|
281
|
+
// TextArea는 size별로 radius/spacing을 분기해 input과 동일한 스케일 감각을 유지한다.
|
|
282
|
+
--input-textarea-radius: var(--input-textarea-radius-medium);
|
|
283
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-medium);
|
|
284
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-medium);
|
|
285
|
+
--input-textarea-gap: var(--input-textarea-gap-medium);
|
|
286
|
+
align-items: stretch;
|
|
287
|
+
flex-direction: column;
|
|
288
|
+
gap: var(--input-textarea-gap);
|
|
289
|
+
border-radius: var(--input-textarea-radius);
|
|
290
|
+
padding: var(--input-textarea-padding-block)
|
|
291
|
+
var(--input-textarea-padding-inline);
|
|
292
|
+
|
|
293
|
+
&[data-size="small"] {
|
|
294
|
+
--input-textarea-radius: var(--input-textarea-radius-small);
|
|
295
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-small);
|
|
296
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-small);
|
|
297
|
+
--input-textarea-gap: var(--input-textarea-gap-small);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
&[data-size="large"] {
|
|
301
|
+
--input-textarea-radius: var(--input-textarea-radius-large);
|
|
302
|
+
--input-textarea-padding-inline: var(--input-textarea-padding-inline-large);
|
|
303
|
+
--input-textarea-padding-block: var(--input-textarea-padding-block-large);
|
|
304
|
+
--input-textarea-gap: var(--input-textarea-gap-large);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.input-textarea-element {
|
|
309
|
+
min-height: var(--input-textarea-min-height);
|
|
310
|
+
height: var(--input-textarea-height);
|
|
311
|
+
resize: none;
|
|
312
|
+
margin: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.input-textarea-length {
|
|
316
|
+
margin: 0;
|
|
317
|
+
align-self: flex-end;
|
|
318
|
+
color: var(--input-placeholder-color);
|
|
319
|
+
font-size: var(--input-textarea-counter-font-size);
|
|
320
|
+
line-height: var(--input-textarea-counter-line-height);
|
|
321
|
+
font-weight: var(--input-textarea-counter-font-weight);
|
|
322
|
+
letter-spacing: var(--input-textarea-counter-letter-spacing);
|
|
323
|
+
}
|
|
324
|
+
|
|
280
325
|
.input-field-utilities {
|
|
281
326
|
display: flex;
|
|
282
327
|
align-items: center;
|
|
@@ -421,13 +466,18 @@
|
|
|
421
466
|
.input-helper-text {
|
|
422
467
|
color: var(--input-label-error-color);
|
|
423
468
|
}
|
|
469
|
+
|
|
470
|
+
.input-textarea-length {
|
|
471
|
+
color: var(--input-label-error-color);
|
|
472
|
+
}
|
|
424
473
|
}
|
|
425
474
|
|
|
426
475
|
.input[data-state="disabled"] {
|
|
427
476
|
.input-label,
|
|
428
477
|
.input-inline-label,
|
|
429
478
|
.input-helper-text,
|
|
430
|
-
.input-affix
|
|
479
|
+
.input-affix,
|
|
480
|
+
.input-textarea-length {
|
|
431
481
|
color: var(--input-helper-disabled-color);
|
|
432
482
|
}
|
|
433
483
|
|
|
@@ -21,6 +21,27 @@
|
|
|
21
21
|
--input-tertiary-radius-base: var(--theme-radius-large-2);
|
|
22
22
|
--input-table-radius-base: 0;
|
|
23
23
|
--input-tertiary-element-min-height: var(--theme-size-medium-2);
|
|
24
|
+
--input-textarea-min-height: calc(var(--input-default-height-medium) * 2);
|
|
25
|
+
--input-textarea-height: 128px;
|
|
26
|
+
/* TextArea size token은 Input 기본 size token을 재할당해 축을 일치시킨다. */
|
|
27
|
+
--input-textarea-radius-small: var(--input-default-radius-base);
|
|
28
|
+
--input-textarea-radius-medium: var(--input-default-radius-base);
|
|
29
|
+
--input-textarea-radius-large: var(--input-default-radius-base);
|
|
30
|
+
--input-textarea-padding-inline-small: var(--spacing-padding-6);
|
|
31
|
+
--input-textarea-padding-inline-medium: var(--spacing-padding-6);
|
|
32
|
+
--input-textarea-padding-inline-large: var(--spacing-padding-6);
|
|
33
|
+
--input-textarea-padding-block-small: var(--spacing-padding-5);
|
|
34
|
+
--input-textarea-padding-block-medium: var(--spacing-padding-5);
|
|
35
|
+
--input-textarea-padding-block-large: var(--spacing-padding-6);
|
|
36
|
+
--input-textarea-gap-small: var(--input-default-gap);
|
|
37
|
+
--input-textarea-gap-medium: var(--input-default-gap);
|
|
38
|
+
--input-textarea-gap-large: var(--input-default-gap);
|
|
39
|
+
--input-textarea-counter-font-size: var(--font-label-medium-size);
|
|
40
|
+
--input-textarea-counter-line-height: var(--font-label-medium-line-height);
|
|
41
|
+
--input-textarea-counter-font-weight: var(--font-label-medium-weight);
|
|
42
|
+
--input-textarea-counter-letter-spacing: var(
|
|
43
|
+
--font-label-medium-letter-spacing
|
|
44
|
+
);
|
|
24
45
|
--input-table-text-small-size: var(--font-body-xxsmall-size);
|
|
25
46
|
--input-table-text-small-line-height: var(--font-body-xxsmall-line-height);
|
|
26
47
|
--input-table-text-small-weight: var(--font-body-xxsmall-weight);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import type { UseFormRegisterReturn } from "react-hook-form";
|
|
3
|
+
import type { FormFieldWidth } from "../../form/types/props";
|
|
4
|
+
import type { InputPriority, InputSize, InputState } from "./foundation";
|
|
5
|
+
|
|
6
|
+
type NativeTextAreaProps = ComponentPropsWithoutRef<"textarea">;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input TextArea; textarea base props
|
|
10
|
+
* @property {NativeTextAreaProps} ... <textarea /> native attrs
|
|
11
|
+
* @property {InputPriority} [priority] 스타일 카테고리 타입
|
|
12
|
+
* @property {InputSize} [size] 스타일 사이즈 타입
|
|
13
|
+
* @property {InputState} [state] input 상태
|
|
14
|
+
* @property {boolean} [block] width: 100% 여부
|
|
15
|
+
* @property {ReactNode} [inlineLabel] tertiary 내부 라벨
|
|
16
|
+
* @property {string} [inputClassName] 실제 `<textarea>` className
|
|
17
|
+
* @property {string} [boxClassName] `.input-box` className
|
|
18
|
+
* @property {UseFormRegisterReturn} [register] react-hook-form register 반환값
|
|
19
|
+
* @property {FormFieldWidth} [width] width preset 옵션
|
|
20
|
+
* @property {InputState} [data-simulated-state] Storybook 시각 상태 강제용
|
|
21
|
+
* @property {number | string} [height] textarea 높이(px 또는 CSS length)
|
|
22
|
+
* @property {number} [length] 카운터 최대 글자수(지정 시 우측 하단 카운터 노출)
|
|
23
|
+
*/
|
|
24
|
+
export interface InputTextAreaProps extends Omit<NativeTextAreaProps, "size"> {
|
|
25
|
+
/**
|
|
26
|
+
* semantic color/token 세트
|
|
27
|
+
*/
|
|
28
|
+
priority?: InputPriority;
|
|
29
|
+
/**
|
|
30
|
+
* 높이/타이포 세트
|
|
31
|
+
*/
|
|
32
|
+
size?: InputSize;
|
|
33
|
+
/**
|
|
34
|
+
* 시각 상태. disabled prop과 조합된다
|
|
35
|
+
*/
|
|
36
|
+
state?: InputState;
|
|
37
|
+
/**
|
|
38
|
+
* true면 width:100%
|
|
39
|
+
*/
|
|
40
|
+
block?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* tertiary priority에서 border 내부 상단에 배치할 라벨 텍스트/노드
|
|
43
|
+
*/
|
|
44
|
+
inlineLabel?: ReactNode;
|
|
45
|
+
/**
|
|
46
|
+
* 실제 `<textarea>` className
|
|
47
|
+
*/
|
|
48
|
+
inputClassName?: string;
|
|
49
|
+
/**
|
|
50
|
+
* `.input-box` className
|
|
51
|
+
*/
|
|
52
|
+
boxClassName?: string;
|
|
53
|
+
/**
|
|
54
|
+
* react-hook-form register 반환값
|
|
55
|
+
*/
|
|
56
|
+
register?: UseFormRegisterReturn;
|
|
57
|
+
/**
|
|
58
|
+
* width preset 옵션
|
|
59
|
+
*/
|
|
60
|
+
width?: FormFieldWidth;
|
|
61
|
+
/**
|
|
62
|
+
* Storybook 등에서 강제 상태 표현용
|
|
63
|
+
*/
|
|
64
|
+
"data-simulated-state"?: InputState;
|
|
65
|
+
/**
|
|
66
|
+
* textarea 높이(px 또는 CSS length)
|
|
67
|
+
*/
|
|
68
|
+
height?: number | string;
|
|
69
|
+
/**
|
|
70
|
+
* 카운터 최대 글자수(지정 시 우측 하단 카운터 노출)
|
|
71
|
+
*/
|
|
72
|
+
length?: number;
|
|
73
|
+
}
|