@ncds/ui-admin 1.8.26-alpha.4 → 1.8.26-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/assets/scripts/notification/FloatingNotification.js +23 -7
- package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +14 -3
- package/dist/cjs/assets/scripts/notification/MessageNotification.js +3 -0
- package/dist/cjs/assets/scripts/notification/Notification.js +8 -2
- package/dist/cjs/assets/scripts/notification/messageTemplate.js +21 -5
- package/dist/cjs/constant/notification.js +8 -2
- package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +10 -28
- package/dist/cjs/src/components/overlays/notification/FullWidthNotification.js +9 -24
- package/dist/cjs/src/components/overlays/notification/MessageNotification.js +12 -0
- package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +5 -2
- package/dist/cjs/src/components/overlays/notification/Notification.js +1 -0
- package/dist/cjs/src/components/overlays/notification/__tests__/CalloutNotification.test.js +8 -2
- package/dist/cjs/src/components/overlays/notification/__tests__/Notification.test.js +176 -0
- package/dist/cjs/src/components/overlays/notification/useAutoClose.js +39 -0
- package/dist/cjs/src/utils/notification/__tests__/resolveAutoClose.test.js +44 -0
- package/dist/cjs/src/utils/notification/index.js +12 -0
- package/dist/cjs/src/utils/notification/resolveAutoClose.js +22 -0
- package/dist/esm/assets/scripts/notification/FloatingNotification.js +23 -7
- package/dist/esm/assets/scripts/notification/FullWidthNotification.js +14 -3
- package/dist/esm/assets/scripts/notification/MessageNotification.js +3 -0
- package/dist/esm/assets/scripts/notification/Notification.js +8 -2
- package/dist/esm/assets/scripts/notification/messageTemplate.js +21 -5
- package/dist/esm/constant/notification.js +7 -1
- package/dist/esm/src/components/overlays/notification/FloatingNotification.js +11 -29
- package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +10 -25
- package/dist/esm/src/components/overlays/notification/MessageNotification.js +12 -0
- package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +5 -2
- package/dist/esm/src/components/overlays/notification/Notification.js +1 -0
- package/dist/esm/src/components/overlays/notification/__tests__/CalloutNotification.test.js +9 -3
- package/dist/esm/src/components/overlays/notification/__tests__/Notification.test.js +177 -1
- package/dist/esm/src/components/overlays/notification/useAutoClose.js +32 -0
- package/dist/esm/src/utils/notification/__tests__/resolveAutoClose.test.js +42 -0
- package/dist/esm/src/utils/notification/index.js +1 -0
- package/dist/esm/src/utils/notification/resolveAutoClose.js +15 -0
- package/dist/temp/assets/scripts/notification/FloatingNotification.js +10 -6
- package/dist/temp/assets/scripts/notification/FullWidthNotification.js +8 -5
- package/dist/temp/assets/scripts/notification/MessageNotification.js +3 -1
- package/dist/temp/assets/scripts/notification/Notification.js +7 -1
- package/dist/temp/assets/scripts/notification/const/types.d.ts +7 -0
- package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +1 -0
- package/dist/temp/assets/scripts/notification/messageTemplate.js +19 -9
- package/dist/temp/constant/notification.d.ts +6 -0
- package/dist/temp/constant/notification.js +6 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +8 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.js +6 -29
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +7 -0
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +6 -25
- package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +12 -0
- package/dist/temp/src/components/overlays/notification/MessageNotification.js +8 -2
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +5 -3
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +14 -1
- package/dist/temp/src/components/overlays/notification/Notification.js +1 -1
- package/dist/temp/src/components/overlays/notification/__tests__/CalloutNotification.test.js +8 -1
- package/dist/temp/src/components/overlays/notification/__tests__/Notification.test.js +128 -1
- package/dist/temp/src/components/overlays/notification/useAutoClose.d.ts +15 -0
- package/dist/temp/src/components/overlays/notification/useAutoClose.js +25 -0
- package/dist/temp/src/utils/notification/__tests__/resolveAutoClose.test.d.ts +1 -0
- package/dist/temp/src/utils/notification/__tests__/resolveAutoClose.test.js +22 -0
- package/dist/temp/src/utils/notification/index.d.ts +1 -0
- package/dist/temp/src/utils/notification/index.js +1 -0
- package/dist/temp/src/utils/notification/resolveAutoClose.d.ts +14 -0
- package/dist/temp/src/utils/notification/resolveAutoClose.js +12 -0
- package/dist/types/assets/scripts/notification/const/types.d.ts +7 -0
- package/dist/types/assets/scripts/notification/messageTemplate.d.ts +1 -0
- package/dist/types/constant/notification.d.ts +6 -0
- package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +8 -0
- package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +7 -0
- package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +12 -0
- package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
- package/dist/types/src/components/overlays/notification/Notification.d.ts +14 -1
- package/dist/types/src/components/overlays/notification/useAutoClose.d.ts +15 -0
- package/dist/types/src/utils/notification/__tests__/resolveAutoClose.test.d.ts +1 -0
- package/dist/types/src/utils/notification/index.d.ts +1 -0
- package/dist/types/src/utils/notification/resolveAutoClose.d.ts +14 -0
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AlertTriangle, CheckCircle, InfoCircle, MessageChatSquare, XClose } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import { forwardRef
|
|
4
|
+
import { forwardRef } from 'react';
|
|
5
5
|
import { COLOR } from '../../../../constant/color';
|
|
6
6
|
import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
|
|
7
7
|
import { isSafeUrl } from '../../../utils/url';
|
|
8
8
|
import { getLinkTargetAttributes } from './linkTarget';
|
|
9
9
|
import { NotificationActionButtons } from './NotificationActionButtons';
|
|
10
|
+
import { useAutoClose } from './useAutoClose';
|
|
10
11
|
const iconMap = {
|
|
11
12
|
neutral: MessageChatSquare,
|
|
12
13
|
error: AlertTriangle,
|
|
@@ -27,43 +28,27 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
27
28
|
supportingText,
|
|
28
29
|
color = 'neutral',
|
|
29
30
|
onClose,
|
|
31
|
+
hideCloseButton = false,
|
|
30
32
|
className,
|
|
31
33
|
actions,
|
|
32
|
-
autoClose
|
|
34
|
+
autoClose,
|
|
33
35
|
supportTextLink,
|
|
34
36
|
supportTextLinkTarget,
|
|
35
37
|
onHidePermanently,
|
|
36
38
|
hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth,
|
|
37
39
|
...rest
|
|
38
40
|
} = _ref;
|
|
39
|
-
|
|
41
|
+
// full-width 는 부모가 언마운트로 닫으므로 hideCloseButton 기본 autoClose 를 적용하지 않는다.
|
|
42
|
+
const shouldRemove = useAutoClose({
|
|
43
|
+
autoClose,
|
|
44
|
+
onClose
|
|
45
|
+
});
|
|
40
46
|
// unsafe URL(javascript: 등)이면 링크 없이 텍스트만 렌더한다 — Vanilla `renderSupportingText` 와 같은 정책.
|
|
41
47
|
const safeSupportTextLink = supportTextLink && isSafeUrl(supportTextLink) ? supportTextLink : undefined;
|
|
42
48
|
const closeIconSize = {
|
|
43
49
|
width: 20,
|
|
44
50
|
height: 20
|
|
45
51
|
};
|
|
46
|
-
// autoClose 타이머 관리
|
|
47
|
-
const timerRef = useRef(null);
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
// autoClose가 0보다 크면 무조건 타이머 설정
|
|
50
|
-
if (autoClose > 0) {
|
|
51
|
-
timerRef.current = setTimeout(() => {
|
|
52
|
-
if (onClose) {
|
|
53
|
-
onClose();
|
|
54
|
-
}
|
|
55
|
-
// DOM에서 바로 제거
|
|
56
|
-
setShouldRemove(true);
|
|
57
|
-
}, autoClose);
|
|
58
|
-
}
|
|
59
|
-
// cleanup 함수: 컴포넌트 언마운트 시 타이머 정리
|
|
60
|
-
return () => {
|
|
61
|
-
if (timerRef.current) {
|
|
62
|
-
clearTimeout(timerRef.current);
|
|
63
|
-
timerRef.current = null;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}, [autoClose, onClose]);
|
|
67
52
|
// DOM에서 완전히 제거
|
|
68
53
|
if (shouldRemove) {
|
|
69
54
|
return null;
|
|
@@ -117,7 +102,7 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
117
102
|
className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'),
|
|
118
103
|
onClick: onHidePermanently,
|
|
119
104
|
children: hidePermanentlyLabel
|
|
120
|
-
}), onClose && _jsx("button", {
|
|
105
|
+
}), onClose && !hideCloseButton && _jsx("button", {
|
|
121
106
|
type: "button",
|
|
122
107
|
className: "ncua-full-width-notification__close-button",
|
|
123
108
|
onClick: onClose,
|
|
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|
|
4
4
|
import { forwardRef } from 'react';
|
|
5
5
|
import { MESSAGE_CLOSE_ICON_COLOR } from '../../../../constant/notification';
|
|
6
6
|
import { MessageNotificationActions, MessageNotificationContent } from './MessageNotificationParts';
|
|
7
|
+
import { useAutoClose } from './useAutoClose';
|
|
7
8
|
const iconMap = {
|
|
8
9
|
neutral: Pin02,
|
|
9
10
|
error: AlertTriangle,
|
|
@@ -19,6 +20,8 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
19
20
|
icon: Icon,
|
|
20
21
|
color = 'neutral',
|
|
21
22
|
onClose,
|
|
23
|
+
hideCloseButton = false,
|
|
24
|
+
autoClose,
|
|
22
25
|
className,
|
|
23
26
|
actions,
|
|
24
27
|
onHidePermanently,
|
|
@@ -33,6 +36,14 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
33
36
|
const validColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
34
37
|
// FeaturedIcon 에는 info 색이 없다. block+info 의 violet 톤은 message SCSS 가 덮어쓴다.
|
|
35
38
|
const featuredIconColor = validColor === 'info' ? 'neutral' : validColor;
|
|
39
|
+
// message 는 부모가 언마운트로 닫으므로 hideCloseButton 기본 autoClose 를 적용하지 않는다.
|
|
40
|
+
const shouldRemove = useAutoClose({
|
|
41
|
+
autoClose,
|
|
42
|
+
onClose
|
|
43
|
+
});
|
|
44
|
+
if (shouldRemove) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
36
47
|
return _jsx("div", {
|
|
37
48
|
ref: ref,
|
|
38
49
|
className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, {
|
|
@@ -55,6 +66,7 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
55
66
|
}), _jsx(MessageNotificationActions, {
|
|
56
67
|
actions: actions,
|
|
57
68
|
onClose: onClose,
|
|
69
|
+
hideCloseButton: hideCloseButton,
|
|
58
70
|
onHidePermanently: onHidePermanently,
|
|
59
71
|
hidePermanentlyLabel: hidePermanentlyLabel,
|
|
60
72
|
closeIconColor: MESSAGE_CLOSE_ICON_COLOR[validColor]
|
|
@@ -54,12 +54,15 @@ const MessageNotificationActions = _ref2 => {
|
|
|
54
54
|
let {
|
|
55
55
|
actions,
|
|
56
56
|
onClose,
|
|
57
|
+
hideCloseButton = false,
|
|
57
58
|
onHidePermanently,
|
|
58
59
|
hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.message,
|
|
59
60
|
closeIconColor
|
|
60
61
|
} = _ref2;
|
|
61
62
|
const hasActions = Boolean(actions?.length);
|
|
62
|
-
|
|
63
|
+
// footer 컨테이너와 닫기 버튼이 같은 판정을 보게 해 빈 footer 가 남지 않게 한다.
|
|
64
|
+
const showCloseButton = Boolean(onClose) && !hideCloseButton;
|
|
65
|
+
const hasFooter = Boolean(onHidePermanently) || showCloseButton;
|
|
63
66
|
if (!hasActions && !hasFooter) return null;
|
|
64
67
|
return _jsxs("div", {
|
|
65
68
|
className: "ncua-message-notification__actions-container",
|
|
@@ -75,7 +78,7 @@ const MessageNotificationActions = _ref2 => {
|
|
|
75
78
|
className: "ncua-notification__action-button ncua-message-notification__hide-link",
|
|
76
79
|
onClick: onHidePermanently,
|
|
77
80
|
children: hidePermanentlyLabel
|
|
78
|
-
}),
|
|
81
|
+
}), showCloseButton && _jsx("button", {
|
|
79
82
|
type: "button",
|
|
80
83
|
className: "ncua-message-notification__close-button",
|
|
81
84
|
onClick: onClose,
|
|
@@ -51,6 +51,7 @@ const Notification = /*#__PURE__*/forwardRef(
|
|
|
51
51
|
actions: _actions,
|
|
52
52
|
autoClose: _autoClose,
|
|
53
53
|
onClose: _onClose,
|
|
54
|
+
hideCloseButton: _hideCloseButton,
|
|
54
55
|
onHidePermanently: _onHidePermanently,
|
|
55
56
|
hidePermanentlyLabel: _hidePermanentlyLabel,
|
|
56
57
|
portal: _portal,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createElement } from 'react';
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
4
|
import { act } from 'react-dom/test-utils';
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { CalloutNotification } from '../CalloutNotification';
|
|
7
7
|
import { Notification } from '../Notification';
|
|
8
8
|
// React 18에서 act() 사용 시 필요한 플래그 (미설정 시 console.error 경고 발생)
|
|
@@ -43,6 +43,7 @@ afterEach(() => {
|
|
|
43
43
|
container?.remove();
|
|
44
44
|
root = null;
|
|
45
45
|
container = null;
|
|
46
|
+
vi.restoreAllMocks();
|
|
46
47
|
});
|
|
47
48
|
describe('CalloutNotification — width prop', () => {
|
|
48
49
|
it('width 미지정 시 너비 클래스를 붙이지 않는다 (부모 너비를 채우는 기본 스타일 유지)', () => {
|
|
@@ -91,6 +92,7 @@ describe('Notification type="callout" — 위임 계약', () => {
|
|
|
91
92
|
});
|
|
92
93
|
// Notification 의 callout 분기가 제외하는 prop 전체. 여기 누락된 prop 은 DOM 속성으로 출력된다.
|
|
93
94
|
it('callout 이 지원하지 않는 prop 은 DOM 속성으로 새지 않는다', () => {
|
|
95
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
94
96
|
const element = renderNotification({
|
|
95
97
|
icon: () => null,
|
|
96
98
|
supportingText: '보조 문구',
|
|
@@ -103,12 +105,16 @@ describe('Notification type="callout" — 위임 계약', () => {
|
|
|
103
105
|
portal: true,
|
|
104
106
|
onClose: () => undefined,
|
|
105
107
|
onHidePermanently: () => undefined,
|
|
106
|
-
hidePermanentlyLabel: '다시 보지 않기'
|
|
108
|
+
hidePermanentlyLabel: '다시 보지 않기',
|
|
109
|
+
hideCloseButton: true
|
|
107
110
|
});
|
|
108
|
-
for (const attr of ['icon', 'supportingtext', 'supporttextlink', 'actions', 'autoclose', 'portal', 'onclose', 'onhidepermanently', 'hidepermanentlylabel']) {
|
|
111
|
+
for (const attr of ['icon', 'supportingtext', 'supporttextlink', 'actions', 'autoclose', 'portal', 'onclose', 'onhidepermanently', 'hidepermanentlylabel', 'hideclosebutton']) {
|
|
109
112
|
expect(element.hasAttribute(attr), attr).toBe(false);
|
|
110
113
|
}
|
|
111
114
|
expect(element.textContent).toBe('안내 문구');
|
|
115
|
+
// 불리언 true 는 React 가 미지 속성으로 출력하지 않아 hasAttribute 로는 누수를 못 잡는다 — 경고 로그로 검출한다.
|
|
116
|
+
const leaked = consoleError.mock.calls.some(call => call.some(arg => String(arg).includes('hideCloseButton')));
|
|
117
|
+
expect(leaked).toBe(false);
|
|
112
118
|
});
|
|
113
119
|
it('ref 를 callout 루트 엘리먼트로 전달한다', () => {
|
|
114
120
|
let captured = null;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { createElement } from 'react';
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
4
|
import { act } from 'react-dom/test-utils';
|
|
5
|
-
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS } from '../../../../../constant/notification';
|
|
6
7
|
import { Notification } from '../Notification';
|
|
7
8
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
8
9
|
let root = null;
|
|
@@ -77,4 +78,179 @@ describe('Notification — type 별 위임', () => {
|
|
|
77
78
|
});
|
|
78
79
|
expect(element).toBeNull();
|
|
79
80
|
});
|
|
81
|
+
});
|
|
82
|
+
const CLOSE_BUTTON = {
|
|
83
|
+
floating: '.ncua-floating-notification__close-button',
|
|
84
|
+
'full-width': '.ncua-full-width-notification__close-button',
|
|
85
|
+
message: '.ncua-message-notification__close-button'
|
|
86
|
+
};
|
|
87
|
+
const MESSAGE_FOOTER = '.ncua-message-notification__footer-container';
|
|
88
|
+
const MESSAGE_HIDE_LINK = '.ncua-message-notification__hide-link';
|
|
89
|
+
/** DES-SPEC-027 §5.3 성공 토스트 자동 닫힘 시간. */
|
|
90
|
+
const AUTO_CLOSE_MS = 3000;
|
|
91
|
+
describe('Notification — hideCloseButton', () => {
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.useRealTimers();
|
|
94
|
+
});
|
|
95
|
+
it('R-01: floating — onClose 가 있어도 hideCloseButton 이면 닫기 버튼을 렌더하지 않는다', () => {
|
|
96
|
+
const element = render({
|
|
97
|
+
type: 'floating',
|
|
98
|
+
onClose: vi.fn(),
|
|
99
|
+
hideCloseButton: true
|
|
100
|
+
});
|
|
101
|
+
expect(element?.querySelector(CLOSE_BUTTON.floating)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
it('R-02: full-width — onClose 가 있어도 hideCloseButton 이면 닫기 버튼을 렌더하지 않는다', () => {
|
|
104
|
+
const element = render({
|
|
105
|
+
type: 'full-width',
|
|
106
|
+
onClose: vi.fn(),
|
|
107
|
+
hideCloseButton: true
|
|
108
|
+
});
|
|
109
|
+
expect(element?.querySelector(CLOSE_BUTTON['full-width'])).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
it('R-03: message — hideCloseButton 이면 닫기 버튼과 빈 footer 컨테이너를 모두 렌더하지 않는다', () => {
|
|
112
|
+
const element = render({
|
|
113
|
+
type: 'message',
|
|
114
|
+
onClose: vi.fn(),
|
|
115
|
+
hideCloseButton: true
|
|
116
|
+
});
|
|
117
|
+
expect(element?.querySelector(CLOSE_BUTTON.message)).toBeNull();
|
|
118
|
+
expect(element?.querySelector(MESSAGE_FOOTER)).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
it.each(['floating', 'full-width', 'message'])('R-04: type=%s — hideCloseButton 미지정(기본 false)이면 onClose 만으로 닫기 버튼을 렌더한다', type => {
|
|
121
|
+
const element = render({
|
|
122
|
+
type,
|
|
123
|
+
onClose: vi.fn()
|
|
124
|
+
});
|
|
125
|
+
expect(element?.querySelector(CLOSE_BUTTON[type])).not.toBeNull();
|
|
126
|
+
});
|
|
127
|
+
it.each(['floating', 'full-width', 'message'])('R-05: type=%s — onClose 없이 hideCloseButton 만 주면 닫기 버튼 없이 오류 없이 렌더한다', type => {
|
|
128
|
+
const element = render({
|
|
129
|
+
type,
|
|
130
|
+
hideCloseButton: true
|
|
131
|
+
});
|
|
132
|
+
expect(element).not.toBeNull();
|
|
133
|
+
expect(element?.querySelector(CLOSE_BUTTON[type])).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
it('R-06: floating — hideCloseButton 이어도 autoClose 만료 시 onClose 가 1회 호출되고 DOM 에서 제거된다', () => {
|
|
136
|
+
vi.useFakeTimers();
|
|
137
|
+
const onClose = vi.fn();
|
|
138
|
+
render({
|
|
139
|
+
type: 'floating',
|
|
140
|
+
autoClose: AUTO_CLOSE_MS,
|
|
141
|
+
hideCloseButton: true,
|
|
142
|
+
onClose
|
|
143
|
+
});
|
|
144
|
+
act(() => {
|
|
145
|
+
vi.advanceTimersByTime(AUTO_CLOSE_MS);
|
|
146
|
+
});
|
|
147
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(container?.querySelector('.ncua-floating-notification')).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
it('R-07: full-width — hideCloseButton 이어도 autoClose 만료 시 onClose 가 1회 호출되고 DOM 에서 제거된다', () => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
const onClose = vi.fn();
|
|
153
|
+
render({
|
|
154
|
+
type: 'full-width',
|
|
155
|
+
autoClose: AUTO_CLOSE_MS,
|
|
156
|
+
hideCloseButton: true,
|
|
157
|
+
onClose
|
|
158
|
+
});
|
|
159
|
+
act(() => {
|
|
160
|
+
vi.advanceTimersByTime(AUTO_CLOSE_MS);
|
|
161
|
+
});
|
|
162
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(container?.querySelector('.ncua-full-width-notification')).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
it('R-09: message — hideCloseButton + onHidePermanently 이면 footer 에 다시 보지 않기만 렌더한다', () => {
|
|
166
|
+
const element = render({
|
|
167
|
+
type: 'message',
|
|
168
|
+
onClose: vi.fn(),
|
|
169
|
+
hideCloseButton: true,
|
|
170
|
+
onHidePermanently: vi.fn()
|
|
171
|
+
});
|
|
172
|
+
expect(element?.querySelector(MESSAGE_FOOTER)).not.toBeNull();
|
|
173
|
+
expect(element?.querySelector(MESSAGE_HIDE_LINK)).not.toBeNull();
|
|
174
|
+
expect(element?.querySelector(CLOSE_BUTTON.message)).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
it('R-10: message — hideCloseButton 이어도 autoClose 만료 시 onClose 가 1회 호출되고 DOM 에서 제거된다', () => {
|
|
177
|
+
vi.useFakeTimers();
|
|
178
|
+
const onClose = vi.fn();
|
|
179
|
+
render({
|
|
180
|
+
type: 'message',
|
|
181
|
+
autoClose: AUTO_CLOSE_MS,
|
|
182
|
+
hideCloseButton: true,
|
|
183
|
+
onClose
|
|
184
|
+
});
|
|
185
|
+
act(() => {
|
|
186
|
+
vi.advanceTimersByTime(AUTO_CLOSE_MS);
|
|
187
|
+
});
|
|
188
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
189
|
+
expect(container?.querySelector('.ncua-message-notification')).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('Notification — hideCloseButton 기본 autoClose', () => {
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
vi.useRealTimers();
|
|
195
|
+
});
|
|
196
|
+
it('R-11: floating — hideCloseButton 만 주고 autoClose 를 생략하면 3초 뒤 onClose 가 호출되고 제거된다', () => {
|
|
197
|
+
vi.useFakeTimers();
|
|
198
|
+
const onClose = vi.fn();
|
|
199
|
+
render({
|
|
200
|
+
type: 'floating',
|
|
201
|
+
hideCloseButton: true,
|
|
202
|
+
onClose
|
|
203
|
+
});
|
|
204
|
+
act(() => {
|
|
205
|
+
vi.advanceTimersByTime(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS - 1);
|
|
206
|
+
});
|
|
207
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
208
|
+
act(() => {
|
|
209
|
+
vi.advanceTimersByTime(1);
|
|
210
|
+
});
|
|
211
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(container?.firstElementChild).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
it.each(['full-width', 'message'])('R-14: type=%s — hideCloseButton 만 주고 autoClose 를 생략해도 자동으로 닫히지 않는다 (부모 언마운트 용도)', type => {
|
|
215
|
+
vi.useFakeTimers();
|
|
216
|
+
const onClose = vi.fn();
|
|
217
|
+
render({
|
|
218
|
+
type,
|
|
219
|
+
hideCloseButton: true,
|
|
220
|
+
onClose
|
|
221
|
+
});
|
|
222
|
+
act(() => {
|
|
223
|
+
vi.advanceTimersByTime(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS * 2);
|
|
224
|
+
});
|
|
225
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
226
|
+
expect(container?.firstElementChild).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
it('R-12: floating — hideCloseButton + autoClose={0} 을 명시하면 자동으로 닫히지 않는다 (부모 언마운트 용도)', () => {
|
|
229
|
+
vi.useFakeTimers();
|
|
230
|
+
const onClose = vi.fn();
|
|
231
|
+
render({
|
|
232
|
+
type: 'floating',
|
|
233
|
+
hideCloseButton: true,
|
|
234
|
+
autoClose: 0,
|
|
235
|
+
onClose
|
|
236
|
+
});
|
|
237
|
+
act(() => {
|
|
238
|
+
vi.advanceTimersByTime(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS * 2);
|
|
239
|
+
});
|
|
240
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
241
|
+
expect(container?.querySelector('.ncua-floating-notification')).not.toBeNull();
|
|
242
|
+
});
|
|
243
|
+
it('R-13: floating — hideCloseButton 이 아니면 autoClose 생략 시 기본 자동 닫힘이 없다 (회귀)', () => {
|
|
244
|
+
vi.useFakeTimers();
|
|
245
|
+
const onClose = vi.fn();
|
|
246
|
+
render({
|
|
247
|
+
type: 'floating',
|
|
248
|
+
onClose
|
|
249
|
+
});
|
|
250
|
+
act(() => {
|
|
251
|
+
vi.advanceTimersByTime(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS * 2);
|
|
252
|
+
});
|
|
253
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
254
|
+
expect(container?.querySelector('.ncua-floating-notification')).not.toBeNull();
|
|
255
|
+
});
|
|
80
256
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { resolveAutoClose } from '../../../utils/notification';
|
|
3
|
+
/**
|
|
4
|
+
* autoClose 타이머. 만료 시 `onClose`를 호출하고 `true`(제거됨)를 반환한다.
|
|
5
|
+
* floating · full-width · message 가 공유한다.
|
|
6
|
+
*
|
|
7
|
+
* `onClose`는 매 렌더 새 함수일 수 있어 ref 로 latest 만 보관하고 deps 에서 뺀다 —
|
|
8
|
+
* 인라인 핸들러를 넘겨도 부모 리렌더마다 타이머가 리셋되지 않는다.
|
|
9
|
+
*/
|
|
10
|
+
export const useAutoClose = _ref => {
|
|
11
|
+
let {
|
|
12
|
+
autoClose,
|
|
13
|
+
hideCloseButton,
|
|
14
|
+
onClose
|
|
15
|
+
} = _ref;
|
|
16
|
+
const [shouldRemove, setShouldRemove] = useState(false);
|
|
17
|
+
const onCloseRef = useRef(onClose);
|
|
18
|
+
onCloseRef.current = onClose;
|
|
19
|
+
const autoCloseMs = resolveAutoClose({
|
|
20
|
+
autoClose,
|
|
21
|
+
hideCloseButton
|
|
22
|
+
});
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (autoCloseMs <= 0) return;
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
onCloseRef.current?.();
|
|
27
|
+
setShouldRemove(true);
|
|
28
|
+
}, autoCloseMs);
|
|
29
|
+
return () => clearTimeout(timer);
|
|
30
|
+
}, [autoCloseMs]);
|
|
31
|
+
return shouldRemove;
|
|
32
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS } from '../../../../constant/notification';
|
|
3
|
+
import { resolveAutoClose } from '../resolveAutoClose';
|
|
4
|
+
const EXPLICIT_MS = 5000;
|
|
5
|
+
const FALLBACK_MS = 4000;
|
|
6
|
+
const NO_AUTO_CLOSE = 0;
|
|
7
|
+
describe('resolveAutoClose', () => {
|
|
8
|
+
it('autoClose 를 명시하면 hideCloseButton·fallback 과 무관하게 그대로 쓴다 (0 포함)', () => {
|
|
9
|
+
expect(resolveAutoClose({
|
|
10
|
+
autoClose: EXPLICIT_MS,
|
|
11
|
+
hideCloseButton: true,
|
|
12
|
+
fallback: FALLBACK_MS
|
|
13
|
+
})).toBe(EXPLICIT_MS);
|
|
14
|
+
expect(resolveAutoClose({
|
|
15
|
+
autoClose: NO_AUTO_CLOSE,
|
|
16
|
+
hideCloseButton: true,
|
|
17
|
+
fallback: FALLBACK_MS
|
|
18
|
+
})).toBe(NO_AUTO_CLOSE);
|
|
19
|
+
});
|
|
20
|
+
it('autoClose 미지정이면 fallback 이 양수일 때 fallback 을 쓴다', () => {
|
|
21
|
+
expect(resolveAutoClose({
|
|
22
|
+
fallback: FALLBACK_MS
|
|
23
|
+
})).toBe(FALLBACK_MS);
|
|
24
|
+
expect(resolveAutoClose({
|
|
25
|
+
hideCloseButton: true,
|
|
26
|
+
fallback: FALLBACK_MS
|
|
27
|
+
})).toBe(FALLBACK_MS);
|
|
28
|
+
});
|
|
29
|
+
it('autoClose·fallback 모두 없으면 hideCloseButton 일 때만 기본 3초를 준다', () => {
|
|
30
|
+
expect(resolveAutoClose({
|
|
31
|
+
hideCloseButton: true
|
|
32
|
+
})).toBe(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS);
|
|
33
|
+
expect(resolveAutoClose({
|
|
34
|
+
hideCloseButton: true,
|
|
35
|
+
fallback: NO_AUTO_CLOSE
|
|
36
|
+
})).toBe(HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS);
|
|
37
|
+
expect(resolveAutoClose({
|
|
38
|
+
hideCloseButton: false
|
|
39
|
+
})).toBe(NO_AUTO_CLOSE);
|
|
40
|
+
expect(resolveAutoClose({})).toBe(NO_AUTO_CLOSE);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveAutoClose } from './resolveAutoClose';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS } from '../../../constant/notification';
|
|
2
|
+
/**
|
|
3
|
+
* 실제 적용할 autoClose(ms)를 정한다. React·Vanilla 두 구현이 공유한다.
|
|
4
|
+
* 우선순위: 명시 `autoClose` > `fallback`(양수) > `hideCloseButton`이면 3초 > 0.
|
|
5
|
+
*/
|
|
6
|
+
export const resolveAutoClose = _ref => {
|
|
7
|
+
let {
|
|
8
|
+
autoClose,
|
|
9
|
+
hideCloseButton = false,
|
|
10
|
+
fallback = 0
|
|
11
|
+
} = _ref;
|
|
12
|
+
if (autoClose !== undefined) return autoClose;
|
|
13
|
+
if (fallback > 0) return fallback;
|
|
14
|
+
return hideCloseButton ? HIDE_CLOSE_BUTTON_AUTO_CLOSE_MS : 0;
|
|
15
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MEDIA_QUERY } from '../../../src/constant/breakpoint';
|
|
2
|
+
import { resolveAutoClose } from '../../../src/utils/notification';
|
|
2
3
|
import { FeaturedIcon } from '../featuredIcon';
|
|
3
4
|
import { ButtonCloseX } from '../shared/ButtonCloseX';
|
|
4
5
|
import { sanitizeHtml } from '../utils/sanitize';
|
|
@@ -10,9 +11,10 @@ export class FloatingNotification {
|
|
|
10
11
|
color: 'neutral',
|
|
11
12
|
className: '',
|
|
12
13
|
actions: [],
|
|
13
|
-
|
|
14
|
+
hideCloseButton: false,
|
|
14
15
|
supportingText: undefined,
|
|
15
16
|
...options,
|
|
17
|
+
autoClose: resolveAutoClose({ autoClose: options.autoClose, hideCloseButton: options.hideCloseButton }),
|
|
16
18
|
};
|
|
17
19
|
this.element = this.createElement();
|
|
18
20
|
this.bindEvents();
|
|
@@ -20,7 +22,7 @@ export class FloatingNotification {
|
|
|
20
22
|
this.setupMobileListener();
|
|
21
23
|
}
|
|
22
24
|
createElement() {
|
|
23
|
-
const { title, supportingText, color, className, actions, onClose } = this.options;
|
|
25
|
+
const { title, supportingText, color, className, actions, onClose, hideCloseButton } = this.options;
|
|
24
26
|
// 플로팅 알림에서 info 색상 사용 시 neutral로 대체
|
|
25
27
|
let actualColor = color;
|
|
26
28
|
if (color === 'info') {
|
|
@@ -48,6 +50,7 @@ export class FloatingNotification {
|
|
|
48
50
|
supportingText,
|
|
49
51
|
actions,
|
|
50
52
|
onClose,
|
|
53
|
+
hideCloseButton,
|
|
51
54
|
isMobile: mobile,
|
|
52
55
|
}));
|
|
53
56
|
// FeaturedIcon을 container에 추가
|
|
@@ -60,7 +63,7 @@ export class FloatingNotification {
|
|
|
60
63
|
return wrapper;
|
|
61
64
|
}
|
|
62
65
|
buildTemplate(params) {
|
|
63
|
-
const { title, supportingText, actions, onClose, isMobile } = params;
|
|
66
|
+
const { title, supportingText, actions, onClose, hideCloseButton, isMobile } = params;
|
|
64
67
|
return `
|
|
65
68
|
<div class="${CLASS_NAMES.FLOATING.CONTENT}">
|
|
66
69
|
<div class="${CLASS_NAMES.FLOATING.CONTAINER}">
|
|
@@ -73,11 +76,12 @@ export class FloatingNotification {
|
|
|
73
76
|
</div>
|
|
74
77
|
</div>
|
|
75
78
|
</div>
|
|
76
|
-
${this.renderCloseButton(onClose, isMobile)}
|
|
79
|
+
${this.renderCloseButton({ onClose, isMobile, hideCloseButton })}
|
|
77
80
|
`;
|
|
78
81
|
}
|
|
79
|
-
renderCloseButton(
|
|
80
|
-
|
|
82
|
+
renderCloseButton(params) {
|
|
83
|
+
const { onClose, isMobile = false, hideCloseButton = false } = params;
|
|
84
|
+
if (!onClose || hideCloseButton)
|
|
81
85
|
return '';
|
|
82
86
|
const size = getSizes.closeButton(isMobile);
|
|
83
87
|
return ButtonCloseX(size, 'light', CLASS_NAMES.FLOATING.CLOSE_BUTTON, '알림 닫기', onClose);
|
|
@@ -9,6 +9,7 @@ export class FullWidthNotification {
|
|
|
9
9
|
className: '',
|
|
10
10
|
actions: [],
|
|
11
11
|
autoClose: 0,
|
|
12
|
+
hideCloseButton: false,
|
|
12
13
|
supportingText: undefined,
|
|
13
14
|
...options,
|
|
14
15
|
};
|
|
@@ -17,7 +18,7 @@ export class FullWidthNotification {
|
|
|
17
18
|
this.setupAutoClose();
|
|
18
19
|
}
|
|
19
20
|
createElement() {
|
|
20
|
-
const { title, supportingText, color, className, actions, onClose, supportTextLink, supportTextLinkTarget, onHidePermanently, hidePermanentlyLabel, } = this.options;
|
|
21
|
+
const { title, supportingText, color, className, actions, onClose, hideCloseButton, supportTextLink, supportTextLinkTarget, onHidePermanently, hidePermanentlyLabel, } = this.options;
|
|
21
22
|
const wrapper = createWrapperElement(CLASS_NAMES.FULL_WIDTH.BASE, color, className);
|
|
22
23
|
const iconHtml = FULL_WIDTH_ICON_MAP[color](FULL_WIDTH_SIZES.ICON);
|
|
23
24
|
wrapper.innerHTML = sanitizeHtml(this.buildTemplate({
|
|
@@ -26,6 +27,7 @@ export class FullWidthNotification {
|
|
|
26
27
|
supportingText,
|
|
27
28
|
actions,
|
|
28
29
|
onClose,
|
|
30
|
+
hideCloseButton,
|
|
29
31
|
supportTextLink,
|
|
30
32
|
onHidePermanently,
|
|
31
33
|
hidePermanentlyLabel,
|
|
@@ -34,7 +36,7 @@ export class FullWidthNotification {
|
|
|
34
36
|
return wrapper;
|
|
35
37
|
}
|
|
36
38
|
buildTemplate(params) {
|
|
37
|
-
const { iconHtml, title, supportingText, actions, onClose, supportTextLink, onHidePermanently, hidePermanentlyLabel, } = params;
|
|
39
|
+
const { iconHtml, title, supportingText, actions, onClose, hideCloseButton, supportTextLink, onHidePermanently, hidePermanentlyLabel, } = params;
|
|
38
40
|
return `
|
|
39
41
|
<div class="${CLASS_NAMES.FULL_WIDTH.CONTAINER}">
|
|
40
42
|
<div class="${CLASS_NAMES.FULL_WIDTH.CONTENT}">
|
|
@@ -48,7 +50,7 @@ export class FullWidthNotification {
|
|
|
48
50
|
<div class="${CLASS_NAMES.FULL_WIDTH.ACTIONS_CONTAINER}">
|
|
49
51
|
${actions && actions.length > 0 ? renderActions(actions, CLASS_NAMES.FULL_WIDTH.ACTIONS) : ''}
|
|
50
52
|
${this.renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
|
|
51
|
-
${this.renderCloseButton(onClose)}
|
|
53
|
+
${this.renderCloseButton({ onClose, hideCloseButton })}
|
|
52
54
|
</div>
|
|
53
55
|
</div>
|
|
54
56
|
</div>
|
|
@@ -70,8 +72,9 @@ export class FullWidthNotification {
|
|
|
70
72
|
</button>
|
|
71
73
|
`;
|
|
72
74
|
}
|
|
73
|
-
renderCloseButton(
|
|
74
|
-
|
|
75
|
+
renderCloseButton(params) {
|
|
76
|
+
const { onClose, hideCloseButton = false } = params;
|
|
77
|
+
if (!onClose || hideCloseButton)
|
|
75
78
|
return '';
|
|
76
79
|
return `
|
|
77
80
|
<button type="button" class="${CLASS_NAMES.FULL_WIDTH.CLOSE_BUTTON}" aria-label="알림 닫기">
|
|
@@ -10,6 +10,7 @@ export class MessageNotification {
|
|
|
10
10
|
className: '',
|
|
11
11
|
actions: [],
|
|
12
12
|
autoClose: 0,
|
|
13
|
+
hideCloseButton: false,
|
|
13
14
|
supportingText: undefined,
|
|
14
15
|
...options,
|
|
15
16
|
};
|
|
@@ -18,7 +19,7 @@ export class MessageNotification {
|
|
|
18
19
|
this.setupAutoClose();
|
|
19
20
|
}
|
|
20
21
|
createElement() {
|
|
21
|
-
const { title, supportingText, color, className, actions, onClose, onHidePermanently, hidePermanentlyLabel, variant, supportTextLink, supportTextLinkTarget, } = this.options;
|
|
22
|
+
const { title, supportingText, color, className, actions, onClose, hideCloseButton, onHidePermanently, hidePermanentlyLabel, variant, supportTextLink, supportTextLinkTarget, } = this.options;
|
|
22
23
|
const isBlock = variant === 'block';
|
|
23
24
|
// default 는 neutral, error, warning, success 4가지 색상만 지원 — info 는 block 에서만 유지한다.
|
|
24
25
|
const actualColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
@@ -32,6 +33,7 @@ export class MessageNotification {
|
|
|
32
33
|
color: actualColor,
|
|
33
34
|
actions,
|
|
34
35
|
onClose,
|
|
36
|
+
hideCloseButton,
|
|
35
37
|
onHidePermanently,
|
|
36
38
|
hidePermanentlyLabel,
|
|
37
39
|
}));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAutoClose } from '../../../src/utils/notification';
|
|
1
2
|
import { FloatingNotification } from './FloatingNotification';
|
|
2
3
|
import { FullWidthNotification } from './FullWidthNotification';
|
|
3
4
|
import { MessageNotification } from './MessageNotification';
|
|
@@ -72,8 +73,13 @@ export class NcuaNotification {
|
|
|
72
73
|
supportingText,
|
|
73
74
|
color,
|
|
74
75
|
type: 'floating',
|
|
75
|
-
autoClose: autoCloseMap[color],
|
|
76
76
|
...options,
|
|
77
|
+
// 명시 autoClose > 색상별 기본값 > (floating 이고 hideCloseButton 이면 3초). neutral 은 기본 0 이라 세 번째 분기가 필요.
|
|
78
|
+
autoClose: resolveAutoClose({
|
|
79
|
+
autoClose: options?.autoClose,
|
|
80
|
+
hideCloseButton: (options?.type ?? 'floating') === 'floating' && options?.hideCloseButton,
|
|
81
|
+
fallback: autoCloseMap[color],
|
|
82
|
+
}),
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
// 클래스를 생성하지 않고도 기본 floating 알림 생성을 하기 위한 함수들
|
|
@@ -40,6 +40,13 @@ export interface BaseNotificationOptions {
|
|
|
40
40
|
supportingText?: string;
|
|
41
41
|
color?: NotificationColor;
|
|
42
42
|
onClose?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* 닫기(X) 버튼 숨김 여부. `true`면 `onClose`가 있어도 X 버튼을 렌더하지 않는다.
|
|
45
|
+
* autoClose 만료 시 `onClose` 호출은 그대로 유지된다. 기본값 `false`.
|
|
46
|
+
* floating 은 `autoClose` 미지정 시 3초 뒤 자동으로 닫힌다(직접 `remove()` 로 닫으려면 `autoClose: 0` 명시).
|
|
47
|
+
* full-width · message 는 `autoClose` 를 명시했을 때만 자동으로 닫힌다.
|
|
48
|
+
*/
|
|
49
|
+
hideCloseButton?: boolean;
|
|
43
50
|
className?: string;
|
|
44
51
|
actions?: NotificationAction[];
|
|
45
52
|
autoClose?: number;
|