@uniai-fe/uds-primitives 0.2.7 → 0.2.9
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 +4 -0
- package/package.json +1 -1
- 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 +11 -4
- package/src/components/dropdown/markup/foundation/Trigger.tsx +10 -4
- package/src/components/dropdown/styles/dropdown.scss +4 -0
- package/src/components/dropdown/types/base.ts +0 -13
- package/src/components/dropdown/types/props.ts +11 -14
- 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 +55 -36
- package/src/components/select/markup/foundation/Base.tsx +11 -3
- package/src/components/select/markup/foundation/Container.tsx +37 -34
- package/src/components/select/markup/multiple/Multiple.tsx +56 -32
- package/src/components/select/markup/multiple/SelectedChip.tsx +5 -2
- package/src/components/select/types/index.ts +1 -0
- package/src/components/select/types/interaction.ts +30 -0
- package/src/components/select/types/option.ts +77 -3
- package/src/components/select/types/props.ts +74 -28
package/dist/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -12,6 +12,16 @@ import DropdownTrigger from "./foundation/Trigger";
|
|
|
12
12
|
* Dropdown reference template; trigger/panel/menu 조합을 제공한다.
|
|
13
13
|
* @component
|
|
14
14
|
* @param {DropdownTemplateProps} props Dropdown template props
|
|
15
|
+
* @param {ReactNode} props.trigger trigger 요소
|
|
16
|
+
* @param {DropdownTemplateItem[]} props.items 렌더링할 menu item 리스트
|
|
17
|
+
* @param {string[]} [props.selectedIds] 선택된 item id 배열
|
|
18
|
+
* @param {(item: DropdownTemplateItem) => void} [props.onSelect] item 선택 콜백
|
|
19
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] menu size scale
|
|
20
|
+
* @param {"match" | "fit-content" | "max-content" | string | number} [props.width="match"] panel width 옵션
|
|
21
|
+
* @param {DropdownMenuProps} [props.rootProps] Dropdown.Root 전달 props
|
|
22
|
+
* @param {DropdownContainerProps} [props.containerProps] Dropdown.Container 전달 props
|
|
23
|
+
* @param {DropdownMenuListProps} [props.menuListProps] Dropdown.Menu.List 전달 props
|
|
24
|
+
* @param {ReactNode} [props.alt] empty 상태 대체 콘텐츠
|
|
15
25
|
*/
|
|
16
26
|
const DropdownTemplate = ({
|
|
17
27
|
trigger,
|
|
@@ -23,31 +33,45 @@ const DropdownTemplate = ({
|
|
|
23
33
|
rootProps,
|
|
24
34
|
containerProps,
|
|
25
35
|
menuListProps,
|
|
36
|
+
alt,
|
|
26
37
|
}: DropdownTemplateProps) => {
|
|
38
|
+
const hasItems = items.length > 0;
|
|
39
|
+
|
|
27
40
|
return (
|
|
28
41
|
<DropdownRoot {...rootProps}>
|
|
29
42
|
<DropdownTrigger asChild>{trigger}</DropdownTrigger>
|
|
30
43
|
<DropdownContainer {...containerProps} size={size} width={width}>
|
|
31
44
|
<DropdownMenuList {...menuListProps}>
|
|
32
|
-
{
|
|
45
|
+
{hasItems ? (
|
|
46
|
+
<>
|
|
47
|
+
{items.map(item => (
|
|
48
|
+
<DropdownMenuItem
|
|
49
|
+
key={item.id}
|
|
50
|
+
label={item.label}
|
|
51
|
+
description={item.description}
|
|
52
|
+
disabled={item.disabled}
|
|
53
|
+
left={item.left}
|
|
54
|
+
right={item.right}
|
|
55
|
+
multiple={item.multiple}
|
|
56
|
+
isSelected={selectedIds?.includes(item.id)}
|
|
57
|
+
onSelect={event => {
|
|
58
|
+
if (item.disabled) {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
onSelect?.(item);
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
33
68
|
<DropdownMenuItem
|
|
34
|
-
|
|
35
|
-
label={
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
left={item.left}
|
|
39
|
-
right={item.right}
|
|
40
|
-
multiple={item.multiple}
|
|
41
|
-
isSelected={selectedIds?.includes(item.id)}
|
|
42
|
-
onSelect={event => {
|
|
43
|
-
if (item.disabled) {
|
|
44
|
-
event.preventDefault();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
onSelect?.(item);
|
|
48
|
-
}}
|
|
69
|
+
// 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
|
|
70
|
+
label={alt ?? "데이터가 없습니다."}
|
|
71
|
+
disabled
|
|
72
|
+
className="dropdown-menu-alt"
|
|
49
73
|
/>
|
|
50
|
-
)
|
|
74
|
+
)}
|
|
51
75
|
</DropdownMenuList>
|
|
52
76
|
</DropdownContainer>
|
|
53
77
|
</DropdownRoot>
|
|
@@ -8,11 +8,23 @@ import type { DropdownContainerProps } from "../../types/props";
|
|
|
8
8
|
import { useDropdownContext } from "./Provider";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Dropdown
|
|
11
|
+
* Dropdown Foundation; Container 패널 렌더링 컴포넌트
|
|
12
12
|
* @component
|
|
13
13
|
* @param {DropdownContainerProps} props Dropdown container props
|
|
14
|
-
* @param {
|
|
14
|
+
* @param {React.ReactNode} props.children dropdown panel 콘텐츠
|
|
15
|
+
* @param {string} [props.className] panel className
|
|
15
16
|
* @param {DropdownSize} [props.size="medium"] option height scale
|
|
17
|
+
* @param {DropdownPanelWidth} [props.width="match"] panel width 옵션
|
|
18
|
+
* @param {HTMLElement | null} [props.portalContainer] portal 컨테이너
|
|
19
|
+
* @param {"start" | "center" | "end"} [props.align="start"] 정렬 기준
|
|
20
|
+
* @param {"top" | "right" | "bottom" | "left"} [props.side="bottom"] 패널 위치
|
|
21
|
+
* @param {number} [props.sideOffset=4] trigger 와 패널 사이 간격
|
|
22
|
+
* @param {number} [props.alignOffset] 정렬 보정값
|
|
23
|
+
* @param {React.CSSProperties} [props.style] 인라인 스타일
|
|
24
|
+
* @example
|
|
25
|
+
* <DropdownContainer width="match">
|
|
26
|
+
* <Dropdown.Menu.List />
|
|
27
|
+
* </DropdownContainer>
|
|
16
28
|
*/
|
|
17
29
|
const DropdownContainer = forwardRef<HTMLDivElement, DropdownContainerProps>(
|
|
18
30
|
(
|
|
@@ -9,11 +9,20 @@ import { Checkbox } from "../../../checkbox/markup/Checkbox";
|
|
|
9
9
|
import type { CheckboxProps } from "../../../checkbox/types";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Dropdown
|
|
12
|
+
* Dropdown Foundation; Menu Item 옵션 렌더링 컴포넌트
|
|
13
13
|
* @component
|
|
14
14
|
* @param {DropdownMenuItemProps} props dropdown menu option props
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
15
|
+
* @param {React.ReactNode} [props.label] 옵션 라벨
|
|
16
|
+
* @param {React.ReactNode} [props.description] 보조 텍스트
|
|
17
|
+
* @param {React.ReactNode} [props.left] 좌측 콘텐츠
|
|
18
|
+
* @param {React.ReactNode} [props.right] 우측 콘텐츠
|
|
19
|
+
* @param {boolean} [props.isSelected=false] 선택 상태
|
|
20
|
+
* @param {boolean} [props.multiple=false] multi select 스타일 여부
|
|
21
|
+
* @param {CheckboxProps} [props.checkboxProps] multiple 시 checkbox props
|
|
22
|
+
* @param {React.ReactNode} [props.children] label 미지정 시 fallback 콘텐츠
|
|
23
|
+
* @param {string} [props.className] Dropdown item className
|
|
24
|
+
* @example
|
|
25
|
+
* <DropdownMenuItem label="옵션 A" isSelected />
|
|
17
26
|
*/
|
|
18
27
|
const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
|
|
19
28
|
(
|
|
@@ -33,6 +42,13 @@ const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
|
|
|
33
42
|
ref,
|
|
34
43
|
) => {
|
|
35
44
|
const labelContent = label ?? children;
|
|
45
|
+
// 변경: label/children이 string|number일 때만 준비된 label span으로 매핑하고, 그 외 ReactNode는 그대로 렌더링한다.
|
|
46
|
+
const resolvedLabelContent =
|
|
47
|
+
typeof labelContent === "string" || typeof labelContent === "number" ? (
|
|
48
|
+
<span className="dropdown-menu-item-label">{labelContent}</span>
|
|
49
|
+
) : (
|
|
50
|
+
labelContent
|
|
51
|
+
);
|
|
36
52
|
const hasDescription = Boolean(description);
|
|
37
53
|
const shouldRenderCheckbox = multiple && !left;
|
|
38
54
|
|
|
@@ -84,9 +100,7 @@ const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
|
|
|
84
100
|
>
|
|
85
101
|
{renderLeft()}
|
|
86
102
|
<span className="dropdown-menu-item-body">
|
|
87
|
-
{
|
|
88
|
-
<span className="dropdown-menu-item-label">{labelContent}</span>
|
|
89
|
-
) : null}
|
|
103
|
+
{resolvedLabelContent}
|
|
90
104
|
{description ? (
|
|
91
105
|
<span className="dropdown-menu-item-description">
|
|
92
106
|
{description}
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import type { DropdownMenuProps } from "@radix-ui/react-dropdown-menu";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
|
|
6
|
-
import type { DropdownRootProps } from "../../types/base";
|
|
7
7
|
import { DropdownProvider } from "./Provider";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Dropdown
|
|
10
|
+
* Dropdown Foundation; Root Provider 래핑 컴포넌트
|
|
11
11
|
* @component
|
|
12
|
-
* @param {
|
|
12
|
+
* @param {DropdownMenuProps} props Dropdown Root props
|
|
13
13
|
* @param {ReactNode} props.children Dropdown 하위 node
|
|
14
14
|
* @param {boolean} [props.modal=false] Radix modal 모드
|
|
15
|
+
* @param {boolean} [props.open] 제어형 open 상태
|
|
16
|
+
* @param {boolean} [props.defaultOpen] 비제어형 초기 open 상태
|
|
17
|
+
* @param {(open: boolean) => void} [props.onOpenChange] open 변경 콜백
|
|
18
|
+
* @example
|
|
19
|
+
* <DropdownRoot>
|
|
20
|
+
* <Dropdown.Trigger>열기</Dropdown.Trigger>
|
|
21
|
+
* </DropdownRoot>
|
|
15
22
|
*/
|
|
16
23
|
const DropdownRoot = ({
|
|
17
24
|
children,
|
|
18
25
|
modal = false,
|
|
19
26
|
...rootProps
|
|
20
|
-
}:
|
|
27
|
+
}: DropdownMenuProps & { children: ReactNode }) => {
|
|
21
28
|
return (
|
|
22
29
|
<DropdownProvider>
|
|
23
30
|
<DropdownMenu.Root modal={modal} {...rootProps}>
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu";
|
|
4
5
|
import { forwardRef } from "react";
|
|
5
6
|
|
|
6
|
-
import type { DropdownTriggerProps } from "../../types/props";
|
|
7
7
|
import { mergeRefs } from "../../utils";
|
|
8
8
|
import { useDropdownContext } from "./Provider";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Dropdown
|
|
11
|
+
* Dropdown Foundation; Trigger ref 공유 컴포넌트
|
|
12
12
|
* @component
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {DropdownMenuTriggerProps} props Dropdown trigger props
|
|
14
14
|
* @param {boolean} [props.asChild=true] asChild 패턴 유지 여부
|
|
15
|
+
* @param {React.ReactNode} props.children Trigger 하위 node
|
|
16
|
+
* @param {string} [props.className] Trigger className
|
|
17
|
+
* @example
|
|
18
|
+
* <DropdownTrigger asChild>
|
|
19
|
+
* <button type="button">열기</button>
|
|
20
|
+
* </DropdownTrigger>
|
|
15
21
|
*/
|
|
16
|
-
const DropdownTrigger = forwardRef<HTMLElement,
|
|
22
|
+
const DropdownTrigger = forwardRef<HTMLElement, DropdownMenuTriggerProps>(
|
|
17
23
|
({ asChild = true, children, ...rest }, ref) => {
|
|
18
24
|
const { triggerRef } = useDropdownContext("Dropdown.Trigger");
|
|
19
25
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { DropdownMenuProps } from "@radix-ui/react-dropdown-menu";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Dropdown size scale
|
|
5
3
|
* @typedef {"small" | "medium" | "large"} DropdownSize
|
|
@@ -18,14 +16,3 @@ export type DropdownPanelWidth =
|
|
|
18
16
|
| "max-content"
|
|
19
17
|
| string
|
|
20
18
|
| number;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Dropdown root props
|
|
24
|
-
* @property {boolean} [modal] Radix modal 모드 여부
|
|
25
|
-
*/
|
|
26
|
-
export interface DropdownRootProps extends DropdownMenuProps {
|
|
27
|
-
/**
|
|
28
|
-
* Radix modal 모드 여부
|
|
29
|
-
*/
|
|
30
|
-
modal?: DropdownMenuProps["modal"];
|
|
31
|
-
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DropdownMenuProps,
|
|
2
3
|
DropdownMenuContentProps,
|
|
3
4
|
DropdownMenuItemProps as RadixDropdownMenuItemProps,
|
|
4
|
-
DropdownMenuTriggerProps,
|
|
5
5
|
} from "@radix-ui/react-dropdown-menu";
|
|
6
6
|
import type { HTMLAttributes, MutableRefObject, ReactNode } from "react";
|
|
7
7
|
|
|
8
8
|
import type { CheckboxProps } from "../../checkbox/types";
|
|
9
|
-
import type {
|
|
10
|
-
DropdownPanelWidth,
|
|
11
|
-
DropdownRootProps,
|
|
12
|
-
DropdownSize,
|
|
13
|
-
} from "./base";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Dropdown trigger props
|
|
17
|
-
* @property {boolean} [asChild=true] trigger를 asChild 패턴으로 감쌀지 여부
|
|
18
|
-
*/
|
|
19
|
-
export type DropdownTriggerProps = DropdownMenuTriggerProps;
|
|
9
|
+
import type { DropdownPanelWidth, DropdownSize } from "./base";
|
|
20
10
|
|
|
21
11
|
/**
|
|
22
12
|
* Dropdown Container props
|
|
@@ -142,11 +132,13 @@ export interface DropdownTemplateItem {
|
|
|
142
132
|
* @property {ReactNode} trigger trigger 요소
|
|
143
133
|
* @property {DropdownTemplateItem[]} items 렌더링할 menu item 리스트
|
|
144
134
|
* @property {string[]} [selectedIds] 선택된 item id 배열
|
|
135
|
+
* @property {(item: DropdownTemplateItem) => void} [onSelect] item 선택 콜백
|
|
145
136
|
* @property {DropdownSize} [size="medium"] surface height scale
|
|
146
137
|
* @property {DropdownPanelWidth} [width="match"] panel width 옵션
|
|
147
|
-
* @property {
|
|
138
|
+
* @property {DropdownMenuProps} [rootProps] Root 에 전달할 props
|
|
148
139
|
* @property {DropdownContainerProps} [containerProps] Container 에 전달할 props
|
|
149
140
|
* @property {DropdownMenuListProps} [menuListProps] MenuList 에 전달할 props
|
|
141
|
+
* @property {ReactNode} [alt] item이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
150
142
|
*/
|
|
151
143
|
export interface DropdownTemplateProps {
|
|
152
144
|
trigger: ReactNode;
|
|
@@ -157,8 +149,9 @@ export interface DropdownTemplateProps {
|
|
|
157
149
|
width?: DropdownPanelWidth;
|
|
158
150
|
/**
|
|
159
151
|
* Root 에 전달할 props
|
|
152
|
+
* - 타입 출처를 명확히 하기 위해 Radix 원본 타입을 직접 사용한다.
|
|
160
153
|
*/
|
|
161
|
-
rootProps?:
|
|
154
|
+
rootProps?: DropdownMenuProps;
|
|
162
155
|
/**
|
|
163
156
|
* Container 에 전달할 props
|
|
164
157
|
*/
|
|
@@ -167,4 +160,8 @@ export interface DropdownTemplateProps {
|
|
|
167
160
|
* MenuList 에 전달할 props
|
|
168
161
|
*/
|
|
169
162
|
menuListProps?: DropdownMenuListProps;
|
|
163
|
+
/**
|
|
164
|
+
* item이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
165
|
+
*/
|
|
166
|
+
alt?: ReactNode;
|
|
170
167
|
}
|
|
@@ -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
|
+
};
|
|
@@ -8,10 +8,8 @@ import { Dropdown } from "../../dropdown/markup";
|
|
|
8
8
|
import { SelectTriggerBase, SelectTriggerSelected } from "./foundation";
|
|
9
9
|
import Container from "./foundation/Container";
|
|
10
10
|
import { useSelectDropdownOpenState } from "../hooks";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
SelectDropdownOption,
|
|
14
|
-
} from "../types/props";
|
|
11
|
+
import type { SelectDropdownOption } from "../types/option";
|
|
12
|
+
import type { SelectDefaultComponentProps } from "../types/props";
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* Select default trigger; 단일 선택 드롭다운을 렌더링한다.
|
|
@@ -24,7 +22,18 @@ import type {
|
|
|
24
22
|
* @param {"small" | "medium" | "large"} [props.size="medium"] size 스케일
|
|
25
23
|
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
26
24
|
* @param {boolean} [props.block] block 여부
|
|
25
|
+
* @param {FormFieldWidth} [props.width] container width preset
|
|
27
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 상태 변경 콜백
|
|
28
37
|
*/
|
|
29
38
|
const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
30
39
|
(
|
|
@@ -48,6 +57,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
48
57
|
dropdownRootProps,
|
|
49
58
|
dropdownContainerProps,
|
|
50
59
|
dropdownMenuListProps,
|
|
60
|
+
alt,
|
|
51
61
|
open,
|
|
52
62
|
defaultOpen,
|
|
53
63
|
onOpenChange,
|
|
@@ -73,7 +83,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
73
83
|
defaultOpen,
|
|
74
84
|
onOpenChange,
|
|
75
85
|
});
|
|
76
|
-
//
|
|
86
|
+
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
77
87
|
|
|
78
88
|
const handleOptionSelect = (option: SelectDropdownOption) => {
|
|
79
89
|
onOptionSelect?.(option);
|
|
@@ -81,7 +91,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
81
91
|
};
|
|
82
92
|
|
|
83
93
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
84
|
-
const
|
|
94
|
+
const hasOptions = options.length > 0;
|
|
85
95
|
|
|
86
96
|
return (
|
|
87
97
|
<Container
|
|
@@ -115,36 +125,45 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
115
125
|
/>
|
|
116
126
|
</SelectTriggerBase>
|
|
117
127
|
</Dropdown.Trigger>
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
<Dropdown.Container
|
|
129
|
+
{...dropdownContainerProps}
|
|
130
|
+
size={panelSize}
|
|
131
|
+
width={dropdownWidth}
|
|
132
|
+
>
|
|
133
|
+
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
134
|
+
{hasOptions ? (
|
|
135
|
+
<>
|
|
136
|
+
{/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
|
|
137
|
+
{options.map(option => (
|
|
138
|
+
<Dropdown.Menu.Item
|
|
139
|
+
key={option.id}
|
|
140
|
+
label={option.label}
|
|
141
|
+
description={option.description}
|
|
142
|
+
disabled={option.disabled}
|
|
143
|
+
left={option.left}
|
|
144
|
+
right={option.right}
|
|
145
|
+
multiple={Boolean(option.multiple)}
|
|
146
|
+
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
147
|
+
onSelect={event => {
|
|
148
|
+
if (option.disabled) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
handleOptionSelect(option);
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</>
|
|
157
|
+
) : (
|
|
158
|
+
<Dropdown.Menu.Item
|
|
159
|
+
// 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
|
|
160
|
+
label={alt ?? "선택할 항목이 없습니다."}
|
|
161
|
+
disabled
|
|
162
|
+
className="dropdown-menu-alt"
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</Dropdown.Menu.List>
|
|
166
|
+
</Dropdown.Container>
|
|
148
167
|
</Dropdown.Root>
|
|
149
168
|
</Container>
|
|
150
169
|
);
|
|
@@ -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
14
|
* @param {"primary" | "secondary"} [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;
|
|
@@ -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
25
|
* @param {"primary" | "secondary"} [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,
|
|
@@ -100,10 +115,10 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
100
115
|
defaultOpen,
|
|
101
116
|
onOpenChange,
|
|
102
117
|
});
|
|
103
|
-
//
|
|
118
|
+
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
104
119
|
|
|
105
120
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
106
|
-
const
|
|
121
|
+
const hasOptions = options.length > 0;
|
|
107
122
|
const MAX_VISIBLE_TAGS = 3;
|
|
108
123
|
const visibleTags = hasTags ? derivedTags.slice(0, MAX_VISIBLE_TAGS) : [];
|
|
109
124
|
const overflowCount = hasTags
|
|
@@ -166,36 +181,45 @@ const SelectMultipleTrigger = forwardRef<
|
|
|
166
181
|
)}
|
|
167
182
|
</SelectTriggerBase>
|
|
168
183
|
</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
|
-
|
|
184
|
+
<Dropdown.Container
|
|
185
|
+
{...dropdownContainerProps}
|
|
186
|
+
size={panelSize}
|
|
187
|
+
width={dropdownWidth}
|
|
188
|
+
>
|
|
189
|
+
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
190
|
+
{hasOptions ? (
|
|
191
|
+
<>
|
|
192
|
+
{/* multi select 전용 옵션을 Dropdown.Menu.Item으로 노출한다. */}
|
|
193
|
+
{options.map(option => (
|
|
194
|
+
<Dropdown.Menu.Item
|
|
195
|
+
key={option.id}
|
|
196
|
+
label={option.label}
|
|
197
|
+
description={option.description}
|
|
198
|
+
disabled={option.disabled}
|
|
199
|
+
left={option.left}
|
|
200
|
+
right={option.right}
|
|
201
|
+
multiple
|
|
202
|
+
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
203
|
+
onSelect={event => {
|
|
204
|
+
if (option.disabled) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
onOptionSelect?.(option);
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
</>
|
|
213
|
+
) : (
|
|
214
|
+
<Dropdown.Menu.Item
|
|
215
|
+
// 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
|
|
216
|
+
label={alt ?? "선택할 항목이 없습니다."}
|
|
217
|
+
disabled
|
|
218
|
+
className="dropdown-menu-alt"
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</Dropdown.Menu.List>
|
|
222
|
+
</Dropdown.Container>
|
|
199
223
|
</Dropdown.Root>
|
|
200
224
|
</Container>
|
|
201
225
|
);
|
|
@@ -6,13 +6,16 @@ import RemoveIcon from "../../img/remove.svg";
|
|
|
6
6
|
import type { SelectMultipleChipProps } from "../../types/multiple";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Select
|
|
9
|
+
* Select Markup; Multiple 선택값 Chip 렌더링 컴포넌트
|
|
10
10
|
* @component
|
|
11
11
|
* @param {SelectMultipleChipProps} props chip props
|
|
12
12
|
* @param {React.ReactNode} props.label chip 라벨
|
|
13
13
|
* @param {React.ReactNode} [props.suffix] 라벨 뒤에 붙는 서브 라벨
|
|
14
|
-
* @param {boolean} [props.removable] remove 버튼 노출 여부
|
|
14
|
+
* @param {boolean} [props.removable=true] remove 버튼 노출 여부
|
|
15
15
|
* @param {() => void} [props.onRemove] remove 클릭 핸들러
|
|
16
|
+
* @param {"value" | "summary"} [props.kind="value"] chip 용도 구분
|
|
17
|
+
* @example
|
|
18
|
+
* <SelectMultipleSelectedChip label="Apple" removable onRemove={() => {}} />
|
|
16
19
|
*/
|
|
17
20
|
export function SelectMultipleSelectedChip({
|
|
18
21
|
label,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SelectDropdownBehaviorProps } from "./props";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Select Hook Types; Dropdown open state hook 입력 파라미터
|
|
5
|
+
* @property {boolean} [open] 외부 제어형 open 상태
|
|
6
|
+
* @property {boolean} [defaultOpen] 비제어형 초기 open 상태
|
|
7
|
+
* @property {(open: boolean) => void} [onOpenChange] open 상태 변경 콜백
|
|
8
|
+
*/
|
|
9
|
+
export interface UseSelectDropdownOpenStateParams extends SelectDropdownBehaviorProps {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Select Hook Types; Dropdown open state hook 반환값
|
|
13
|
+
* @property {boolean} open 최종 open 상태
|
|
14
|
+
* @property {(nextOpen: boolean) => void} setOpen open 상태 업데이트 함수
|
|
15
|
+
* @property {boolean} isControlled open prop 기반 제어형 여부
|
|
16
|
+
*/
|
|
17
|
+
export interface UseSelectDropdownOpenStateReturn {
|
|
18
|
+
/**
|
|
19
|
+
* 최종 open 상태
|
|
20
|
+
*/
|
|
21
|
+
open: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* open 상태 업데이트 함수
|
|
24
|
+
*/
|
|
25
|
+
setOpen: (nextOpen: boolean) => void;
|
|
26
|
+
/**
|
|
27
|
+
* open prop 기반 제어형 여부
|
|
28
|
+
*/
|
|
29
|
+
isControlled: boolean;
|
|
30
|
+
}
|
|
@@ -1,13 +1,87 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Select option
|
|
4
|
+
* Select option value 타입
|
|
5
5
|
* @typedef {string | number} SelectOptionValue
|
|
6
6
|
*/
|
|
7
7
|
export type SelectOptionValue = string | number;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Select option
|
|
10
|
+
* Select option data; form 상태와 직결되는 데이터 계약
|
|
11
|
+
* @property {string} id 렌더링/선택 추적용 고유 id
|
|
12
|
+
* @property {SelectOptionValue} value 실제 form value
|
|
13
|
+
* @property {ReactNode} label 사용자 노출 라벨
|
|
14
|
+
* @property {boolean} [disabled] 비활성 여부
|
|
15
|
+
* @property {OptionData} [data] 추가 데이터 payload
|
|
16
|
+
*/
|
|
17
|
+
export interface SelectOptionData<OptionData = unknown> {
|
|
18
|
+
/**
|
|
19
|
+
* 렌더링/선택 추적용 고유 id
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* 실제 form value
|
|
24
|
+
*/
|
|
25
|
+
value: SelectOptionValue;
|
|
26
|
+
/**
|
|
27
|
+
* 사용자 노출 라벨
|
|
28
|
+
*/
|
|
29
|
+
label: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* 비활성 여부
|
|
32
|
+
*/
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* 추가 데이터 payload
|
|
36
|
+
*/
|
|
37
|
+
data?: OptionData;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Select option render data; Dropdown.Menu.Item 렌더링 보조 정보
|
|
42
|
+
* @property {ReactNode} [description] 보조 텍스트
|
|
43
|
+
* @property {ReactNode} [left] 좌측 콘텐츠
|
|
44
|
+
* @property {ReactNode} [right] 우측 콘텐츠
|
|
45
|
+
* @property {boolean} [multiple] multi select 스타일 여부
|
|
46
|
+
*/
|
|
47
|
+
export interface SelectOptionRenderData {
|
|
48
|
+
/**
|
|
49
|
+
* 보조 텍스트
|
|
50
|
+
*/
|
|
51
|
+
description?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* 좌측 콘텐츠
|
|
54
|
+
*/
|
|
55
|
+
left?: ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* 우측 콘텐츠
|
|
58
|
+
*/
|
|
59
|
+
right?: ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* multi select 스타일 여부
|
|
62
|
+
*/
|
|
63
|
+
multiple?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Select dropdown option; data 계약과 render 계약을 합쳐 Select 입력 모델을 구성한다.
|
|
68
|
+
* @extends SelectOptionData
|
|
69
|
+
* @extends SelectOptionRenderData
|
|
70
|
+
* @property {string} id 렌더링/선택 추적용 고유 id
|
|
71
|
+
* @property {SelectOptionValue} value 실제 form value
|
|
72
|
+
* @property {ReactNode} label 사용자 노출 라벨
|
|
73
|
+
* @property {boolean} [disabled] 비활성 여부
|
|
74
|
+
* @property {OptionData} [data] 추가 데이터 payload
|
|
75
|
+
* @property {ReactNode} [description] 보조 텍스트
|
|
76
|
+
* @property {ReactNode} [left] 좌측 콘텐츠
|
|
77
|
+
* @property {ReactNode} [right] 우측 콘텐츠
|
|
78
|
+
* @property {boolean} [multiple] multi select 스타일 여부
|
|
79
|
+
*/
|
|
80
|
+
export interface SelectDropdownOption<OptionData = unknown>
|
|
81
|
+
extends SelectOptionData<OptionData>, SelectOptionRenderData {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Select legacy option 데이터 구조; 과거 key/optionName 스키마 호환을 위해 유지한다.
|
|
11
85
|
* @property {string} key 렌더링 키
|
|
12
86
|
* @property {SelectOptionValue} value 실제 값
|
|
13
87
|
* @property {ReactNode} optionName 사용자 노출 라벨
|
|
@@ -15,7 +89,7 @@ export type SelectOptionValue = string | number;
|
|
|
15
89
|
* @property {boolean} [disabled] 비활성 여부
|
|
16
90
|
* @property {OptionData} [data] 추가 데이터 payload
|
|
17
91
|
*/
|
|
18
|
-
export interface
|
|
92
|
+
export interface SelectLegacyOption<OptionData = unknown> {
|
|
19
93
|
/**
|
|
20
94
|
* 렌더링 키
|
|
21
95
|
*/
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import type { DropdownMenuProps } from "@radix-ui/react-dropdown-menu";
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
5
|
DropdownContainerProps,
|
|
5
6
|
DropdownMenuListProps,
|
|
6
7
|
DropdownPanelWidth,
|
|
7
|
-
DropdownRootProps,
|
|
8
|
-
DropdownTemplateItem,
|
|
9
8
|
} from "../../dropdown/types";
|
|
10
9
|
import type { FormFieldWidth } from "../../form/types";
|
|
11
10
|
import type { SelectPriority, SelectSize, SelectState } from "./base";
|
|
11
|
+
import type { SelectDropdownOption } from "./option";
|
|
12
12
|
import type {
|
|
13
13
|
SelectTriggerDefaultProps,
|
|
14
14
|
SelectTriggerMultipleProps,
|
|
@@ -116,18 +116,22 @@ export interface SelectWidthOption {
|
|
|
116
116
|
/**
|
|
117
117
|
* Select root props
|
|
118
118
|
* @typedef {SelectStyleOptions & SelectValueOptions & SelectComponentState} SelectProps
|
|
119
|
+
* @property {SelectPriority} [priority] priority scale
|
|
120
|
+
* @property {SelectSize} [size] size scale
|
|
121
|
+
* @property {SelectState} [state] visual state
|
|
122
|
+
* @property {boolean} [block] block 여부
|
|
123
|
+
* @property {ReactNode} [displayLabel] 선택된 라벨
|
|
124
|
+
* @property {ReactNode} [placeholder] placeholder 텍스트
|
|
125
|
+
* @property {ReactNode[]} [tags] multi select 태그 리스트
|
|
126
|
+
* @property {boolean} [multiple] multi select 여부
|
|
127
|
+
* @property {boolean} [isOpen] dropdown open 여부
|
|
128
|
+
* @property {FormFieldWidth} [width] width preset 옵션
|
|
119
129
|
*/
|
|
120
130
|
export type SelectProps = SelectStyleOptions &
|
|
121
131
|
SelectValueOptions &
|
|
122
132
|
SelectComponentState &
|
|
123
133
|
SelectWidthOption;
|
|
124
134
|
|
|
125
|
-
/**
|
|
126
|
-
* Select dropdown option; Dropdown.Template item 계약을 그대로 따른다.
|
|
127
|
-
* @extends DropdownTemplateItem
|
|
128
|
-
*/
|
|
129
|
-
export interface SelectDropdownOption extends DropdownTemplateItem {}
|
|
130
|
-
|
|
131
135
|
/**
|
|
132
136
|
* Select dropdown 옵션 구성 props
|
|
133
137
|
* @property {SelectDropdownOption[]} [options] dropdown option 리스트
|
|
@@ -135,9 +139,10 @@ export interface SelectDropdownOption extends DropdownTemplateItem {}
|
|
|
135
139
|
* @property {(option: SelectDropdownOption) => void} [onOptionSelect] option 선택 콜백
|
|
136
140
|
* @property {SelectSize} [dropdownSize] dropdown surface size 스케일
|
|
137
141
|
* @property {DropdownPanelWidth} [dropdownWidth="match"] dropdown panel width 옵션
|
|
138
|
-
* @property {
|
|
142
|
+
* @property {DropdownMenuProps} [dropdownRootProps] Dropdown.Root 전달 props(제어 props 제외)
|
|
139
143
|
* @property {DropdownContainerProps} [dropdownContainerProps] Dropdown.Container 전달 props(children/size 제외)
|
|
140
144
|
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
145
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
141
146
|
*/
|
|
142
147
|
export interface SelectDropdownConfigProps {
|
|
143
148
|
/**
|
|
@@ -162,9 +167,10 @@ export interface SelectDropdownConfigProps {
|
|
|
162
167
|
dropdownWidth?: DropdownPanelWidth;
|
|
163
168
|
/**
|
|
164
169
|
* Dropdown.Root 전달 props(제어 props 제외)
|
|
170
|
+
* - 타입 출처를 명확히 하기 위해 Radix 원본 타입을 직접 사용한다.
|
|
165
171
|
*/
|
|
166
172
|
dropdownRootProps?: Omit<
|
|
167
|
-
|
|
173
|
+
DropdownMenuProps,
|
|
168
174
|
"open" | "defaultOpen" | "onOpenChange"
|
|
169
175
|
>;
|
|
170
176
|
/**
|
|
@@ -178,6 +184,10 @@ export interface SelectDropdownConfigProps {
|
|
|
178
184
|
* Dropdown.Menu.List 전달 props
|
|
179
185
|
*/
|
|
180
186
|
dropdownMenuListProps?: DropdownMenuListProps;
|
|
187
|
+
/**
|
|
188
|
+
* option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
189
|
+
*/
|
|
190
|
+
alt?: ReactNode;
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
/**
|
|
@@ -203,26 +213,62 @@ export interface SelectDropdownBehaviorProps {
|
|
|
203
213
|
|
|
204
214
|
/**
|
|
205
215
|
* Select.Default 컴포넌트 props
|
|
206
|
-
* @
|
|
207
|
-
* @
|
|
208
|
-
* @
|
|
216
|
+
* @typedef {SelectTriggerDefaultProps & SelectDropdownConfigProps & SelectDropdownBehaviorProps & SelectWidthOption} SelectDefaultComponentProps
|
|
217
|
+
* @property {ReactNode} [displayLabel] 선택된 라벨
|
|
218
|
+
* @property {ReactNode} [placeholder] placeholder 텍스트
|
|
219
|
+
* @property {SelectPriority} [priority] priority scale
|
|
220
|
+
* @property {SelectSize} [size] size scale
|
|
221
|
+
* @property {SelectState} [state] visual state
|
|
222
|
+
* @property {boolean} [block] block 여부
|
|
223
|
+
* @property {boolean} [isOpen] dropdown open 여부
|
|
224
|
+
* @property {boolean} [disabled] disabled 여부
|
|
225
|
+
* @property {SelectTriggerButtonType} [buttonType] button type
|
|
226
|
+
* @property {FormFieldWidth} [width] width preset 옵션
|
|
227
|
+
* @property {SelectDropdownOption[]} [options] dropdown option 리스트
|
|
228
|
+
* @property {string[]} [selectedOptionIds] 선택된 option id 리스트
|
|
229
|
+
* @property {(option: SelectDropdownOption) => void} [onOptionSelect] option 선택 콜백
|
|
230
|
+
* @property {SelectSize} [dropdownSize] dropdown surface size 스케일
|
|
231
|
+
* @property {DropdownPanelWidth} [dropdownWidth="match"] dropdown panel width 옵션
|
|
232
|
+
* @property {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [dropdownRootProps] Dropdown.Root 전달 props
|
|
233
|
+
* @property {Omit<DropdownContainerProps, "children" | "size" | "width">} [dropdownContainerProps] Dropdown.Container 전달 props
|
|
234
|
+
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
235
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
236
|
+
* @property {boolean} [open] dropdown open 상태
|
|
237
|
+
* @property {boolean} [defaultOpen] uncontrolled 초기 open 상태
|
|
238
|
+
* @property {(open: boolean) => void} [onOpenChange] open state change 콜백
|
|
209
239
|
*/
|
|
210
|
-
export
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
SelectDropdownBehaviorProps,
|
|
215
|
-
SelectWidthOption {}
|
|
240
|
+
export type SelectDefaultComponentProps = SelectTriggerDefaultProps &
|
|
241
|
+
SelectDropdownConfigProps &
|
|
242
|
+
SelectDropdownBehaviorProps &
|
|
243
|
+
SelectWidthOption;
|
|
216
244
|
|
|
217
245
|
/**
|
|
218
246
|
* Select.Multiple 컴포넌트 props
|
|
219
|
-
* @
|
|
220
|
-
* @
|
|
221
|
-
* @
|
|
247
|
+
* @typedef {SelectTriggerMultipleProps & SelectDropdownConfigProps & SelectDropdownBehaviorProps & SelectWidthOption} SelectMultipleComponentProps
|
|
248
|
+
* @property {ReactNode} [displayLabel] 선택된 라벨
|
|
249
|
+
* @property {ReactNode} [placeholder] placeholder 텍스트
|
|
250
|
+
* @property {SelectMultipleTag[]} [tags] multi select tag 리스트
|
|
251
|
+
* @property {SelectPriority} [priority] priority scale
|
|
252
|
+
* @property {SelectSize} [size] size scale
|
|
253
|
+
* @property {SelectState} [state] visual state
|
|
254
|
+
* @property {boolean} [block] block 여부
|
|
255
|
+
* @property {boolean} [isOpen] dropdown open 여부
|
|
256
|
+
* @property {boolean} [disabled] disabled 여부
|
|
257
|
+
* @property {FormFieldWidth} [width] width preset 옵션
|
|
258
|
+
* @property {SelectDropdownOption[]} [options] dropdown option 리스트
|
|
259
|
+
* @property {string[]} [selectedOptionIds] 선택된 option id 리스트
|
|
260
|
+
* @property {(option: SelectDropdownOption) => void} [onOptionSelect] option 선택 콜백
|
|
261
|
+
* @property {SelectSize} [dropdownSize] dropdown surface size 스케일
|
|
262
|
+
* @property {DropdownPanelWidth} [dropdownWidth="match"] dropdown panel width 옵션
|
|
263
|
+
* @property {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [dropdownRootProps] Dropdown.Root 전달 props
|
|
264
|
+
* @property {Omit<DropdownContainerProps, "children" | "size" | "width">} [dropdownContainerProps] Dropdown.Container 전달 props
|
|
265
|
+
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
266
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
267
|
+
* @property {boolean} [open] dropdown open 상태
|
|
268
|
+
* @property {boolean} [defaultOpen] uncontrolled 초기 open 상태
|
|
269
|
+
* @property {(open: boolean) => void} [onOpenChange] open state change 콜백
|
|
222
270
|
*/
|
|
223
|
-
export
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
SelectDropdownBehaviorProps,
|
|
228
|
-
SelectWidthOption {}
|
|
271
|
+
export type SelectMultipleComponentProps = SelectTriggerMultipleProps &
|
|
272
|
+
SelectDropdownConfigProps &
|
|
273
|
+
SelectDropdownBehaviorProps &
|
|
274
|
+
SelectWidthOption;
|