@ncds/ui-admin 1.8.25 → 1.8.26-alpha.1
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/notification/FullWidthNotification.js +2 -0
- package/dist/cjs/assets/scripts/notification/MessageNotification.js +30 -23
- package/dist/cjs/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/cjs/assets/scripts/notification/const/icons.js +11 -2
- package/dist/cjs/assets/scripts/notification/const/index.js +9 -4
- package/dist/cjs/assets/scripts/notification/const/sizes.js +6 -1
- package/dist/cjs/assets/scripts/notification/const/types.js +1 -10
- package/dist/cjs/assets/scripts/notification/messageTemplate.js +20 -8
- package/dist/cjs/assets/scripts/notification/utils.js +21 -17
- package/dist/cjs/constant/notification.js +15 -1
- package/dist/cjs/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +4 -7
- package/dist/cjs/src/components/overlays/notification/FullWidthNotification.js +13 -8
- package/dist/cjs/src/components/overlays/notification/MessageNotification.js +20 -14
- package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +22 -15
- package/dist/cjs/src/components/overlays/notification/Notification.js +6 -0
- package/dist/cjs/src/components/overlays/notification/NotificationActionButtons.js +27 -0
- package/dist/cjs/src/components/overlays/notification/__tests__/supportTextLink.test.js +58 -0
- package/dist/cjs/src/components/overlays/notification/linkTarget.js +18 -0
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/cjs/src/utils/index.js +11 -0
- package/dist/cjs/src/utils/url/__tests__/isSafeUrl.test.js +49 -0
- package/dist/cjs/src/utils/url/index.js +16 -0
- package/dist/cjs/src/utils/url/isSafeUrl.js +32 -0
- package/dist/esm/assets/scripts/notification/FullWidthNotification.js +3 -1
- package/dist/esm/assets/scripts/notification/MessageNotification.js +32 -25
- package/dist/esm/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/esm/assets/scripts/notification/const/icons.js +10 -1
- package/dist/esm/assets/scripts/notification/const/index.js +2 -3
- package/dist/esm/assets/scripts/notification/const/sizes.js +5 -0
- package/dist/esm/assets/scripts/notification/const/types.js +1 -8
- package/dist/esm/assets/scripts/notification/messageTemplate.js +21 -9
- package/dist/esm/assets/scripts/notification/utils.js +19 -16
- package/dist/esm/constant/notification.js +14 -0
- package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/esm/src/components/overlays/notification/FloatingNotification.js +4 -7
- package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +13 -8
- package/dist/esm/src/components/overlays/notification/MessageNotification.js +21 -15
- package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +22 -15
- package/dist/esm/src/components/overlays/notification/Notification.js +6 -0
- package/dist/esm/src/components/overlays/notification/NotificationActionButtons.js +21 -0
- package/dist/esm/src/components/overlays/notification/__tests__/supportTextLink.test.js +55 -0
- package/dist/esm/src/components/overlays/notification/linkTarget.js +12 -0
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/esm/src/utils/index.js +3 -1
- package/dist/esm/src/utils/url/__tests__/isSafeUrl.test.js +47 -0
- package/dist/esm/src/utils/url/index.js +1 -0
- package/dist/esm/src/utils/url/isSafeUrl.js +26 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.js +3 -2
- package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +2 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.js +30 -25
- package/dist/temp/assets/scripts/notification/const/classNames.d.ts +3 -0
- package/dist/temp/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/temp/assets/scripts/notification/const/icons.d.ts +6 -0
- package/dist/temp/assets/scripts/notification/const/icons.js +9 -0
- package/dist/temp/assets/scripts/notification/const/index.d.ts +3 -4
- package/dist/temp/assets/scripts/notification/const/index.js +2 -3
- package/dist/temp/assets/scripts/notification/const/sizes.d.ts +4 -0
- package/dist/temp/assets/scripts/notification/const/sizes.js +5 -0
- package/dist/temp/assets/scripts/notification/const/types.d.ts +28 -1
- package/dist/temp/assets/scripts/notification/const/types.js +1 -8
- package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +5 -1
- package/dist/temp/assets/scripts/notification/messageTemplate.js +16 -11
- package/dist/temp/assets/scripts/notification/utils.d.ts +10 -3
- package/dist/temp/assets/scripts/notification/utils.js +21 -18
- package/dist/temp/constant/notification.d.ts +13 -0
- package/dist/temp/constant/notification.js +14 -0
- package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/temp/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/temp/src/components/overlays/notification/FloatingNotification.js +2 -2
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +6 -1
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +7 -2
- package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +15 -1
- package/dist/temp/src/components/overlays/notification/MessageNotification.js +11 -12
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +10 -5
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +9 -6
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +23 -2
- package/dist/temp/src/components/overlays/notification/Notification.js +3 -3
- package/dist/temp/src/components/overlays/notification/NotificationActionButtons.d.ts +12 -0
- package/dist/temp/src/components/overlays/notification/NotificationActionButtons.js +9 -0
- package/dist/temp/src/components/overlays/notification/__tests__/supportTextLink.test.d.ts +1 -0
- package/dist/temp/src/components/overlays/notification/__tests__/supportTextLink.test.js +73 -0
- package/dist/temp/src/components/overlays/notification/linkTarget.d.ts +11 -0
- package/dist/temp/src/components/overlays/notification/linkTarget.js +9 -0
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/temp/src/utils/index.d.ts +1 -0
- package/dist/temp/src/utils/index.js +2 -0
- package/dist/temp/src/utils/url/__tests__/isSafeUrl.test.d.ts +1 -0
- package/dist/temp/src/utils/url/__tests__/isSafeUrl.test.js +61 -0
- package/dist/temp/src/utils/url/index.d.ts +1 -0
- package/dist/temp/src/utils/url/index.js +1 -0
- package/dist/temp/src/utils/url/isSafeUrl.d.ts +8 -0
- package/dist/temp/src/utils/url/isSafeUrl.js +29 -0
- package/dist/types/assets/scripts/notification/MessageNotification.d.ts +2 -0
- package/dist/types/assets/scripts/notification/const/classNames.d.ts +3 -0
- package/dist/types/assets/scripts/notification/const/icons.d.ts +6 -0
- package/dist/types/assets/scripts/notification/const/index.d.ts +3 -4
- package/dist/types/assets/scripts/notification/const/sizes.d.ts +4 -0
- package/dist/types/assets/scripts/notification/const/types.d.ts +28 -1
- package/dist/types/assets/scripts/notification/messageTemplate.d.ts +5 -1
- package/dist/types/assets/scripts/notification/utils.d.ts +10 -3
- package/dist/types/constant/notification.d.ts +13 -0
- package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +6 -1
- package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +15 -1
- package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +10 -5
- package/dist/types/src/components/overlays/notification/Notification.d.ts +23 -2
- package/dist/types/src/components/overlays/notification/NotificationActionButtons.d.ts +12 -0
- package/dist/types/src/components/overlays/notification/__tests__/supportTextLink.test.d.ts +1 -0
- package/dist/types/src/components/overlays/notification/linkTarget.d.ts +11 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/dist/types/src/utils/url/__tests__/isSafeUrl.test.d.ts +1 -0
- package/dist/types/src/utils/url/index.d.ts +1 -0
- package/dist/types/src/utils/url/isSafeUrl.d.ts +8 -0
- package/dist/ui-admin/assets/styles/style.css +54 -34
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { sideSlotRender } from './utils';
|
|
|
7
7
|
*/
|
|
8
8
|
const BADGE_ICON_SIZE = 12;
|
|
9
9
|
/** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
|
|
10
|
-
const NEW_BADGE_ICON_SIZE = { sm: 12, md:
|
|
10
|
+
const NEW_BADGE_ICON_SIZE = { sm: 12, md: 14 };
|
|
11
11
|
const Badge = ({ label, type = 'pill-outline', color = 'neutral', className, leadingIcon, trailingIcon, size = 'xs', }) => {
|
|
12
12
|
// new-badge: 신규 콘텐츠 'N' 마크 전용 타입. label·color·leadingIcon·trailingIcon은 무시되고
|
|
13
13
|
// 색은 --pink-600 으로 고정된다. (디자이너 명세: Icon/FeaturedIcon/Badge로 대체 불가한 전용 표시)
|
|
@@ -5,12 +5,12 @@ import { forwardRef, useEffect, useId, useRef, useState } from 'react';
|
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { MEDIA_QUERY } from '../../../constant/breakpoint';
|
|
7
7
|
import { useMediaQuery } from '../../../hooks/useMediaQuery';
|
|
8
|
-
import { Button } from '../../action/button';
|
|
9
8
|
import { ButtonCloseX } from '../../action/button/ButtonCloseX';
|
|
10
9
|
import { FeaturedIcon, } from '../../image-and-icons/featured-icon/FeaturedIcon';
|
|
11
10
|
// 호스트 싱글톤은 vanilla/React 양쪽이 동일한 함수를 공유한다 — host.ts 가 가까운 진입점 역할을
|
|
12
11
|
// 하므로 React 컴포넌트는 deep relative path 로 vanilla internals 를 직접 import 하지 않는다.
|
|
13
12
|
import { mountFloatingNotificationHost } from './host';
|
|
13
|
+
import { NotificationActionButtons } from './NotificationActionButtons';
|
|
14
14
|
/**
|
|
15
15
|
* 색상별 a11y role.
|
|
16
16
|
* - error/warning → role="alert" (implicit aria-live="assertive": 즉시 발화)
|
|
@@ -110,7 +110,7 @@ const FloatingNotification = forwardRef(({ title, supportingText, color = 'neutr
|
|
|
110
110
|
const card = (
|
|
111
111
|
// a11y/예측가능성 — caller 가 className/id 등 일부는 덮어쓸 수 있게 {...rest} 를 먼저 펼치고,
|
|
112
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:
|
|
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: _jsx(NotificationActionButtons, { actions: actions }) }))] })] }) }), onClose && (_jsx(ButtonCloseX, { theme: "light", className: "ncua-floating-notification__close-button", size: isMobile ? 'sm' : 'xs', onClick: onClose }))] }));
|
|
114
114
|
// 기본은 인라인 렌더 — 부모 JSX 트리에 카드를 그대로 둔다.
|
|
115
115
|
if (!portal) {
|
|
116
116
|
return card;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
-
import type { NotificationAction, NotificationColor } from './Notification';
|
|
2
|
+
import type { NotificationAction, NotificationColor, NotificationLinkTarget } from './Notification';
|
|
3
3
|
interface FullWidthNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
4
4
|
/**
|
|
5
5
|
* 알림 제목 텍스트
|
|
@@ -36,6 +36,11 @@ interface FullWidthNotificationProps extends Omit<ComponentPropsWithoutRef<'div'
|
|
|
36
36
|
* info 링크 영역 (선택사항)
|
|
37
37
|
*/
|
|
38
38
|
supportTextLink?: string;
|
|
39
|
+
/**
|
|
40
|
+
* 텍스트 링크를 여는 대상 (선택사항). `'_blank'`이면 새 탭 + `rel="noopener noreferrer"`
|
|
41
|
+
* @default '_self'
|
|
42
|
+
*/
|
|
43
|
+
supportTextLinkTarget?: NotificationLinkTarget;
|
|
39
44
|
/**
|
|
40
45
|
* 다시보지 않기 이벤트 핸들러 (선택사항)
|
|
41
46
|
*/
|
|
@@ -4,6 +4,9 @@ import classNames from 'classnames';
|
|
|
4
4
|
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { COLOR } from '../../../../constant/color';
|
|
6
6
|
import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
|
|
7
|
+
import { isSafeUrl } from '../../../utils/url';
|
|
8
|
+
import { getLinkTargetAttributes } from './linkTarget';
|
|
9
|
+
import { NotificationActionButtons } from './NotificationActionButtons';
|
|
7
10
|
const iconMap = {
|
|
8
11
|
neutral: MessageChatSquare,
|
|
9
12
|
error: AlertTriangle,
|
|
@@ -18,8 +21,10 @@ const iconColorMap = {
|
|
|
18
21
|
success: 'green600',
|
|
19
22
|
info: 'violet600',
|
|
20
23
|
};
|
|
21
|
-
const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, supportTextLink, onHidePermanently, hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth, ...rest }, ref) => {
|
|
24
|
+
const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, supportTextLink, supportTextLinkTarget, onHidePermanently, hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth, ...rest }, ref) => {
|
|
22
25
|
const [shouldRemove, setShouldRemove] = useState(false);
|
|
26
|
+
// unsafe URL(javascript: 등)이면 링크 없이 텍스트만 렌더한다 — Vanilla `renderSupportingText` 와 같은 정책.
|
|
27
|
+
const safeSupportTextLink = supportTextLink && isSafeUrl(supportTextLink) ? supportTextLink : undefined;
|
|
23
28
|
const closeIconSize = {
|
|
24
29
|
width: 20,
|
|
25
30
|
height: 20,
|
|
@@ -52,6 +57,6 @@ const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neut
|
|
|
52
57
|
return (_jsx("div", { ref: ref, className: classNames('ncua-full-width-notification', `ncua-full-width-notification--${color}`, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-full-width-notification__container", children: _jsxs("div", { className: "ncua-full-width-notification__content", children: [_jsxs("div", { className: "ncua-full-width-notification__content-wrapper", children: [(() => {
|
|
53
58
|
const IconComponent = iconMap[color];
|
|
54
59
|
return (_jsx(IconComponent, { width: 16, height: 16, color: COLOR[iconColorMap[color]], className: "ncua-full-width-notification__icon" }));
|
|
55
|
-
})(), _jsxs("div", { className: "ncua-full-width-notification__text-container", children: [_jsx("span", { className: "ncua-full-width-notification__title", children: title }), !
|
|
60
|
+
})(), _jsxs("div", { className: "ncua-full-width-notification__text-container", children: [_jsx("span", { className: "ncua-full-width-notification__title", children: title }), !safeSupportTextLink && supportingText && (_jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText })), safeSupportTextLink && supportingText && (_jsx("a", { href: safeSupportTextLink, className: "ncua-full-width-notification__link", ...getLinkTargetAttributes(supportTextLinkTarget), children: _jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText }) }))] })] }), _jsxs("div", { className: "ncua-full-width-notification__actions-container", children: [actions && (_jsx("div", { className: "ncua-full-width-notification__actions", children: _jsx(NotificationActionButtons, { actions: actions }) })), onHidePermanently && (_jsx("button", { type: "button", className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'), onClick: onHidePermanently, children: hidePermanentlyLabel })), onClose && (_jsx("button", { type: "button", className: "ncua-full-width-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { ...closeIconSize, color: COLOR[iconColorMap[color]] }) }))] })] }) }) }));
|
|
56
61
|
});
|
|
57
62
|
export { FullWidthNotification };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import type { SlotIconComponent } from '../../../types/side-slot';
|
|
3
|
-
import type { NotificationAction, NotificationColor } from './Notification';
|
|
3
|
+
import type { NotificationAction, NotificationColor, NotificationLinkTarget, NotificationVariant } from './Notification';
|
|
4
4
|
interface MessageNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
5
5
|
/**
|
|
6
6
|
* 알림 제목 텍스트
|
|
@@ -41,6 +41,20 @@ interface MessageNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>,
|
|
|
41
41
|
* @default '다시 보지 않기'
|
|
42
42
|
*/
|
|
43
43
|
hidePermanentlyLabel?: ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* 표시 변형. `'block'`은 서브 페이지 공지용(흰 배경, FeaturedIcon sm, 제목 + 텍스트 링크 1행, info 유지)
|
|
46
|
+
* @default 'default'
|
|
47
|
+
*/
|
|
48
|
+
variant?: NotificationVariant;
|
|
49
|
+
/**
|
|
50
|
+
* 텍스트 링크 URL. `variant="block"`에서만 동작하며 `supportingText`가 링크 텍스트가 된다.
|
|
51
|
+
*/
|
|
52
|
+
supportTextLink?: string;
|
|
53
|
+
/**
|
|
54
|
+
* 텍스트 링크를 여는 대상. `'_blank'`이면 새 탭 + `rel="noopener noreferrer"`
|
|
55
|
+
* @default '_self'
|
|
56
|
+
*/
|
|
57
|
+
supportTextLinkTarget?: NotificationLinkTarget;
|
|
44
58
|
}
|
|
45
59
|
declare const MessageNotification: import("react").ForwardRefExoticComponent<MessageNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
46
60
|
export type { MessageNotificationProps };
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { AlertCircle, AlertTriangle, CheckCircle, Pin02 } from '@ncds/ui-admin-icon';
|
|
2
|
+
import { AlertCircle, AlertTriangle, CheckCircle, InfoCircle, Pin02 } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { forwardRef } from 'react';
|
|
5
|
-
import {
|
|
5
|
+
import { MESSAGE_CLOSE_ICON_COLOR } from '../../../../constant/notification';
|
|
6
6
|
import { MessageNotificationActions, MessageNotificationContent } from './MessageNotificationParts';
|
|
7
7
|
const iconMap = {
|
|
8
8
|
neutral: Pin02,
|
|
9
9
|
error: AlertTriangle,
|
|
10
10
|
warning: AlertCircle,
|
|
11
11
|
success: CheckCircle,
|
|
12
|
+
// info 는 variant="block" 에서만 도달한다 (default 는 neutral 로 폴백).
|
|
13
|
+
info: InfoCircle,
|
|
12
14
|
};
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
error
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// message 타입은 neutral, error, warning, success 4가지 색상만 지원
|
|
21
|
-
const validColor = color === 'info' ? 'neutral' : color;
|
|
22
|
-
return (_jsx("div", { ref: ref, className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-message-notification__container", children: _jsxs("div", { className: "ncua-message-notification__content", children: [_jsx(MessageNotificationContent, { title: title, supportingText: supportingText, icon: Icon || iconMap[validColor], iconColor: validColor, showIcon: Boolean(iconMap[validColor]) }), _jsx(MessageNotificationActions, { actions: actions, onClose: onClose, onHidePermanently: onHidePermanently, hidePermanentlyLabel: hidePermanentlyLabel, closeIconColor: COLOR[iconColorMap[validColor] || 'gray700'] })] }) }) }));
|
|
15
|
+
const MessageNotification = forwardRef(({ title, supportingText, icon: Icon, color = 'neutral', onClose, className, actions, onHidePermanently, hidePermanentlyLabel, variant = 'default', supportTextLink, supportTextLinkTarget, ...rest }, ref) => {
|
|
16
|
+
const isBlock = variant === 'block';
|
|
17
|
+
// default 는 neutral, error, warning, success 4가지 색상만 지원 — info 는 block 에서만 유지한다.
|
|
18
|
+
const validColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
19
|
+
// FeaturedIcon 에는 info 색이 없다. block+info 의 violet 톤은 message SCSS 가 덮어쓴다.
|
|
20
|
+
const featuredIconColor = validColor === 'info' ? 'neutral' : validColor;
|
|
21
|
+
return (_jsx("div", { ref: ref, className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, { 'ncua-message-notification--block': isBlock }, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-message-notification__container", children: _jsxs("div", { className: "ncua-message-notification__content", children: [_jsx(MessageNotificationContent, { title: title, supportingText: supportingText, supportTextLink: isBlock ? supportTextLink : undefined, supportTextLinkTarget: supportTextLinkTarget, icon: Icon || iconMap[validColor], iconColor: featuredIconColor, iconSize: isBlock ? 'sm' : 'md' }), _jsx(MessageNotificationActions, { actions: actions, onClose: onClose, onHidePermanently: onHidePermanently, hidePermanentlyLabel: hidePermanentlyLabel, closeIconColor: MESSAGE_CLOSE_ICON_COLOR[validColor] })] }) }) }));
|
|
23
22
|
});
|
|
24
23
|
MessageNotification.displayName = 'MessageNotification';
|
|
25
24
|
export { MessageNotification };
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { SlotIconComponent } from '../../../types/side-slot';
|
|
3
|
-
import { type FeaturedIconColor } from '../../image-and-icons/featured-icon/FeaturedIcon';
|
|
4
|
-
import type { NotificationAction } from './Notification';
|
|
3
|
+
import { type FeaturedIconColor, type FeaturedIconSize } from '../../image-and-icons/featured-icon/FeaturedIcon';
|
|
4
|
+
import type { NotificationAction, NotificationLinkTarget } from './Notification';
|
|
5
5
|
interface MessageNotificationContentProps {
|
|
6
6
|
title: ReactNode;
|
|
7
7
|
supportingText?: ReactNode;
|
|
8
|
+
/** 지정 시 `supportingText`를 이 URL 의 텍스트 링크로 렌더한다 (block 변형 전용). http/https·상대경로가 아니면 텍스트만 렌더. */
|
|
9
|
+
supportTextLink?: string;
|
|
10
|
+
/** 텍스트 링크를 여는 대상. `'_blank'`이면 새 탭 + `rel="noopener noreferrer"`. */
|
|
11
|
+
supportTextLinkTarget?: NotificationLinkTarget;
|
|
8
12
|
icon?: SlotIconComponent;
|
|
9
13
|
iconColor: FeaturedIconColor;
|
|
10
|
-
|
|
14
|
+
/** default 는 md(40/20), block 은 sm(32/16). */
|
|
15
|
+
iconSize: FeaturedIconSize;
|
|
11
16
|
}
|
|
12
|
-
/** 아이콘 + 제목/서포팅 텍스트 영역. */
|
|
13
|
-
declare const MessageNotificationContent: ({ title, supportingText, icon, iconColor,
|
|
17
|
+
/** 아이콘 + 제목/서포팅 텍스트(또는 텍스트 링크) 영역. */
|
|
18
|
+
declare const MessageNotificationContent: ({ title, supportingText, supportTextLink, supportTextLinkTarget, icon, iconColor, iconSize, }: MessageNotificationContentProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
19
|
interface MessageNotificationActionsProps {
|
|
15
20
|
actions?: NotificationAction[];
|
|
16
21
|
onClose?: () => void;
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Pin02, XClose } from '@ncds/ui-admin-icon';
|
|
3
3
|
import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
|
|
4
|
-
import {
|
|
4
|
+
import { isSafeUrl } from '../../../utils/url';
|
|
5
5
|
import { FeaturedIcon, } from '../../image-and-icons/featured-icon/FeaturedIcon';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { getLinkTargetAttributes } from './linkTarget';
|
|
7
|
+
import { NotificationActionButtons } from './NotificationActionButtons';
|
|
8
|
+
/** 아이콘 + 제목/서포팅 텍스트(또는 텍스트 링크) 영역. */
|
|
9
|
+
const MessageNotificationContent = ({ title, supportingText, supportTextLink, supportTextLinkTarget, icon, iconColor, iconSize, }) => {
|
|
8
10
|
const featuredIconProps = {
|
|
9
11
|
icon: icon || Pin02,
|
|
10
|
-
size:
|
|
12
|
+
size: iconSize,
|
|
11
13
|
color: iconColor,
|
|
12
14
|
theme: 'light-circle',
|
|
13
15
|
};
|
|
14
|
-
|
|
16
|
+
const supportingTextNode = supportingText && (_jsx("span", { className: "ncua-message-notification__supporting-text", children: supportingText }));
|
|
17
|
+
return (_jsxs("div", { className: "ncua-message-notification__content-wrapper", children: [_jsx(FeaturedIcon, { ...featuredIconProps, className: "ncua-message-notification__icon" }), _jsxs("div", { className: "ncua-message-notification__text-container", children: [_jsx("span", { className: "ncua-message-notification__title", children: title }), supportTextLink && isSafeUrl(supportTextLink) && supportingTextNode ? (_jsx("a", { href: supportTextLink, className: "ncua-message-notification__link", ...getLinkTargetAttributes(supportTextLinkTarget), children: supportingTextNode })) : (supportingTextNode)] })] }));
|
|
15
18
|
};
|
|
16
19
|
/**
|
|
17
20
|
* 액션 버튼 그룹 + 닫기 그룹을 감싸는 우측 영역.
|
|
@@ -23,6 +26,6 @@ const MessageNotificationActions = ({ actions, onClose, onHidePermanently, hideP
|
|
|
23
26
|
const hasFooter = Boolean(onHidePermanently || onClose);
|
|
24
27
|
if (!hasActions && !hasFooter)
|
|
25
28
|
return null;
|
|
26
|
-
return (_jsxs("div", { className: "ncua-message-notification__actions-container", children: [hasActions && (_jsx("div", { className: "ncua-message-notification__actions", children:
|
|
29
|
+
return (_jsxs("div", { className: "ncua-message-notification__actions-container", children: [hasActions && (_jsx("div", { className: "ncua-message-notification__actions", children: _jsx(NotificationActionButtons, { actions: actions ?? [] }) })), hasFooter && (_jsxs("div", { className: "ncua-message-notification__footer-container", children: [onHidePermanently && (_jsx("button", { type: "button", className: "ncua-notification__action-button ncua-message-notification__hide-link", onClick: onHidePermanently, children: hidePermanentlyLabel })), onClose && (_jsx("button", { type: "button", className: "ncua-message-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { width: 20, height: 20, color: closeIconColor }) }))] }))] }));
|
|
27
30
|
};
|
|
28
31
|
export { MessageNotificationActions, MessageNotificationContent };
|
|
@@ -6,6 +6,9 @@ import { type CalloutNotificationProps, type CalloutNotificationWidth } from './
|
|
|
6
6
|
type NotificationType = 'floating' | 'full-width' | 'message' | 'callout';
|
|
7
7
|
type NotificationColor = Extract<ColorTone, 'neutral' | 'error' | 'warning' | 'success' | 'info'>;
|
|
8
8
|
type NotificationSize = 'desktop' | 'mobile';
|
|
9
|
+
type NotificationVariant = 'default' | 'block';
|
|
10
|
+
/** 텍스트 링크(`supportTextLink`)를 여는 대상. `'_blank'`이면 새 탭 + `rel="noopener noreferrer"`. */
|
|
11
|
+
type NotificationLinkTarget = '_self' | '_blank';
|
|
9
12
|
interface NotificationAction {
|
|
10
13
|
/**
|
|
11
14
|
* 액션 버튼 텍스트
|
|
@@ -78,9 +81,27 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
|
|
|
78
81
|
hidePermanentlyLabel?: ReactNode;
|
|
79
82
|
/**
|
|
80
83
|
* supportingText 에 연결할 링크 URL (선택사항)
|
|
81
|
-
* full-width
|
|
84
|
+
* full-width 타입, message 타입의 `variant="block"`에서 사용 가능
|
|
82
85
|
*/
|
|
83
86
|
supportTextLink?: string;
|
|
87
|
+
/**
|
|
88
|
+
* 텍스트 링크를 여는 대상 (선택사항). `supportTextLink`가 렌더될 때만 동작한다.
|
|
89
|
+
* `'_blank'`이면 새 탭으로 열고 `rel="noopener noreferrer"`를 함께 붙인다.
|
|
90
|
+
* full-width 타입, message 타입의 `variant="block"`에서 사용 가능
|
|
91
|
+
* @default '_self'
|
|
92
|
+
*/
|
|
93
|
+
supportTextLinkTarget?: NotificationLinkTarget;
|
|
94
|
+
/**
|
|
95
|
+
* message 타입의 표시 변형 (선택사항). message 타입에서만 동작한다.
|
|
96
|
+
*
|
|
97
|
+
* - `'default'`: 기존 message (color 별 배경·테두리, 제목 + 본문 2행, info → neutral 폴백)
|
|
98
|
+
* - `'block'`: 서브 페이지 공지용. 흰 배경·gray-100 테두리·shadow-sm, FeaturedIcon sm,
|
|
99
|
+
* 제목 + 텍스트 링크 1행(`supportingText` 가 링크 텍스트, `supportTextLink` 가 href).
|
|
100
|
+
* `color="info"` 를 그대로 렌더한다.
|
|
101
|
+
*
|
|
102
|
+
* @default 'default'
|
|
103
|
+
*/
|
|
104
|
+
variant?: NotificationVariant;
|
|
84
105
|
/**
|
|
85
106
|
* 너비 동작 (선택사항)
|
|
86
107
|
* callout 타입에서만 사용 가능. 미지정 시 부모 너비를 채운다(100%). 'fit' 은 콘텐츠 너비에 맞춘다
|
|
@@ -101,6 +122,6 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
|
|
|
101
122
|
portal?: boolean;
|
|
102
123
|
}
|
|
103
124
|
declare const Notification: import("react").ForwardRefExoticComponent<NotificationProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
104
|
-
export type { NotificationType, NotificationColor, NotificationSize, NotificationAction, NotificationProps };
|
|
125
|
+
export type { NotificationType, NotificationColor, NotificationSize, NotificationVariant, NotificationLinkTarget, NotificationAction, NotificationProps, };
|
|
105
126
|
export type { CalloutNotificationProps, CalloutNotificationWidth };
|
|
106
127
|
export { Notification };
|
|
@@ -7,15 +7,15 @@ import { MessageNotification } from './MessageNotification';
|
|
|
7
7
|
const Notification = forwardRef(
|
|
8
8
|
// 단일 type 에만 유효한 prop 은 rest 에서 분리해 해당 분기로만 명시 전달한다.
|
|
9
9
|
// rest 에 남기면 다른 type 의 하위 컴포넌트가 div 로 스프레드해 DOM 속성으로 출력된다.
|
|
10
|
-
({ type = 'floating', color = 'neutral', width, supportTextLink, ...rest }, ref) => {
|
|
10
|
+
({ type = 'floating', color = 'neutral', width, supportTextLink, supportTextLinkTarget, variant, ...rest }, ref) => {
|
|
11
11
|
if (type === 'floating') {
|
|
12
12
|
return _jsx(FloatingNotification, { color: color, ...rest, ref: ref });
|
|
13
13
|
}
|
|
14
14
|
if (type === 'full-width') {
|
|
15
|
-
return (_jsx(FullWidthNotification, { color: color, supportTextLink: supportTextLink, ...rest, ref: ref }));
|
|
15
|
+
return (_jsx(FullWidthNotification, { color: color, supportTextLink: supportTextLink, supportTextLinkTarget: supportTextLinkTarget, ...rest, ref: ref }));
|
|
16
16
|
}
|
|
17
17
|
if (type === 'message') {
|
|
18
|
-
return _jsx(MessageNotification, { color: color, ...rest, ref: ref });
|
|
18
|
+
return (_jsx(MessageNotification, { color: color, variant: variant, supportTextLink: supportTextLink, supportTextLinkTarget: supportTextLinkTarget, ...rest, ref: ref }));
|
|
19
19
|
}
|
|
20
20
|
if (type === 'callout') {
|
|
21
21
|
// 나머지 3개 type 이 공유하는 prop 은 callout 이 지원하지 않으므로 여기서 제외한다.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NotificationAction } from './Notification';
|
|
2
|
+
interface NotificationActionButtonsProps {
|
|
3
|
+
actions: NotificationAction[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 알림 액션 버튼 목록. 모든 type 이 같은 규격(`ncua-btn--xs` + hierarchy, 기본 `text`)을 쓴다(#535).
|
|
7
|
+
* Vanilla `renderActions`(assets/scripts/notification/utils.ts)와 같은 클래스를 산출해야 한다.
|
|
8
|
+
* 감싸는 래퍼(`__actions`)는 type 마다 클래스가 달라 호출부가 담당한다.
|
|
9
|
+
*/
|
|
10
|
+
declare const NotificationActionButtons: ({ actions }: NotificationActionButtonsProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export type { NotificationActionButtonsProps };
|
|
12
|
+
export { NotificationActionButtons };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../../action/button';
|
|
3
|
+
/**
|
|
4
|
+
* 알림 액션 버튼 목록. 모든 type 이 같은 규격(`ncua-btn--xs` + hierarchy, 기본 `text`)을 쓴다(#535).
|
|
5
|
+
* Vanilla `renderActions`(assets/scripts/notification/utils.ts)와 같은 클래스를 산출해야 한다.
|
|
6
|
+
* 감싸는 래퍼(`__actions`)는 type 마다 클래스가 달라 호출부가 담당한다.
|
|
7
|
+
*/
|
|
8
|
+
const NotificationActionButtons = ({ actions }) => (_jsx(_Fragment, { children: actions.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action.onClick }, `${action.label}-${action.hierarchy}`))) }));
|
|
9
|
+
export { NotificationActionButtons };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { FullWidthNotification } from '../FullWidthNotification';
|
|
7
|
+
import { MessageNotification } from '../MessageNotification';
|
|
8
|
+
/**
|
|
9
|
+
* React 텍스트 링크(`supportTextLink`)의 프로토콜 검사 (#538 리뷰 H2).
|
|
10
|
+
* Vanilla `renderSupportingText` 와 같은 정책 — unsafe URL 이면 링크 없이 텍스트만 렌더한다.
|
|
11
|
+
*/
|
|
12
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
const cleanups = [];
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const cleanup of cleanups.splice(0))
|
|
16
|
+
cleanup();
|
|
17
|
+
});
|
|
18
|
+
function mount(element) {
|
|
19
|
+
const container = document.createElement('div');
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
const root = createRoot(container);
|
|
22
|
+
act(() => {
|
|
23
|
+
root.render(element);
|
|
24
|
+
});
|
|
25
|
+
cleanups.push(() => {
|
|
26
|
+
act(() => root.unmount());
|
|
27
|
+
container.remove();
|
|
28
|
+
});
|
|
29
|
+
return container;
|
|
30
|
+
}
|
|
31
|
+
const IMPLEMENTATIONS = [
|
|
32
|
+
[
|
|
33
|
+
'MessageNotification(block)',
|
|
34
|
+
(supportTextLink) => mount(createElement(MessageNotification, {
|
|
35
|
+
title: '공지',
|
|
36
|
+
variant: 'block',
|
|
37
|
+
supportingText: '자세히 보기',
|
|
38
|
+
supportTextLink,
|
|
39
|
+
})),
|
|
40
|
+
'.ncua-message-notification__supporting-text',
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
'FullWidthNotification',
|
|
44
|
+
(supportTextLink) => mount(createElement(FullWidthNotification, {
|
|
45
|
+
title: '공지',
|
|
46
|
+
supportingText: '자세히 보기',
|
|
47
|
+
supportTextLink,
|
|
48
|
+
})),
|
|
49
|
+
'.ncua-full-width-notification__supporting-text',
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
const UNSAFE = [
|
|
53
|
+
'javascript:alert(1)',
|
|
54
|
+
' JavaScript:alert(1)',
|
|
55
|
+
'JAVASCRIPT:alert(1)',
|
|
56
|
+
'\tjavascript:alert(1)',
|
|
57
|
+
'data:text/html,<script>alert(1)</script>',
|
|
58
|
+
];
|
|
59
|
+
const SAFE = ['https://example.com/notice/1', 'http://example.com/notice/1', '/notice/1', 'notice/1'];
|
|
60
|
+
describe.each(IMPLEMENTATIONS)('%s — supportTextLink 프로토콜 검사', (_, render, supportingTextSelector) => {
|
|
61
|
+
it.each(UNSAFE)('unsafe %j → 링크 없이 텍스트만 렌더한다', (url) => {
|
|
62
|
+
const container = render(url);
|
|
63
|
+
expect(container.querySelector('a')).toBeNull();
|
|
64
|
+
expect(container.querySelector(supportingTextSelector)?.textContent).toBe('자세히 보기');
|
|
65
|
+
});
|
|
66
|
+
it.each(SAFE)('safe %j → 그대로 링크로 렌더한다', (url) => {
|
|
67
|
+
const container = render(url);
|
|
68
|
+
const anchors = container.querySelectorAll('a');
|
|
69
|
+
expect(anchors).toHaveLength(1);
|
|
70
|
+
expect(anchors[0].getAttribute('href')).toBe(url);
|
|
71
|
+
expect(anchors[0].querySelector(supportingTextSelector)?.textContent).toBe('자세히 보기');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NotificationLinkTarget } from './Notification';
|
|
2
|
+
/**
|
|
3
|
+
* 텍스트 링크(`supportTextLink`)의 `<a>`에 붙일 target·rel 속성.
|
|
4
|
+
*
|
|
5
|
+
* `'_blank'`일 때만 새 탭 + `rel="noopener noreferrer"`를 반환하고, `'_self'`(기본)는 아무 속성도 붙이지 않는다.
|
|
6
|
+
* Vanilla `applySupportTextLinkTarget`과 같은 마크업을 산출해야 한다.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getLinkTargetAttributes(target?: NotificationLinkTarget): {
|
|
9
|
+
readonly target: "_blank";
|
|
10
|
+
readonly rel: "noopener noreferrer";
|
|
11
|
+
} | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 텍스트 링크(`supportTextLink`)의 `<a>`에 붙일 target·rel 속성.
|
|
3
|
+
*
|
|
4
|
+
* `'_blank'`일 때만 새 탭 + `rel="noopener noreferrer"`를 반환하고, `'_self'`(기본)는 아무 속성도 붙이지 않는다.
|
|
5
|
+
* Vanilla `applySupportTextLinkTarget`과 같은 마크업을 산출해야 한다.
|
|
6
|
+
*/
|
|
7
|
+
export function getLinkTargetAttributes(target) {
|
|
8
|
+
return target === '_blank' ? { target: '_blank', rel: 'noopener noreferrer' } : undefined;
|
|
9
|
+
}
|