@uniai-fe/uds-primitives 0.2.8 → 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 +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/select/hooks/index.ts +1 -45
- package/src/components/select/hooks/interaction.ts +62 -0
- package/src/components/select/markup/Default.tsx +53 -32
- 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/props.ts +7 -0
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}
|
|
@@ -7,11 +7,18 @@ import type { ReactNode } from "react";
|
|
|
7
7
|
import { DropdownProvider } from "./Provider";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Dropdown
|
|
10
|
+
* Dropdown Foundation; Root Provider 래핑 컴포넌트
|
|
11
11
|
* @component
|
|
12
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,
|
|
@@ -8,10 +8,16 @@ 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
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
22
|
const DropdownTrigger = forwardRef<HTMLElement, DropdownMenuTriggerProps>(
|
|
17
23
|
({ asChild = true, children, ...rest }, ref) => {
|
|
@@ -138,6 +138,7 @@ export interface DropdownTemplateItem {
|
|
|
138
138
|
* @property {DropdownMenuProps} [rootProps] Root 에 전달할 props
|
|
139
139
|
* @property {DropdownContainerProps} [containerProps] Container 에 전달할 props
|
|
140
140
|
* @property {DropdownMenuListProps} [menuListProps] MenuList 에 전달할 props
|
|
141
|
+
* @property {ReactNode} [alt] item이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
141
142
|
*/
|
|
142
143
|
export interface DropdownTemplateProps {
|
|
143
144
|
trigger: ReactNode;
|
|
@@ -159,4 +160,8 @@ export interface DropdownTemplateProps {
|
|
|
159
160
|
* MenuList 에 전달할 props
|
|
160
161
|
*/
|
|
161
162
|
menuListProps?: DropdownMenuListProps;
|
|
163
|
+
/**
|
|
164
|
+
* item이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
165
|
+
*/
|
|
166
|
+
alt?: ReactNode;
|
|
162
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
|
+
};
|
|
@@ -22,7 +22,18 @@ import type { SelectDefaultComponentProps } from "../types/props";
|
|
|
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,
|
|
@@ -71,7 +83,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
71
83
|
defaultOpen,
|
|
72
84
|
onOpenChange,
|
|
73
85
|
});
|
|
74
|
-
//
|
|
86
|
+
// 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
|
|
75
87
|
|
|
76
88
|
const handleOptionSelect = (option: SelectDropdownOption) => {
|
|
77
89
|
onOptionSelect?.(option);
|
|
@@ -79,7 +91,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
79
91
|
};
|
|
80
92
|
|
|
81
93
|
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
82
|
-
const
|
|
94
|
+
const hasOptions = options.length > 0;
|
|
83
95
|
|
|
84
96
|
return (
|
|
85
97
|
<Container
|
|
@@ -113,36 +125,45 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
113
125
|
/>
|
|
114
126
|
</SelectTriggerBase>
|
|
115
127
|
</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
|
-
|
|
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>
|
|
146
167
|
</Dropdown.Root>
|
|
147
168
|
</Container>
|
|
148
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
|
+
}
|
|
@@ -142,6 +142,7 @@ export type SelectProps = SelectStyleOptions &
|
|
|
142
142
|
* @property {DropdownMenuProps} [dropdownRootProps] Dropdown.Root 전달 props(제어 props 제외)
|
|
143
143
|
* @property {DropdownContainerProps} [dropdownContainerProps] Dropdown.Container 전달 props(children/size 제외)
|
|
144
144
|
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
145
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
145
146
|
*/
|
|
146
147
|
export interface SelectDropdownConfigProps {
|
|
147
148
|
/**
|
|
@@ -183,6 +184,10 @@ export interface SelectDropdownConfigProps {
|
|
|
183
184
|
* Dropdown.Menu.List 전달 props
|
|
184
185
|
*/
|
|
185
186
|
dropdownMenuListProps?: DropdownMenuListProps;
|
|
187
|
+
/**
|
|
188
|
+
* option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
189
|
+
*/
|
|
190
|
+
alt?: ReactNode;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
/**
|
|
@@ -227,6 +232,7 @@ export interface SelectDropdownBehaviorProps {
|
|
|
227
232
|
* @property {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [dropdownRootProps] Dropdown.Root 전달 props
|
|
228
233
|
* @property {Omit<DropdownContainerProps, "children" | "size" | "width">} [dropdownContainerProps] Dropdown.Container 전달 props
|
|
229
234
|
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
235
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
230
236
|
* @property {boolean} [open] dropdown open 상태
|
|
231
237
|
* @property {boolean} [defaultOpen] uncontrolled 초기 open 상태
|
|
232
238
|
* @property {(open: boolean) => void} [onOpenChange] open state change 콜백
|
|
@@ -257,6 +263,7 @@ export type SelectDefaultComponentProps = SelectTriggerDefaultProps &
|
|
|
257
263
|
* @property {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [dropdownRootProps] Dropdown.Root 전달 props
|
|
258
264
|
* @property {Omit<DropdownContainerProps, "children" | "size" | "width">} [dropdownContainerProps] Dropdown.Container 전달 props
|
|
259
265
|
* @property {DropdownMenuListProps} [dropdownMenuListProps] Dropdown.Menu.List 전달 props
|
|
266
|
+
* @property {ReactNode} [alt] option이 비어 있을 때 렌더링할 alternate 콘텐츠
|
|
260
267
|
* @property {boolean} [open] dropdown open 상태
|
|
261
268
|
* @property {boolean} [defaultOpen] uncontrolled 초기 open 상태
|
|
262
269
|
* @property {(open: boolean) => void} [onOpenChange] open state change 콜백
|