@uniai-fe/uds-primitives 0.3.20 → 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/dropdown/markup/Template.tsx +20 -50
- package/src/components/dropdown/types/props.ts +0 -12
- 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/src/components/select/markup/Default.tsx +1 -3
- package/src/components/select/markup/multiple/Multiple.tsx +1 -3
- package/src/components/select/types/props.ts +5 -5
- package/src/components/select/types/trigger.ts +2 -2
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
|
@@ -31,9 +31,7 @@ const normalizeSelectedIdsByMode = (
|
|
|
31
31
|
* @param {DropdownTemplateProps} props Dropdown template props
|
|
32
32
|
* @param {ReactNode} props.trigger trigger 요소
|
|
33
33
|
* @param {DropdownTemplateItem[]} props.items 렌더링할 menu item 리스트
|
|
34
|
-
* @param {string[]} [props.selectedIds] 선택된 item id 배열
|
|
35
34
|
* @param {(payload: DropdownTemplateChangePayload) => void} [props.onChange] 선택 결과 변경 콜백
|
|
36
|
-
* @param {(item: DropdownTemplateItem) => void} [props.onSelect] item 선택 콜백
|
|
37
35
|
* @param {"small" | "medium" | "large"} [props.size="medium"] menu size scale
|
|
38
36
|
* @param {"match" | "fit-content" | "max-content" | string | number} [props.width="match"] panel width 옵션
|
|
39
37
|
* @param {DropdownMenuProps} [props.rootProps] Dropdown.Root 전달 props
|
|
@@ -44,9 +42,7 @@ const normalizeSelectedIdsByMode = (
|
|
|
44
42
|
const DropdownTemplate = ({
|
|
45
43
|
trigger,
|
|
46
44
|
items,
|
|
47
|
-
selectedIds,
|
|
48
45
|
onChange,
|
|
49
|
-
onSelect,
|
|
50
46
|
size = "medium",
|
|
51
47
|
width = "match",
|
|
52
48
|
rootProps,
|
|
@@ -55,15 +51,7 @@ const DropdownTemplate = ({
|
|
|
55
51
|
alt,
|
|
56
52
|
}: DropdownTemplateProps) => {
|
|
57
53
|
/**
|
|
58
|
-
* 1) 선택
|
|
59
|
-
* - selectedIds prop이 들어오면 외부가 상태를 소유하는 controlled 모드다.
|
|
60
|
-
* - selectedIds prop이 없으면 Template 내부 state가 상태를 소유하는 uncontrolled 모드다.
|
|
61
|
-
* - 이 분기 기준은 렌더마다 동일해야 하므로 가장 먼저 계산한다.
|
|
62
|
-
*/
|
|
63
|
-
const isSelectionControlled = selectedIds !== undefined;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 2) 선택 정책(single/multiple) 결정
|
|
54
|
+
* 1) 선택 정책(single/multiple) 결정
|
|
67
55
|
* - item 중 하나라도 multiple=true이면 multiple 정책으로 해석한다.
|
|
68
56
|
* - multiple 정책에서는 item id 배열 전체를 유지한다.
|
|
69
57
|
* - single 정책에서는 언제나 최대 1개 id만 유지한다.
|
|
@@ -71,7 +59,7 @@ const DropdownTemplate = ({
|
|
|
71
59
|
const isMultiple = items.some(item => item.multiple);
|
|
72
60
|
|
|
73
61
|
/**
|
|
74
|
-
*
|
|
62
|
+
* 2) 초기 선택값 계산
|
|
75
63
|
* - source of truth: items[].selected
|
|
76
64
|
* - multiple: selected=true인 모든 id를 초기값으로 사용한다.
|
|
77
65
|
* - single: selected=true가 여러 개여도 첫 번째 id 하나만 사용한다.
|
|
@@ -86,9 +74,8 @@ const DropdownTemplate = ({
|
|
|
86
74
|
}, [isMultiple, items]);
|
|
87
75
|
|
|
88
76
|
/**
|
|
89
|
-
*
|
|
90
|
-
* -
|
|
91
|
-
* - uncontrolled 모드에서는 이 state가 선택 결과의 단일 source가 된다.
|
|
77
|
+
* 3) 내부 선택 state 선언
|
|
78
|
+
* - Template은 items[].selected를 초기값으로 받아 내부 상태를 관리한다.
|
|
92
79
|
* - 초기값은 selectedIdsFromItems를 그대로 사용한다.
|
|
93
80
|
*/
|
|
94
81
|
const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState<
|
|
@@ -96,19 +83,14 @@ const DropdownTemplate = ({
|
|
|
96
83
|
>(() => selectedIdsFromItems);
|
|
97
84
|
|
|
98
85
|
/**
|
|
99
|
-
*
|
|
100
|
-
* -
|
|
101
|
-
* - uncontrolled 모드에서는 내부 state를 items 변경에 맞춰 정합성 보정한다.
|
|
86
|
+
* 4) options(items) 변경 동기화
|
|
87
|
+
* - 내부 state를 items 변경에 맞춰 정합성 보정한다.
|
|
102
88
|
* 1) 기존 선택 id 중 현재 items에 존재하는 id만 필터링한다.
|
|
103
89
|
* 2) 유효 id가 남아 있으면 그대로 유지한다(single은 1개만 유지).
|
|
104
90
|
* 3) 유효 id가 없으면 selectedIdsFromItems(초기 선택 규칙)로 재초기화한다.
|
|
105
91
|
* - 이 effect는 외부 데이터 재조회/필터 변경 시 stale id를 자동 정리하는 역할을 한다.
|
|
106
92
|
*/
|
|
107
93
|
useEffect(() => {
|
|
108
|
-
if (isSelectionControlled) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
94
|
setUncontrolledSelectedIds(previousSelectedIds => {
|
|
113
95
|
const itemIdSet = new Set(items.map(item => item.id));
|
|
114
96
|
const filteredIds = previousSelectedIds.filter(selectedId =>
|
|
@@ -128,38 +110,36 @@ const DropdownTemplate = ({
|
|
|
128
110
|
|
|
129
111
|
return nextSelectedIds;
|
|
130
112
|
});
|
|
131
|
-
}, [isMultiple,
|
|
113
|
+
}, [isMultiple, items, selectedIdsFromItems]);
|
|
132
114
|
|
|
133
115
|
/**
|
|
134
|
-
*
|
|
135
|
-
* -
|
|
136
|
-
* - uncontrolled: 내부 state(uncontrolledSelectedIds)를 그대로 사용한다.
|
|
116
|
+
* 5) 최종 선택 id 계산
|
|
117
|
+
* - 내부 state(uncontrolledSelectedIds)를 single/multiple 정책으로 정규화한다.
|
|
137
118
|
* - 렌더/이벤트/selected 스타일 계산은 이 값만 참조한다.
|
|
138
119
|
*/
|
|
139
120
|
const resolvedSelectedIds = normalizeSelectedIdsByMode(
|
|
140
|
-
|
|
121
|
+
uncontrolledSelectedIds,
|
|
141
122
|
isMultiple,
|
|
142
123
|
);
|
|
143
124
|
|
|
144
125
|
/**
|
|
145
|
-
*
|
|
126
|
+
* 6) empty panel 분기
|
|
146
127
|
* - item이 비어 있으면 alt 또는 기본 문구를 disabled item으로 렌더링한다.
|
|
147
128
|
* - 별도 li를 만들지 않고 MenuItem을 재사용해 구조를 통일한다.
|
|
148
129
|
*/
|
|
149
130
|
const hasItems = items.length > 0;
|
|
150
131
|
|
|
151
132
|
/**
|
|
152
|
-
*
|
|
133
|
+
* 7) item 선택 처리
|
|
153
134
|
* - 클릭된 item(id) 기준으로 다음 선택 배열을 계산한다.
|
|
154
135
|
* - multiple: 토글(add/remove)
|
|
155
136
|
* - single: 클릭한 id 하나로 교체
|
|
156
|
-
* -
|
|
137
|
+
* - 내부 state를 즉시 반영한다.
|
|
157
138
|
* - onChange(payload)로 "현재 결과 전체"를 전달한다.
|
|
158
|
-
* - onSelect(item)은 하위호환을 위해 마지막에 유지 호출한다.
|
|
159
139
|
*/
|
|
160
140
|
const handleItemSelect = (itemId: string) => {
|
|
161
141
|
/**
|
|
162
|
-
*
|
|
142
|
+
* 7-1) 클릭된 item 조회
|
|
163
143
|
* - items 목록에 없는 id면 안전하게 종료한다.
|
|
164
144
|
* - 외부 데이터 race 조건에서 방어적으로 동작하기 위한 가드다.
|
|
165
145
|
*/
|
|
@@ -170,7 +150,7 @@ const DropdownTemplate = ({
|
|
|
170
150
|
}
|
|
171
151
|
|
|
172
152
|
/**
|
|
173
|
-
*
|
|
153
|
+
* 7-2) 다음 selectedIds 계산
|
|
174
154
|
* - wasSelected: 토글 기준 값
|
|
175
155
|
* - nextSelectedIds: 모드 정책을 적용한 다음 상태
|
|
176
156
|
* - nextSelectedItems: payload 전달용 상세 데이터
|
|
@@ -186,16 +166,13 @@ const DropdownTemplate = ({
|
|
|
186
166
|
);
|
|
187
167
|
|
|
188
168
|
/**
|
|
189
|
-
*
|
|
190
|
-
* -
|
|
191
|
-
* 내부 state를 갱신하지 않는다.
|
|
169
|
+
* 7-3) 내부 state 업데이트
|
|
170
|
+
* - Template 내부 선택 상태를 즉시 반영한다.
|
|
192
171
|
*/
|
|
193
|
-
|
|
194
|
-
setUncontrolledSelectedIds(nextSelectedIds);
|
|
195
|
-
}
|
|
172
|
+
setUncontrolledSelectedIds(nextSelectedIds);
|
|
196
173
|
|
|
197
174
|
/**
|
|
198
|
-
*
|
|
175
|
+
* 7-4) 선택 결과 이벤트(onChange)
|
|
199
176
|
* - 서비스/상위 컴포넌트가 선택 결과 전체를 즉시 사용할 수 있게
|
|
200
177
|
* 현재 item + 선택 결과 배열을 함께 제공한다.
|
|
201
178
|
*/
|
|
@@ -206,17 +183,10 @@ const DropdownTemplate = ({
|
|
|
206
183
|
selectedItems: nextSelectedItems,
|
|
207
184
|
multiple: isMultiple,
|
|
208
185
|
});
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 8-5) legacy 계약 이벤트(onSelect)
|
|
212
|
-
* - 하위호환 경로를 유지하기 위해 onChange 이후 동일 선택을 전달한다.
|
|
213
|
-
* - 추후 마이그레이션 완료 시 제거 대상이다.
|
|
214
|
-
*/
|
|
215
|
-
onSelect?.(currentItem);
|
|
216
186
|
};
|
|
217
187
|
|
|
218
188
|
/**
|
|
219
|
-
*
|
|
189
|
+
* 8) 렌더 구성
|
|
220
190
|
* - Root → Trigger → Container → MenuList depth를 유지한다.
|
|
221
191
|
* - 각 item은 resolvedSelectedIds 기준으로 selected 스타일을 결정한다.
|
|
222
192
|
* - disabled item은 select 이벤트를 차단하고 상태만 노출한다.
|
|
@@ -167,9 +167,7 @@ export interface DropdownTemplateChangePayload {
|
|
|
167
167
|
* Dropdown template props
|
|
168
168
|
* @property {ReactNode} trigger trigger 요소
|
|
169
169
|
* @property {DropdownTemplateItem[]} items 렌더링할 menu item 리스트
|
|
170
|
-
* @property {string[]} [selectedIds] 선택된 item id 배열
|
|
171
170
|
* @property {(payload: DropdownTemplateChangePayload) => void} [onChange] 선택 결과 변경 콜백
|
|
172
|
-
* @property {(item: DropdownTemplateItem) => void} [onSelect] item 선택 콜백
|
|
173
171
|
* @property {DropdownSize} [size="medium"] surface height scale
|
|
174
172
|
* @property {DropdownPanelWidth} [width="match"] panel width 옵션
|
|
175
173
|
* @property {DropdownMenuProps} [rootProps] Root 에 전달할 props
|
|
@@ -180,20 +178,10 @@ export interface DropdownTemplateChangePayload {
|
|
|
180
178
|
export interface DropdownTemplateProps {
|
|
181
179
|
trigger: ReactNode;
|
|
182
180
|
items: DropdownTemplateItem[];
|
|
183
|
-
/**
|
|
184
|
-
* 선택된 item id 배열
|
|
185
|
-
* @deprecated onChange + items[].selected 기반 계약을 우선 사용한다.
|
|
186
|
-
*/
|
|
187
|
-
selectedIds?: string[];
|
|
188
181
|
/**
|
|
189
182
|
* 선택 결과 변경 콜백
|
|
190
183
|
*/
|
|
191
184
|
onChange?: (payload: DropdownTemplateChangePayload) => void;
|
|
192
|
-
/**
|
|
193
|
-
* item 선택 콜백
|
|
194
|
-
* @deprecated onChange로 대체되며 하위호환을 위해 유지된다.
|
|
195
|
-
*/
|
|
196
|
-
onSelect?: (item: DropdownTemplateItem) => void;
|
|
197
185
|
size?: DropdownSize;
|
|
198
186
|
width?: DropdownPanelWidth;
|
|
199
187
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -41,7 +41,6 @@ const SELECT_CUSTOM_OPTION_VALUE = "CUSTOM";
|
|
|
41
41
|
* @param {"default" | "focused" | "error" | "disabled"} [props.state="default"] trigger state
|
|
42
42
|
* @param {boolean} [props.block] block 레이아웃 여부
|
|
43
43
|
* @param {FormFieldWidth} [props.width] container width
|
|
44
|
-
* @param {boolean} [props.isOpen] legacy open alias
|
|
45
44
|
* @param {boolean} [props.disabled] disabled 여부
|
|
46
45
|
* @param {boolean} [props.readOnly] readOnly 여부
|
|
47
46
|
* @param {"button" | "submit" | "reset"} [props.buttonType] trigger button type
|
|
@@ -74,7 +73,6 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
74
73
|
state = "default",
|
|
75
74
|
block,
|
|
76
75
|
width,
|
|
77
|
-
isOpen,
|
|
78
76
|
disabled,
|
|
79
77
|
readOnly,
|
|
80
78
|
buttonType,
|
|
@@ -182,7 +180,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
182
180
|
|
|
183
181
|
// 12) open 제어형/비제어형 계약은 공용 hook으로 통합 처리한다.
|
|
184
182
|
const { open: dropdownOpen, setOpen } = useSelectDropdownOpenState({
|
|
185
|
-
open
|
|
183
|
+
open,
|
|
186
184
|
defaultOpen,
|
|
187
185
|
onOpen,
|
|
188
186
|
});
|
|
@@ -33,7 +33,6 @@ const SELECT_MULTIPLE_ALL_OPTION_BASE_ID = "__select_multiple_all__";
|
|
|
33
33
|
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
34
34
|
* @param {boolean} [props.block] block 여부
|
|
35
35
|
* @param {FormFieldWidth} [props.width] container width preset
|
|
36
|
-
* @param {boolean} [props.isOpen] dropdown open 여부
|
|
37
36
|
* @param {boolean} [props.disabled] disabled 여부
|
|
38
37
|
* @param {boolean} [props.readOnly] readOnly 여부
|
|
39
38
|
* @param {SelectDropdownExtension} [props.dropdown] dropdown 확장 옵션
|
|
@@ -57,7 +56,6 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
57
56
|
state = "default",
|
|
58
57
|
block,
|
|
59
58
|
width,
|
|
60
|
-
isOpen,
|
|
61
59
|
disabled,
|
|
62
60
|
readOnly,
|
|
63
61
|
tags,
|
|
@@ -257,7 +255,7 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
257
255
|
* - open/defaultOpen/onOpen 계약을 hook으로 통합 처리한다.
|
|
258
256
|
*/
|
|
259
257
|
const { open: dropdownOpen, setOpen } = useSelectDropdownOpenState({
|
|
260
|
-
open
|
|
258
|
+
open,
|
|
261
259
|
defaultOpen,
|
|
262
260
|
onOpen,
|
|
263
261
|
});
|
|
@@ -73,13 +73,13 @@ export interface SelectValueOptions {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Select; 상태 옵션
|
|
76
|
-
* @property {boolean} [
|
|
76
|
+
* @property {boolean} [open] dropdown open 여부
|
|
77
77
|
*/
|
|
78
78
|
export interface SelectComponentState {
|
|
79
79
|
/**
|
|
80
80
|
* dropdown open 여부
|
|
81
81
|
*/
|
|
82
|
-
|
|
82
|
+
open?: boolean;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -295,7 +295,7 @@ export interface SelectCustomOptionExtension {
|
|
|
295
295
|
* @property {ReactNode} [placeholder] placeholder 텍스트
|
|
296
296
|
* @property {ReactNode[]} [tags] multi select 태그 리스트
|
|
297
297
|
* @property {boolean} [multiple] multi select 여부
|
|
298
|
-
* @property {boolean} [
|
|
298
|
+
* @property {boolean} [open] dropdown open 여부
|
|
299
299
|
* @property {FormFieldWidth} [width] width preset 옵션
|
|
300
300
|
*/
|
|
301
301
|
export type SelectProps = SelectStyleOptions &
|
|
@@ -311,7 +311,7 @@ export type SelectProps = SelectStyleOptions &
|
|
|
311
311
|
* @property {SelectSize} [size] size scale
|
|
312
312
|
* @property {SelectState} [state] visual state
|
|
313
313
|
* @property {boolean} [block] block 여부
|
|
314
|
-
* @property {boolean} [
|
|
314
|
+
* @property {boolean} [open] dropdown open 여부
|
|
315
315
|
* @property {boolean} [disabled] disabled 여부
|
|
316
316
|
* @property {boolean} [readOnly] readOnly 여부
|
|
317
317
|
* @property {SelectTriggerButtonType} [buttonType] button type
|
|
@@ -362,7 +362,7 @@ export interface SelectMultipleAllOptionProps {
|
|
|
362
362
|
* @property {SelectSize} [size] size scale
|
|
363
363
|
* @property {SelectState} [state] visual state
|
|
364
364
|
* @property {boolean} [block] block 여부
|
|
365
|
-
* @property {boolean} [
|
|
365
|
+
* @property {boolean} [open] dropdown open 여부
|
|
366
366
|
* @property {boolean} [disabled] disabled 여부
|
|
367
367
|
* @property {boolean} [readOnly] readOnly 여부
|
|
368
368
|
* @property {FormFieldWidth} [width] width preset 옵션
|
|
@@ -129,7 +129,7 @@ export interface SelectTriggerDefaultProps extends HTMLAttributes<HTMLElement> {
|
|
|
129
129
|
/**
|
|
130
130
|
* dropdown open 여부
|
|
131
131
|
*/
|
|
132
|
-
|
|
132
|
+
open?: boolean;
|
|
133
133
|
/**
|
|
134
134
|
* disabled 여부
|
|
135
135
|
*/
|
|
@@ -190,7 +190,7 @@ export interface SelectTriggerMultipleProps extends HTMLAttributes<HTMLElement>
|
|
|
190
190
|
/**
|
|
191
191
|
* dropdown open 여부
|
|
192
192
|
*/
|
|
193
|
-
|
|
193
|
+
open?: boolean;
|
|
194
194
|
/**
|
|
195
195
|
* disabled 여부
|
|
196
196
|
*/
|