@uniai-fe/uds-primitives 0.3.6 → 0.3.8
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/dist/styles.css +160 -40
- package/package.json +1 -1
- package/src/components/dropdown/markup/Template.tsx +187 -3
- package/src/components/dropdown/types/props.ts +52 -3
- package/src/components/input/markup/address/Template.tsx +11 -2
- package/src/components/input/markup/foundation/Input.tsx +10 -2
- package/src/components/input/styles/foundation.scss +68 -30
- package/src/components/input/styles/variables.scss +32 -0
- package/src/components/select/markup/Default.tsx +191 -7
- package/src/components/select/markup/foundation/Base.tsx +7 -1
- package/src/components/select/markup/multiple/Multiple.tsx +191 -14
- package/src/components/select/styles/select.scss +57 -14
- package/src/components/select/styles/variables.scss +26 -1
- package/src/components/select/types/option.ts +74 -13
- package/src/components/select/types/props.ts +22 -6
- package/src/components/select/types/trigger.ts +13 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/* Layout presets */
|
|
3
3
|
--input-width: 100%;
|
|
4
4
|
--input-flex: 0 1 auto;
|
|
5
|
+
--input-layout-gap: var(--spacing-gap-3);
|
|
5
6
|
|
|
6
7
|
/* Input sizing tokens; Button 변수 규칙과 동일한 prefix 패턴을 맞춘다. */
|
|
7
8
|
--input-default-height-small: var(--theme-size-medium-1);
|
|
@@ -10,10 +11,16 @@
|
|
|
10
11
|
--input-tertiary-height-base: calc(var(--theme-size-medium-2) + 24px);
|
|
11
12
|
--input-default-padding-inline: var(--spacing-padding-6);
|
|
12
13
|
--input-default-padding-block: var(--spacing-padding-4);
|
|
14
|
+
--input-secondary-padding-block: var(--spacing-padding-4);
|
|
13
15
|
--input-default-gap: var(--spacing-gap-4);
|
|
16
|
+
--input-affix-gap: var(--spacing-gap-3);
|
|
17
|
+
--input-utility-gap: var(--spacing-gap-2);
|
|
18
|
+
--input-tertiary-row-gap: var(--spacing-gap-1);
|
|
19
|
+
--input-tertiary-control-row-gap: var(--spacing-gap-1);
|
|
14
20
|
--input-default-radius-base: var(--theme-radius-large-1);
|
|
15
21
|
--input-tertiary-radius-base: var(--theme-radius-large-2);
|
|
16
22
|
--input-table-radius-base: 0;
|
|
23
|
+
--input-tertiary-element-min-height: var(--theme-size-medium-2);
|
|
17
24
|
--input-table-text-small-size: var(--font-body-xxsmall-size);
|
|
18
25
|
--input-table-text-small-line-height: var(--font-body-xxsmall-line-height);
|
|
19
26
|
--input-table-text-small-weight: var(--font-body-xxsmall-weight);
|
|
@@ -28,14 +35,27 @@
|
|
|
28
35
|
--input-label-color: var(--color-label-standard);
|
|
29
36
|
--input-label-accent-color: var(--color-primary-default);
|
|
30
37
|
--input-label-error-color: var(--color-error);
|
|
38
|
+
--input-label-font-size: var(--font-label-small-size);
|
|
39
|
+
--input-label-line-height: var(--font-label-small-line-height);
|
|
40
|
+
--input-label-font-weight: var(--font-label-small-weight);
|
|
31
41
|
--input-helper-color: var(--color-label-neutral);
|
|
32
42
|
--input-helper-success-color: var(--color-success);
|
|
33
43
|
--input-helper-error-color: var(--color-error);
|
|
34
44
|
--input-helper-disabled-color: var(--color-label-disabled);
|
|
45
|
+
--input-helper-font-size: var(--font-label-small-size);
|
|
46
|
+
--input-helper-line-height: var(--font-label-small-line-height);
|
|
35
47
|
|
|
36
48
|
/* Text & placeholder colors */
|
|
37
49
|
--input-text-color: var(--color-label-strong);
|
|
50
|
+
--input-text-readonly-color: var(--color-label-strong);
|
|
51
|
+
--input-text-disabled-color: var(--color-label-disabled);
|
|
38
52
|
--input-placeholder-color: var(--color-label-alternative);
|
|
53
|
+
--input-placeholder-disabled-color: var(--color-label-disabled);
|
|
54
|
+
--input-placeholder-readonly-color: var(--input-placeholder-disabled-color);
|
|
55
|
+
--input-font-size: var(--font-body-medium-size);
|
|
56
|
+
--input-line-height: var(--font-body-medium-line-height);
|
|
57
|
+
--input-font-weight: 400;
|
|
58
|
+
--input-letter-spacing: 0px;
|
|
39
59
|
|
|
40
60
|
/* Border tokens */
|
|
41
61
|
--input-border-color: var(--color-border-standard-cool-gray);
|
|
@@ -44,6 +64,8 @@
|
|
|
44
64
|
--input-border-active-color: var(--color-blue-80);
|
|
45
65
|
--input-border-success-color: var(--color-blue-80);
|
|
46
66
|
--input-border-table-default-color: transparent;
|
|
67
|
+
--input-border-table-disabled-color: var(--input-border-table-default-color);
|
|
68
|
+
--input-border-table-readonly-color: var(--input-border-table-disabled-color);
|
|
47
69
|
/* error는 Figma 44% alpha */
|
|
48
70
|
--input-border-error-color: rgba(218, 29, 11, 0.44); // --color-feedback-error
|
|
49
71
|
--input-border-disabled-color: var(--color-border-standard-cool-gray);
|
|
@@ -53,4 +75,14 @@
|
|
|
53
75
|
--input-surface-color: var(--color-common-100);
|
|
54
76
|
--input-surface-muted-color: var(--color-neutral-99);
|
|
55
77
|
--input-surface-disabled-color: var(--color-neutral-95);
|
|
78
|
+
--input-table-surface-color: transparent;
|
|
79
|
+
--input-table-surface-disabled-color: var(--input-table-surface-color);
|
|
80
|
+
--input-table-surface-readonly-color: var(
|
|
81
|
+
--input-table-surface-disabled-color
|
|
82
|
+
);
|
|
83
|
+
--input-secondary-surface-color: transparent;
|
|
84
|
+
|
|
85
|
+
/* Status/affix colors */
|
|
86
|
+
--input-status-error-color: var(--color-error);
|
|
87
|
+
--input-status-success-color: var(--color-primary-default);
|
|
56
88
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import { forwardRef } from "react";
|
|
4
|
+
import { forwardRef, useEffect, useMemo, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import type { DropdownSize } from "../../dropdown/types";
|
|
7
7
|
import { Dropdown } from "../../dropdown/markup";
|
|
@@ -11,12 +11,20 @@ import { useSelectDropdownOpenState } from "../hooks";
|
|
|
11
11
|
import type { SelectDropdownOption } from "../types/option";
|
|
12
12
|
import type { SelectDefaultComponentProps } from "../types/props";
|
|
13
13
|
|
|
14
|
+
const isSameIdList = (previousIds: string[], nextIds: string[]) =>
|
|
15
|
+
previousIds.length === nextIds.length &&
|
|
16
|
+
previousIds.every((selectedId, index) => selectedId === nextIds[index]);
|
|
17
|
+
|
|
18
|
+
const normalizeSingleSelectedIds = (selectedIds: string[]) =>
|
|
19
|
+
selectedIds.length > 0 ? [selectedIds[0]] : [];
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Select default trigger; 단일 선택 드롭다운을 렌더링한다.
|
|
16
23
|
* @component
|
|
17
24
|
* @param {SelectDefaultComponentProps} props default trigger props
|
|
18
25
|
* @param {SelectDropdownOption[]} [props.options] dropdown option 목록
|
|
19
26
|
* @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
|
|
27
|
+
* @param {(payload: SelectOptionChangePayload) => void} [props.onChange] 선택 결과 변경 콜백
|
|
20
28
|
* @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
|
|
21
29
|
* @param {"primary" | "secondary" | "table"} [props.priority="primary"] priority 스케일
|
|
22
30
|
* @param {"small" | "medium" | "large"} [props.size="medium"] size 스케일
|
|
@@ -24,6 +32,7 @@ import type { SelectDefaultComponentProps } from "../types/props";
|
|
|
24
32
|
* @param {boolean} [props.block] block 여부
|
|
25
33
|
* @param {FormFieldWidth} [props.width] container width preset
|
|
26
34
|
* @param {boolean} [props.disabled] disabled 여부
|
|
35
|
+
* @param {boolean} [props.readOnly] readOnly 여부
|
|
27
36
|
* @param {SelectTriggerButtonType} [props.buttonType] trigger button type
|
|
28
37
|
* @param {"small" | "medium" | "large"} [props.dropdownSize] dropdown panel size
|
|
29
38
|
* @param {"match" | "fit-content" | "max-content" | string | number} [props.dropdownWidth="match"] dropdown panel width
|
|
@@ -48,9 +57,11 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
48
57
|
width,
|
|
49
58
|
isOpen,
|
|
50
59
|
disabled,
|
|
60
|
+
readOnly,
|
|
51
61
|
buttonType,
|
|
52
62
|
options = [],
|
|
53
63
|
selectedOptionIds,
|
|
64
|
+
onChange,
|
|
54
65
|
onOptionSelect,
|
|
55
66
|
dropdownSize,
|
|
56
67
|
dropdownWidth = "match",
|
|
@@ -65,37 +76,209 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
65
76
|
},
|
|
66
77
|
ref,
|
|
67
78
|
) => {
|
|
68
|
-
|
|
79
|
+
/**
|
|
80
|
+
* 1) 레이아웃 기본값 계산
|
|
81
|
+
* - table priority는 셀 컨텍스트에서 full width가 기본 동작이므로
|
|
82
|
+
* width 미지정 시 block=true처럼 동작하도록 보정한다.
|
|
83
|
+
* - 이 값은 trigger/container 양쪽에서 동일하게 사용한다.
|
|
84
|
+
*/
|
|
69
85
|
const resolvedBlock =
|
|
70
86
|
block || (priority === "table" && width === undefined);
|
|
71
|
-
const resolvedSelectedIds = selectedOptionIds ?? [];
|
|
72
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 2) 선택 상태 모드 결정
|
|
90
|
+
* - selectedOptionIds prop이 들어오면 controlled 모드(외부가 source of truth)
|
|
91
|
+
* - selectedOptionIds prop이 없으면 uncontrolled 모드(내부 state가 source of truth)
|
|
92
|
+
*/
|
|
93
|
+
const isSelectionControlled = selectedOptionIds !== undefined;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 3) option 조회 최적화 맵 생성
|
|
97
|
+
* - 옵션 탐색(find/filter) 반복을 줄이기 위해 id 기반 맵을 만든다.
|
|
98
|
+
* - display label 계산, payload selectedOptions 계산, 렌더 시 선택 확인에 재사용한다.
|
|
99
|
+
*/
|
|
100
|
+
const optionMap = useMemo(
|
|
101
|
+
() => new Map(options.map(option => [option.id, option])),
|
|
102
|
+
[options],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 4) uncontrolled 초기 선택값 계산
|
|
107
|
+
* - options[].selected를 초기 선택 입력으로 사용한다.
|
|
108
|
+
* - single-select이므로 selected=true가 여러 개여도 첫 id 하나만 채택한다.
|
|
109
|
+
*/
|
|
110
|
+
const selectedIdsFromOptions = useMemo(() => {
|
|
111
|
+
const initialSelectedIds = options
|
|
112
|
+
.filter(option => option.selected)
|
|
113
|
+
.map(option => option.id);
|
|
114
|
+
return normalizeSingleSelectedIds(initialSelectedIds);
|
|
115
|
+
}, [options]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 5) uncontrolled 내부 state 선언
|
|
119
|
+
* - controlled 모드에서는 실제 UI source로 쓰이지 않는다.
|
|
120
|
+
* - uncontrolled 모드에서는 최종 선택 id를 내부 state가 소유한다.
|
|
121
|
+
*/
|
|
122
|
+
const [uncontrolledSelectedOptionIds, setUncontrolledSelectedOptionIds] =
|
|
123
|
+
useState<string[]>(() => selectedIdsFromOptions);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 6) options 변경 시 내부 state 정합성 보정(uncontrolled 전용)
|
|
127
|
+
* - controlled 모드는 외부 selectedOptionIds를 신뢰하므로 내부 state를 건드리지 않는다.
|
|
128
|
+
* - uncontrolled 모드는 옵션 리스트가 바뀌면 stale id를 정리해야 한다.
|
|
129
|
+
* 1) 기존 선택 id가 현재 options에 존재하는지 필터링
|
|
130
|
+
* 2) 남아 있으면 single 정책으로 첫 id 하나만 유지
|
|
131
|
+
* 3) 남지 않으면 selectedIdsFromOptions(초기 선택 규칙)로 재동기화
|
|
132
|
+
*/
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (isSelectionControlled) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setUncontrolledSelectedOptionIds(previousSelectedIds => {
|
|
139
|
+
const optionIdSet = new Set(options.map(option => option.id));
|
|
140
|
+
const filteredIds = previousSelectedIds.filter(selectedId =>
|
|
141
|
+
optionIdSet.has(selectedId),
|
|
142
|
+
);
|
|
143
|
+
const nextCandidateIds =
|
|
144
|
+
filteredIds.length > 0 ? filteredIds : selectedIdsFromOptions;
|
|
145
|
+
const nextSelectedIds = normalizeSingleSelectedIds(nextCandidateIds);
|
|
146
|
+
|
|
147
|
+
// 선택 결과가 동일하면 기존 배열 참조를 유지해 effect 루프를 방지한다.
|
|
148
|
+
if (isSameIdList(previousSelectedIds, nextSelectedIds)) {
|
|
149
|
+
return previousSelectedIds;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return nextSelectedIds;
|
|
153
|
+
});
|
|
154
|
+
}, [isSelectionControlled, options, selectedIdsFromOptions]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 7) 최종 선택 id 계산
|
|
158
|
+
* - controlled: selectedOptionIds를 우선 사용
|
|
159
|
+
* - uncontrolled: 내부 state를 사용
|
|
160
|
+
* - single-select invariant 보장을 위해 항상 최대 1개 id만 반환한다.
|
|
161
|
+
*/
|
|
162
|
+
const resolvedSelectedIds = useMemo(() => {
|
|
163
|
+
const sourceIds = isSelectionControlled
|
|
164
|
+
? (selectedOptionIds ?? [])
|
|
165
|
+
: uncontrolledSelectedOptionIds;
|
|
166
|
+
return normalizeSingleSelectedIds(sourceIds);
|
|
167
|
+
}, [
|
|
168
|
+
isSelectionControlled,
|
|
169
|
+
selectedOptionIds,
|
|
170
|
+
uncontrolledSelectedOptionIds,
|
|
171
|
+
]);
|
|
172
|
+
const selectedIdSet = useMemo(
|
|
173
|
+
() => new Set(resolvedSelectedIds),
|
|
174
|
+
[resolvedSelectedIds],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 8) 표시 라벨 계산
|
|
179
|
+
* - 외부 displayLabel이 있으면 최우선 사용
|
|
180
|
+
* - 없으면 현재 선택 id 기준으로 optionMap에서 label을 조회한다.
|
|
181
|
+
* - optionMap을 사용해 O(1) 조회로 단순화한다.
|
|
182
|
+
*/
|
|
73
183
|
const resolvedDisplayLabel =
|
|
74
184
|
displayLabel ??
|
|
75
185
|
(resolvedSelectedIds.length > 0
|
|
76
|
-
?
|
|
186
|
+
? optionMap.get(resolvedSelectedIds[0])?.label
|
|
77
187
|
: undefined);
|
|
78
188
|
|
|
189
|
+
/**
|
|
190
|
+
* 9) placeholder/label 표시 상태 계산
|
|
191
|
+
* - null/undefined/빈 문자열이면 placeholder로 간주한다.
|
|
192
|
+
*/
|
|
79
193
|
const hasLabel =
|
|
80
194
|
resolvedDisplayLabel !== undefined &&
|
|
81
195
|
resolvedDisplayLabel !== null &&
|
|
82
196
|
resolvedDisplayLabel !== "";
|
|
83
197
|
|
|
198
|
+
/**
|
|
199
|
+
* 10) dropdown open 상태 관리
|
|
200
|
+
* - open/defaultOpen/onOpenChange 계약은 기존 useSelectDropdownOpenState를 그대로 사용한다.
|
|
201
|
+
* - 내부 state와 controlled open 상태를 동시에 지원한다.
|
|
202
|
+
*/
|
|
84
203
|
const { open: dropdownOpen, setOpen } = useSelectDropdownOpenState({
|
|
85
204
|
open: open ?? isOpen,
|
|
86
205
|
defaultOpen,
|
|
87
206
|
onOpenChange,
|
|
88
207
|
});
|
|
89
|
-
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
90
208
|
|
|
209
|
+
/**
|
|
210
|
+
* 11) 상호작용 차단 조건 계산
|
|
211
|
+
* - disabled/readOnly일 때는 open 토글과 option select를 모두 차단한다.
|
|
212
|
+
*/
|
|
213
|
+
const isInteractionBlocked = disabled || readOnly;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 12) open 상태 변경 핸들러
|
|
217
|
+
* - 차단 상태면 강제로 닫힘(false) 유지
|
|
218
|
+
* - 허용 상태면 전달받은 nextOpen 반영
|
|
219
|
+
*/
|
|
220
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
221
|
+
if (isInteractionBlocked) {
|
|
222
|
+
setOpen(false);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setOpen(nextOpen);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 13) option 선택 처리(single)
|
|
230
|
+
* - single-select 정책: 클릭된 option 하나를 최종 선택값으로 고정
|
|
231
|
+
* - uncontrolled 모드면 내부 state 갱신
|
|
232
|
+
* - onChange(payload)로 현재 선택 결과 전체를 전달
|
|
233
|
+
* - legacy onOptionSelect는 하위호환으로 유지
|
|
234
|
+
*/
|
|
91
235
|
const handleOptionSelect = (option: SelectDropdownOption) => {
|
|
236
|
+
if (isInteractionBlocked) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const nextSelectedOptionIds = [option.id];
|
|
241
|
+
const nextSelectedOptions = nextSelectedOptionIds
|
|
242
|
+
.map(selectedOptionId => optionMap.get(selectedOptionId))
|
|
243
|
+
.filter(
|
|
244
|
+
(
|
|
245
|
+
selectedOption,
|
|
246
|
+
): selectedOption is NonNullable<typeof selectedOption> =>
|
|
247
|
+
Boolean(selectedOption),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!isSelectionControlled) {
|
|
251
|
+
setUncontrolledSelectedOptionIds(nextSelectedOptionIds);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onChange?.({
|
|
255
|
+
mode: "single",
|
|
256
|
+
selectedOptionIds: nextSelectedOptionIds,
|
|
257
|
+
selectedValues: nextSelectedOptions.map(
|
|
258
|
+
selectedOption => selectedOption.value,
|
|
259
|
+
),
|
|
260
|
+
selectedOptions: nextSelectedOptions,
|
|
261
|
+
currentOption: option,
|
|
262
|
+
isSelected: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
92
265
|
onOptionSelect?.(option);
|
|
93
266
|
setOpen(false);
|
|
94
267
|
};
|
|
95
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 14) dropdown 패널 렌더링 파생값
|
|
271
|
+
* - panelSize: trigger size와 동일 축을 기본으로 사용
|
|
272
|
+
* - hasOptions: empty 상태 분기
|
|
273
|
+
*/
|
|
96
274
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
97
275
|
const hasOptions = options.length > 0;
|
|
98
276
|
|
|
277
|
+
/**
|
|
278
|
+
* 15) 렌더
|
|
279
|
+
* - Container → Dropdown.Root → Trigger → Container → Menu.List depth 유지
|
|
280
|
+
* - empty 상태는 별도 구조를 만들지 않고 Dropdown.Menu.Item을 disabled로 재사용
|
|
281
|
+
*/
|
|
99
282
|
return (
|
|
100
283
|
<Container
|
|
101
284
|
className={clsx("select-trigger-container", className)}
|
|
@@ -104,7 +287,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
104
287
|
>
|
|
105
288
|
<Dropdown.Root
|
|
106
289
|
open={dropdownOpen}
|
|
107
|
-
onOpenChange={
|
|
290
|
+
onOpenChange={handleOpenChange}
|
|
108
291
|
modal={false}
|
|
109
292
|
{...dropdownRootProps}
|
|
110
293
|
>
|
|
@@ -118,6 +301,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
118
301
|
block={resolvedBlock}
|
|
119
302
|
open={dropdownOpen}
|
|
120
303
|
disabled={disabled}
|
|
304
|
+
readOnly={readOnly}
|
|
121
305
|
buttonType={buttonType}
|
|
122
306
|
{...rest}
|
|
123
307
|
>
|
|
@@ -146,7 +330,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
146
330
|
left={option.left}
|
|
147
331
|
right={option.right}
|
|
148
332
|
multiple={Boolean(option.multiple)}
|
|
149
|
-
isSelected={
|
|
333
|
+
isSelected={selectedIdSet.has(option.id)}
|
|
150
334
|
onSelect={event => {
|
|
151
335
|
if (option.disabled) {
|
|
152
336
|
event.preventDefault();
|
|
@@ -18,6 +18,7 @@ import type { SelectTriggerBaseProps } from "../../types/trigger";
|
|
|
18
18
|
* @param {boolean} [props.block=false] block 레이아웃 여부
|
|
19
19
|
* @param {boolean} [props.multiple=false] multi select 여부
|
|
20
20
|
* @param {boolean} [props.disabled=false] disabled 여부
|
|
21
|
+
* @param {boolean} [props.readOnly=false] readOnly 여부
|
|
21
22
|
* @param {ElementType} [props.as="button"] polymorphic 태그
|
|
22
23
|
* @param {"button" | "submit" | "reset"} [props.buttonType="button"] native button type
|
|
23
24
|
* @param {string} [props.className] trigger className
|
|
@@ -37,6 +38,7 @@ const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
|
|
|
37
38
|
block = false,
|
|
38
39
|
multiple = false,
|
|
39
40
|
disabled = false,
|
|
41
|
+
readOnly = false,
|
|
40
42
|
as = "button",
|
|
41
43
|
buttonType = "button",
|
|
42
44
|
className,
|
|
@@ -46,6 +48,7 @@ const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
|
|
|
46
48
|
ref,
|
|
47
49
|
) => {
|
|
48
50
|
const Icon = SelectIcon.Chevron[priority][size];
|
|
51
|
+
const resolvedState = disabled || readOnly ? "disabled" : state;
|
|
49
52
|
const {
|
|
50
53
|
["aria-haspopup"]: ariaHasPopup,
|
|
51
54
|
["aria-expanded"]: ariaExpanded,
|
|
@@ -56,10 +59,13 @@ const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
|
|
|
56
59
|
className: clsx("select-button", className),
|
|
57
60
|
"data-priority": priority,
|
|
58
61
|
"data-size": size,
|
|
59
|
-
|
|
62
|
+
// 변경: readOnly도 disabled와 동일한 시각 상태를 사용하고 텍스트 컬러만 별도 스타일로 분리한다.
|
|
63
|
+
"data-state": resolvedState,
|
|
64
|
+
"data-readonly": readOnly ? "true" : undefined,
|
|
60
65
|
"data-open": open || undefined,
|
|
61
66
|
"data-multiple": multiple || undefined,
|
|
62
67
|
"data-block": block ? "true" : undefined,
|
|
68
|
+
"aria-readonly": readOnly ? "true" : undefined,
|
|
63
69
|
"aria-haspopup": ariaHasPopup ?? "listbox",
|
|
64
70
|
"aria-expanded": ariaExpanded ?? open,
|
|
65
71
|
...restProps,
|