@ncds/ui-admin 1.8.25 → 1.8.26-alpha.0
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/forms-and-input/combo-box/ComboBox.js +16 -126
- package/dist/cjs/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- 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/forms-and-input/combo-box/ComboBox.js +17 -127
- package/dist/esm/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- 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/forms-and-input/combo-box/ComboBox.d.ts +1 -1
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +13 -119
- package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- 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.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.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/forms-and-input/combo-box/ComboBox.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
- package/dist/cjs/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -455
- package/dist/esm/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -452
- package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -293
- /package/dist/temp/src/components/{forms-and-input/combo-box/__tests__/ComboBox.test.d.ts → overlays/notification/__tests__/supportTextLink.test.d.ts} +0 -0
- /package/dist/{types/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.d.ts → temp/src/utils/url/__tests__/isSafeUrl.test.d.ts} +0 -0
|
@@ -31,6 +31,7 @@ class FullWidthNotification {
|
|
|
31
31
|
actions,
|
|
32
32
|
onClose,
|
|
33
33
|
supportTextLink,
|
|
34
|
+
supportTextLinkTarget,
|
|
34
35
|
onHidePermanently,
|
|
35
36
|
hidePermanentlyLabel
|
|
36
37
|
} = this.options;
|
|
@@ -46,6 +47,7 @@ class FullWidthNotification {
|
|
|
46
47
|
onHidePermanently,
|
|
47
48
|
hidePermanentlyLabel
|
|
48
49
|
}));
|
|
50
|
+
(0, _utils.applySupportTextLinkTarget)(wrapper, _const.CLASS_NAMES.FULL_WIDTH.TEXT_CONTAINER, _const.CLASS_NAMES.FULL_WIDTH.LINK, supportTextLinkTarget);
|
|
49
51
|
return wrapper;
|
|
50
52
|
}
|
|
51
53
|
buildTemplate(params) {
|
|
@@ -32,38 +32,28 @@ class MessageNotification {
|
|
|
32
32
|
actions,
|
|
33
33
|
onClose,
|
|
34
34
|
onHidePermanently,
|
|
35
|
-
hidePermanentlyLabel
|
|
35
|
+
hidePermanentlyLabel,
|
|
36
|
+
variant,
|
|
37
|
+
supportTextLink,
|
|
38
|
+
supportTextLinkTarget
|
|
36
39
|
} = this.options;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const iconFunction = _const.FLOATING_ICON_MAP[actualColor];
|
|
44
|
-
// FeaturedIcon 생성
|
|
45
|
-
let featuredIconElement = null;
|
|
46
|
-
if (iconFunction) {
|
|
47
|
-
const iconSvg = iconFunction(_const.MESSAGE_SIZES.ICON_PIXEL);
|
|
48
|
-
this.featuredIcon = _featuredIcon.FeaturedIcon.create({
|
|
49
|
-
svgString: iconSvg,
|
|
50
|
-
theme: 'light-circle',
|
|
51
|
-
color: actualColor,
|
|
52
|
-
size: _const.MESSAGE_SIZES.FEATURED_ICON,
|
|
53
|
-
// React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
|
|
54
|
-
// 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
|
|
55
|
-
className: _const.CLASS_NAMES.MESSAGE.ICON
|
|
56
|
-
});
|
|
57
|
-
featuredIconElement = this.featuredIcon.getElement();
|
|
58
|
-
}
|
|
40
|
+
const isBlock = variant === 'block';
|
|
41
|
+
// default 는 neutral, error, warning, success 4가지 색상만 지원 — info 는 block 에서만 유지한다.
|
|
42
|
+
const actualColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
43
|
+
const wrapperClassName = [isBlock ? _const.CLASS_NAMES.MESSAGE.BLOCK : '', className].filter(Boolean).join(' ');
|
|
44
|
+
const wrapper = (0, _utils.createWrapperElement)(_const.CLASS_NAMES.MESSAGE.BASE, actualColor, wrapperClassName);
|
|
45
|
+
const featuredIconElement = this.createFeaturedIcon(actualColor, isBlock);
|
|
59
46
|
wrapper.innerHTML = (0, _sanitize.sanitizeHtml)((0, _messageTemplate.buildMessageTemplate)({
|
|
60
47
|
title,
|
|
61
48
|
supportingText,
|
|
49
|
+
supportTextLink: isBlock ? supportTextLink : undefined,
|
|
50
|
+
color: actualColor,
|
|
62
51
|
actions,
|
|
63
52
|
onClose,
|
|
64
53
|
onHidePermanently,
|
|
65
54
|
hidePermanentlyLabel
|
|
66
55
|
}));
|
|
56
|
+
(0, _utils.applySupportTextLinkTarget)(wrapper, _const.CLASS_NAMES.MESSAGE.TEXT_CONTAINER, _const.CLASS_NAMES.MESSAGE.LINK, supportTextLinkTarget);
|
|
67
57
|
// FeaturedIcon을 content-wrapper에 추가
|
|
68
58
|
if (featuredIconElement) {
|
|
69
59
|
const contentWrapper = wrapper.querySelector(`.${_const.CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}`);
|
|
@@ -73,6 +63,23 @@ class MessageNotification {
|
|
|
73
63
|
}
|
|
74
64
|
return wrapper;
|
|
75
65
|
}
|
|
66
|
+
/** 색상별 FeaturedIcon. default 는 md(40/20), block 은 sm(32/16). */
|
|
67
|
+
createFeaturedIcon(color, isBlock) {
|
|
68
|
+
const iconFunction = _const.MESSAGE_ICON_MAP[color];
|
|
69
|
+
if (!iconFunction) return null;
|
|
70
|
+
const sizes = isBlock ? _const.MESSAGE_BLOCK_SIZES : _const.MESSAGE_SIZES;
|
|
71
|
+
this.featuredIcon = _featuredIcon.FeaturedIcon.create({
|
|
72
|
+
svgString: iconFunction(sizes.ICON_PIXEL),
|
|
73
|
+
theme: 'light-circle',
|
|
74
|
+
// FeaturedIcon 에는 info 색이 없다. block+info 의 violet 톤은 message SCSS 가 덮어쓴다.
|
|
75
|
+
color: color === 'info' ? 'neutral' : color,
|
|
76
|
+
size: sizes.FEATURED_ICON,
|
|
77
|
+
// React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
|
|
78
|
+
// 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
|
|
79
|
+
className: _const.CLASS_NAMES.MESSAGE.ICON
|
|
80
|
+
});
|
|
81
|
+
return this.featuredIcon.getElement();
|
|
82
|
+
}
|
|
76
83
|
bindEvents() {
|
|
77
84
|
(0, _utils.bindNotificationEvents)(this.element, this.options.actions, this.options.onClose, () => this.remove());
|
|
78
85
|
// 다시보지 않기 버튼 이벤트 바인딩
|
|
@@ -15,6 +15,7 @@ const CLASS_NAMES = exports.CLASS_NAMES = {
|
|
|
15
15
|
TEXT_CONTAINER: 'ncua-full-width-notification__text-container',
|
|
16
16
|
TITLE: 'ncua-full-width-notification__title',
|
|
17
17
|
SUPPORTING_TEXT: 'ncua-full-width-notification__supporting-text',
|
|
18
|
+
LINK: 'ncua-full-width-notification__link',
|
|
18
19
|
ACTIONS_CONTAINER: 'ncua-full-width-notification__actions-container',
|
|
19
20
|
ACTIONS: 'ncua-full-width-notification__actions',
|
|
20
21
|
CLOSE_BUTTON: 'ncua-full-width-notification__close-button'
|
|
@@ -32,6 +33,7 @@ const CLASS_NAMES = exports.CLASS_NAMES = {
|
|
|
32
33
|
},
|
|
33
34
|
MESSAGE: {
|
|
34
35
|
BASE: 'ncua-message-notification',
|
|
36
|
+
BLOCK: 'ncua-message-notification--block',
|
|
35
37
|
CONTAINER: 'ncua-message-notification__container',
|
|
36
38
|
CONTENT: 'ncua-message-notification__content',
|
|
37
39
|
CONTENT_WRAPPER: 'ncua-message-notification__content-wrapper',
|
|
@@ -39,6 +41,7 @@ const CLASS_NAMES = exports.CLASS_NAMES = {
|
|
|
39
41
|
TEXT_CONTAINER: 'ncua-message-notification__text-container',
|
|
40
42
|
TITLE: 'ncua-message-notification__title',
|
|
41
43
|
SUPPORTING_TEXT: 'ncua-message-notification__supporting-text',
|
|
44
|
+
LINK: 'ncua-message-notification__link',
|
|
42
45
|
ACTIONS_CONTAINER: 'ncua-message-notification__actions-container',
|
|
43
46
|
ACTIONS: 'ncua-message-notification__actions',
|
|
44
47
|
// 다시 보지 않기 + 닫기 그룹 전용 래퍼. ACTIONS_CONTAINER를 재사용하면
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.SVG_ICONS = exports.ICON_MAP = exports.FULL_WIDTH_ICON_MAP = exports.FLOATING_ICON_MAP = void 0;
|
|
6
|
+
exports.SVG_ICONS = exports.MESSAGE_ICON_MAP = exports.ICON_MAP = exports.FULL_WIDTH_ICON_MAP = exports.FLOATING_ICON_MAP = void 0;
|
|
7
7
|
// SVG 아이콘들 (크기는 동적으로 설정)
|
|
8
8
|
const SVG_ICONS = exports.SVG_ICONS = {
|
|
9
9
|
'pin-02': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.377 15.616 2.72 21.273m8.974-14.631-1.56 1.56a2 2 0 0 1-.264.242 1 1 0 0 1-.207.111c-.082.032-.17.05-.347.085l-3.665.733c-.952.19-1.428.286-1.65.537a1 1 0 0 0-.243.8c.046.333.39.677 1.076 1.363l7.086 7.086c.686.687 1.03 1.03 1.362 1.076a1 1 0 0 0 .801-.242c.251-.223.346-.7.537-1.651l.733-3.665c.035-.176.053-.265.085-.347a1 1 0 0 1 .11-.207c.051-.072.115-.136.242-.263l1.561-1.561c.082-.082.122-.122.167-.158q.06-.047.126-.085c.05-.029.103-.051.208-.097l2.495-1.069c.727-.312 1.091-.467 1.256-.72a1 1 0 0 0 .144-.747c-.06-.295-.34-.575-.9-1.135l-5.142-5.143c-.56-.56-.84-.84-1.135-.9a1 1 0 0 0-.748.145c-.252.165-.407.529-.72 1.256l-1.068 2.495a2 2 0 0 1-.097.208 1 1 0 0 1-.085.126 2 2 0 0 1-.158.167"></path></svg>`,
|
|
@@ -28,4 +28,13 @@ const FULL_WIDTH_ICON_MAP = exports.FULL_WIDTH_ICON_MAP = {
|
|
|
28
28
|
success: SVG_ICONS['check-circle'],
|
|
29
29
|
info: SVG_ICONS['info-circle']
|
|
30
30
|
};
|
|
31
|
-
const ICON_MAP = exports.ICON_MAP = FULL_WIDTH_ICON_MAP;
|
|
31
|
+
const ICON_MAP = exports.ICON_MAP = FULL_WIDTH_ICON_MAP;
|
|
32
|
+
/**
|
|
33
|
+
* message 타입 아이콘. floating 과 같은 4색에 block 변형 전용 info 를 더한다.
|
|
34
|
+
* `SVG_ICONS['info-circle']`은 full-width 전용 class·color 가 박혀 있어 쓰지 않는다
|
|
35
|
+
* (`.ncua-full-width-notification__icon`은 모바일에서 숨겨진다).
|
|
36
|
+
*/
|
|
37
|
+
const MESSAGE_ICON_MAP = exports.MESSAGE_ICON_MAP = {
|
|
38
|
+
...FLOATING_ICON_MAP,
|
|
39
|
+
info: size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`
|
|
40
|
+
};
|
|
@@ -57,10 +57,16 @@ Object.defineProperty(exports, "ICON_PIXEL_SIZES", {
|
|
|
57
57
|
return _sizes.ICON_PIXEL_SIZES;
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
-
Object.defineProperty(exports, "
|
|
60
|
+
Object.defineProperty(exports, "MESSAGE_BLOCK_SIZES", {
|
|
61
61
|
enumerable: true,
|
|
62
62
|
get: function () {
|
|
63
|
-
return
|
|
63
|
+
return _sizes.MESSAGE_BLOCK_SIZES;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
Object.defineProperty(exports, "MESSAGE_ICON_MAP", {
|
|
67
|
+
enumerable: true,
|
|
68
|
+
get: function () {
|
|
69
|
+
return _icons.MESSAGE_ICON_MAP;
|
|
64
70
|
}
|
|
65
71
|
});
|
|
66
72
|
Object.defineProperty(exports, "MESSAGE_SIZES", {
|
|
@@ -83,5 +89,4 @@ Object.defineProperty(exports, "getSizes", {
|
|
|
83
89
|
});
|
|
84
90
|
var _classNames = require("./classNames");
|
|
85
91
|
var _icons = require("./icons");
|
|
86
|
-
var _sizes = require("./sizes");
|
|
87
|
-
var _types = require("./types");
|
|
92
|
+
var _sizes = require("./sizes");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.getSizes = exports.MESSAGE_SIZES = exports.ICON_PIXEL_SIZES = exports.FULL_WIDTH_SIZES = exports.FEATURED_ICON_SIZES = exports.CLOSE_BUTTON_SVG_SIZES = exports.CLOSE_BUTTON_SIZES = void 0;
|
|
6
|
+
exports.getSizes = exports.MESSAGE_SIZES = exports.MESSAGE_BLOCK_SIZES = exports.ICON_PIXEL_SIZES = exports.FULL_WIDTH_SIZES = exports.FEATURED_ICON_SIZES = exports.CLOSE_BUTTON_SVG_SIZES = exports.CLOSE_BUTTON_SIZES = void 0;
|
|
7
7
|
// 알림 컴포넌트 사이즈 관련 상수들
|
|
8
8
|
// FeaturedIcon 사이즈
|
|
9
9
|
const FEATURED_ICON_SIZES = exports.FEATURED_ICON_SIZES = {
|
|
@@ -39,6 +39,11 @@ const MESSAGE_SIZES = exports.MESSAGE_SIZES = {
|
|
|
39
39
|
ICON_PIXEL: '20',
|
|
40
40
|
CLOSE_BUTTON: '20'
|
|
41
41
|
};
|
|
42
|
+
// Message 알림 Block 변형 사이즈. 'sm' = 32px 원 / 16px 아이콘. 닫기 버튼은 default 와 같다.
|
|
43
|
+
const MESSAGE_BLOCK_SIZES = exports.MESSAGE_BLOCK_SIZES = {
|
|
44
|
+
FEATURED_ICON: 'sm',
|
|
45
|
+
ICON_PIXEL: '16'
|
|
46
|
+
};
|
|
42
47
|
// 사이즈 유틸리티 함수들
|
|
43
48
|
const getSizes = exports.getSizes = {
|
|
44
49
|
featuredIcon: isMobile => isMobile ? FEATURED_ICON_SIZES.MOBILE : FEATURED_ICON_SIZES.DESKTOP,
|
|
@@ -2,13 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.MESSAGE_CLOSE_ICON_COLORS = void 0;
|
|
7
|
-
// Message Notification 닫기 버튼 아이콘 색상 맵
|
|
8
|
-
const MESSAGE_CLOSE_ICON_COLORS = exports.MESSAGE_CLOSE_ICON_COLORS = {
|
|
9
|
-
neutral: '#6B7280',
|
|
10
|
-
error: '#EF4444',
|
|
11
|
-
warning: '#F97316',
|
|
12
|
-
success: '#16A34A',
|
|
13
|
-
info: '#6B7280' // info는 message 타입에서 지원하지 않지만 fallback용
|
|
14
|
-
};
|
|
5
|
+
});
|
|
@@ -9,7 +9,7 @@ var _const = require("./const");
|
|
|
9
9
|
var _utils = require("./utils");
|
|
10
10
|
/**
|
|
11
11
|
* message 타입 액션 버튼 사이즈. TO-BE 규격 28px이며 React `<Button size="xs">`와 같다.
|
|
12
|
-
*
|
|
12
|
+
* `renderActions` 기본값도 'xs'(#535)지만 message 규격을 명시적으로 고정해 둔다.
|
|
13
13
|
*/
|
|
14
14
|
const ACTION_BUTTON_SIZE = 'xs';
|
|
15
15
|
/**
|
|
@@ -21,6 +21,8 @@ function buildMessageTemplate(params) {
|
|
|
21
21
|
const {
|
|
22
22
|
title,
|
|
23
23
|
supportingText,
|
|
24
|
+
supportTextLink,
|
|
25
|
+
color,
|
|
24
26
|
actions,
|
|
25
27
|
onClose,
|
|
26
28
|
onHidePermanently,
|
|
@@ -32,14 +34,15 @@ function buildMessageTemplate(params) {
|
|
|
32
34
|
<div class="${_const.CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}">
|
|
33
35
|
<div class="${_const.CLASS_NAMES.MESSAGE.TEXT_CONTAINER}">
|
|
34
36
|
<span class="${_const.CLASS_NAMES.MESSAGE.TITLE}">${title}</span>
|
|
35
|
-
${(0, _utils.renderSupportingText)(supportingText, _const.CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
|
|
37
|
+
${(0, _utils.renderSupportingText)(supportingText, _const.CLASS_NAMES.MESSAGE.SUPPORTING_TEXT, supportTextLink, _const.CLASS_NAMES.MESSAGE.LINK)}
|
|
36
38
|
</div>
|
|
37
39
|
</div>
|
|
38
40
|
${renderActionsContainer({
|
|
39
41
|
actions,
|
|
40
42
|
onClose,
|
|
41
43
|
onHidePermanently,
|
|
42
|
-
hidePermanentlyLabel
|
|
44
|
+
hidePermanentlyLabel,
|
|
45
|
+
color
|
|
43
46
|
})}
|
|
44
47
|
</div>
|
|
45
48
|
</div>
|
|
@@ -55,13 +58,15 @@ function renderActionsContainer(params) {
|
|
|
55
58
|
actions,
|
|
56
59
|
onClose,
|
|
57
60
|
onHidePermanently,
|
|
58
|
-
hidePermanentlyLabel
|
|
61
|
+
hidePermanentlyLabel,
|
|
62
|
+
color
|
|
59
63
|
} = params;
|
|
60
64
|
const actionsHtml = (0, _utils.renderActions)(actions ?? [], _const.CLASS_NAMES.MESSAGE.ACTIONS, ACTION_BUTTON_SIZE);
|
|
61
65
|
const footerHtml = renderFooterContainer({
|
|
62
66
|
onClose,
|
|
63
67
|
onHidePermanently,
|
|
64
|
-
hidePermanentlyLabel
|
|
68
|
+
hidePermanentlyLabel,
|
|
69
|
+
color
|
|
65
70
|
});
|
|
66
71
|
if (!actionsHtml && !footerHtml) return '';
|
|
67
72
|
return `<div class="${_const.CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">${actionsHtml}${footerHtml}</div>`;
|
|
@@ -71,13 +76,14 @@ function renderFooterContainer(params) {
|
|
|
71
76
|
const {
|
|
72
77
|
onClose,
|
|
73
78
|
onHidePermanently,
|
|
74
|
-
hidePermanentlyLabel
|
|
79
|
+
hidePermanentlyLabel,
|
|
80
|
+
color
|
|
75
81
|
} = params;
|
|
76
82
|
if (!onHidePermanently && !onClose) return '';
|
|
77
83
|
return `
|
|
78
84
|
<div class="${_const.CLASS_NAMES.MESSAGE.FOOTER_CONTAINER}">
|
|
79
85
|
${renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
|
|
80
|
-
${renderCloseButton(onClose)}
|
|
86
|
+
${renderCloseButton(onClose, color)}
|
|
81
87
|
</div>
|
|
82
88
|
`;
|
|
83
89
|
}
|
|
@@ -96,11 +102,17 @@ function renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
|
|
|
96
102
|
</button>
|
|
97
103
|
`;
|
|
98
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* 닫기(X) 버튼. 아이콘 색은 React `<XClose color>`와 같이 svg 의 `color` 속성으로 주고
|
|
107
|
+
* path 는 `stroke="currentColor"`로 상속받는다 — 두 구현이 같은 마크업을 낸다.
|
|
108
|
+
*/
|
|
99
109
|
function renderCloseButton(onClose) {
|
|
110
|
+
let color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'neutral';
|
|
100
111
|
if (!onClose) return '';
|
|
112
|
+
const iconColor = _notification.MESSAGE_CLOSE_ICON_COLOR[color];
|
|
101
113
|
return `
|
|
102
114
|
<button type="button" class="${_const.CLASS_NAMES.MESSAGE.CLOSE_BUTTON}" aria-label="알림 닫기">
|
|
103
|
-
${_const.SVG_ICONS['x-close'](_const.MESSAGE_SIZES.CLOSE_BUTTON).replace('
|
|
115
|
+
${_const.SVG_ICONS['x-close'](_const.MESSAGE_SIZES.CLOSE_BUTTON).replace('<svg ', `<svg color="${iconColor}" `)}
|
|
104
116
|
</button>
|
|
105
117
|
`;
|
|
106
118
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.applySupportTextLinkTarget = applySupportTextLinkTarget;
|
|
6
7
|
exports.bindNotificationEvents = bindNotificationEvents;
|
|
7
8
|
exports.buildClassName = buildClassName;
|
|
8
9
|
exports.createWrapperElement = createWrapperElement;
|
|
@@ -12,6 +13,7 @@ exports.renderSupportingText = renderSupportingText;
|
|
|
12
13
|
exports.resolveActionHierarchy = resolveActionHierarchy;
|
|
13
14
|
exports.setupAutoClose = setupAutoClose;
|
|
14
15
|
var _breakpoint = require("../../../src/constant/breakpoint");
|
|
16
|
+
var _url = require("../../../src/utils/url");
|
|
15
17
|
var _const = require("./const");
|
|
16
18
|
// 공통 유틸리티 함수들
|
|
17
19
|
function createWrapperElement(baseClass, color, className) {
|
|
@@ -27,18 +29,6 @@ function buildClassName(baseClass, color, className) {
|
|
|
27
29
|
}
|
|
28
30
|
return classes.join(' ');
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
31
|
-
* http/https 프로토콜만 허용한다.
|
|
32
|
-
* javascript:, data:, file: 등 위험 프로토콜로 인한 XSS / 외부 페이로드 로딩을 차단.
|
|
33
|
-
*/
|
|
34
|
-
function isSafeUrl(url) {
|
|
35
|
-
try {
|
|
36
|
-
const parsed = new URL(url, window.location.origin);
|
|
37
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
32
|
/**
|
|
43
33
|
* HTML 속성 값에 안전하게 삽입하기 위한 최소 이스케이프.
|
|
44
34
|
* 본문(body)이 아닌 속성 컨텍스트에서의 따옴표 탈출(`" onclick="..."`)을 방지.
|
|
@@ -47,17 +37,32 @@ function escapeHtmlAttr(str) {
|
|
|
47
37
|
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
48
38
|
}
|
|
49
39
|
function renderSupportingText(supportingText, className, supportTextLink) {
|
|
40
|
+
let linkClassName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _const.CLASS_NAMES.FULL_WIDTH.LINK;
|
|
50
41
|
if (!supportingText) return '';
|
|
51
42
|
// 본문(supportingText) 자체는 이 함수의 호출 결과가 wrapper.innerHTML에
|
|
52
43
|
// 들어가기 전에 DOMPurify로 sanitize되므로 별도 이스케이프하지 않는다.
|
|
53
44
|
// 이렇게 해야 <br>, <strong> 등 서식용 태그가 그대로 동작한다.
|
|
54
|
-
if (supportTextLink && isSafeUrl(supportTextLink)) {
|
|
45
|
+
if (supportTextLink && (0, _url.isSafeUrl)(supportTextLink)) {
|
|
55
46
|
const safeLink = escapeHtmlAttr(supportTextLink);
|
|
56
|
-
return `<a href="${safeLink}" class="
|
|
47
|
+
return `<a href="${safeLink}" class="${linkClassName}"><span class="${className}">${supportingText}</span></a>`;
|
|
57
48
|
}
|
|
58
49
|
// unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
|
|
59
50
|
return `<span class="${className}">${supportingText}</span>`;
|
|
60
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* `supportTextLinkTarget`이 `'_blank'`이면 `renderSupportingText`가 만든 링크를 새 탭으로 연다.
|
|
54
|
+
*
|
|
55
|
+
* target 은 sanitize **이후** 우리가 만든 링크(텍스트 컨테이너의 직계 `<a>`)에만 지정한다.
|
|
56
|
+
* DOMPurify 에 target 을 전역 허용하면 사용자 입력 HTML 의 `<a target>`까지 rel 없이 통과하기 때문이다.
|
|
57
|
+
* 런타임 입력이므로 `'_blank'`가 아닌 값은 전부 `'_self'`(속성 미부여)로 취급한다.
|
|
58
|
+
*/
|
|
59
|
+
function applySupportTextLinkTarget(root, textContainerClassName, linkClassName, target) {
|
|
60
|
+
if (target !== '_blank') return;
|
|
61
|
+
const link = root.querySelector(`.${textContainerClassName} > a.${linkClassName}`);
|
|
62
|
+
if (!link) return;
|
|
63
|
+
link.setAttribute('target', '_blank');
|
|
64
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
65
|
+
}
|
|
61
66
|
/** 액션 버튼 색상을 지정하지 않았을 때의 기본값. React `MessageNotificationParts`와 같다. */
|
|
62
67
|
const DEFAULT_ACTION_HIERARCHY = 'text';
|
|
63
68
|
/**
|
|
@@ -89,11 +94,10 @@ function buildActionKey(action) {
|
|
|
89
94
|
/**
|
|
90
95
|
* 액션 버튼 그룹 HTML을 만든다.
|
|
91
96
|
*
|
|
92
|
-
* `size` 기본값 '
|
|
93
|
-
* message 타입만 TO-BE 규격(28px)에 맞춰 'xs'를 넘긴다 — React `<Button size="xs">`와 동일.
|
|
97
|
+
* `size` 기본값 'xs'는 디자인 기준(#535 — 모든 type 액션 버튼 xs)이며 React `<Button size="xs">`와 같다.
|
|
94
98
|
*/
|
|
95
99
|
function renderActions(actions, wrapperClass) {
|
|
96
|
-
let size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '
|
|
100
|
+
let size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'xs';
|
|
97
101
|
// 액션이 없으면 빈 문자열 반환
|
|
98
102
|
if (!actions || actions.length === 0) {
|
|
99
103
|
return '';
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.HIDE_PERMANENTLY_LABEL = void 0;
|
|
6
|
+
exports.MESSAGE_CLOSE_ICON_COLOR = exports.HIDE_PERMANENTLY_LABEL = void 0;
|
|
7
|
+
var _color = require("./color");
|
|
7
8
|
/**
|
|
8
9
|
* 다시보지 않기 버튼의 타입별 기본 문구.
|
|
9
10
|
*
|
|
@@ -18,4 +19,17 @@ exports.HIDE_PERMANENTLY_LABEL = void 0;
|
|
|
18
19
|
const HIDE_PERMANENTLY_LABEL = exports.HIDE_PERMANENTLY_LABEL = {
|
|
19
20
|
message: '다시 보지 않기',
|
|
20
21
|
fullWidth: '다시보지 않기'
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* message 타입 닫기(X) 아이콘의 color 별 색.
|
|
25
|
+
*
|
|
26
|
+
* React(`MessageNotification.tsx`)와 Vanilla(`messageTemplate.ts`)가 함께 참조하는 단일 출처다.
|
|
27
|
+
* `info`는 `variant: 'block'`에서만 렌더된다(default 는 neutral 로 폴백).
|
|
28
|
+
*/
|
|
29
|
+
const MESSAGE_CLOSE_ICON_COLOR = exports.MESSAGE_CLOSE_ICON_COLOR = {
|
|
30
|
+
neutral: _color.COLOR.gray700,
|
|
31
|
+
error: _color.COLOR.red500,
|
|
32
|
+
warning: _color.COLOR.orange500,
|
|
33
|
+
success: _color.COLOR.green600,
|
|
34
|
+
info: _color.COLOR.violet600
|
|
21
35
|
};
|
|
@@ -30,112 +30,6 @@ const notifyFormChange = (register, multiple, newValue) => {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
-
/** '' · undefined · null 이면 빈 값. 숫자 0은 빈 값이 아니다 */
|
|
34
|
-
const isEmptyValue = value => value === '' || value == null;
|
|
35
|
-
/** react-hook-form hidden input 값 — Tag 와 같은 빈 값 규칙(0은 값)을 따른다 */
|
|
36
|
-
const toHiddenInputValue = (value, multiple) => {
|
|
37
|
-
if (multiple) return JSON.stringify(value || []);
|
|
38
|
-
return isEmptyValue(value) ? '' : String(value);
|
|
39
|
-
};
|
|
40
|
-
/** SelectDropdown 은 null 을 받지 않으므로 undefined 로 바꾼다 */
|
|
41
|
-
const toDropdownValue = value => value ?? undefined;
|
|
42
|
-
/** multiple 용 — 배열이 아니면(null · undefined 포함) 빈 배열 */
|
|
43
|
-
const toIdArray = value => Array.isArray(value) ? value : [];
|
|
44
|
-
/** 두 id 목록이 같은 id 들로 이루어졌는지 — 순서는 보지 않는다 (사용처가 onChange 값을 정렬해 돌려줘도 같은 값) */
|
|
45
|
-
const isSameIds = (currentIds, nextIds) => currentIds.length === nextIds.length && currentIds.every(id => nextIds.includes(id));
|
|
46
|
-
const isSameTags = (nextTags, prevTags) => nextTags.length === prevTags.length && nextTags.every((tag, index) => tag.id === prevTags[index].id && tag.label === prevTags[index].label);
|
|
47
|
-
const findOption = (optionItems, id) => optionItems.find(option => option.id === id);
|
|
48
|
-
/** 단일 모드 value 에 해당하는 옵션 — 빈 값·배열이면 없음 */
|
|
49
|
-
const findSingleOption = (value, optionItems) => {
|
|
50
|
-
if (isEmptyValue(value) || Array.isArray(value)) return undefined;
|
|
51
|
-
return findOption(optionItems, value);
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* 단일 모드 Tag — value 와 optionItems 에서 계산한다.
|
|
55
|
-
* 검색 필터로 목록에서 빠진 경우에만, 같은 id 로 기억해 둔 Tag 를 대신 쓴다.
|
|
56
|
-
*/
|
|
57
|
-
const resolveSingleTag = (value, optionItems, lastResolvedTag) => {
|
|
58
|
-
if (isEmptyValue(value) || Array.isArray(value)) return null;
|
|
59
|
-
const found = findOption(optionItems, value);
|
|
60
|
-
if (found) return {
|
|
61
|
-
id: found.id,
|
|
62
|
-
label: found.label
|
|
63
|
-
};
|
|
64
|
-
return lastResolvedTag?.id === value ? lastResolvedTag : null;
|
|
65
|
-
};
|
|
66
|
-
/** 단일 모드 Tag 목록 (0~1개). 목록에서 찾은 Tag 를 기억해 검색 필터 중에도 라벨을 유지한다 */
|
|
67
|
-
const useSingleSelectedTags = (value, optionItems, enabled) => {
|
|
68
|
-
const [lastResolvedTag, setLastResolvedTag] = (0, _react.useState)(null);
|
|
69
|
-
const found = enabled ? findSingleOption(value, optionItems) : undefined;
|
|
70
|
-
const foundId = found?.id;
|
|
71
|
-
const foundLabel = found?.label;
|
|
72
|
-
(0, _react.useEffect)(() => {
|
|
73
|
-
if (foundId === undefined || foundLabel === undefined) return;
|
|
74
|
-
setLastResolvedTag({
|
|
75
|
-
id: foundId,
|
|
76
|
-
label: foundLabel
|
|
77
|
-
});
|
|
78
|
-
}, [foundId, foundLabel]);
|
|
79
|
-
const selectedTag = enabled ? resolveSingleTag(value, optionItems, lastResolvedTag) : null;
|
|
80
|
-
return selectedTag ? [selectedTag] : [];
|
|
81
|
-
};
|
|
82
|
-
/** multiple 모드 Tag — 목록에서 못 찾은 id 는 이미 표시 중인 Tag 의 라벨을 쓴다 */
|
|
83
|
-
const resolveMultipleTags = (ids, optionItems, prevTags) => {
|
|
84
|
-
const nextTags = ids.map(id => {
|
|
85
|
-
const found = findOption(optionItems, id);
|
|
86
|
-
return found ? {
|
|
87
|
-
id: found.id,
|
|
88
|
-
label: found.label
|
|
89
|
-
} : prevTags.find(tag => tag.id === id);
|
|
90
|
-
}).filter(tag => tag !== undefined);
|
|
91
|
-
// 결과가 같으면 이전 배열을 그대로 돌려준다 — 새 배열로 바꾸면 effect 가 다시 렌더를 일으킨다
|
|
92
|
-
return isSameTags(nextTags, prevTags) ? prevTags : nextTags;
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* multiple 모드 Tag — 「선택 완료」·X 클릭·외부 value 변경 때만 확정 id 를 바꾼다.
|
|
96
|
-
* ComboBox 가 직접 onChange 로 내보낸 값은 markEmitted 로 기록해 두고, 그 값이 value 로 돌아오면 한 번만 무시한다(확정 아님).
|
|
97
|
-
* 바깥 값이 들어오면 남아 있던 기록도 지운다 — 오래된 기록 때문에 이후 같은 바깥 값이 무시되지 않게 하기 위함.
|
|
98
|
-
* optionItems 만 바뀌면 확정 id 는 그대로 두고 라벨만 다시 찾는다 — 「선택 완료」 전 검색으로 Tag 가 확정되지 않게 하기 위함.
|
|
99
|
-
*/
|
|
100
|
-
const useMultipleSelectedTags = (value, optionItems, enabled) => {
|
|
101
|
-
const [selectedTags, setSelectedTags] = (0, _react.useState)([]);
|
|
102
|
-
const lastEmittedRef = (0, _react.useRef)(null);
|
|
103
|
-
const committedIdsRef = (0, _react.useRef)([]);
|
|
104
|
-
const prevValueIdsRef = (0, _react.useRef)(null);
|
|
105
|
-
(0, _react.useEffect)(() => {
|
|
106
|
-
if (!enabled) return;
|
|
107
|
-
const ids = toIdArray(value);
|
|
108
|
-
const isEcho = lastEmittedRef.current !== null && isSameIds(ids, lastEmittedRef.current);
|
|
109
|
-
const isValueChanged = prevValueIdsRef.current === null || !isSameIds(ids, prevValueIdsRef.current);
|
|
110
|
-
prevValueIdsRef.current = ids;
|
|
111
|
-
if (isEcho) {
|
|
112
|
-
lastEmittedRef.current = null;
|
|
113
|
-
} else if (isValueChanged) {
|
|
114
|
-
committedIdsRef.current = ids;
|
|
115
|
-
lastEmittedRef.current = null;
|
|
116
|
-
}
|
|
117
|
-
setSelectedTags(prev => resolveMultipleTags(committedIdsRef.current, optionItems, prev));
|
|
118
|
-
}, [enabled, value, optionItems]);
|
|
119
|
-
/** 「선택 완료」로 Tag 를 확정한다 */
|
|
120
|
-
const commitTags = ids => {
|
|
121
|
-
committedIdsRef.current = ids;
|
|
122
|
-
setSelectedTags(prev => resolveMultipleTags(ids, optionItems, prev));
|
|
123
|
-
};
|
|
124
|
-
/** X 클릭 — 확정 id 에서 그 id 만 뺀다. value 기준으로 계산하면 「선택 완료」 전에 클릭한 항목까지 확정된다 */
|
|
125
|
-
const removeCommittedTag = tagId => {
|
|
126
|
-
commitTags(committedIdsRef.current.filter(id => id !== tagId));
|
|
127
|
-
};
|
|
128
|
-
/** ComboBox 가 onChange 로 내보낸 값을 기록한다 — value 로 돌아왔을 때 확정으로 보지 않기 위함 */
|
|
129
|
-
const markEmitted = ids => {
|
|
130
|
-
lastEmittedRef.current = ids;
|
|
131
|
-
};
|
|
132
|
-
return {
|
|
133
|
-
selectedTags,
|
|
134
|
-
commitTags,
|
|
135
|
-
removeCommittedTag,
|
|
136
|
-
markEmitted
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
33
|
const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
140
34
|
let {
|
|
141
35
|
placeholder = '검색하세요',
|
|
@@ -175,24 +69,22 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
175
69
|
} = (0, _usePortalState.usePortalState)(usePortal);
|
|
176
70
|
const [inputValue, setInputValue] = (0, _react.useState)(searchValue);
|
|
177
71
|
const [showAllItems, setShowAllItems] = (0, _react.useState)(false);
|
|
178
|
-
const
|
|
179
|
-
const {
|
|
180
|
-
selectedTags,
|
|
181
|
-
commitTags,
|
|
182
|
-
removeCommittedTag,
|
|
183
|
-
markEmitted
|
|
184
|
-
} = useMultipleSelectedTags(value, optionItems, multiple);
|
|
72
|
+
const [selectedTags, setSelectedTags] = (0, _react.useState)([]);
|
|
185
73
|
const handleOptionSelect = option => {
|
|
186
74
|
if (disabled) return;
|
|
187
75
|
if (multiple) {
|
|
188
76
|
const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
|
|
189
77
|
if (newValue === null) return;
|
|
190
|
-
markEmitted(newValue);
|
|
191
78
|
onChange?.(newValue);
|
|
192
79
|
notifyFormChange(register, multiple, newValue);
|
|
193
80
|
return;
|
|
194
81
|
}
|
|
195
82
|
const newValue = option.id;
|
|
83
|
+
const tag = {
|
|
84
|
+
id: option.id,
|
|
85
|
+
label: option.label
|
|
86
|
+
};
|
|
87
|
+
setSelectedTags([tag]);
|
|
196
88
|
onChange?.(newValue);
|
|
197
89
|
notifyFormChange(register, multiple, newValue);
|
|
198
90
|
};
|
|
@@ -299,6 +191,7 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
299
191
|
const {
|
|
300
192
|
buttonText: selectAllButtonText,
|
|
301
193
|
toggleSelectAll,
|
|
194
|
+
getSelectedTagsData,
|
|
302
195
|
removeTag,
|
|
303
196
|
isMaxSelectionActive,
|
|
304
197
|
tryToggle
|
|
@@ -308,7 +201,6 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
308
201
|
const handleSelectAll = () => {
|
|
309
202
|
if (!multiple || !onChange) return;
|
|
310
203
|
const newSelectedValues = toggleSelectAll();
|
|
311
|
-
markEmitted(newSelectedValues);
|
|
312
204
|
onChange(newSelectedValues);
|
|
313
205
|
};
|
|
314
206
|
const handleEdit = () => {
|
|
@@ -316,32 +208,30 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
316
208
|
};
|
|
317
209
|
const handleComplete = completedValue => {
|
|
318
210
|
if (!multiple) return;
|
|
319
|
-
|
|
211
|
+
const tags = getSelectedTagsData();
|
|
212
|
+
setSelectedTags(tags);
|
|
320
213
|
setInputValue('');
|
|
321
214
|
closeDropdown();
|
|
322
215
|
onComplete?.(completedValue);
|
|
323
216
|
};
|
|
324
217
|
const handleRemoveTag = tagId => {
|
|
325
|
-
// X 버튼은 disabled 에서 숨기지만, 다른 호출 경로가 생겨도 값이 지워지지 않게 함수에서도 막는다
|
|
326
|
-
if (disabled) return;
|
|
327
218
|
if (!onChange) return;
|
|
328
219
|
if (multiple) {
|
|
329
220
|
const newSelectedValues = removeTag(tagId);
|
|
330
|
-
markEmitted(newSelectedValues);
|
|
331
221
|
onChange(newSelectedValues);
|
|
332
|
-
|
|
222
|
+
setSelectedTags(prev => prev.filter(tag => tag.id !== tagId));
|
|
333
223
|
return;
|
|
334
224
|
}
|
|
335
225
|
onChange('');
|
|
226
|
+
setSelectedTags([]);
|
|
336
227
|
};
|
|
337
228
|
const comboboxId = `combobox-options-${id || 'default'}`;
|
|
338
229
|
const hasVisibleOptions = showAllItems || inputValue;
|
|
339
230
|
const displayedOptions = hasVisibleOptions ? optionItems : [];
|
|
340
231
|
const showNoResults = !hasVisibleOptions || optionItems.length === 0;
|
|
341
232
|
const showFooter = multiple || showFooterButtons;
|
|
342
|
-
const hiddenInputValue =
|
|
343
|
-
const
|
|
344
|
-
const hasTags = tags.length > 0;
|
|
233
|
+
const hiddenInputValue = multiple ? JSON.stringify(value || []) : String(value || '');
|
|
234
|
+
const hasTags = selectedTags.length > 0;
|
|
345
235
|
const {
|
|
346
236
|
className: portalPanelClassName,
|
|
347
237
|
style: portalPanelStyle
|
|
@@ -352,7 +242,7 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
352
242
|
direction: dropdownDirection,
|
|
353
243
|
size: size,
|
|
354
244
|
options: displayedOptions,
|
|
355
|
-
value:
|
|
245
|
+
value: value,
|
|
356
246
|
focusedIndex: focusedIndex,
|
|
357
247
|
maxHeight: maxHeight,
|
|
358
248
|
listboxId: comboboxId,
|
|
@@ -431,10 +321,10 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
|
|
|
431
321
|
})]
|
|
432
322
|
}), hasTags && (0, _jsxRuntime.jsx)("div", {
|
|
433
323
|
className: "ncua-combobox__tags",
|
|
434
|
-
children:
|
|
324
|
+
children: selectedTags.map(tag => (0, _jsxRuntime.jsx)(_tag.Tag, {
|
|
435
325
|
text: tag.label,
|
|
436
326
|
size: "sm",
|
|
437
|
-
close:
|
|
327
|
+
close: true,
|
|
438
328
|
onButtonClick: () => handleRemoveTag(tag.id)
|
|
439
329
|
}, tag.id))
|
|
440
330
|
})]
|
|
@@ -476,7 +476,7 @@ const clickButtonByLabel = (scope, label) => {
|
|
|
476
476
|
(0, _vitest.expect)(onSearch).toHaveBeenCalledWith('사');
|
|
477
477
|
(0, _vitest.expect)(view.content.querySelectorAll('.ncua-select-dropdown__option')).toHaveLength(createOptions().length);
|
|
478
478
|
});
|
|
479
|
-
(0, _vitest.it)('옵션을 선택하면 onChange 가
|
|
479
|
+
(0, _vitest.it)('옵션을 선택하면 onChange 가 호출되고 선택 태그가 표시된다', () => {
|
|
480
480
|
// Given: 옵션이 노출된 기본 모드 ComboBox 에서
|
|
481
481
|
const onChange = _vitest.vi.fn();
|
|
482
482
|
const view = mountComboBox({
|
|
@@ -489,9 +489,9 @@ const clickButtonByLabel = (scope, label) => {
|
|
|
489
489
|
(0, _testUtils.act)(() => {
|
|
490
490
|
requireElement(view.content, '.ncua-select-dropdown__option').click();
|
|
491
491
|
});
|
|
492
|
-
// Then: onChange 가 선택 값으로
|
|
493
|
-
// 선택 태그는 value 를 따르므로(#530) onChange 값을 value 로 돌려주는 경우의 태그 표시는 ComboBox.test.ts S-10 에서 검증한다
|
|
492
|
+
// Then: onChange 가 선택 값으로 호출되고 태그가 표시된다
|
|
494
493
|
(0, _vitest.expect)(onChange).toHaveBeenCalledWith('apple');
|
|
494
|
+
(0, _vitest.expect)(view.container.querySelectorAll('.ncua-combobox__tags .ncua-tag')).toHaveLength(1);
|
|
495
495
|
});
|
|
496
496
|
(0, _vitest.it)('Escape 를 누르면 인라인 패널이 닫힌다', () => {
|
|
497
497
|
// Given: 열려 있는 기본 모드 ComboBox 에서
|