@ncds/ui-admin 1.8.4 → 1.8.6
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/forms-and-input/slider/Slider.js +2 -3
- 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/types/component-meta.js +8 -1
- 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/forms-and-input/slider/Slider.js +1 -2
- 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/types/component-meta.js +5 -1
- 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/forms-and-input/slider/Slider.d.ts +0 -1
- package/dist/temp/src/components/forms-and-input/slider/Slider.js +0 -1
- 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/types/component-meta.d.ts +6 -2
- package/dist/temp/src/types/component-meta.js +14 -1
- 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/forms-and-input/slider/Slider.d.ts +0 -1
- 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/types/component-meta.d.ts +6 -2
- 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 +312 -64
- package/package.json +1 -1
|
@@ -29,6 +29,13 @@ export type DataGridTableProps = {
|
|
|
29
29
|
maxHeight?: string | number;
|
|
30
30
|
hoverable?: boolean;
|
|
31
31
|
selectable?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* 가로 스크롤 활성화 시 외부 wrapper에 overflow-x: auto를 적용하고,
|
|
34
|
+
* 내부의 SelectBox·Dropdown이 FloatingProvider를 통해 자동으로 Portal 렌더로 전환된다.
|
|
35
|
+
*/
|
|
36
|
+
horizontalScroll?: boolean;
|
|
37
|
+
/** 가로 스크롤 트리거 임계 너비. horizontalScroll=true 일 때만 의미가 있다. */
|
|
38
|
+
minWidth?: string | number;
|
|
32
39
|
};
|
|
33
40
|
export type DataGridPaginationProps = ComponentProps<'div'> & {
|
|
34
41
|
children: ReactNode;
|
|
@@ -6,6 +6,8 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
|
|
|
6
6
|
maxHeight?: string | number | undefined;
|
|
7
7
|
hoverable?: boolean | undefined;
|
|
8
8
|
selectable?: boolean | undefined;
|
|
9
|
+
horizontalScroll?: boolean | undefined;
|
|
10
|
+
minWidth?: string | number | undefined;
|
|
9
11
|
children: ReactNode;
|
|
10
12
|
} & import("react").RefAttributes<HTMLDivElement>> & {
|
|
11
13
|
Header: {
|
|
@@ -24,6 +26,7 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
|
|
|
24
26
|
sortDirection?: SortDirection | undefined;
|
|
25
27
|
onSort?: (() => void) | undefined;
|
|
26
28
|
width?: string | number | undefined;
|
|
29
|
+
minWidth?: string | number | undefined;
|
|
27
30
|
} & import("react").RefAttributes<HTMLTableCellElement>>;
|
|
28
31
|
Cell: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement>, "ref"> & {
|
|
29
32
|
isHeader?: boolean | undefined;
|
|
@@ -37,7 +40,7 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
|
|
|
37
40
|
displayName: string;
|
|
38
41
|
};
|
|
39
42
|
ColGroup: {
|
|
40
|
-
({ widths }: TableColGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
({ widths, minWidths }: TableColGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
41
44
|
displayName: string;
|
|
42
45
|
};
|
|
43
46
|
Empty: {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, ChevronSelectorVertical, ChevronUp } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import { Children, forwardRef,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
4
|
+
import { Children, forwardRef, useRef, } from 'react';
|
|
5
|
+
import { FloatingProvider } from '../../../contexts/FloatingContext';
|
|
6
|
+
import { TABLE_HEADER_HEIGHT, useTableHorizontalScrollbar, useTableVerticalScrollbar } from './useTableScrollbars';
|
|
7
|
+
// 가로 스크롤 디자인 기준 폭 — 14인치 모니터 + LNB 고려한 디자인 권장 너비
|
|
8
|
+
const DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH = 1140;
|
|
9
|
+
const FLOATING_PORTAL_VALUE = { preferPortal: true };
|
|
10
|
+
// TABLE_HEADER_HEIGHT·DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH를 CSS 커스텀 프로퍼티로 주입 — SCSS fallback 단일 소스
|
|
11
|
+
const WRAPPER_STYLE = {
|
|
12
|
+
'--ncua-table-header-height': `${TABLE_HEADER_HEIGHT}px`,
|
|
13
|
+
'--ncua-table-default-min-width': `${DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH}px`,
|
|
14
|
+
};
|
|
11
15
|
// Sort Icons (@ncds/ui-admin-icon)
|
|
12
16
|
// ──────────────────────────────────────────────
|
|
13
17
|
const SORT_ICONS = {
|
|
@@ -33,12 +37,12 @@ const Row = forwardRef(({ children, className, selected, status, ...rest }, ref)
|
|
|
33
37
|
'ncua-table__row--error': status === 'error',
|
|
34
38
|
}), ...rest, children: children })));
|
|
35
39
|
Row.displayName = 'Table.Row';
|
|
36
|
-
const HeaderCell = forwardRef(({ children, className, sortDirection, onSort, width, style, ...rest }, ref) => {
|
|
40
|
+
const HeaderCell = forwardRef(({ children, className, sortDirection, onSort, width, minWidth, style, ...rest }, ref) => {
|
|
37
41
|
const isSortable = sortDirection !== undefined && onSort !== undefined;
|
|
38
42
|
const SortIcon = isSortable ? SORT_ICONS[sortDirection] : undefined;
|
|
39
43
|
return (_jsx("th", { ref: ref, className: classNames('ncua-table__header-cell', className, {
|
|
40
44
|
'ncua-table__header-cell--sortable': isSortable,
|
|
41
|
-
}), style: { ...style, width }, "aria-sort": isSortable ? ARIA_SORT_MAP[sortDirection] : undefined, onClick: isSortable ? onSort : undefined, ...rest, children: isSortable && SortIcon ? (_jsxs("span", { className: "ncua-table__header-cell-inner", children: [_jsx("span", { className: "ncua-table__header-cell-text", children: children }), _jsx("span", { className: "ncua-table__sort-icon", children: _jsx(SortIcon, { width: 16, height: 16 }) })] })) : (children) }));
|
|
45
|
+
}), style: { ...style, width, minWidth }, "aria-sort": isSortable ? ARIA_SORT_MAP[sortDirection] : undefined, onClick: isSortable ? onSort : undefined, ...rest, children: isSortable && SortIcon ? (_jsxs("span", { className: "ncua-table__header-cell-inner", children: [_jsx("span", { className: "ncua-table__header-cell-text", children: children }), _jsx("span", { className: "ncua-table__sort-icon", children: _jsx(SortIcon, { width: 16, height: 16 }) })] })) : (children) }));
|
|
42
46
|
});
|
|
43
47
|
HeaderCell.displayName = 'Table.HeaderCell';
|
|
44
48
|
const Cell = forwardRef(({ children, className, isHeader, ...rest }, ref) => {
|
|
@@ -52,9 +56,9 @@ const Footer = ({ children, className }) => (_jsx("div", { className: classNames
|
|
|
52
56
|
Footer.displayName = 'Table.Footer';
|
|
53
57
|
const Pagination = ({ children, className }) => (_jsx("div", { className: classNames('ncua-table__pagination', className), children: children }));
|
|
54
58
|
Pagination.displayName = 'Table.Pagination';
|
|
55
|
-
const ColGroup = ({ widths }) => {
|
|
59
|
+
const ColGroup = ({ widths, minWidths }) => {
|
|
56
60
|
const resolveColWidth = (width) => {
|
|
57
|
-
if (width === 'auto')
|
|
61
|
+
if (width === undefined || width === 'auto')
|
|
58
62
|
return undefined;
|
|
59
63
|
if (typeof width === 'number')
|
|
60
64
|
return `${width}px`;
|
|
@@ -62,7 +66,7 @@ const ColGroup = ({ widths }) => {
|
|
|
62
66
|
};
|
|
63
67
|
return (_jsx("colgroup", { children: widths.map((width, index) => (
|
|
64
68
|
// biome-ignore lint/suspicious/noArrayIndexKey: colgroup columns never reorder or change
|
|
65
|
-
_jsx("col", { style: { width: resolveColWidth(width) } }, index))) }));
|
|
69
|
+
_jsx("col", { style: { width: resolveColWidth(width), minWidth: resolveColWidth(minWidths?.[index]) } }, index))) }));
|
|
66
70
|
};
|
|
67
71
|
ColGroup.displayName = 'Table.ColGroup';
|
|
68
72
|
const Empty = ({ colSpan, children }) => (_jsx("tr", { children: _jsx("td", { colSpan: colSpan, className: "ncua-table__empty", role: "status", "aria-live": "polite", children: children || '등록된 게시물이 없습니다.' }) }));
|
|
@@ -104,7 +108,7 @@ const sortChildren = (children) => {
|
|
|
104
108
|
// ──────────────────────────────────────────────
|
|
105
109
|
// Main Table component
|
|
106
110
|
// ──────────────────────────────────────────────
|
|
107
|
-
const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, maxHeight, hoverable = true, selectable = false, children, className, ...rest }, ref) => {
|
|
111
|
+
const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, maxHeight, hoverable = true, selectable = false, horizontalScroll = false, minWidth, children, className, ...rest }, ref) => {
|
|
108
112
|
const tableClasses = classNames('ncua-table', className, {
|
|
109
113
|
'ncua-table--horizontal': type === 'horizontal',
|
|
110
114
|
'ncua-table--vertical': type === 'vertical',
|
|
@@ -123,64 +127,45 @@ const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, m
|
|
|
123
127
|
// Custom scrollbar refs (used only in fixed-header mode)
|
|
124
128
|
const scrollContainerRef = useRef(null);
|
|
125
129
|
const scrollAreaRef = useRef(null);
|
|
130
|
+
const scrollbarRef = useRef(null);
|
|
126
131
|
const thumbRef = useRef(null);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
if (!scrollEl || !thumbEl)
|
|
161
|
-
return;
|
|
162
|
-
areaEl?.setAttribute('data-dragging', '');
|
|
163
|
-
const startY = e.clientY;
|
|
164
|
-
const startScrollTop = scrollEl.scrollTop;
|
|
165
|
-
const { scrollHeight, clientHeight } = scrollEl;
|
|
166
|
-
const thumbHeight = thumbEl.offsetHeight;
|
|
167
|
-
const scrollRatio = (scrollHeight - clientHeight) / (clientHeight - thumbHeight);
|
|
168
|
-
const onMove = (ev) => {
|
|
169
|
-
scrollEl.scrollTop = startScrollTop + (ev.clientY - startY) * scrollRatio;
|
|
170
|
-
};
|
|
171
|
-
const onUp = () => {
|
|
172
|
-
areaEl?.removeAttribute('data-dragging');
|
|
173
|
-
document.removeEventListener('mousemove', onMove);
|
|
174
|
-
document.removeEventListener('mouseup', onUp);
|
|
132
|
+
// 가로 스크롤바 refs (horizontalScroll 모드)
|
|
133
|
+
const hScrollContainerRef = useRef(null);
|
|
134
|
+
const hScrollbarRef = useRef(null);
|
|
135
|
+
const hThumbRef = useRef(null);
|
|
136
|
+
const fixedScrollEnabled = !!(fixedHeader && maxHeight);
|
|
137
|
+
const { handleThumbMouseDown } = useTableVerticalScrollbar({
|
|
138
|
+
enabled: fixedScrollEnabled,
|
|
139
|
+
scrollContainerRef,
|
|
140
|
+
scrollAreaRef,
|
|
141
|
+
thumbRef,
|
|
142
|
+
});
|
|
143
|
+
const { handleHThumbMouseDown } = useTableHorizontalScrollbar({
|
|
144
|
+
enabled: horizontalScroll,
|
|
145
|
+
hScrollContainerRef,
|
|
146
|
+
hScrollbarRef,
|
|
147
|
+
hThumbRef,
|
|
148
|
+
});
|
|
149
|
+
// <colgroup> + <thead> + <tbody> 묶음 — fixed-header 분기와 horizontalScroll 분기 모두에서 재사용
|
|
150
|
+
const renderTable = () => (_jsxs("table", { className: "ncua-table__table", role: "table", children: [colGroupContent, headerContent, tableContent] }));
|
|
151
|
+
// fixed-header 시 scroll-area + scrollbar 래핑, 아니면 <table> 그대로.
|
|
152
|
+
// withScrollbar=false 이면 scrollbar를 제외 — horizontalScroll 분기에서 scrollbar를
|
|
153
|
+
// h-scroll-container 형제 위치에 별도 렌더해 가로 스크롤 시 viewport 우측에 자연 고정.
|
|
154
|
+
const renderScrollableArea = (includeVerticalScrollbar = true) => fixedScrollEnabled ? (_jsxs("div", { ref: scrollAreaRef, className: "ncua-table__scroll-area", children: [_jsx("div", { ref: scrollContainerRef, className: "ncua-table__scroll-container", style: scrollStyle, children: renderTable() }), includeVerticalScrollbar && (_jsx("div", { ref: scrollbarRef, className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) }))] })) : (renderTable());
|
|
155
|
+
// horizontalScroll=true 시 외곽 wrapper + FloatingProvider 부착.
|
|
156
|
+
// 핵심 — __h-scroll-container 는 <table>(또는 scroll-area) 만 감싸고, footer/pagination 은
|
|
157
|
+
// 그 바깥에서 항상 고정 위치. 세로 스크롤바는 h-scroll-container 형제로 배치되어
|
|
158
|
+
// 가로 스크롤에 영향받지 않고 .ncua-table 우측에 absolute 고정된다.
|
|
159
|
+
if (horizontalScroll) {
|
|
160
|
+
const resolvedMinWidth = minWidth ?? DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH;
|
|
161
|
+
// CSS 변수로 전달 — SCSS 에서 max(100%, var(--ncua-table-min-width)) 로 부모 너비를 항상 보장한다.
|
|
162
|
+
// (inline min-width 를 직접 주면 부모보다 작은 값에서 wrapper 가 좁아져 콘텐츠가 깨짐)
|
|
163
|
+
const innerStyle = {
|
|
164
|
+
'--ncua-table-min-width': typeof resolvedMinWidth === 'number' ? `${resolvedMinWidth}px` : resolvedMinWidth,
|
|
175
165
|
};
|
|
176
|
-
|
|
177
|
-
document.addEventListener('mouseup', onUp);
|
|
178
|
-
};
|
|
179
|
-
if (fixedHeader && maxHeight) {
|
|
180
|
-
return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", children: [_jsxs("div", { className: tableClasses, ...rest, children: [_jsxs("div", { ref: scrollAreaRef, className: "ncua-table__scroll-area", children: [_jsx("div", { ref: scrollContainerRef, className: "ncua-table__scroll-container", style: scrollStyle, children: _jsxs("table", { className: "ncua-table__table", role: "table", children: [colGroupContent, headerContent, tableContent] }) }), _jsx("div", { className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) })] }), footerContent] }), paginationContent] }));
|
|
166
|
+
return (_jsx(FloatingProvider, { value: FLOATING_PORTAL_VALUE, children: _jsxs("div", { ref: ref, className: "ncua-table-wrapper", style: WRAPPER_STYLE, children: [_jsxs("div", { className: tableClasses, ...rest, children: [_jsxs("div", { ref: hScrollContainerRef, className: "ncua-table__h-scroll-container", children: [_jsx("div", { className: "ncua-table__h-scroll-inner", style: innerStyle, children: renderScrollableArea(false) }), _jsx("div", { ref: hScrollbarRef, className: "ncua-table__h-scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: hThumbRef, className: "ncua-table__h-scrollbar-thumb", onMouseDown: handleHThumbMouseDown }) })] }), fixedScrollEnabled && (_jsx("div", { ref: scrollbarRef, className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) })), footerContent] }), paginationContent] }) }));
|
|
181
167
|
}
|
|
182
|
-
|
|
183
|
-
return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", children: [_jsxs("div", { className: tableClasses, ...rest, children: [tableElement, footerContent] }), paginationContent] }));
|
|
168
|
+
return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", style: WRAPPER_STYLE, children: [_jsxs("div", { className: tableClasses, ...rest, children: [renderScrollableArea(), footerContent] }), paginationContent] }));
|
|
184
169
|
});
|
|
185
170
|
TableComponent.displayName = 'Table';
|
|
186
171
|
// ──────────────────────────────────────────────
|
|
@@ -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 };
|