@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
|
@@ -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 };
|
|
@@ -1,56 +1,124 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AlertCircle, AlertTriangle, CheckCircle, Pin02 } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { forwardRef, useEffect, useId, useRef, useState } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
5
6
|
import { MEDIA_QUERY } from '../../../constant/breakpoint';
|
|
6
7
|
import { useMediaQuery } from '../../../hooks/useMediaQuery';
|
|
7
8
|
import { Button } from '../../action/button';
|
|
8
9
|
import { ButtonCloseX } from '../../action/button/ButtonCloseX';
|
|
9
10
|
import { FeaturedIcon, } from '../../image-and-icons/featured-icon/FeaturedIcon';
|
|
11
|
+
// 호스트 싱글톤은 vanilla/React 양쪽이 동일한 함수를 공유한다 — host.ts 가 가까운 진입점 역할을
|
|
12
|
+
// 하므로 React 컴포넌트는 deep relative path 로 vanilla internals 를 직접 import 하지 않는다.
|
|
13
|
+
import { mountFloatingNotificationHost } from './host';
|
|
14
|
+
/**
|
|
15
|
+
* 색상별 a11y role.
|
|
16
|
+
* - error/warning → role="alert" (implicit aria-live="assertive": 즉시 발화)
|
|
17
|
+
* - 그 외 → role="status" (implicit aria-live="polite": 현재 발화 끝난 후 발화)
|
|
18
|
+
* role 이 implicit live-region 을 가져오므로 호스트 컨테이너에는 aria-live 를 두지 않는다.
|
|
19
|
+
*/
|
|
20
|
+
const ASSERTIVE_COLORS = {
|
|
21
|
+
error: true,
|
|
22
|
+
warning: true,
|
|
23
|
+
};
|
|
10
24
|
const iconMap = {
|
|
11
25
|
neutral: Pin02,
|
|
12
26
|
error: AlertTriangle,
|
|
13
27
|
warning: AlertCircle,
|
|
14
28
|
success: CheckCircle,
|
|
15
|
-
// info는 floating
|
|
29
|
+
// info 는 full-width 전용이라 floating 아이콘 매핑 없음.
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* NotificationColor → FeaturedIconColor 매핑.
|
|
33
|
+
* `info` 는 FeaturedIconColor 에 존재하지 않으므로 `neutral` 로 안전 fallback.
|
|
34
|
+
* `satisfies` 가 매핑 누락을 컴파일 타임에 강제하므로 향후 색상이 추가되면 즉시 타입 에러로 잡힌다.
|
|
35
|
+
*/
|
|
36
|
+
const iconColorMap = {
|
|
37
|
+
neutral: 'neutral',
|
|
38
|
+
error: 'error',
|
|
39
|
+
warning: 'warning',
|
|
40
|
+
success: 'success',
|
|
41
|
+
info: 'neutral',
|
|
16
42
|
};
|
|
17
|
-
|
|
43
|
+
/**
|
|
44
|
+
* `.ncua-floating-notification-host` 싱글톤을 보장하고 반환하는 훅.
|
|
45
|
+
*
|
|
46
|
+
* 이름의 `useMount...` 는 단순 조회가 아닌 **DOM 부수효과** 를 동반함을 신호한다 —
|
|
47
|
+
* 내부적으로 `mountFloatingNotificationHost()` 를 호출해 document.body 에 호스트를 append 하고
|
|
48
|
+
* (없으면) 글로벌 scroll/resize/MutationObserver 까지 부착한다. 호스트는 페이지 lifetime
|
|
49
|
+
* 싱글톤이므로 언마운트 시 제거하지 않는다.
|
|
50
|
+
*
|
|
51
|
+
* `mountFloatingNotificationHost` 는 **동기 함수** 이므로 effect 안의 호출 → setHost 가 같은 tick
|
|
52
|
+
* 에 끝나 cancellation race 가 없다. cancel flag 는 의미 없어 두지 않는다.
|
|
53
|
+
*/
|
|
54
|
+
function useMountFloatingNotificationHost(enabled) {
|
|
55
|
+
const [host, setHost] = useState(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!enabled)
|
|
58
|
+
return;
|
|
59
|
+
setHost(mountFloatingNotificationHost());
|
|
60
|
+
}, [enabled]);
|
|
61
|
+
return host;
|
|
62
|
+
}
|
|
63
|
+
const FloatingNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, portal = false, ...rest }, ref) => {
|
|
18
64
|
const [shouldRemove, setShouldRemove] = useState(false);
|
|
19
|
-
const iconColor = color;
|
|
20
65
|
const featuredIconProps = {
|
|
21
66
|
icon: iconMap[color] || Pin02,
|
|
22
67
|
size: 'sm',
|
|
23
|
-
color:
|
|
68
|
+
color: iconColorMap[color],
|
|
24
69
|
theme: 'dark-circle',
|
|
25
70
|
};
|
|
26
71
|
const isMobile = useMediaQuery(MEDIA_QUERY.mobile, {
|
|
27
72
|
onMatched: onClose,
|
|
28
73
|
});
|
|
74
|
+
// onClose 는 매 렌더 새 함수 ref 일 수 있으므로 ref 로 latest 만 보관.
|
|
75
|
+
// → autoClose 타이머 effect 의 deps 에서 onClose 를 빼서 타이머 재시작 회피.
|
|
76
|
+
// render 본문에서 직접 할당 — useEffect 로 미루면 같은 commit 안에서 fire 되는 타이머가 stale onClose 를 볼 위험.
|
|
77
|
+
const onCloseRef = useRef(onClose);
|
|
78
|
+
onCloseRef.current = onClose;
|
|
29
79
|
// autoClose 타이머 관리
|
|
30
80
|
const timerRef = useRef(null);
|
|
81
|
+
// onClose 는 onCloseRef 로 latest 만 추적하므로 deps 에서 제외 (의도된 패턴).
|
|
31
82
|
useEffect(() => {
|
|
32
|
-
// autoClose가 0보다 크면
|
|
83
|
+
// autoClose 가 0 보다 크면 타이머 설정.
|
|
33
84
|
if (autoClose > 0) {
|
|
34
85
|
timerRef.current = setTimeout(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
// DOM에서 바로 제거
|
|
86
|
+
onCloseRef.current?.();
|
|
87
|
+
// DOM 에서 바로 제거
|
|
39
88
|
setShouldRemove(true);
|
|
40
89
|
}, autoClose);
|
|
41
90
|
}
|
|
42
|
-
// cleanup
|
|
91
|
+
// cleanup: 컴포넌트 언마운트 또는 autoClose 변경 시 타이머 정리
|
|
43
92
|
return () => {
|
|
44
93
|
if (timerRef.current) {
|
|
45
94
|
clearTimeout(timerRef.current);
|
|
46
95
|
timerRef.current = null;
|
|
47
96
|
}
|
|
48
97
|
};
|
|
49
|
-
}, [autoClose
|
|
98
|
+
}, [autoClose]);
|
|
99
|
+
// portal=true 일 때만 호스트가 필요하다.
|
|
100
|
+
const host = useMountFloatingNotificationHost(portal);
|
|
101
|
+
// a11y — 카드 root 의 role 과 제목/본문 ID. 색상에 따라 alert(error/warning) / status(나머지) 로 분기.
|
|
102
|
+
const reactId = useId();
|
|
103
|
+
const titleId = `${reactId}-title`;
|
|
104
|
+
const descId = `${reactId}-desc`;
|
|
105
|
+
const role = ASSERTIVE_COLORS[color] ? 'alert' : 'status';
|
|
50
106
|
// DOM에서 완전히 제거
|
|
51
107
|
if (shouldRemove) {
|
|
52
108
|
return null;
|
|
53
109
|
}
|
|
54
|
-
|
|
110
|
+
const card = (
|
|
111
|
+
// a11y/예측가능성 — caller 가 className/id 등 일부는 덮어쓸 수 있게 {...rest} 를 먼저 펼치고,
|
|
112
|
+
// role/aria-labelledby/aria-describedby 같은 컴포넌트 본질 prop 은 그 뒤에 두어 우선 적용.
|
|
113
|
+
_jsxs("div", { ...rest, ref: ref, className: classNames('ncua-floating-notification', `ncua-floating-notification--${color}`, className), role: role, "aria-labelledby": titleId, "aria-describedby": supportingText ? descId : undefined, children: [_jsx("div", { className: "ncua-floating-notification__content", children: _jsxs("div", { className: "ncua-floating-notification__container", children: [iconMap[color] && _jsx(FeaturedIcon, { ...featuredIconProps, size: isMobile ? 'md' : 'sm' }), _jsxs("div", { className: "ncua-floating-notification__text-container", children: [_jsx("div", { className: "ncua-floating-notification__title-wrapper", children: _jsx("span", { id: titleId, className: "ncua-floating-notification__title", children: title }) }), supportingText && (_jsx("span", { id: descId, className: "ncua-floating-notification__supporting-text", children: supportingText })), actions && (_jsx("div", { className: "ncua-floating-notification__actions", children: actions.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action?.onClick }, `${action.label}-${action.hierarchy}`))) }))] })] }) }), onClose && (_jsx(ButtonCloseX, { theme: "light", className: "ncua-floating-notification__close-button", size: isMobile ? 'sm' : 'xs', onClick: onClose }))] }));
|
|
114
|
+
// 기본은 인라인 렌더 — 부모 JSX 트리에 카드를 그대로 둔다.
|
|
115
|
+
if (!portal) {
|
|
116
|
+
return card;
|
|
117
|
+
}
|
|
118
|
+
// portal=true 인데 SSR이거나 첫 렌더(호스트 미생성)에서는 null. useEffect 후 host 설정되면 재렌더되어 portal 마운트.
|
|
119
|
+
if (!host) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return createPortal(card, host);
|
|
55
123
|
});
|
|
56
124
|
export { FloatingNotification };
|
|
@@ -2,7 +2,8 @@ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
|
2
2
|
import type { ColorTone } from '../../../../constant/color';
|
|
3
3
|
import type { SlotIconComponent } from '../../../types/side-slot';
|
|
4
4
|
import type { ButtonTheme } from '../../action/button';
|
|
5
|
-
|
|
5
|
+
import { type CalloutNotificationProps } from './CalloutNotification';
|
|
6
|
+
type NotificationType = 'floating' | 'full-width' | 'message' | 'callout';
|
|
6
7
|
type NotificationColor = Extract<ColorTone, 'neutral' | 'error' | 'warning' | 'success' | 'info'>;
|
|
7
8
|
type NotificationSize = 'desktop' | 'mobile';
|
|
8
9
|
interface NotificationAction {
|
|
@@ -26,9 +27,9 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
|
|
|
26
27
|
*/
|
|
27
28
|
type?: NotificationType;
|
|
28
29
|
/**
|
|
29
|
-
* 알림 제목 텍스트
|
|
30
|
+
* 알림 제목 텍스트 (callout 유형에서는 안내 텍스트로 사용)
|
|
30
31
|
*/
|
|
31
|
-
title
|
|
32
|
+
title?: ReactNode;
|
|
32
33
|
/**
|
|
33
34
|
* 알림 본문 텍스트 (선택사항)
|
|
34
35
|
*/
|
|
@@ -66,7 +67,21 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
|
|
|
66
67
|
* message, full-width 타입에서 사용 가능
|
|
67
68
|
*/
|
|
68
69
|
onHidePermanently?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Portal 마운트 여부. `floating` 타입에서만 동작한다.
|
|
72
|
+
*
|
|
73
|
+
* 기본값 `false`에서는 부모 JSX 트리 안에 카드로 그대로 렌더된다.
|
|
74
|
+
* `true`로 설정하면 자동으로 document.body의 `.ncua-floating-notification-host`
|
|
75
|
+
* 호스트에 createPortal로 마운트되어 우측 상단에 노출되고 LIFO로 스택된다
|
|
76
|
+
* — 최신 토스트가 상단에 노출되고 이전 토스트들이 아래로 12px씩 겹쳐 쌓인다
|
|
77
|
+
* (vanilla `Notification.show()`와 동일, `--ncua-floating-notification-stack-overlap`
|
|
78
|
+
* 변수로 조정 가능).
|
|
79
|
+
*
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
portal?: boolean;
|
|
69
83
|
}
|
|
70
84
|
declare const Notification: import("react").ForwardRefExoticComponent<NotificationProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
71
85
|
export type { NotificationType, NotificationColor, NotificationSize, NotificationAction, NotificationProps };
|
|
86
|
+
export type { CalloutNotificationProps };
|
|
72
87
|
export { Notification };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
|
+
import { CalloutNotification } from './CalloutNotification';
|
|
3
4
|
import { FloatingNotification } from './FloatingNotification';
|
|
4
5
|
import { FullWidthNotification } from './FullWidthNotification';
|
|
5
6
|
import { MessageNotification } from './MessageNotification';
|
|
@@ -13,6 +14,9 @@ const Notification = forwardRef(({ type = 'floating', color = 'neutral', ...rest
|
|
|
13
14
|
if (type === 'message') {
|
|
14
15
|
return _jsx(MessageNotification, { color: color, ...rest, ref: ref });
|
|
15
16
|
}
|
|
17
|
+
if (type === 'callout') {
|
|
18
|
+
return _jsx(CalloutNotification, { color: color, ...rest });
|
|
19
|
+
}
|
|
16
20
|
return null;
|
|
17
21
|
});
|
|
18
22
|
Notification.displayName = 'Notification';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating Notification 호스트 관리 — React 측 진입점.
|
|
3
|
+
*
|
|
4
|
+
* 실제 구현은 vanilla/React 가 공유하는 `assets/scripts/notification/positionSync.ts` 에 있고,
|
|
5
|
+
* 외부 노출은 그 폴더의 public barrel(`assets/scripts/notification/index.ts`)이 담당한다.
|
|
6
|
+
* 이 모듈은 React 컴포넌트가 vanilla 내부 파일을 직접 참조하지 않도록 barrel 을 경유하는
|
|
7
|
+
* 한 단계 re-export 만 제공한다 — positionSync.ts 의 위치/이름이 바뀌어도 host.ts 만 영향.
|
|
8
|
+
*/
|
|
9
|
+
export { mountFloatingNotificationHost } from '../../../../assets/scripts/notification';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating Notification 호스트 관리 — React 측 진입점.
|
|
3
|
+
*
|
|
4
|
+
* 실제 구현은 vanilla/React 가 공유하는 `assets/scripts/notification/positionSync.ts` 에 있고,
|
|
5
|
+
* 외부 노출은 그 폴더의 public barrel(`assets/scripts/notification/index.ts`)이 담당한다.
|
|
6
|
+
* 이 모듈은 React 컴포넌트가 vanilla 내부 파일을 직접 참조하지 않도록 barrel 을 경유하는
|
|
7
|
+
* 한 단계 re-export 만 제공한다 — positionSync.ts 의 위치/이름이 바뀌어도 host.ts 만 영향.
|
|
8
|
+
*/
|
|
9
|
+
export { mountFloatingNotificationHost } from '../../../../assets/scripts/notification';
|
|
@@ -12,6 +12,10 @@ interface TooltipProps {
|
|
|
12
12
|
iconColor?: string;
|
|
13
13
|
iconStyle?: 'help-circle' | 'alert-circle';
|
|
14
14
|
zIndex?: number;
|
|
15
|
+
/** 외부에서 직접 패널 표시 여부를 제어 */
|
|
16
|
+
forceVisible?: boolean;
|
|
17
|
+
/** true이면 Portal 없이 앵커 내부에 패널을 인라인 렌더링. CSS 기반 위치 지정이 필요한 경우 사용 */
|
|
18
|
+
disablePortal?: boolean;
|
|
15
19
|
}
|
|
16
|
-
export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, forceVisible, disablePortal, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
21
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AlertCircle, AlertCircleFill, HelpCircle, HelpCircleFill } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -74,7 +74,19 @@ const computePanelCoords = (prefer, anchor, panel) => {
|
|
|
74
74
|
top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - panel.height - VIEWPORT_MARGIN));
|
|
75
75
|
return { top: Math.round(top), left: Math.round(left), calculatedPosition };
|
|
76
76
|
};
|
|
77
|
-
|
|
77
|
+
const buildPanelStyle = (disablePortal, coords, visible, zIndex) => {
|
|
78
|
+
const opacity = visible ? 1 : 0;
|
|
79
|
+
if (disablePortal)
|
|
80
|
+
return { opacity };
|
|
81
|
+
return { top: `${coords.top}px`, left: `${coords.left}px`, opacity, ...(zIndex && { zIndex }) };
|
|
82
|
+
};
|
|
83
|
+
const renderIcon = (iconStyle, iconType, iconSize, iconColor) => {
|
|
84
|
+
if (iconStyle === 'help-circle') {
|
|
85
|
+
return iconType === 'stroke' ? (_jsx(HelpCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(HelpCircleFill, { width: iconSize, height: iconSize, color: iconColor }));
|
|
86
|
+
}
|
|
87
|
+
return iconType === 'stroke' ? (_jsx(AlertCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(AlertCircleFill, { width: iconSize, height: iconSize, color: iconColor }));
|
|
88
|
+
};
|
|
89
|
+
export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position = 'auto', size = 'sm', title, content, hideArrow = false, type = 'short', iconColor = 'var(--gray-300)', iconStyle = 'help-circle', className, zIndex, forceVisible, disablePortal = false, }) => {
|
|
78
90
|
const iconSize = size === 'sm' ? ICON_SIZE_SM : ICON_SIZE_DEFAULT;
|
|
79
91
|
const anchorRef = useRef(null);
|
|
80
92
|
const panelRef = useRef(null);
|
|
@@ -84,6 +96,7 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
84
96
|
const [calculatedPosition, setCalculatedPosition] = useState(position === 'auto' ? 'bottom' : position);
|
|
85
97
|
const [isVisible, setIsVisible] = useState(false);
|
|
86
98
|
const [isManuallyClose, setIsManuallyClose] = useState(false);
|
|
99
|
+
const effectiveVisible = forceVisible ?? isVisible;
|
|
87
100
|
useEffect(() => {
|
|
88
101
|
setMounted(true);
|
|
89
102
|
return () => {
|
|
@@ -94,16 +107,16 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
94
107
|
};
|
|
95
108
|
}, []);
|
|
96
109
|
const updatePosition = useCallback(() => {
|
|
97
|
-
if (!anchorRef.current || !panelRef.current)
|
|
110
|
+
if (disablePortal || !anchorRef.current || !panelRef.current)
|
|
98
111
|
return;
|
|
99
112
|
const anchor = anchorRef.current.getBoundingClientRect();
|
|
100
113
|
const panel = panelRef.current.getBoundingClientRect();
|
|
101
114
|
const next = computePanelCoords(position, anchor, panel);
|
|
102
115
|
setCoords({ top: next.top, left: next.left });
|
|
103
116
|
setCalculatedPosition(next.calculatedPosition);
|
|
104
|
-
}, [position]);
|
|
117
|
+
}, [position, disablePortal]);
|
|
105
118
|
useEffect(() => {
|
|
106
|
-
if (!
|
|
119
|
+
if (!effectiveVisible || disablePortal)
|
|
107
120
|
return;
|
|
108
121
|
updatePosition();
|
|
109
122
|
window.addEventListener('resize', updatePosition, { passive: true });
|
|
@@ -112,14 +125,11 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
112
125
|
window.removeEventListener('resize', updatePosition);
|
|
113
126
|
window.removeEventListener('scroll', updatePosition, { capture: true });
|
|
114
127
|
};
|
|
115
|
-
}, [
|
|
128
|
+
}, [effectiveVisible, updatePosition, disablePortal]);
|
|
116
129
|
const handleMouseEnter = useCallback(() => {
|
|
117
130
|
if (isManuallyClose)
|
|
118
131
|
return;
|
|
119
|
-
// opacity 전환 전에 좌표 확정 (ref 가드는 updatePosition 내부)
|
|
120
132
|
updatePosition();
|
|
121
|
-
// 웹폰트 로드·max-content 재계산 등 비동기 layout 안정화 후 한 번 더 보정
|
|
122
|
-
// 빠른 hover in/out 시 이전 프레임 요청은 취소해 중복/unmount 후 실행 방지
|
|
123
133
|
if (rafIdRef.current !== null)
|
|
124
134
|
cancelAnimationFrame(rafIdRef.current);
|
|
125
135
|
rafIdRef.current = requestAnimationFrame(() => {
|
|
@@ -144,19 +154,12 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
144
154
|
'ncua-tooltip--stroke': iconType === 'stroke',
|
|
145
155
|
'ncua-tooltip--auto': position === 'auto',
|
|
146
156
|
}, className), [size, type, hideArrow, iconType, position, className]);
|
|
147
|
-
const panelClassName = useMemo(() => classNames('ncua-tooltip-panel', 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
|
|
148
|
-
'ncua-tooltip__bg--visible':
|
|
149
|
-
'ncua-tooltip__bg--force-hidden': isManuallyClose,
|
|
150
|
-
}), [tooltipType, finalPosition,
|
|
151
|
-
const panelStyle =
|
|
152
|
-
top: `${coords.top}px`,
|
|
153
|
-
left: `${coords.left}px`,
|
|
154
|
-
opacity: isVisible ? 1 : 0,
|
|
155
|
-
...(zIndex && { zIndex }),
|
|
156
|
-
};
|
|
157
|
+
const panelClassName = useMemo(() => classNames({ 'ncua-tooltip-panel': !disablePortal }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
|
|
158
|
+
'ncua-tooltip__bg--visible': effectiveVisible,
|
|
159
|
+
'ncua-tooltip__bg--force-hidden': isManuallyClose && !forceVisible,
|
|
160
|
+
}), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal]);
|
|
161
|
+
const panelStyle = buildPanelStyle(disablePortal, coords, effectiveVisible, zIndex);
|
|
157
162
|
const portalTarget = mounted ? resolvePortalTarget() : null;
|
|
158
163
|
const panel = (_jsxs("span", { ref: panelRef, className: panelClassName, style: panelStyle, children: [title && _jsx("span", { className: "ncua-tooltip__title", children: title }), content && _jsx("span", { className: "ncua-tooltip__content", children: content }), type === 'long' && (_jsx(ButtonCloseX, { className: "ncua-tooltip__close-button", size: "xs", theme: tooltipType === 'white' ? 'dark' : 'light', onClick: handleCloseClick, "aria-label": "\uD234\uD301 \uB2EB\uAE30" }))] }));
|
|
159
|
-
return (_jsxs(
|
|
160
|
-
(iconType === 'stroke' ? (_jsx(HelpCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(HelpCircleFill, { width: iconSize, height: iconSize, color: iconColor }))), iconStyle === 'alert-circle' &&
|
|
161
|
-
(iconType === 'stroke' ? (_jsx(AlertCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(AlertCircleFill, { width: iconSize, height: iconSize, color: iconColor })))] }), portalTarget && createPortal(panel, portalTarget)] }));
|
|
164
|
+
return (_jsxs("span", { ref: anchorRef, className: tooltipClassName, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [renderIcon(iconStyle, iconType, iconSize, iconColor), disablePortal ? panel : portalTarget && createPortal(panel, portalTarget)] }));
|
|
162
165
|
};
|
|
@@ -17,6 +17,12 @@ type SelectDropdownProps = ComponentPropsWithRef<'div'> & {
|
|
|
17
17
|
multiple?: boolean;
|
|
18
18
|
showFooterButtons?: boolean;
|
|
19
19
|
selectAllButtonText?: string;
|
|
20
|
+
/**
|
|
21
|
+
* footer의 "전체 선택" Link 노출 여부 (default: `true`).
|
|
22
|
+
* footer 자체(편집/선택 완료 버튼)는 영향받지 않는다.
|
|
23
|
+
* 호출자가 도메인 조건(예: 최대 선택 개수 제한)에 따라 `false`로 내리면 Link만 숨겨진다.
|
|
24
|
+
*/
|
|
25
|
+
showSelectAllAction?: boolean;
|
|
20
26
|
onSelectAll?: () => void;
|
|
21
27
|
onEdit?: () => void;
|
|
22
28
|
onComplete?: () => void;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
import { Button } from '../action/button';
|
|
5
|
-
const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', options, value, focusedIndex, maxHeight, listboxId = 'selectbox-options-default', onOptionSelect, onMouseMove, isKeyboardNavigation, onOptionHover, children, multiple = false, showFooterButtons = false, selectAllButtonText = '전체 선택', onSelectAll, onEdit, onComplete, className, activeDescendantId, componentType, align = 'left', ...props }, ref) => {
|
|
5
|
+
const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', options, value, focusedIndex, maxHeight, listboxId = 'selectbox-options-default', onOptionSelect, onMouseMove, isKeyboardNavigation, onOptionHover, children, multiple = false, showFooterButtons = false, selectAllButtonText = '전체 선택', showSelectAllAction = true, onSelectAll, onEdit, onComplete, className, activeDescendantId, componentType, align = 'left', ...props }, ref) => {
|
|
6
6
|
if (!isOpen)
|
|
7
7
|
return null;
|
|
8
8
|
return (_jsxs("div", { ref: ref, className: classNames('ncua-select-dropdown', `ncua-select-dropdown--${direction}`, `ncua-select-dropdown--${size}`, {
|
|
@@ -20,7 +20,7 @@ const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', op
|
|
|
20
20
|
'ncua-select-dropdown__option--selected': isSelected,
|
|
21
21
|
'ncua-select-dropdown__option--focused': isFocused,
|
|
22
22
|
}), onClick: () => onOptionSelect(option), onMouseEnter: handleMouseEnter, role: "option", "aria-selected": isSelected, children: [option.icon && (_jsx("span", { className: "ncua-select-dropdown__option-icon", children: _jsx(option.icon, { width: 16, height: 16 }) })), _jsx("span", { className: "ncua-select-dropdown__option-text", children: option.label })] }, option.id));
|
|
23
|
-
}), children] }) }), showFooterButtons && (_jsx("div", { className: "ncua-select-dropdown__footer", children: _jsxs("div", { className: "ncua-select-dropdown__footer-buttons", children: [_jsx("div", { className: "ncua-select-dropdown__footer-left", children: multiple && (_jsx(Button, { label: selectAllButtonText, hierarchy: "text", size: "xs", onClick: onSelectAll, underline: true })) }), _jsxs("div", { className: "ncua-select-dropdown__footer-right", children: [_jsx(Button, { label: "\uD3B8\uC9D1", hierarchy: "secondary-gray", size: "xs", onClick: onEdit }), multiple && _jsx(Button, { label: "\uC120\uD0DD \uC644\uB8CC", hierarchy: "secondary", size: "xs", onClick: onComplete })] })] }) }))] }));
|
|
23
|
+
}), children] }) }), showFooterButtons && (_jsx("div", { className: "ncua-select-dropdown__footer", children: _jsxs("div", { className: "ncua-select-dropdown__footer-buttons", children: [_jsx("div", { className: "ncua-select-dropdown__footer-left", children: multiple && showSelectAllAction && (_jsx(Button, { label: selectAllButtonText, hierarchy: "text", size: "xs", onClick: onSelectAll, underline: true })) }), _jsxs("div", { className: "ncua-select-dropdown__footer-right", children: [_jsx(Button, { label: "\uD3B8\uC9D1", hierarchy: "secondary-gray", size: "xs", onClick: onEdit }), multiple && _jsx(Button, { label: "\uC120\uD0DD \uC644\uB8CC", hierarchy: "secondary", size: "xs", onClick: onComplete })] })] }) }))] }));
|
|
24
24
|
});
|
|
25
25
|
SelectDropdown.displayName = 'SelectDropdown';
|
|
26
26
|
export { SelectDropdown };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type FloatingContextValue = {
|
|
2
|
+
preferPortal: boolean;
|
|
3
|
+
portalContainer?: HTMLElement | null;
|
|
4
|
+
};
|
|
5
|
+
export declare const FloatingProvider: import("react").Provider<FloatingContextValue | null>;
|
|
6
|
+
export declare const useFloatingContext: () => FloatingContextValue | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FloatingContext';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FloatingContext';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
|
+
type FloatingDirection = 'up' | 'down';
|
|
3
|
+
type UseFloatingPositionParams = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
7
|
+
floatingRef: RefObject<HTMLElement | null>;
|
|
8
|
+
direction: FloatingDirection;
|
|
9
|
+
offset?: number;
|
|
10
|
+
align?: 'left' | 'right';
|
|
11
|
+
/**
|
|
12
|
+
* true면 floating 요소의 width를 trigger 너비에 맞춘다.
|
|
13
|
+
* Portal 모드에서 부모가 body로 바뀌면서 min-width: 100% 같은 CSS가 viewport 너비로
|
|
14
|
+
* 폭주하는 것을 막는다 (예: SelectBox 옵션 패널).
|
|
15
|
+
*/
|
|
16
|
+
matchTriggerWidth?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare const useFloatingPosition: ({ enabled, isOpen, triggerRef, floatingRef, direction, offset, align, matchTriggerWidth, }: UseFloatingPositionParams) => CSSProperties | null;
|
|
19
|
+
export {};
|