@ncds/ui-admin 1.8.4 → 1.8.5
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/cjs/assets/scripts/featuredIcon.js +87 -0
- package/dist/cjs/assets/scripts/notification/FloatingNotification.js +178 -0
- package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +133 -0
- package/dist/cjs/assets/scripts/notification/MessageNotification.js +159 -0
- package/dist/cjs/assets/scripts/notification/Notification.js +120 -0
- package/dist/cjs/assets/scripts/notification/const/classNames.js +50 -0
- package/dist/cjs/assets/scripts/notification/const/icons.js +31 -0
- package/dist/cjs/assets/scripts/notification/const/index.js +87 -0
- package/dist/cjs/assets/scripts/notification/const/sizes.js +46 -0
- package/dist/cjs/assets/scripts/notification/const/types.js +14 -0
- package/dist/cjs/assets/scripts/notification/index.js +116 -0
- package/dist/cjs/assets/scripts/notification/positionSync.js +180 -0
- package/dist/cjs/assets/scripts/notification/utils.js +122 -0
- package/dist/cjs/assets/scripts/shared/ButtonCloseX.js +45 -0
- package/dist/cjs/assets/scripts/utils/sanitize.js +39 -0
- package/dist/cjs/src/components/data-display/data-grid/DataGrid.js +5 -1
- package/dist/cjs/src/components/data-display/table/Table.js +118 -96
- package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +187 -0
- package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
- package/dist/cjs/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
- package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +67 -29
- package/dist/cjs/src/components/overlays/dropdown/Dropdown.js +47 -19
- package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +25 -0
- package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +86 -13
- package/dist/cjs/src/components/overlays/notification/Notification.js +7 -0
- package/dist/cjs/src/components/overlays/notification/host.js +12 -0
- package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +57 -44
- package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +2 -1
- package/dist/cjs/src/contexts/FloatingContext.js +11 -0
- package/dist/cjs/src/contexts/index.js +16 -0
- package/dist/cjs/src/hooks/index.js +11 -0
- package/dist/cjs/src/hooks/useFloatingPosition.js +78 -0
- package/dist/cjs/src/hooks/usePortalState.js +17 -0
- package/dist/cjs/src/utils/dropdown/maxSelection.js +35 -0
- package/dist/cjs/src/utils/dropdown/multiSelect.js +72 -15
- package/dist/esm/assets/scripts/featuredIcon.js +80 -0
- package/dist/esm/assets/scripts/notification/FloatingNotification.js +171 -0
- package/dist/esm/assets/scripts/notification/FullWidthNotification.js +126 -0
- package/dist/esm/assets/scripts/notification/MessageNotification.js +152 -0
- package/dist/esm/assets/scripts/notification/Notification.js +113 -0
- package/dist/esm/assets/scripts/notification/const/classNames.js +44 -0
- package/dist/esm/assets/scripts/notification/const/icons.js +25 -0
- package/dist/esm/assets/scripts/notification/const/index.js +4 -0
- package/dist/esm/assets/scripts/notification/const/sizes.js +40 -0
- package/dist/esm/assets/scripts/notification/const/types.js +8 -0
- package/dist/esm/assets/scripts/notification/index.js +10 -0
- package/dist/esm/assets/scripts/notification/positionSync.js +171 -0
- package/dist/esm/assets/scripts/notification/utils.js +109 -0
- package/dist/esm/assets/scripts/shared/ButtonCloseX.js +37 -0
- package/dist/esm/assets/scripts/utils/sanitize.js +31 -0
- package/dist/esm/src/components/data-display/data-grid/DataGrid.js +5 -1
- package/dist/esm/src/components/data-display/table/Table.js +118 -96
- package/dist/esm/src/components/data-display/table/useTableScrollbars.js +179 -0
- package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
- package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
- package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +67 -29
- package/dist/esm/src/components/overlays/dropdown/Dropdown.js +47 -19
- package/dist/esm/src/components/overlays/notification/CalloutNotification.js +19 -0
- package/dist/esm/src/components/overlays/notification/FloatingNotification.js +86 -14
- package/dist/esm/src/components/overlays/notification/Notification.js +7 -0
- package/dist/esm/src/components/overlays/notification/host.js +9 -0
- package/dist/esm/src/components/overlays/tooltip/Tooltip.js +58 -45
- package/dist/esm/src/components/select-dropdown/SelectDropdown.js +2 -1
- package/dist/esm/src/contexts/FloatingContext.js +4 -0
- package/dist/esm/src/contexts/index.js +1 -0
- package/dist/esm/src/hooks/index.js +1 -0
- package/dist/esm/src/hooks/useFloatingPosition.js +71 -0
- package/dist/esm/src/hooks/usePortalState.js +10 -0
- package/dist/esm/src/utils/dropdown/maxSelection.js +27 -0
- package/dist/esm/src/utils/dropdown/multiSelect.js +70 -14
- package/dist/temp/assets/scripts/featuredIcon.d.ts +22 -0
- package/dist/temp/assets/scripts/featuredIcon.js +79 -0
- package/dist/temp/assets/scripts/notification/FloatingNotification.d.ts +24 -0
- package/dist/temp/assets/scripts/notification/FloatingNotification.js +156 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.js +111 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +22 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.js +140 -0
- package/dist/temp/assets/scripts/notification/Notification.d.ts +22 -0
- package/dist/temp/assets/scripts/notification/Notification.js +112 -0
- package/dist/temp/assets/scripts/notification/const/classNames.d.ts +43 -0
- package/dist/temp/assets/scripts/notification/const/classNames.js +44 -0
- package/dist/temp/assets/scripts/notification/const/icons.d.ts +25 -0
- package/dist/temp/assets/scripts/notification/const/icons.js +25 -0
- package/dist/temp/assets/scripts/notification/const/index.d.ts +5 -0
- package/dist/temp/assets/scripts/notification/const/index.js +4 -0
- package/dist/temp/assets/scripts/notification/const/sizes.d.ts +32 -0
- package/dist/temp/assets/scripts/notification/const/sizes.js +40 -0
- package/dist/temp/assets/scripts/notification/const/types.d.ts +19 -0
- package/dist/temp/assets/scripts/notification/const/types.js +8 -0
- package/dist/temp/assets/scripts/notification/index.d.ts +8 -0
- package/dist/temp/assets/scripts/notification/index.js +10 -0
- package/dist/temp/assets/scripts/notification/positionSync.d.ts +50 -0
- package/dist/temp/assets/scripts/notification/positionSync.js +170 -0
- package/dist/temp/assets/scripts/notification/utils.d.ts +8 -0
- package/dist/temp/assets/scripts/notification/utils.js +115 -0
- package/dist/temp/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
- package/dist/temp/assets/scripts/shared/ButtonCloseX.js +33 -0
- package/dist/temp/assets/scripts/utils/sanitize.d.ts +22 -0
- package/dist/temp/assets/scripts/utils/sanitize.js +31 -0
- package/dist/temp/src/components/data-display/data-grid/DataGrid.js +1 -1
- package/dist/temp/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
- package/dist/temp/src/components/data-display/table/Table.d.ts +4 -1
- package/dist/temp/src/components/data-display/table/Table.js +53 -68
- package/dist/temp/src/components/data-display/table/types.d.ts +18 -0
- package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
- package/dist/temp/src/components/data-display/table/useTableScrollbars.js +136 -0
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +7 -11
- package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +1 -1
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +30 -3
- package/dist/temp/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
- package/dist/temp/src/components/overlays/dropdown/Dropdown.js +35 -11
- package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
- package/dist/temp/src/components/overlays/notification/CalloutNotification.js +6 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.js +81 -13
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +18 -3
- package/dist/temp/src/components/overlays/notification/Notification.js +4 -0
- package/dist/temp/src/components/overlays/notification/host.d.ts +9 -0
- package/dist/temp/src/components/overlays/notification/host.js +9 -0
- package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
- package/dist/temp/src/components/overlays/tooltip/Tooltip.js +25 -22
- package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
- package/dist/temp/src/components/select-dropdown/SelectDropdown.js +2 -2
- package/dist/temp/src/contexts/FloatingContext.d.ts +6 -0
- package/dist/temp/src/contexts/FloatingContext.js +4 -0
- package/dist/temp/src/contexts/index.d.ts +1 -0
- package/dist/temp/src/contexts/index.js +1 -0
- package/dist/temp/src/hooks/index.d.ts +1 -0
- package/dist/temp/src/hooks/index.js +1 -0
- package/dist/temp/src/hooks/useFloatingPosition.d.ts +19 -0
- package/dist/temp/src/hooks/useFloatingPosition.js +55 -0
- package/dist/temp/src/hooks/usePortalState.d.ts +6 -0
- package/dist/temp/src/hooks/usePortalState.js +7 -0
- package/dist/temp/src/utils/dropdown/maxSelection.d.ts +24 -0
- package/dist/temp/src/utils/dropdown/maxSelection.js +28 -0
- package/dist/temp/src/utils/dropdown/multiSelect.d.ts +42 -2
- package/dist/temp/src/utils/dropdown/multiSelect.js +66 -13
- package/dist/types/assets/scripts/featuredIcon.d.ts +22 -0
- package/dist/types/assets/scripts/notification/FloatingNotification.d.ts +24 -0
- package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
- package/dist/types/assets/scripts/notification/MessageNotification.d.ts +22 -0
- package/dist/types/assets/scripts/notification/Notification.d.ts +22 -0
- package/dist/types/assets/scripts/notification/const/classNames.d.ts +43 -0
- package/dist/types/assets/scripts/notification/const/icons.d.ts +25 -0
- package/dist/types/assets/scripts/notification/const/index.d.ts +5 -0
- package/dist/types/assets/scripts/notification/const/sizes.d.ts +32 -0
- package/dist/types/assets/scripts/notification/const/types.d.ts +19 -0
- package/dist/types/assets/scripts/notification/index.d.ts +8 -0
- package/dist/types/assets/scripts/notification/positionSync.d.ts +50 -0
- package/dist/types/assets/scripts/notification/utils.d.ts +8 -0
- package/dist/types/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
- package/dist/types/assets/scripts/utils/sanitize.d.ts +22 -0
- package/dist/types/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
- package/dist/types/src/components/data-display/table/Table.d.ts +4 -1
- package/dist/types/src/components/data-display/table/types.d.ts +18 -0
- package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
- package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
- package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
- package/dist/types/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
- package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
- package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
- package/dist/types/src/components/overlays/notification/Notification.d.ts +18 -3
- package/dist/types/src/components/overlays/notification/host.d.ts +9 -0
- package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
- package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
- package/dist/types/src/contexts/FloatingContext.d.ts +6 -0
- package/dist/types/src/contexts/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/useFloatingPosition.d.ts +19 -0
- package/dist/types/src/hooks/usePortalState.d.ts +6 -0
- package/dist/types/src/utils/dropdown/maxSelection.d.ts +24 -0
- package/dist/types/src/utils/dropdown/multiSelect.d.ts +42 -2
- package/dist/ui-admin/assets/styles/style.css +304 -64
- package/package.json +1 -1
|
@@ -8,6 +8,17 @@ export type TableProps = Omit<ComponentProps<'div'>, 'ref'> & {
|
|
|
8
8
|
maxHeight?: string | number;
|
|
9
9
|
hoverable?: boolean;
|
|
10
10
|
selectable?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 가로 스크롤 활성화. 외곽에 overflow-x: auto wrapper가 추가되며,
|
|
13
|
+
* 내부의 SelectBox·Dropdown 등 floating 컴포넌트가 FloatingProvider를 통해
|
|
14
|
+
* 자동으로 React Portal 렌더로 전환된다.
|
|
15
|
+
*/
|
|
16
|
+
horizontalScroll?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 가로 스크롤 트리거 임계 너비. horizontalScroll=true 일 때만 의미가 있으며,
|
|
19
|
+
* 기본값은 1140 (14인치 모니터 + LNB 기준 디자인 권장 너비).
|
|
20
|
+
*/
|
|
21
|
+
minWidth?: string | number;
|
|
11
22
|
children: ReactNode;
|
|
12
23
|
};
|
|
13
24
|
export type TableHeaderProps = {
|
|
@@ -26,6 +37,11 @@ export type TableHeaderCellProps = Omit<ComponentProps<'th'>, 'ref'> & {
|
|
|
26
37
|
sortDirection?: SortDirection;
|
|
27
38
|
onSort?: () => void;
|
|
28
39
|
width?: string | number;
|
|
40
|
+
/**
|
|
41
|
+
* 셀 최소 너비. 가로 스크롤 정책에 따라 글자수 기준으로 설정한다.
|
|
42
|
+
* 예: 제목·메인 10자 ≈ 160px, 이름·일자 5자 ≈ 80px, 버튼 xxs ≈ 40~60px, 기타 2자 ≈ 32px.
|
|
43
|
+
*/
|
|
44
|
+
minWidth?: string | number;
|
|
29
45
|
};
|
|
30
46
|
export type TableCellProps = Omit<ComponentProps<'td'>, 'ref'> & {
|
|
31
47
|
isHeader?: boolean;
|
|
@@ -40,6 +56,8 @@ export type TablePaginationProps = {
|
|
|
40
56
|
};
|
|
41
57
|
export type TableColGroupProps = {
|
|
42
58
|
widths: (string | number)[];
|
|
59
|
+
/** 컬럼별 최소 너비. 가로 스크롤 시 셀이 더 좁아지지 않도록 강제한다. */
|
|
60
|
+
minWidths?: (string | number)[];
|
|
43
61
|
};
|
|
44
62
|
export type TableEmptyProps = {
|
|
45
63
|
colSpan: number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type MouseEvent, type RefObject } from 'react';
|
|
2
|
+
export declare const TABLE_HEADER_HEIGHT = 40;
|
|
3
|
+
export declare const SCROLLBAR_THUMB_MIN_HEIGHT = 40;
|
|
4
|
+
export declare const H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
|
|
5
|
+
export declare const H_SCROLLBAR_SIDE_GAP = 8;
|
|
6
|
+
export declare const SCROLLBAR_TRACK_OFFSET = 16;
|
|
7
|
+
type VerticalScrollbarOptions = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
scrollContainerRef: RefObject<HTMLDivElement | null>;
|
|
10
|
+
scrollAreaRef: RefObject<HTMLDivElement | null>;
|
|
11
|
+
thumbRef: RefObject<HTMLDivElement | null>;
|
|
12
|
+
};
|
|
13
|
+
export declare const useTableVerticalScrollbar: ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }: VerticalScrollbarOptions) => {
|
|
14
|
+
handleThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
|
|
15
|
+
};
|
|
16
|
+
type HorizontalScrollbarOptions = {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
hScrollContainerRef: RefObject<HTMLDivElement | null>;
|
|
19
|
+
hScrollbarRef: RefObject<HTMLDivElement | null>;
|
|
20
|
+
hThumbRef: RefObject<HTMLDivElement | null>;
|
|
21
|
+
};
|
|
22
|
+
export declare const useTableHorizontalScrollbar: ({ enabled, hScrollContainerRef, hScrollbarRef, hThumbRef, }: HorizontalScrollbarOptions) => {
|
|
23
|
+
handleHThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
// ──────────────────────────────────────────────
|
|
3
|
+
// 상수 — Table.tsx 와 _table.scss 양쪽에서 동기화 필요
|
|
4
|
+
// ──────────────────────────────────────────────
|
|
5
|
+
// $table-header-height
|
|
6
|
+
export const TABLE_HEADER_HEIGHT = 40;
|
|
7
|
+
// 세로/가로 thumb 최소 크기
|
|
8
|
+
export const SCROLLBAR_THUMB_MIN_HEIGHT = 40;
|
|
9
|
+
export const H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
|
|
10
|
+
// SCSS .ncua-table__h-scrollbar { left/right: var(--spacing-s) = 8px } 와 동기화
|
|
11
|
+
export const H_SCROLLBAR_SIDE_GAP = 8;
|
|
12
|
+
// 세로 트랙 상하 여백 합계 — top 8 + bottom 8 = 16 (header 회피분 40 은 별도 처리)
|
|
13
|
+
// biome-ignore lint/style/useExportsLast: 상수는 문서 주석과 함께 상단에 정의
|
|
14
|
+
export const SCROLLBAR_TRACK_OFFSET = 16;
|
|
15
|
+
const startDrag = (e, options) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
const { axis, scrollEl, thumbEl, draggingTarget = scrollEl, sideGap = 0 } = options;
|
|
18
|
+
draggingTarget.setAttribute('data-dragging', '');
|
|
19
|
+
if (axis === 'y') {
|
|
20
|
+
const startY = e.clientY;
|
|
21
|
+
const startScrollTop = scrollEl.scrollTop;
|
|
22
|
+
const { scrollHeight, clientHeight } = scrollEl;
|
|
23
|
+
const thumbHeight = thumbEl.offsetHeight;
|
|
24
|
+
const ratio = (scrollHeight - clientHeight) / (clientHeight - thumbHeight);
|
|
25
|
+
const onMove = (ev) => {
|
|
26
|
+
scrollEl.scrollTop = startScrollTop + (ev.clientY - startY) * ratio;
|
|
27
|
+
};
|
|
28
|
+
const onUp = () => {
|
|
29
|
+
draggingTarget.removeAttribute('data-dragging');
|
|
30
|
+
document.removeEventListener('mousemove', onMove);
|
|
31
|
+
document.removeEventListener('mouseup', onUp);
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener('mousemove', onMove);
|
|
34
|
+
document.addEventListener('mouseup', onUp);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const startX = e.clientX;
|
|
38
|
+
const startScrollLeft = scrollEl.scrollLeft;
|
|
39
|
+
const { scrollWidth, clientWidth } = scrollEl;
|
|
40
|
+
const thumbWidth = thumbEl.offsetWidth;
|
|
41
|
+
const trackWidth = clientWidth - sideGap * 2;
|
|
42
|
+
const ratio = (scrollWidth - clientWidth) / (trackWidth - thumbWidth);
|
|
43
|
+
const onMove = (ev) => {
|
|
44
|
+
scrollEl.scrollLeft = startScrollLeft + (ev.clientX - startX) * ratio;
|
|
45
|
+
};
|
|
46
|
+
const onUp = () => {
|
|
47
|
+
draggingTarget.removeAttribute('data-dragging');
|
|
48
|
+
document.removeEventListener('mousemove', onMove);
|
|
49
|
+
document.removeEventListener('mouseup', onUp);
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener('mousemove', onMove);
|
|
52
|
+
document.addEventListener('mouseup', onUp);
|
|
53
|
+
};
|
|
54
|
+
export const useTableVerticalScrollbar = ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }) => {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled)
|
|
57
|
+
return;
|
|
58
|
+
const scrollEl = scrollContainerRef.current;
|
|
59
|
+
const thumbEl = thumbRef.current;
|
|
60
|
+
if (!scrollEl || !thumbEl)
|
|
61
|
+
return;
|
|
62
|
+
const update = () => {
|
|
63
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
64
|
+
if (scrollHeight <= clientHeight) {
|
|
65
|
+
thumbEl.style.height = '0';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
|
|
69
|
+
const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, (clientHeight / scrollHeight) * trackHeight);
|
|
70
|
+
const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
|
|
71
|
+
thumbEl.style.height = `${thumbHeight}px`;
|
|
72
|
+
thumbEl.style.transform = `translateY(${thumbTop}px)`;
|
|
73
|
+
};
|
|
74
|
+
scrollEl.addEventListener('scroll', update, { passive: true });
|
|
75
|
+
const observer = new ResizeObserver(update);
|
|
76
|
+
observer.observe(scrollEl);
|
|
77
|
+
if (scrollAreaRef.current)
|
|
78
|
+
observer.observe(scrollAreaRef.current);
|
|
79
|
+
update();
|
|
80
|
+
return () => {
|
|
81
|
+
scrollEl.removeEventListener('scroll', update);
|
|
82
|
+
observer.disconnect();
|
|
83
|
+
};
|
|
84
|
+
}, [enabled, scrollContainerRef, scrollAreaRef, thumbRef]);
|
|
85
|
+
const handleThumbMouseDown = (e) => {
|
|
86
|
+
const scrollEl = scrollContainerRef.current;
|
|
87
|
+
const thumbEl = thumbRef.current;
|
|
88
|
+
const areaEl = scrollAreaRef.current;
|
|
89
|
+
if (!scrollEl || !thumbEl)
|
|
90
|
+
return;
|
|
91
|
+
startDrag(e, { axis: 'y', scrollEl, thumbEl, draggingTarget: areaEl ?? scrollEl });
|
|
92
|
+
};
|
|
93
|
+
return { handleThumbMouseDown };
|
|
94
|
+
};
|
|
95
|
+
export const useTableHorizontalScrollbar = ({ enabled, hScrollContainerRef, hScrollbarRef, hThumbRef, }) => {
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!enabled)
|
|
98
|
+
return;
|
|
99
|
+
const hScrollEl = hScrollContainerRef.current;
|
|
100
|
+
const hScrollbarEl = hScrollbarRef.current;
|
|
101
|
+
const hThumbEl = hThumbRef.current;
|
|
102
|
+
if (!hScrollEl || !hScrollbarEl || !hThumbEl)
|
|
103
|
+
return;
|
|
104
|
+
const update = () => {
|
|
105
|
+
const { scrollLeft, scrollWidth, clientWidth } = hScrollEl;
|
|
106
|
+
if (scrollWidth <= clientWidth) {
|
|
107
|
+
hThumbEl.style.width = '0';
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// transform으로 스크롤 오프셋 보정 — reflow 없이 compositor-only 이동
|
|
111
|
+
hScrollbarEl.style.transform = `translateX(${scrollLeft}px)`;
|
|
112
|
+
hScrollbarEl.style.width = `${clientWidth - H_SCROLLBAR_SIDE_GAP * 2}px`;
|
|
113
|
+
const trackWidth = clientWidth - H_SCROLLBAR_SIDE_GAP * 2;
|
|
114
|
+
const thumbWidth = Math.max(H_SCROLLBAR_THUMB_MIN_WIDTH, (clientWidth / scrollWidth) * trackWidth);
|
|
115
|
+
const thumbLeft = (scrollLeft / (scrollWidth - clientWidth)) * (trackWidth - thumbWidth);
|
|
116
|
+
hThumbEl.style.width = `${thumbWidth}px`;
|
|
117
|
+
hThumbEl.style.transform = `translateX(${thumbLeft}px)`;
|
|
118
|
+
};
|
|
119
|
+
hScrollEl.addEventListener('scroll', update, { passive: true });
|
|
120
|
+
const ro = new ResizeObserver(update);
|
|
121
|
+
ro.observe(hScrollEl);
|
|
122
|
+
update();
|
|
123
|
+
return () => {
|
|
124
|
+
hScrollEl.removeEventListener('scroll', update);
|
|
125
|
+
ro.disconnect();
|
|
126
|
+
};
|
|
127
|
+
}, [enabled, hScrollContainerRef, hScrollbarRef, hThumbRef]);
|
|
128
|
+
const handleHThumbMouseDown = (e) => {
|
|
129
|
+
const hScrollEl = hScrollContainerRef.current;
|
|
130
|
+
const hThumbEl = hThumbRef.current;
|
|
131
|
+
if (!hScrollEl || !hThumbEl)
|
|
132
|
+
return;
|
|
133
|
+
startDrag(e, { axis: 'x', scrollEl: hScrollEl, thumbEl: hThumbEl, sideGap: H_SCROLLBAR_SIDE_GAP });
|
|
134
|
+
};
|
|
135
|
+
return { handleHThumbMouseDown };
|
|
136
|
+
};
|
|
@@ -21,6 +21,14 @@ interface ComboBoxProps extends Omit<ComponentPropsWithRef<'div'>, 'size' | 'onC
|
|
|
21
21
|
required?: boolean;
|
|
22
22
|
multiple?: boolean;
|
|
23
23
|
showFooterButtons?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 최대 선택 가능 개수 (multiple=true에서만 유효).
|
|
26
|
+
* - `0` 이상의 정수: 제한 활성 → footer의 "전체선택" Link 미노출 (DES-SPEC-009 §3.10.1).
|
|
27
|
+
* - `0`은 모든 새 선택을 차단한다.
|
|
28
|
+
* - 양수는 도달 후 추가 선택을 무시한다 (이미 선택된 항목 해제는 정상).
|
|
29
|
+
* - 음수 / 비정수 / `null` / `undefined`: 제한 없음.
|
|
30
|
+
*/
|
|
31
|
+
maxSelection?: number | null;
|
|
24
32
|
onEdit?: () => void;
|
|
25
33
|
}
|
|
26
34
|
declare const ComboBox: import("react").ForwardRefExoticComponent<Omit<ComboBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -10,13 +10,6 @@ import { SelectDropdown } from '../../select-dropdown';
|
|
|
10
10
|
import { HintText } from '../../shared/hintText/HintText';
|
|
11
11
|
import { InputBase } from '../input-base/InputBase';
|
|
12
12
|
const defaultMaxHeight = 275;
|
|
13
|
-
const toggleMultiSelectValue = (currentValue, optionId) => {
|
|
14
|
-
const currentValues = Array.isArray(currentValue) ? currentValue : [];
|
|
15
|
-
if (currentValues.includes(optionId)) {
|
|
16
|
-
return currentValues.filter((v) => v !== optionId);
|
|
17
|
-
}
|
|
18
|
-
return [...currentValues, optionId];
|
|
19
|
-
};
|
|
20
13
|
const notifyFormChange = (register, multiple, newValue) => {
|
|
21
14
|
if (register?.onChange) {
|
|
22
15
|
register.onChange({
|
|
@@ -27,7 +20,7 @@ const notifyFormChange = (register, multiple, newValue) => {
|
|
|
27
20
|
});
|
|
28
21
|
}
|
|
29
22
|
};
|
|
30
|
-
const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, hintText, children, size = 'xs', destructive = false, value, optionItems = [], onChange, onSearch, disabled = false, register, maxHeight = defaultMaxHeight, searchValue = '', label, required = false, multiple = false, showFooterButtons = false, onEdit, ...props }, ref) => {
|
|
23
|
+
const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, hintText, children, size = 'xs', destructive = false, value, optionItems = [], onChange, onSearch, disabled = false, register, maxHeight = defaultMaxHeight, searchValue = '', label, required = false, multiple = false, showFooterButtons = false, maxSelection, onEdit, ...props }, ref) => {
|
|
31
24
|
const internalRef = useRef(null);
|
|
32
25
|
const dropdownRef = useRef(null);
|
|
33
26
|
const inputRef = useRef(null);
|
|
@@ -38,7 +31,9 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
|
|
|
38
31
|
if (disabled)
|
|
39
32
|
return;
|
|
40
33
|
if (multiple) {
|
|
41
|
-
const newValue =
|
|
34
|
+
const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
|
|
35
|
+
if (newValue === null)
|
|
36
|
+
return;
|
|
42
37
|
onChange?.(newValue);
|
|
43
38
|
notifyFormChange(register, multiple, newValue);
|
|
44
39
|
return;
|
|
@@ -103,6 +98,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
|
|
|
103
98
|
// 나머지는 useDropdown 훅에서 처리
|
|
104
99
|
dropdownHandleKeyDown(e);
|
|
105
100
|
};
|
|
101
|
+
// biome-ignore lint/style/noNonNullAssertion: forwardRef 패턴에서 internalRef는 첫 렌더 후 항상 존재
|
|
106
102
|
useImperativeHandle(ref, () => internalRef.current, []);
|
|
107
103
|
const trailingElement = {
|
|
108
104
|
type: 'custom',
|
|
@@ -116,7 +112,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
|
|
|
116
112
|
color: 'gray300',
|
|
117
113
|
};
|
|
118
114
|
const currentSelectedValues = multiple && Array.isArray(value) ? value : [];
|
|
119
|
-
const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, } = useMultiSelect(currentSelectedValues, optionItems);
|
|
115
|
+
const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, isMaxSelectionActive, tryToggle, } = useMultiSelect(currentSelectedValues, optionItems, { maxSelection });
|
|
120
116
|
const handleSelectAll = () => {
|
|
121
117
|
if (!multiple || !onChange)
|
|
122
118
|
return;
|
|
@@ -160,7 +156,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
|
|
|
160
156
|
}, className), ...props, children: [_jsxs("div", { className: "ncua-combobox__content", children: [_jsx(InputBase, { ref: inputRef, size: size, label: label, required: required, placeholder: placeholder, value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onClick: handleInputClick, disabled: disabled, destructive: destructive, leadingElement: leadingElement, trailingElement: trailingElement, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": comboboxId, "aria-disabled": disabled, autoComplete: "off", clearText: !!inputValue, onClearText: () => {
|
|
161
157
|
setInputValue('');
|
|
162
158
|
onSearch?.('');
|
|
163
|
-
} }), _jsxs(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: displayedOptions, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: comboboxId, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, isKeyboardNavigation: isKeyboardNavigation, multiple: multiple, showFooterButtons: showFooter, selectAllButtonText: selectAllButtonText, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, componentType: "combobox", children: [showNoResults ? _jsx("li", { className: "ncua-select-dropdown__no-results", children: "\uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) : null, children] })] }), register && _jsx("input", { type: "hidden", ...register, value: hiddenInputValue }), hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), hasTags && (_jsx("div", { className: "ncua-combobox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) }))] }));
|
|
159
|
+
} }), _jsxs(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: displayedOptions, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: comboboxId, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, isKeyboardNavigation: isKeyboardNavigation, multiple: multiple, showFooterButtons: showFooter, selectAllButtonText: selectAllButtonText, showSelectAllAction: !isMaxSelectionActive, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, componentType: "combobox", children: [showNoResults ? _jsx("li", { className: "ncua-select-dropdown__no-results", children: "\uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) : null, children] })] }), register && _jsx("input", { type: "hidden", ...register, value: hiddenInputValue }), hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), hasTags && (_jsx("div", { className: "ncua-combobox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) }))] }));
|
|
164
160
|
});
|
|
165
161
|
ComboBox.displayName = 'ComboBox';
|
|
166
162
|
export { defaultMaxHeight, ComboBox };
|
|
@@ -85,7 +85,7 @@ export const ImageFileInput = forwardRef(({ size = 'sm', accept, multiple = fals
|
|
|
85
85
|
};
|
|
86
86
|
const renderImagePreview = (files = []) => {
|
|
87
87
|
const showEmptySlot = maxFileCount ? files.length < maxFileCount : files.length === 0;
|
|
88
|
-
return (_jsxs("div", { className: "ncua-image-file-input__previews", children: [files.map((file, index) => (_jsx(ImagePreview, { file: file, onRemove: () => handleRemoveFile(index) }, `${file.name}-${index}`))), showEmptySlot && (_jsxs("div", { className: "ncua-image-file-input__empty-slot-wrapper", onMouseEnter: () => !disabled && setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), onClick: handleBrowseClick, children: [_jsx(Button, { onlyIcon: true, size: size, className: classNames('ncua-image-file-input__preview-container'), onClick: handleBrowseClick, disabled: disabled, label: imagePreviewTooltipLabel }),
|
|
88
|
+
return (_jsxs("div", { className: "ncua-image-file-input__previews", children: [files.map((file, index) => (_jsx(ImagePreview, { file: file, onRemove: () => handleRemoveFile(index) }, `${file.name}-${index}`))), showEmptySlot && (_jsxs("div", { className: "ncua-image-file-input__empty-slot-wrapper", onMouseEnter: () => !disabled && setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), onClick: handleBrowseClick, children: [_jsx(Button, { onlyIcon: true, size: size, className: classNames('ncua-image-file-input__preview-container'), onClick: handleBrowseClick, disabled: disabled, label: imagePreviewTooltipLabel }), _jsx(Tooltip, { content: imagePreviewTooltipLabel, position: "bottom", tooltipType: "black", forceVisible: isButtonHovered && !disabled, disablePortal: true })] }))] }));
|
|
89
89
|
};
|
|
90
90
|
const renderHintList = () => {
|
|
91
91
|
if (!hintItems || hintItems.length === 0)
|
|
@@ -19,8 +19,21 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
|
|
|
19
19
|
register?: UseFormRegisterReturn;
|
|
20
20
|
maxHeight?: number;
|
|
21
21
|
multiple?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 최대 선택 가능 개수 (multiple=true에서만 유효).
|
|
24
|
+
* - `0` 이상의 정수: 제한 활성 → footer "전체선택" Link 미노출.
|
|
25
|
+
* - `0`은 모든 새 선택을 차단한다.
|
|
26
|
+
* - 양수는 도달 후 추가 선택을 무시한다 (이미 선택된 항목 해제는 정상).
|
|
27
|
+
* - 음수 / 비정수 / `null` / `undefined`: 제한 없음.
|
|
28
|
+
*/
|
|
29
|
+
maxSelection?: number | null;
|
|
22
30
|
onEdit?: () => void;
|
|
23
31
|
align?: 'left' | 'right';
|
|
32
|
+
/**
|
|
33
|
+
* 옵션 패널을 React Portal로 body에 렌더한다.
|
|
34
|
+
* 미지정 시 FloatingContext.preferPortal 값을 따른다 (DataGrid.Table horizontalScroll 내부에서는 자동 true).
|
|
35
|
+
*/
|
|
36
|
+
usePortal?: boolean;
|
|
24
37
|
};
|
|
25
38
|
declare const SelectBox: import("react").ForwardRefExoticComponent<Omit<SelectBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
26
39
|
export type { SelectBoxProps };
|
|
@@ -2,8 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { ChevronDown } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { forwardRef, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
5
6
|
import { COLOR } from '../../../../constant/color';
|
|
6
7
|
import { useDropdown, useScrollLock } from '../../../hooks/dropdown';
|
|
8
|
+
import { useFloatingPosition } from '../../../hooks/useFloatingPosition';
|
|
9
|
+
import { usePortalState } from '../../../hooks/usePortalState';
|
|
7
10
|
import { useMultiSelect } from '../../../utils/dropdown/multiSelect';
|
|
8
11
|
import { Tag } from '../../feedback-and-status/tag';
|
|
9
12
|
import { SelectDropdown } from '../../select-dropdown';
|
|
@@ -37,9 +40,12 @@ function DisplayValue({ displayValue }) {
|
|
|
37
40
|
}
|
|
38
41
|
return (_jsxs("div", { className: "ncua-selectbox__value-container", children: [displayValue.icon && (_jsx("span", { className: "ncua-selectbox__value-icon", children: _jsx(displayValue.icon, { width: 16, height: 16 }) })), _jsx("span", { className: "ncua-selectbox__value-text", children: displayValue.label })] }));
|
|
39
42
|
}
|
|
40
|
-
const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, align = 'left', id, className, children, register, onChange, onEdit, ...props }, ref
|
|
43
|
+
const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, maxSelection, align = 'left', usePortal, id, className, children, register, onChange, onEdit, ...props }, ref
|
|
44
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 옵션/멀티/태그/포탈 등 필수 분기 통합
|
|
45
|
+
) => {
|
|
41
46
|
const internalRef = useRef(null);
|
|
42
47
|
const dropdownRef = useRef(null);
|
|
48
|
+
const { shouldPortal, portalContainer } = usePortalState(usePortal);
|
|
43
49
|
const [selectedTags, setSelectedTags] = useState([]);
|
|
44
50
|
const selectedOption = useMemo(() => {
|
|
45
51
|
if (multiple)
|
|
@@ -55,6 +61,14 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
55
61
|
const handleOptionSelect = (option) => {
|
|
56
62
|
if (disabled)
|
|
57
63
|
return;
|
|
64
|
+
if (multiple) {
|
|
65
|
+
const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
|
|
66
|
+
if (newValue === null)
|
|
67
|
+
return;
|
|
68
|
+
onChange?.(newValue);
|
|
69
|
+
notifyRegister(register, newValue, multiple);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
58
72
|
const newValue = computeNewValue(option, value, multiple);
|
|
59
73
|
onChange?.(newValue);
|
|
60
74
|
notifyRegister(register, newValue, multiple);
|
|
@@ -82,7 +96,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
82
96
|
}, [handleMouseMove, setFocusedIndex]);
|
|
83
97
|
// Multiple select 관련 로직
|
|
84
98
|
const currentSelectedValues = multiple && Array.isArray(value) ? value : [];
|
|
85
|
-
const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, } = useMultiSelect(currentSelectedValues, optionItems);
|
|
99
|
+
const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, isMaxSelectionActive, tryToggle, } = useMultiSelect(currentSelectedValues, optionItems, { maxSelection });
|
|
86
100
|
const handleSelectAll = () => {
|
|
87
101
|
if (!multiple || !onChange)
|
|
88
102
|
return;
|
|
@@ -109,13 +123,26 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
109
123
|
}
|
|
110
124
|
};
|
|
111
125
|
useScrollLock(isOpen, dropdownRef);
|
|
126
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionItems 변경 시 너비 재계산 필요
|
|
112
127
|
useLayoutEffect(() => {
|
|
113
128
|
if (autoWidth && isOpen && dropdownRef.current && internalRef.current) {
|
|
114
129
|
const dropdownWidth = dropdownRef.current.offsetWidth;
|
|
115
130
|
internalRef.current.style.width = `${dropdownWidth}px`;
|
|
116
131
|
}
|
|
117
132
|
}, [autoWidth, isOpen, optionItems]);
|
|
133
|
+
const floatingStyle = useFloatingPosition({
|
|
134
|
+
enabled: shouldPortal,
|
|
135
|
+
isOpen,
|
|
136
|
+
triggerRef: internalRef,
|
|
137
|
+
floatingRef: dropdownRef,
|
|
138
|
+
direction: dropdownDirection,
|
|
139
|
+
align,
|
|
140
|
+
matchTriggerWidth: true,
|
|
141
|
+
});
|
|
142
|
+
// biome-ignore lint/style/noNonNullAssertion: forwardRef 패턴에서 internalRef는 항상 존재
|
|
118
143
|
useImperativeHandle(ref, () => internalRef.current, []);
|
|
144
|
+
const selectDropdownNode = (_jsx(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: optionItems, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: `selectbox-options-${id || 'default'}`, multiple: multiple, showFooterButtons: multiple, selectAllButtonText: selectAllButtonText, showSelectAllAction: !isMaxSelectionActive, componentType: "selectbox", isKeyboardNavigation: isKeyboardNavigation, activeDescendantId: activeDescendantId, align: align, className: shouldPortal ? 'ncua-select-dropdown--portal' : undefined, style: shouldPortal && floatingStyle ? floatingStyle : undefined, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, onOptionHover: handleOptionHover, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, children: children }));
|
|
145
|
+
const portaledDropdown = shouldPortal && portalContainer && isOpen ? createPortal(selectDropdownNode, portalContainer) : null;
|
|
119
146
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: internalRef, className: classNames('ncua-selectbox', `ncua-selectbox--${size}`, {
|
|
120
147
|
'ncua-selectbox--open': isOpen,
|
|
121
148
|
'ncua-selectbox--disabled': disabled,
|
|
@@ -124,7 +151,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
|
|
|
124
151
|
destructive: destructive,
|
|
125
152
|
}, className), ...props, children: [_jsxs("div", { className: classNames('ncua-selectbox__content'), onClick: toggleDropdown, onKeyDown: handleKeyDown, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": `selectbox-options-${id || 'default'}`, "aria-disabled": disabled, "aria-label": selectedOption ? selectedOption.label : placeholder, "aria-activedescendant": activeDescendantId, children: [_jsxs("div", { className: "ncua-selectbox__content-inner", children: [_jsx("div", { className: "ncua-selectbox__value", children: _jsx(DisplayValue, { displayValue: displayValue }) }), _jsx(ChevronDown, { width: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, height: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, color: disabled ? COLOR.gray300 : COLOR.gray500, className: classNames('ncua-selectbox__arrow', {
|
|
126
153
|
'ncua-selectbox__arrow--up': isOpen,
|
|
127
|
-
}) })] }),
|
|
154
|
+
}) })] }), !shouldPortal && selectDropdownNode] }), hintText && (_jsx(HintText, { destructive: destructive, className: "ncua-hint-text", children: hintText })), register && (_jsx("input", { type: "hidden", ...register, value: multiple ? JSON.stringify(value || []) : String(value || '') }))] }), selectedTags.length > 0 && (_jsx("div", { className: "ncua-selectbox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) })), portaledDropdown] }));
|
|
128
155
|
});
|
|
129
156
|
SelectBox.displayName = 'SelectBox';
|
|
130
157
|
export { DEFAULT_MAX_HEIGHT, SelectBox };
|
|
@@ -49,6 +49,11 @@ export type DropdownBaseProps = {
|
|
|
49
49
|
className?: string;
|
|
50
50
|
opened?: boolean;
|
|
51
51
|
closeOnClickOutside?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 메뉴를 React Portal로 body에 렌더한다.
|
|
54
|
+
* 미지정 시 FloatingContext.preferPortal 값을 따른다 (DataGrid.Table horizontalScroll 내부에서는 자동 true).
|
|
55
|
+
*/
|
|
56
|
+
usePortal?: boolean;
|
|
52
57
|
};
|
|
53
58
|
export type ActionDropdownProps = DropdownBaseProps & {
|
|
54
59
|
variant?: 'action';
|
|
@@ -7,6 +7,9 @@ import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-sc
|
|
|
7
7
|
import { attachClosestEdge, extractClosestEdge, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
8
8
|
import { DotsGrid02, DotsVertical, Eye, EyeOff } from '@ncds/ui-admin-icon';
|
|
9
9
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
10
|
+
import { createPortal } from 'react-dom';
|
|
11
|
+
import { useFloatingPosition } from '../../../hooks/useFloatingPosition';
|
|
12
|
+
import { usePortalState } from '../../../hooks/usePortalState';
|
|
10
13
|
import { Button } from '../../action/button';
|
|
11
14
|
import { applyDraftToItems, arrayReorderByEdge, hasDraftChanged, initDraftState } from './utils';
|
|
12
15
|
const DROPDOWN_ID_RADIX = 36;
|
|
@@ -82,14 +85,25 @@ const SortableConfigItem = ({ item, isVisible, showVisibilityToggle, sortable, d
|
|
|
82
85
|
}
|
|
83
86
|
}, children: _jsx(DotsGrid02, {}) })), _jsx("div", { className: "ncua-dropdown__item-content", children: _jsxs("div", { className: "ncua-dropdown__item-text-group", children: [ItemIcon && _jsx(ItemIcon, { className: "ncua-dropdown__item-icon" }), _jsx("span", { className: "ncua-dropdown__item-text", children: item.text })] }) }), showVisibilityToggle && (_jsx("button", { type: "button", className: "ncua-dropdown__item-visibility", "aria-label": `${item.text} 노출`, "aria-pressed": isVisible, onClick: () => onToggleVisibility(item.id), children: isVisible ? _jsx(Eye, {}) : _jsx(EyeOff, {}) }))] }));
|
|
84
87
|
};
|
|
88
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 액션/설정 두 variant 통합 + drag-and-drop 필수 분기
|
|
85
89
|
export const Dropdown = (props) => {
|
|
86
|
-
const { trigger, align = 'left', header, groups, className, opened = false, closeOnClickOutside = true } = props;
|
|
90
|
+
const { trigger, align = 'left', header, groups, className, opened = false, closeOnClickOutside = true, usePortal, } = props;
|
|
87
91
|
const variant = props.variant ?? 'action';
|
|
88
92
|
const closeOnClickItem = variant === 'action' ? (props.closeOnClickItem ?? true) : false;
|
|
89
93
|
const [isOpen, setIsOpen] = useState(opened);
|
|
90
94
|
const dropdownRef = useRef(null);
|
|
91
95
|
const triggerRef = useRef(null);
|
|
96
|
+
const menuRef = useRef(null);
|
|
92
97
|
const menuItemsRef = useRef(null);
|
|
98
|
+
const { shouldPortal, portalContainer } = usePortalState(usePortal);
|
|
99
|
+
const floatingStyle = useFloatingPosition({
|
|
100
|
+
enabled: shouldPortal,
|
|
101
|
+
isOpen,
|
|
102
|
+
triggerRef,
|
|
103
|
+
floatingRef: menuRef,
|
|
104
|
+
direction: 'down',
|
|
105
|
+
align,
|
|
106
|
+
});
|
|
93
107
|
const dropdownIdRef = useRef(`ncua-dropdown-${Math.random().toString(DROPDOWN_ID_RADIX).slice(DROPDOWN_ID_SLICE_START, DROPDOWN_ID_SLICE_END)}`);
|
|
94
108
|
const dropdownId = dropdownIdRef.current;
|
|
95
109
|
useEffect(() => {
|
|
@@ -144,7 +158,10 @@ export const Dropdown = (props) => {
|
|
|
144
158
|
triggerRef.current?.focus();
|
|
145
159
|
};
|
|
146
160
|
const handleClickOutside = (event) => {
|
|
147
|
-
|
|
161
|
+
const target = event.target;
|
|
162
|
+
const insideContainer = dropdownRef.current?.contains(target) ?? false;
|
|
163
|
+
const insidePortaledMenu = menuRef.current?.contains(target) ?? false;
|
|
164
|
+
if (!insideContainer && !insidePortaledMenu) {
|
|
148
165
|
setIsOpen(false);
|
|
149
166
|
}
|
|
150
167
|
};
|
|
@@ -156,6 +173,7 @@ export const Dropdown = (props) => {
|
|
|
156
173
|
}
|
|
157
174
|
}
|
|
158
175
|
};
|
|
176
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: handleClickOutside는 안정적 참조
|
|
159
177
|
useEffect(() => {
|
|
160
178
|
if (closeOnClickOutside) {
|
|
161
179
|
document.addEventListener('mousedown', handleClickOutside);
|
|
@@ -165,6 +183,7 @@ export const Dropdown = (props) => {
|
|
|
165
183
|
}
|
|
166
184
|
}, [closeOnClickOutside]);
|
|
167
185
|
// ESC 키로 닫고 trigger로 포커스 복귀
|
|
186
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: closeAndRestoreFocus는 안정적 참조
|
|
168
187
|
useEffect(() => {
|
|
169
188
|
if (!isOpen)
|
|
170
189
|
return;
|
|
@@ -261,13 +280,18 @@ export const Dropdown = (props) => {
|
|
|
261
280
|
const dropdownClasses = ['ncua-dropdown', className, align === 'right' ? 'ncua-dropdown--right' : '']
|
|
262
281
|
.filter(Boolean)
|
|
263
282
|
.join(' ');
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
const menuClasses = ['ncua-dropdown__menu', shouldPortal ? 'ncua-dropdown__menu--portal' : '']
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(' ');
|
|
286
|
+
const menuNode = isOpen ? (_jsxs("div", { ref: menuRef, className: menuClasses, role: variant === 'config' ? 'dialog' : 'menu', "aria-label": variant === 'config' ? '설정' : undefined, style: shouldPortal && floatingStyle ? floatingStyle : undefined, children: [renderHeader(), _jsx("div", { ref: menuItemsRef, className: "ncua-dropdown__menu-items", children: groups.map((group) => {
|
|
287
|
+
// config variant uses draft.order to drive the rendered order
|
|
288
|
+
const orderedItems = variant === 'config' && draft
|
|
289
|
+
? draft.order
|
|
290
|
+
.map((id) => group.items.find((i) => i.id === id))
|
|
291
|
+
.filter((i) => i !== undefined)
|
|
292
|
+
: group.items;
|
|
293
|
+
return (_jsx("div", { className: "ncua-dropdown__group", children: orderedItems.map((item) => variant === 'config' ? renderConfigItem(item, group.sortable === true) : renderActionItem(item)) }, group.items[0]?.id));
|
|
294
|
+
}) }), renderFooter()] })) : null;
|
|
295
|
+
const portaledMenu = shouldPortal && portalContainer && menuNode ? createPortal(menuNode, portalContainer) : null;
|
|
296
|
+
return (_jsxs("div", { className: dropdownClasses, ref: dropdownRef, children: [renderTrigger(), !shouldPortal && menuNode, portaledMenu] }));
|
|
273
297
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
import type { NotificationColor } from './Notification';
|
|
3
|
+
interface CalloutNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
4
|
+
color?: NotificationColor;
|
|
5
|
+
title?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
declare const CalloutNotification: import("react").ForwardRefExoticComponent<CalloutNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export type { CalloutNotificationProps };
|
|
9
|
+
export { CalloutNotification };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
const CalloutNotification = forwardRef(({ color = 'neutral', className, title, ...rest }, ref) => (_jsx("div", { ref: ref, className: classNames('ncua-callout-notification', `ncua-callout-notification--${color}`, className), ...rest, children: title })));
|
|
5
|
+
CalloutNotification.displayName = 'CalloutNotification';
|
|
6
|
+
export { CalloutNotification };
|
|
@@ -31,6 +31,21 @@ interface FloatingNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>
|
|
|
31
31
|
* @default 0
|
|
32
32
|
*/
|
|
33
33
|
autoClose?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Portal 마운트 여부.
|
|
36
|
+
*
|
|
37
|
+
* - `false` (기본값): 부모 JSX 트리 안에 카드로 그대로 렌더된다.
|
|
38
|
+
* 특정 컨테이너 안에서 표시하거나, 인라인 미리보기 (docs/스토리북) 용도.
|
|
39
|
+
*
|
|
40
|
+
* - `true`: document.body의 `.ncua-floating-notification-host` 싱글톤에
|
|
41
|
+
* `createPortal`로 마운트되어 우측 상단에 노출. 다중 발생 시 최신 토스트가
|
|
42
|
+
* 상단에 노출되고 이전 토스트들이 아래로 12px씩 겹쳐 쌓인다 (LIFO,
|
|
43
|
+
* `--ncua-floating-notification-stack-overlap` 변수로 조정 가능).
|
|
44
|
+
* Vanilla(CDN) 측 `new ncua.Notification({type:'floating'}).show()` 와 동일한 동작.
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
portal?: boolean;
|
|
34
49
|
}
|
|
35
50
|
declare const FloatingNotification: import("react").ForwardRefExoticComponent<FloatingNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
36
51
|
export type { FloatingNotificationProps };
|