@uniai-fe/uds-primitives 0.2.8 → 0.2.10
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 +419 -0
- package/package.json +2 -1
- package/src/components/calendar/types/calendar.ts +5 -0
- package/src/components/dropdown/markup/Template.tsx +41 -17
- package/src/components/dropdown/markup/foundation/Container.tsx +14 -2
- package/src/components/dropdown/markup/foundation/MenuItem.tsx +20 -6
- package/src/components/dropdown/markup/foundation/Root.tsx +8 -1
- package/src/components/dropdown/markup/foundation/Trigger.tsx +7 -1
- package/src/components/dropdown/styles/dropdown.scss +4 -0
- package/src/components/dropdown/types/props.ts +5 -0
- package/src/components/input/markup/date/Template.tsx +36 -5
- package/src/components/input/markup/date/Trigger.tsx +22 -4
- package/src/components/input/markup/foundation/Input.tsx +19 -11
- package/src/components/input/markup/foundation/Utility.tsx +11 -7
- package/src/components/input/styles/date.scss +21 -0
- package/src/components/input/styles/foundation.scss +30 -0
- package/src/components/input/styles/variables.scss +11 -0
- package/src/components/input/types/date.ts +15 -0
- package/src/components/input/types/foundation.ts +18 -11
- package/src/components/input/utils/date.ts +15 -1
- package/src/components/select/hooks/index.ts +1 -45
- package/src/components/select/hooks/interaction.ts +62 -0
- package/src/components/select/markup/Default.tsx +59 -35
- package/src/components/select/markup/foundation/Base.tsx +12 -4
- package/src/components/select/markup/foundation/Container.tsx +37 -34
- package/src/components/select/markup/foundation/Icon.tsx +6 -1
- package/src/components/select/markup/multiple/Multiple.tsx +62 -35
- package/src/components/select/markup/multiple/SelectedChip.tsx +5 -2
- package/src/components/select/styles/select.scss +50 -0
- package/src/components/select/styles/variables.scss +26 -0
- package/src/components/select/types/base.ts +3 -2
- package/src/components/select/types/icon.ts +7 -6
- package/src/components/select/types/index.ts +1 -0
- package/src/components/select/types/interaction.ts +30 -0
- package/src/components/select/types/props.ts +8 -0
- package/src/components/select/types/trigger.ts +4 -0
- package/src/components/table/hooks/index.ts +0 -3
- package/src/components/table/index.tsx +5 -3
- package/src/components/table/markup/Container.tsx +126 -0
- package/src/components/table/markup/foundation/Body.tsx +24 -0
- package/src/components/table/markup/foundation/Cell.tsx +72 -0
- package/src/components/table/markup/foundation/Col.tsx +22 -0
- package/src/components/table/markup/foundation/Colgroup.tsx +29 -0
- package/src/components/table/markup/foundation/Foot.tsx +24 -0
- package/src/components/table/markup/foundation/Head.tsx +24 -0
- package/src/components/table/markup/foundation/Root.tsx +32 -0
- package/src/components/table/markup/foundation/Row.tsx +32 -0
- package/src/components/table/markup/foundation/Td.tsx +37 -0
- package/src/components/table/markup/foundation/Th.tsx +39 -0
- package/src/components/table/markup/foundation/index.tsx +30 -0
- package/src/components/table/markup/index.tsx +8 -2
- package/src/components/table/styles/foundation.scss +247 -0
- package/src/components/table/styles/index.scss +2 -0
- package/src/components/table/styles/variables.scss +29 -0
- package/src/components/table/types/foundation.ts +250 -0
- package/src/components/table/types/index.ts +1 -4
- package/src/components/tooltip/img/info.svg +5 -0
- package/src/components/tooltip/img/information.svg +9 -0
- package/src/components/tooltip/index.scss +1 -0
- package/src/components/tooltip/index.tsx +4 -0
- package/src/components/tooltip/markup/Message.tsx +70 -0
- package/src/components/tooltip/markup/Root.tsx +32 -0
- package/src/components/tooltip/markup/Template.tsx +46 -0
- package/src/components/tooltip/markup/Trigger.tsx +32 -0
- package/src/components/tooltip/markup/index.tsx +18 -0
- package/src/components/tooltip/styles/index.scss +2 -0
- package/src/components/tooltip/styles/tooltip.scss +47 -0
- package/src/components/tooltip/styles/variables.scss +14 -0
- package/src/components/tooltip/types/index.ts +1 -0
- package/src/components/tooltip/types/props.ts +118 -0
- package/src/index.scss +1 -0
- package/src/index.tsx +1 -0
|
@@ -10,7 +10,7 @@ import type { FormFieldWidth } from "../../form/types/props";
|
|
|
10
10
|
/**
|
|
11
11
|
* input; priority option
|
|
12
12
|
*/
|
|
13
|
-
export type InputPriority = "primary" | "secondary" | "tertiary";
|
|
13
|
+
export type InputPriority = "primary" | "secondary" | "tertiary" | "table";
|
|
14
14
|
/**
|
|
15
15
|
* input; size option
|
|
16
16
|
*/
|
|
@@ -34,6 +34,7 @@ export const INPUT_PRIORITIES: InputPriority[] = [
|
|
|
34
34
|
"primary",
|
|
35
35
|
"secondary",
|
|
36
36
|
"tertiary",
|
|
37
|
+
"table",
|
|
37
38
|
];
|
|
38
39
|
/**
|
|
39
40
|
* size 축은 높이/타이포/spacing을 결정한다.
|
|
@@ -56,11 +57,11 @@ type NativeInputProps = ComponentPropsWithoutRef<"input">;
|
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* input; icon options
|
|
59
|
-
* @property {
|
|
60
|
-
* @property {
|
|
61
|
-
* @property {
|
|
62
|
-
* @property {
|
|
63
|
-
* @property {
|
|
60
|
+
* @property {ReactNode} [left] input 왼쪽 컨텐츠
|
|
61
|
+
* @property {ReactNode} [right] input 오른쪽 컨텐츠
|
|
62
|
+
* @property {ReactNode} [clear] input reset버튼 커스텀 컨텐츠
|
|
63
|
+
* @property {ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
|
|
64
|
+
* @property {ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
|
|
64
65
|
*/
|
|
65
66
|
export interface InputIcon {
|
|
66
67
|
/**
|
|
@@ -95,12 +96,13 @@ export interface InputIcon {
|
|
|
95
96
|
* @property {string} [inputClassName]
|
|
96
97
|
* @property {string} [boxClassName]
|
|
97
98
|
* @property {UseFormRegisterReturn} [register]
|
|
98
|
-
* @property {
|
|
99
|
-
* @property {
|
|
100
|
-
* @property {
|
|
101
|
-
* @property {
|
|
102
|
-
* @property {
|
|
99
|
+
* @property {ReactNode} [left] input 왼쪽 컨텐츠
|
|
100
|
+
* @property {ReactNode} [right] input 오른쪽 컨텐츠
|
|
101
|
+
* @property {ReactNode} [clear] input reset버튼 커스텀 컨텐츠
|
|
102
|
+
* @property {ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
|
|
103
|
+
* @property {ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
|
|
103
104
|
* @property {FormFieldWidth} [width] width preset 옵션
|
|
105
|
+
* @property {InputState} [data-simulated-state] Storybook 시각 상태 강제용
|
|
104
106
|
*/
|
|
105
107
|
export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
|
|
106
108
|
/**
|
|
@@ -147,6 +149,7 @@ export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
|
|
|
147
149
|
* @property {ReactNode} [clear] clear 버튼 아이콘
|
|
148
150
|
* @property {ReactNode} [success] success 상태 아이콘
|
|
149
151
|
* @property {ReactNode} [error] error 상태 아이콘
|
|
152
|
+
* @property {InputPriority} priority input priority
|
|
150
153
|
* @property {InputState} state 현재 상태
|
|
151
154
|
* @property {boolean} isDisabled disabled 여부
|
|
152
155
|
* @property {boolean} isFocused focus 여부
|
|
@@ -171,6 +174,10 @@ export interface InputUtilityProps {
|
|
|
171
174
|
* error 상태 아이콘
|
|
172
175
|
*/
|
|
173
176
|
error?: ReactNode;
|
|
177
|
+
/**
|
|
178
|
+
* priority 축
|
|
179
|
+
*/
|
|
180
|
+
priority: InputPriority;
|
|
174
181
|
/**
|
|
175
182
|
* 현재 input 상태
|
|
176
183
|
*/
|
|
@@ -51,7 +51,21 @@ export const serializeCalendarValue = (value: CalendarValue) => value ?? "";
|
|
|
51
51
|
* @param {CalendarValue} value 현재 값
|
|
52
52
|
* @returns {string} 표시 문자열
|
|
53
53
|
*/
|
|
54
|
-
export const formatTriggerValue = (
|
|
54
|
+
export const formatTriggerValue = (
|
|
55
|
+
value: CalendarValue,
|
|
56
|
+
format = DATE_FORMAT,
|
|
57
|
+
) => {
|
|
58
|
+
if (!value) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parsed = dayjs(value);
|
|
63
|
+
if (!parsed.isValid()) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return parsed.format(format);
|
|
68
|
+
};
|
|
55
69
|
|
|
56
70
|
/**
|
|
57
71
|
* columns 값을 Mantine numberOfColumns와 맞춘다.
|
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { SelectDropdownBehaviorProps } from "../types/props";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Select dropdown open 상태를 제어하는 hook
|
|
7
|
-
* @hook
|
|
8
|
-
* @param {SelectDropdownBehaviorProps} props open 제어 옵션
|
|
9
|
-
* @returns {{
|
|
10
|
-
* open: boolean;
|
|
11
|
-
* setOpen: (next: boolean) => void;
|
|
12
|
-
* isControlled: boolean;
|
|
13
|
-
* }} resolved open state
|
|
14
|
-
*/
|
|
15
|
-
export const useSelectDropdownOpenState = ({
|
|
16
|
-
open,
|
|
17
|
-
defaultOpen,
|
|
18
|
-
onOpenChange,
|
|
19
|
-
}: SelectDropdownBehaviorProps) => {
|
|
20
|
-
const isControlled = useMemo(() => typeof open === "boolean", [open]);
|
|
21
|
-
const [uncontrolledOpen, setUncontrolledOpen] = useState(
|
|
22
|
-
defaultOpen ?? false,
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (isControlled) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
setUncontrolledOpen(defaultOpen ?? false);
|
|
30
|
-
}, [defaultOpen, isControlled]);
|
|
31
|
-
|
|
32
|
-
const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
|
|
33
|
-
|
|
34
|
-
const setOpen = useCallback(
|
|
35
|
-
(nextOpen: boolean) => {
|
|
36
|
-
if (!isControlled) {
|
|
37
|
-
setUncontrolledOpen(nextOpen);
|
|
38
|
-
}
|
|
39
|
-
onOpenChange?.(nextOpen);
|
|
40
|
-
},
|
|
41
|
-
[isControlled, onOpenChange],
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
return { open: resolvedOpen, setOpen, isControlled };
|
|
45
|
-
};
|
|
1
|
+
export * from "./interaction";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
UseSelectDropdownOpenStateParams,
|
|
7
|
+
UseSelectDropdownOpenStateReturn,
|
|
8
|
+
} from "../types/interaction";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select Hook; Dropdown open 상태 제어 Hook
|
|
12
|
+
* @hook
|
|
13
|
+
* @param {UseSelectDropdownOpenStateParams} params open 제어 옵션
|
|
14
|
+
* @param {boolean} [params.open] 외부 제어형 open 상태
|
|
15
|
+
* @param {boolean} [params.defaultOpen] 비제어형 초기 open 상태
|
|
16
|
+
* @param {(open: boolean) => void} [params.onOpenChange] open 상태 변경 콜백
|
|
17
|
+
* @returns {{
|
|
18
|
+
* open: boolean;
|
|
19
|
+
* setOpen: (next: boolean) => void;
|
|
20
|
+
* isControlled: boolean;
|
|
21
|
+
* }} resolved open state
|
|
22
|
+
* @example
|
|
23
|
+
* const { open, setOpen } = useSelectDropdownOpenState({
|
|
24
|
+
* open: controlledOpen,
|
|
25
|
+
* defaultOpen: false,
|
|
26
|
+
* onOpenChange: onOpenChangeHandler,
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export const useSelectDropdownOpenState = ({
|
|
30
|
+
open,
|
|
31
|
+
defaultOpen,
|
|
32
|
+
onOpenChange,
|
|
33
|
+
}: UseSelectDropdownOpenStateParams): UseSelectDropdownOpenStateReturn => {
|
|
34
|
+
// 1) 제어형/비제어형 분기 기준을 먼저 확정한다.
|
|
35
|
+
const isControlled = useMemo(() => typeof open === "boolean", [open]);
|
|
36
|
+
// 2) 비제어형일 때만 내부 open state를 소유한다.
|
|
37
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(
|
|
38
|
+
defaultOpen ?? false,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// 3) defaultOpen 변경 시, 비제어형일 때만 내부 state를 동기화한다.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isControlled) return;
|
|
44
|
+
|
|
45
|
+
setUncontrolledOpen(defaultOpen ?? false);
|
|
46
|
+
}, [defaultOpen, isControlled]);
|
|
47
|
+
|
|
48
|
+
// 4) 최종 open state는 제어형 우선, 아니면 내부 state를 사용한다.
|
|
49
|
+
const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
|
|
50
|
+
|
|
51
|
+
// 5) setOpen은 내부 state 갱신 + 외부 콜백 브릿지를 동시에 담당한다.
|
|
52
|
+
const setOpen = useCallback(
|
|
53
|
+
(nextOpen: boolean) => {
|
|
54
|
+
if (!isControlled) setUncontrolledOpen(nextOpen);
|
|
55
|
+
|
|
56
|
+
onOpenChange?.(nextOpen);
|
|
57
|
+
},
|
|
58
|
+
[isControlled, onOpenChange],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return { open: resolvedOpen, setOpen, isControlled };
|
|
62
|
+
};
|
|
@@ -18,11 +18,22 @@ import type { SelectDefaultComponentProps } from "../types/props";
|
|
|
18
18
|
* @param {SelectDropdownOption[]} [props.options] dropdown option 목록
|
|
19
19
|
* @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
|
|
20
20
|
* @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
|
|
21
|
-
* @param {"primary" | "secondary"} [props.priority="primary"] priority 스케일
|
|
21
|
+
* @param {"primary" | "secondary" | "table"} [props.priority="primary"] priority 스케일
|
|
22
22
|
* @param {"small" | "medium" | "large"} [props.size="medium"] size 스케일
|
|
23
23
|
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
24
24
|
* @param {boolean} [props.block] block 여부
|
|
25
|
+
* @param {FormFieldWidth} [props.width] container width preset
|
|
25
26
|
* @param {boolean} [props.disabled] disabled 여부
|
|
27
|
+
* @param {SelectTriggerButtonType} [props.buttonType] trigger button type
|
|
28
|
+
* @param {"small" | "medium" | "large"} [props.dropdownSize] dropdown panel size
|
|
29
|
+
* @param {"match" | "fit-content" | "max-content" | string | number} [props.dropdownWidth="match"] dropdown panel width
|
|
30
|
+
* @param {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [props.dropdownRootProps] Dropdown.Root 전달 props
|
|
31
|
+
* @param {Omit<DropdownContainerProps, "children" | "size" | "width">} [props.dropdownContainerProps] Dropdown.Container 전달 props
|
|
32
|
+
* @param {DropdownMenuListProps} [props.dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
33
|
+
* @param {ReactNode} [props.alt] empty 상태 대체 콘텐츠
|
|
34
|
+
* @param {boolean} [props.open] controlled open 상태
|
|
35
|
+
* @param {boolean} [props.defaultOpen] uncontrolled 초기 open 상태
|
|
36
|
+
* @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 콜백
|
|
26
37
|
*/
|
|
27
38
|
const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
28
39
|
(
|
|
@@ -46,6 +57,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
46
57
|
dropdownRootProps,
|
|
47
58
|
dropdownContainerProps,
|
|
48
59
|
dropdownMenuListProps,
|
|
60
|
+
alt,
|
|
49
61
|
open,
|
|
50
62
|
defaultOpen,
|
|
51
63
|
onOpenChange,
|
|
@@ -53,6 +65,9 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
53
65
|
},
|
|
54
66
|
ref,
|
|
55
67
|
) => {
|
|
68
|
+
// 변경: table priority는 width 미지정 시 기본 full width를 사용한다.
|
|
69
|
+
const resolvedBlock =
|
|
70
|
+
block || (priority === "table" && width === undefined);
|
|
56
71
|
const resolvedSelectedIds = selectedOptionIds ?? [];
|
|
57
72
|
|
|
58
73
|
const resolvedDisplayLabel =
|
|
@@ -71,7 +86,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
71
86
|
defaultOpen,
|
|
72
87
|
onOpenChange,
|
|
73
88
|
});
|
|
74
|
-
//
|
|
89
|
+
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
75
90
|
|
|
76
91
|
const handleOptionSelect = (option: SelectDropdownOption) => {
|
|
77
92
|
onOptionSelect?.(option);
|
|
@@ -79,12 +94,12 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
79
94
|
};
|
|
80
95
|
|
|
81
96
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
82
|
-
const
|
|
97
|
+
const hasOptions = options.length > 0;
|
|
83
98
|
|
|
84
99
|
return (
|
|
85
100
|
<Container
|
|
86
101
|
className={clsx("select-trigger-container", className)}
|
|
87
|
-
block={
|
|
102
|
+
block={resolvedBlock}
|
|
88
103
|
width={width}
|
|
89
104
|
>
|
|
90
105
|
<Dropdown.Root
|
|
@@ -100,7 +115,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
100
115
|
priority={priority}
|
|
101
116
|
size={size}
|
|
102
117
|
state={disabled ? "disabled" : state}
|
|
103
|
-
block={
|
|
118
|
+
block={resolvedBlock}
|
|
104
119
|
open={dropdownOpen}
|
|
105
120
|
disabled={disabled}
|
|
106
121
|
buttonType={buttonType}
|
|
@@ -113,36 +128,45 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
113
128
|
/>
|
|
114
129
|
</SelectTriggerBase>
|
|
115
130
|
</Dropdown.Trigger>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
<Dropdown.Container
|
|
132
|
+
{...dropdownContainerProps}
|
|
133
|
+
size={panelSize}
|
|
134
|
+
width={dropdownWidth}
|
|
135
|
+
>
|
|
136
|
+
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
137
|
+
{hasOptions ? (
|
|
138
|
+
<>
|
|
139
|
+
{/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
|
|
140
|
+
{options.map(option => (
|
|
141
|
+
<Dropdown.Menu.Item
|
|
142
|
+
key={option.id}
|
|
143
|
+
label={option.label}
|
|
144
|
+
description={option.description}
|
|
145
|
+
disabled={option.disabled}
|
|
146
|
+
left={option.left}
|
|
147
|
+
right={option.right}
|
|
148
|
+
multiple={Boolean(option.multiple)}
|
|
149
|
+
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
150
|
+
onSelect={event => {
|
|
151
|
+
if (option.disabled) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
handleOptionSelect(option);
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</>
|
|
160
|
+
) : (
|
|
161
|
+
<Dropdown.Menu.Item
|
|
162
|
+
// 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
|
|
163
|
+
label={alt ?? "선택할 항목이 없습니다."}
|
|
164
|
+
disabled
|
|
165
|
+
className="dropdown-menu-alt"
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</Dropdown.Menu.List>
|
|
169
|
+
</Dropdown.Container>
|
|
146
170
|
</Dropdown.Root>
|
|
147
171
|
</Container>
|
|
148
172
|
);
|
|
@@ -8,16 +8,24 @@ import { SelectIcon } from "./Icon";
|
|
|
8
8
|
import type { SelectTriggerBaseProps } from "../../types/trigger";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Select
|
|
12
|
-
* Chevron 아이콘을 자동 연결하는 기본 요소다.
|
|
11
|
+
* Select Foundation; Trigger Base 슬롯 렌더링 컴포넌트
|
|
13
12
|
* @component
|
|
14
13
|
* @param {SelectTriggerBaseProps} props trigger base props
|
|
15
|
-
* @param {"primary" | "secondary"} [props.priority="primary"] 스타일 우선순위
|
|
14
|
+
* @param {"primary" | "secondary" | "table"} [props.priority="primary"] 스타일 우선순위
|
|
16
15
|
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이 스케일
|
|
17
16
|
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
18
|
-
* @param {boolean} [props.
|
|
17
|
+
* @param {boolean} [props.open=false] dropdown open 상태
|
|
18
|
+
* @param {boolean} [props.block=false] block 레이아웃 여부
|
|
19
|
+
* @param {boolean} [props.multiple=false] multi select 여부
|
|
20
|
+
* @param {boolean} [props.disabled=false] disabled 여부
|
|
19
21
|
* @param {ElementType} [props.as="button"] polymorphic 태그
|
|
20
22
|
* @param {"button" | "submit" | "reset"} [props.buttonType="button"] native button type
|
|
23
|
+
* @param {string} [props.className] trigger className
|
|
24
|
+
* @param {React.ReactNode} props.children trigger 콘텐츠
|
|
25
|
+
* @example
|
|
26
|
+
* <SelectTriggerBase open={false} size="medium">
|
|
27
|
+
* <span>옵션 선택</span>
|
|
28
|
+
* </SelectTriggerBase>
|
|
21
29
|
*/
|
|
22
30
|
const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
|
|
23
31
|
(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
+
import { forwardRef } from "react";
|
|
4
5
|
|
|
5
6
|
import type { SelectContainerProps } from "../../types/props";
|
|
6
7
|
import {
|
|
@@ -14,40 +15,42 @@ import {
|
|
|
14
15
|
* @param {SelectContainerProps} props Select container props
|
|
15
16
|
* @param {string} [props.className] 사용자 정의 className
|
|
16
17
|
* @param {boolean} [props.block] wrapper 전체 폭 확장 여부
|
|
18
|
+
* @param {FormFieldWidth} [props.width] Form.Field width preset
|
|
19
|
+
* @param {CSSProperties} [props.style] wrapper inline style
|
|
17
20
|
* @param {React.ReactNode} props.children trigger 및 dropdown 콘텐츠
|
|
18
21
|
*/
|
|
19
|
-
|
|
20
|
-
className,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
? "
|
|
32
|
-
:
|
|
33
|
-
const widthValue =
|
|
34
|
-
width !== undefined ? getFormFieldWidthValue(width) : undefined;
|
|
35
|
-
const mergedStyle =
|
|
36
|
-
widthValue !== undefined
|
|
37
|
-
? { ...(style ?? {}), ["--select-width" as const]: widthValue }
|
|
38
|
-
: style;
|
|
22
|
+
const SelectContainer = forwardRef<HTMLDivElement, SelectContainerProps>(
|
|
23
|
+
({ className, children, block = false, width, style, ...restProps }, ref) => {
|
|
24
|
+
const widthAttr =
|
|
25
|
+
width !== undefined
|
|
26
|
+
? getFormFieldWidthAttr(width)
|
|
27
|
+
: block
|
|
28
|
+
? "full"
|
|
29
|
+
: undefined;
|
|
30
|
+
const widthValue =
|
|
31
|
+
width !== undefined ? getFormFieldWidthValue(width) : undefined;
|
|
32
|
+
const mergedStyle =
|
|
33
|
+
widthValue !== undefined
|
|
34
|
+
? { ...(style ?? {}), ["--select-width" as const]: widthValue }
|
|
35
|
+
: style;
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"select-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={clsx("select select-container", className, {
|
|
41
|
+
"select-block": block,
|
|
42
|
+
})}
|
|
43
|
+
data-width={widthAttr}
|
|
44
|
+
style={mergedStyle}
|
|
45
|
+
{...restProps}
|
|
46
|
+
>
|
|
47
|
+
{/** dropdown root 및 dropdown menu 등 포함 예정 */}
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
SelectContainer.displayName = "SelectContainer";
|
|
55
|
+
|
|
56
|
+
export default SelectContainer;
|
|
@@ -41,10 +41,13 @@ const SelectChevronSecondaryIcon: SelectIconSizeMap = {
|
|
|
41
41
|
* Select; Chevron 아이콘 컬렉션
|
|
42
42
|
* - primary (small, medium, large)
|
|
43
43
|
* - secondary (small, medium, large)
|
|
44
|
+
* - table (small, medium, large)
|
|
44
45
|
*/
|
|
45
46
|
const SelectChevronIcon: SelectIconPriorityMap = {
|
|
46
47
|
primary: SelectChevronPrimaryIcon,
|
|
47
48
|
secondary: SelectChevronSecondaryIcon,
|
|
49
|
+
// 변경: table priority는 secondary chevron 자산을 재사용한다.
|
|
50
|
+
table: SelectChevronSecondaryIcon,
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -61,10 +64,12 @@ const SelectMultipleRemoveIcon: SelectIconSizeMap = {
|
|
|
61
64
|
|
|
62
65
|
/**
|
|
63
66
|
* Select; Remove 아이콘 컬렉션
|
|
64
|
-
* - primary (small, medium, large)
|
|
67
|
+
* - primary/secondary/table (small, medium, large)
|
|
65
68
|
*/
|
|
66
69
|
const SelectRemoveIcon: SelectIconRemovePriorityMap = {
|
|
67
70
|
primary: SelectMultipleRemoveIcon,
|
|
71
|
+
secondary: SelectMultipleRemoveIcon,
|
|
72
|
+
table: SelectMultipleRemoveIcon,
|
|
68
73
|
};
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -17,13 +17,27 @@ import { useSelectDropdownOpenState } from "../../hooks";
|
|
|
17
17
|
* @component
|
|
18
18
|
* @param {SelectMultipleComponentProps} props multi trigger props
|
|
19
19
|
* @param {SelectMultipleTag[]} [props.tags] 선택된 tag 리스트
|
|
20
|
+
* @param {SelectDropdownOption[]} [props.options] dropdown option 목록
|
|
21
|
+
* @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
|
|
22
|
+
* @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
|
|
20
23
|
* @param {React.ReactNode} [props.displayLabel] fallback 라벨
|
|
21
24
|
* @param {React.ReactNode} [props.placeholder] placeholder 텍스트
|
|
22
|
-
* @param {"primary" | "secondary"} [props.priority="primary"] priority scale
|
|
25
|
+
* @param {"primary" | "secondary" | "table"} [props.priority="primary"] priority scale
|
|
23
26
|
* @param {"small" | "medium" | "large"} [props.size="medium"] size scale
|
|
27
|
+
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
24
28
|
* @param {boolean} [props.block] block 여부
|
|
29
|
+
* @param {FormFieldWidth} [props.width] container width preset
|
|
25
30
|
* @param {boolean} [props.isOpen] dropdown open 여부
|
|
26
31
|
* @param {boolean} [props.disabled] disabled 여부
|
|
32
|
+
* @param {"small" | "medium" | "large"} [props.dropdownSize] dropdown panel size
|
|
33
|
+
* @param {"match" | "fit-content" | "max-content" | string | number} [props.dropdownWidth="match"] dropdown panel width
|
|
34
|
+
* @param {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [props.dropdownRootProps] Dropdown.Root 전달 props
|
|
35
|
+
* @param {Omit<DropdownContainerProps, "children" | "size" | "width">} [props.dropdownContainerProps] Dropdown.Container 전달 props
|
|
36
|
+
* @param {DropdownMenuListProps} [props.dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
37
|
+
* @param {ReactNode} [props.alt] empty 상태 대체 콘텐츠
|
|
38
|
+
* @param {boolean} [props.open] controlled open 상태
|
|
39
|
+
* @param {boolean} [props.defaultOpen] uncontrolled 초기 open 상태
|
|
40
|
+
* @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 콜백
|
|
27
41
|
*/
|
|
28
42
|
const SelectMultipleTrigger = forwardRef<
|
|
29
43
|
HTMLElement,
|
|
@@ -50,6 +64,7 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
50
64
|
dropdownRootProps,
|
|
51
65
|
dropdownContainerProps,
|
|
52
66
|
dropdownMenuListProps,
|
|
67
|
+
alt,
|
|
53
68
|
open,
|
|
54
69
|
defaultOpen,
|
|
55
70
|
onOpenChange,
|
|
@@ -57,6 +72,9 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
57
72
|
},
|
|
58
73
|
ref,
|
|
59
74
|
) => {
|
|
75
|
+
// 변경: table priority는 width 미지정 시 기본 full width를 사용한다.
|
|
76
|
+
const resolvedBlock =
|
|
77
|
+
block || (priority === "table" && width === undefined);
|
|
60
78
|
// hook dependency 안정화를 위해 memoized selected id 배열을 유지한다.
|
|
61
79
|
const resolvedSelectedIds = useMemo(
|
|
62
80
|
() => selectedOptionIds ?? [],
|
|
@@ -100,10 +118,10 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
100
118
|
defaultOpen,
|
|
101
119
|
onOpenChange,
|
|
102
120
|
});
|
|
103
|
-
//
|
|
121
|
+
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
104
122
|
|
|
105
123
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
106
|
-
const
|
|
124
|
+
const hasOptions = options.length > 0;
|
|
107
125
|
const MAX_VISIBLE_TAGS = 3;
|
|
108
126
|
const visibleTags = hasTags ? derivedTags.slice(0, MAX_VISIBLE_TAGS) : [];
|
|
109
127
|
const overflowCount = hasTags
|
|
@@ -113,7 +131,7 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
113
131
|
return (
|
|
114
132
|
<Container
|
|
115
133
|
className={clsx("select-trigger-multiple", className)}
|
|
116
|
-
block={
|
|
134
|
+
block={resolvedBlock}
|
|
117
135
|
width={width}
|
|
118
136
|
>
|
|
119
137
|
<Dropdown.Root
|
|
@@ -129,7 +147,7 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
129
147
|
priority={priority}
|
|
130
148
|
size={size}
|
|
131
149
|
state={disabled ? "disabled" : state}
|
|
132
|
-
block={
|
|
150
|
+
block={resolvedBlock}
|
|
133
151
|
open={dropdownOpen}
|
|
134
152
|
multiple
|
|
135
153
|
disabled={disabled}
|
|
@@ -166,36 +184,45 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
166
184
|
)}
|
|
167
185
|
</SelectTriggerBase>
|
|
168
186
|
</Dropdown.Trigger>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
187
|
+
<Dropdown.Container
|
|
188
|
+
{...dropdownContainerProps}
|
|
189
|
+
size={panelSize}
|
|
190
|
+
width={dropdownWidth}
|
|
191
|
+
>
|
|
192
|
+
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
193
|
+
{hasOptions ? (
|
|
194
|
+
<>
|
|
195
|
+
{/* multi select 전용 옵션을 Dropdown.Menu.Item으로 노출한다. */}
|
|
196
|
+
{options.map(option => (
|
|
197
|
+
<Dropdown.Menu.Item
|
|
198
|
+
key={option.id}
|
|
199
|
+
label={option.label}
|
|
200
|
+
description={option.description}
|
|
201
|
+
disabled={option.disabled}
|
|
202
|
+
left={option.left}
|
|
203
|
+
right={option.right}
|
|
204
|
+
multiple
|
|
205
|
+
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
206
|
+
onSelect={event => {
|
|
207
|
+
if (option.disabled) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
onOptionSelect?.(option);
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
215
|
+
</>
|
|
216
|
+
) : (
|
|
217
|
+
<Dropdown.Menu.Item
|
|
218
|
+
// 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
|
|
219
|
+
label={alt ?? "선택할 항목이 없습니다."}
|
|
220
|
+
disabled
|
|
221
|
+
className="dropdown-menu-alt"
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</Dropdown.Menu.List>
|
|
225
|
+
</Dropdown.Container>
|
|
199
226
|
</Dropdown.Root>
|
|
200
227
|
</Container>
|
|
201
228
|
);
|