@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronSelectorVertical, SearchLg } from '@ncds/ui-admin-icon';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import { forwardRef,
|
|
4
|
+
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
|
5
5
|
import { useDropdown } from '../../../hooks/dropdown';
|
|
6
6
|
import { useFloatingPosition } from '../../../hooks/useFloatingPosition';
|
|
7
7
|
import { usePortalState } from '../../../hooks/usePortalState';
|
|
@@ -23,112 +23,6 @@ const notifyFormChange = (register, multiple, newValue) => {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
-
/** '' · undefined · null 이면 빈 값. 숫자 0은 빈 값이 아니다 */
|
|
27
|
-
const isEmptyValue = value => value === '' || value == null;
|
|
28
|
-
/** react-hook-form hidden input 값 — Tag 와 같은 빈 값 규칙(0은 값)을 따른다 */
|
|
29
|
-
const toHiddenInputValue = (value, multiple) => {
|
|
30
|
-
if (multiple) return JSON.stringify(value || []);
|
|
31
|
-
return isEmptyValue(value) ? '' : String(value);
|
|
32
|
-
};
|
|
33
|
-
/** SelectDropdown 은 null 을 받지 않으므로 undefined 로 바꾼다 */
|
|
34
|
-
const toDropdownValue = value => value ?? undefined;
|
|
35
|
-
/** multiple 용 — 배열이 아니면(null · undefined 포함) 빈 배열 */
|
|
36
|
-
const toIdArray = value => Array.isArray(value) ? value : [];
|
|
37
|
-
/** 두 id 목록이 같은 id 들로 이루어졌는지 — 순서는 보지 않는다 (사용처가 onChange 값을 정렬해 돌려줘도 같은 값) */
|
|
38
|
-
const isSameIds = (currentIds, nextIds) => currentIds.length === nextIds.length && currentIds.every(id => nextIds.includes(id));
|
|
39
|
-
const isSameTags = (nextTags, prevTags) => nextTags.length === prevTags.length && nextTags.every((tag, index) => tag.id === prevTags[index].id && tag.label === prevTags[index].label);
|
|
40
|
-
const findOption = (optionItems, id) => optionItems.find(option => option.id === id);
|
|
41
|
-
/** 단일 모드 value 에 해당하는 옵션 — 빈 값·배열이면 없음 */
|
|
42
|
-
const findSingleOption = (value, optionItems) => {
|
|
43
|
-
if (isEmptyValue(value) || Array.isArray(value)) return undefined;
|
|
44
|
-
return findOption(optionItems, value);
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* 단일 모드 Tag — value 와 optionItems 에서 계산한다.
|
|
48
|
-
* 검색 필터로 목록에서 빠진 경우에만, 같은 id 로 기억해 둔 Tag 를 대신 쓴다.
|
|
49
|
-
*/
|
|
50
|
-
const resolveSingleTag = (value, optionItems, lastResolvedTag) => {
|
|
51
|
-
if (isEmptyValue(value) || Array.isArray(value)) return null;
|
|
52
|
-
const found = findOption(optionItems, value);
|
|
53
|
-
if (found) return {
|
|
54
|
-
id: found.id,
|
|
55
|
-
label: found.label
|
|
56
|
-
};
|
|
57
|
-
return lastResolvedTag?.id === value ? lastResolvedTag : null;
|
|
58
|
-
};
|
|
59
|
-
/** 단일 모드 Tag 목록 (0~1개). 목록에서 찾은 Tag 를 기억해 검색 필터 중에도 라벨을 유지한다 */
|
|
60
|
-
const useSingleSelectedTags = (value, optionItems, enabled) => {
|
|
61
|
-
const [lastResolvedTag, setLastResolvedTag] = useState(null);
|
|
62
|
-
const found = enabled ? findSingleOption(value, optionItems) : undefined;
|
|
63
|
-
const foundId = found?.id;
|
|
64
|
-
const foundLabel = found?.label;
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (foundId === undefined || foundLabel === undefined) return;
|
|
67
|
-
setLastResolvedTag({
|
|
68
|
-
id: foundId,
|
|
69
|
-
label: foundLabel
|
|
70
|
-
});
|
|
71
|
-
}, [foundId, foundLabel]);
|
|
72
|
-
const selectedTag = enabled ? resolveSingleTag(value, optionItems, lastResolvedTag) : null;
|
|
73
|
-
return selectedTag ? [selectedTag] : [];
|
|
74
|
-
};
|
|
75
|
-
/** multiple 모드 Tag — 목록에서 못 찾은 id 는 이미 표시 중인 Tag 의 라벨을 쓴다 */
|
|
76
|
-
const resolveMultipleTags = (ids, optionItems, prevTags) => {
|
|
77
|
-
const nextTags = ids.map(id => {
|
|
78
|
-
const found = findOption(optionItems, id);
|
|
79
|
-
return found ? {
|
|
80
|
-
id: found.id,
|
|
81
|
-
label: found.label
|
|
82
|
-
} : prevTags.find(tag => tag.id === id);
|
|
83
|
-
}).filter(tag => tag !== undefined);
|
|
84
|
-
// 결과가 같으면 이전 배열을 그대로 돌려준다 — 새 배열로 바꾸면 effect 가 다시 렌더를 일으킨다
|
|
85
|
-
return isSameTags(nextTags, prevTags) ? prevTags : nextTags;
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* multiple 모드 Tag — 「선택 완료」·X 클릭·외부 value 변경 때만 확정 id 를 바꾼다.
|
|
89
|
-
* ComboBox 가 직접 onChange 로 내보낸 값은 markEmitted 로 기록해 두고, 그 값이 value 로 돌아오면 한 번만 무시한다(확정 아님).
|
|
90
|
-
* 바깥 값이 들어오면 남아 있던 기록도 지운다 — 오래된 기록 때문에 이후 같은 바깥 값이 무시되지 않게 하기 위함.
|
|
91
|
-
* optionItems 만 바뀌면 확정 id 는 그대로 두고 라벨만 다시 찾는다 — 「선택 완료」 전 검색으로 Tag 가 확정되지 않게 하기 위함.
|
|
92
|
-
*/
|
|
93
|
-
const useMultipleSelectedTags = (value, optionItems, enabled) => {
|
|
94
|
-
const [selectedTags, setSelectedTags] = useState([]);
|
|
95
|
-
const lastEmittedRef = useRef(null);
|
|
96
|
-
const committedIdsRef = useRef([]);
|
|
97
|
-
const prevValueIdsRef = useRef(null);
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (!enabled) return;
|
|
100
|
-
const ids = toIdArray(value);
|
|
101
|
-
const isEcho = lastEmittedRef.current !== null && isSameIds(ids, lastEmittedRef.current);
|
|
102
|
-
const isValueChanged = prevValueIdsRef.current === null || !isSameIds(ids, prevValueIdsRef.current);
|
|
103
|
-
prevValueIdsRef.current = ids;
|
|
104
|
-
if (isEcho) {
|
|
105
|
-
lastEmittedRef.current = null;
|
|
106
|
-
} else if (isValueChanged) {
|
|
107
|
-
committedIdsRef.current = ids;
|
|
108
|
-
lastEmittedRef.current = null;
|
|
109
|
-
}
|
|
110
|
-
setSelectedTags(prev => resolveMultipleTags(committedIdsRef.current, optionItems, prev));
|
|
111
|
-
}, [enabled, value, optionItems]);
|
|
112
|
-
/** 「선택 완료」로 Tag 를 확정한다 */
|
|
113
|
-
const commitTags = ids => {
|
|
114
|
-
committedIdsRef.current = ids;
|
|
115
|
-
setSelectedTags(prev => resolveMultipleTags(ids, optionItems, prev));
|
|
116
|
-
};
|
|
117
|
-
/** X 클릭 — 확정 id 에서 그 id 만 뺀다. value 기준으로 계산하면 「선택 완료」 전에 클릭한 항목까지 확정된다 */
|
|
118
|
-
const removeCommittedTag = tagId => {
|
|
119
|
-
commitTags(committedIdsRef.current.filter(id => id !== tagId));
|
|
120
|
-
};
|
|
121
|
-
/** ComboBox 가 onChange 로 내보낸 값을 기록한다 — value 로 돌아왔을 때 확정으로 보지 않기 위함 */
|
|
122
|
-
const markEmitted = ids => {
|
|
123
|
-
lastEmittedRef.current = ids;
|
|
124
|
-
};
|
|
125
|
-
return {
|
|
126
|
-
selectedTags,
|
|
127
|
-
commitTags,
|
|
128
|
-
removeCommittedTag,
|
|
129
|
-
markEmitted
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
26
|
const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
133
27
|
let {
|
|
134
28
|
placeholder = '검색하세요',
|
|
@@ -168,24 +62,22 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
168
62
|
} = usePortalState(usePortal);
|
|
169
63
|
const [inputValue, setInputValue] = useState(searchValue);
|
|
170
64
|
const [showAllItems, setShowAllItems] = useState(false);
|
|
171
|
-
const
|
|
172
|
-
const {
|
|
173
|
-
selectedTags,
|
|
174
|
-
commitTags,
|
|
175
|
-
removeCommittedTag,
|
|
176
|
-
markEmitted
|
|
177
|
-
} = useMultipleSelectedTags(value, optionItems, multiple);
|
|
65
|
+
const [selectedTags, setSelectedTags] = useState([]);
|
|
178
66
|
const handleOptionSelect = option => {
|
|
179
67
|
if (disabled) return;
|
|
180
68
|
if (multiple) {
|
|
181
69
|
const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
|
|
182
70
|
if (newValue === null) return;
|
|
183
|
-
markEmitted(newValue);
|
|
184
71
|
onChange?.(newValue);
|
|
185
72
|
notifyFormChange(register, multiple, newValue);
|
|
186
73
|
return;
|
|
187
74
|
}
|
|
188
75
|
const newValue = option.id;
|
|
76
|
+
const tag = {
|
|
77
|
+
id: option.id,
|
|
78
|
+
label: option.label
|
|
79
|
+
};
|
|
80
|
+
setSelectedTags([tag]);
|
|
189
81
|
onChange?.(newValue);
|
|
190
82
|
notifyFormChange(register, multiple, newValue);
|
|
191
83
|
};
|
|
@@ -292,6 +184,7 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
292
184
|
const {
|
|
293
185
|
buttonText: selectAllButtonText,
|
|
294
186
|
toggleSelectAll,
|
|
187
|
+
getSelectedTagsData,
|
|
295
188
|
removeTag,
|
|
296
189
|
isMaxSelectionActive,
|
|
297
190
|
tryToggle
|
|
@@ -301,7 +194,6 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
301
194
|
const handleSelectAll = () => {
|
|
302
195
|
if (!multiple || !onChange) return;
|
|
303
196
|
const newSelectedValues = toggleSelectAll();
|
|
304
|
-
markEmitted(newSelectedValues);
|
|
305
197
|
onChange(newSelectedValues);
|
|
306
198
|
};
|
|
307
199
|
const handleEdit = () => {
|
|
@@ -309,32 +201,30 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
309
201
|
};
|
|
310
202
|
const handleComplete = completedValue => {
|
|
311
203
|
if (!multiple) return;
|
|
312
|
-
|
|
204
|
+
const tags = getSelectedTagsData();
|
|
205
|
+
setSelectedTags(tags);
|
|
313
206
|
setInputValue('');
|
|
314
207
|
closeDropdown();
|
|
315
208
|
onComplete?.(completedValue);
|
|
316
209
|
};
|
|
317
210
|
const handleRemoveTag = tagId => {
|
|
318
|
-
// X 버튼은 disabled 에서 숨기지만, 다른 호출 경로가 생겨도 값이 지워지지 않게 함수에서도 막는다
|
|
319
|
-
if (disabled) return;
|
|
320
211
|
if (!onChange) return;
|
|
321
212
|
if (multiple) {
|
|
322
213
|
const newSelectedValues = removeTag(tagId);
|
|
323
|
-
markEmitted(newSelectedValues);
|
|
324
214
|
onChange(newSelectedValues);
|
|
325
|
-
|
|
215
|
+
setSelectedTags(prev => prev.filter(tag => tag.id !== tagId));
|
|
326
216
|
return;
|
|
327
217
|
}
|
|
328
218
|
onChange('');
|
|
219
|
+
setSelectedTags([]);
|
|
329
220
|
};
|
|
330
221
|
const comboboxId = `combobox-options-${id || 'default'}`;
|
|
331
222
|
const hasVisibleOptions = showAllItems || inputValue;
|
|
332
223
|
const displayedOptions = hasVisibleOptions ? optionItems : [];
|
|
333
224
|
const showNoResults = !hasVisibleOptions || optionItems.length === 0;
|
|
334
225
|
const showFooter = multiple || showFooterButtons;
|
|
335
|
-
const hiddenInputValue =
|
|
336
|
-
const
|
|
337
|
-
const hasTags = tags.length > 0;
|
|
226
|
+
const hiddenInputValue = multiple ? JSON.stringify(value || []) : String(value || '');
|
|
227
|
+
const hasTags = selectedTags.length > 0;
|
|
338
228
|
const {
|
|
339
229
|
className: portalPanelClassName,
|
|
340
230
|
style: portalPanelStyle
|
|
@@ -345,7 +235,7 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
345
235
|
direction: dropdownDirection,
|
|
346
236
|
size: size,
|
|
347
237
|
options: displayedOptions,
|
|
348
|
-
value:
|
|
238
|
+
value: value,
|
|
349
239
|
focusedIndex: focusedIndex,
|
|
350
240
|
maxHeight: maxHeight,
|
|
351
241
|
listboxId: comboboxId,
|
|
@@ -424,10 +314,10 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
424
314
|
})]
|
|
425
315
|
}), hasTags && _jsx("div", {
|
|
426
316
|
className: "ncua-combobox__tags",
|
|
427
|
-
children:
|
|
317
|
+
children: selectedTags.map(tag => _jsx(Tag, {
|
|
428
318
|
text: tag.label,
|
|
429
319
|
size: "sm",
|
|
430
|
-
close:
|
|
320
|
+
close: true,
|
|
431
321
|
onButtonClick: () => handleRemoveTag(tag.id)
|
|
432
322
|
}, tag.id))
|
|
433
323
|
})]
|
|
@@ -473,7 +473,7 @@ describe('C-9 기본 경로 회귀 기준선', () => {
|
|
|
473
473
|
expect(onSearch).toHaveBeenCalledWith('사');
|
|
474
474
|
expect(view.content.querySelectorAll('.ncua-select-dropdown__option')).toHaveLength(createOptions().length);
|
|
475
475
|
});
|
|
476
|
-
it('옵션을 선택하면 onChange 가
|
|
476
|
+
it('옵션을 선택하면 onChange 가 호출되고 선택 태그가 표시된다', () => {
|
|
477
477
|
// Given: 옵션이 노출된 기본 모드 ComboBox 에서
|
|
478
478
|
const onChange = vi.fn();
|
|
479
479
|
const view = mountComboBox({
|
|
@@ -486,9 +486,9 @@ describe('C-9 기본 경로 회귀 기준선', () => {
|
|
|
486
486
|
act(() => {
|
|
487
487
|
requireElement(view.content, '.ncua-select-dropdown__option').click();
|
|
488
488
|
});
|
|
489
|
-
// Then: onChange 가 선택 값으로
|
|
490
|
-
// 선택 태그는 value 를 따르므로(#530) onChange 값을 value 로 돌려주는 경우의 태그 표시는 ComboBox.test.ts S-10 에서 검증한다
|
|
489
|
+
// Then: onChange 가 선택 값으로 호출되고 태그가 표시된다
|
|
491
490
|
expect(onChange).toHaveBeenCalledWith('apple');
|
|
491
|
+
expect(view.container.querySelectorAll('.ncua-combobox__tags .ncua-tag')).toHaveLength(1);
|
|
492
492
|
});
|
|
493
493
|
it('Escape 를 누르면 인라인 패널이 닫힌다', () => {
|
|
494
494
|
// Given: 열려 있는 기본 모드 ComboBox 에서
|
|
@@ -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": 즉시 발화)
|
|
@@ -149,12 +149,9 @@ const FloatingNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
149
149
|
children: supportingText
|
|
150
150
|
}), actions && _jsx("div", {
|
|
151
151
|
className: "ncua-floating-notification__actions",
|
|
152
|
-
children:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
label: action.label,
|
|
156
|
-
onClick: action?.onClick
|
|
157
|
-
}, `${action.label}-${action.hierarchy}`))
|
|
152
|
+
children: _jsx(NotificationActionButtons, {
|
|
153
|
+
actions: actions
|
|
154
|
+
})
|
|
158
155
|
})]
|
|
159
156
|
})]
|
|
160
157
|
})
|
|
@@ -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,
|
|
@@ -28,11 +31,14 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
28
31
|
actions,
|
|
29
32
|
autoClose = 0,
|
|
30
33
|
supportTextLink,
|
|
34
|
+
supportTextLinkTarget,
|
|
31
35
|
onHidePermanently,
|
|
32
36
|
hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth,
|
|
33
37
|
...rest
|
|
34
38
|
} = _ref;
|
|
35
39
|
const [shouldRemove, setShouldRemove] = useState(false);
|
|
40
|
+
// unsafe URL(javascript: 등)이면 링크 없이 텍스트만 렌더한다 — Vanilla `renderSupportingText` 와 같은 정책.
|
|
41
|
+
const safeSupportTextLink = supportTextLink && isSafeUrl(supportTextLink) ? supportTextLink : undefined;
|
|
36
42
|
const closeIconSize = {
|
|
37
43
|
width: 20,
|
|
38
44
|
height: 20
|
|
@@ -86,12 +92,13 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
86
92
|
children: [_jsx("span", {
|
|
87
93
|
className: "ncua-full-width-notification__title",
|
|
88
94
|
children: title
|
|
89
|
-
}), !
|
|
95
|
+
}), !safeSupportTextLink && supportingText && _jsx("span", {
|
|
90
96
|
className: "ncua-full-width-notification__supporting-text",
|
|
91
97
|
children: supportingText
|
|
92
|
-
}),
|
|
93
|
-
href:
|
|
98
|
+
}), safeSupportTextLink && supportingText && _jsx("a", {
|
|
99
|
+
href: safeSupportTextLink,
|
|
94
100
|
className: "ncua-full-width-notification__link",
|
|
101
|
+
...getLinkTargetAttributes(supportTextLinkTarget),
|
|
95
102
|
children: _jsx("span", {
|
|
96
103
|
className: "ncua-full-width-notification__supporting-text",
|
|
97
104
|
children: supportingText
|
|
@@ -102,11 +109,9 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
102
109
|
className: "ncua-full-width-notification__actions-container",
|
|
103
110
|
children: [actions && _jsx("div", {
|
|
104
111
|
className: "ncua-full-width-notification__actions",
|
|
105
|
-
children:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
children: action.label
|
|
109
|
-
}, `${action.label}-${action.hierarchy}`))
|
|
112
|
+
children: _jsx(NotificationActionButtons, {
|
|
113
|
+
actions: actions
|
|
114
|
+
})
|
|
110
115
|
}), onHidePermanently && _jsx("button", {
|
|
111
116
|
type: "button",
|
|
112
117
|
className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'),
|
|
@@ -1,20 +1,16 @@
|
|
|
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
|
-
success: CheckCircle
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
neutral: 'gray700',
|
|
15
|
-
error: 'red500',
|
|
16
|
-
warning: 'orange500',
|
|
17
|
-
success: 'green600'
|
|
11
|
+
success: CheckCircle,
|
|
12
|
+
// info 는 variant="block" 에서만 도달한다 (default 는 neutral 로 폴백).
|
|
13
|
+
info: InfoCircle
|
|
18
14
|
};
|
|
19
15
|
const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
20
16
|
let {
|
|
@@ -27,13 +23,21 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
27
23
|
actions,
|
|
28
24
|
onHidePermanently,
|
|
29
25
|
hidePermanentlyLabel,
|
|
26
|
+
variant = 'default',
|
|
27
|
+
supportTextLink,
|
|
28
|
+
supportTextLinkTarget,
|
|
30
29
|
...rest
|
|
31
30
|
} = _ref;
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const isBlock = variant === 'block';
|
|
32
|
+
// default 는 neutral, error, warning, success 4가지 색상만 지원 — info 는 block 에서만 유지한다.
|
|
33
|
+
const validColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
34
|
+
// FeaturedIcon 에는 info 색이 없다. block+info 의 violet 톤은 message SCSS 가 덮어쓴다.
|
|
35
|
+
const featuredIconColor = validColor === 'info' ? 'neutral' : validColor;
|
|
34
36
|
return _jsx("div", {
|
|
35
37
|
ref: ref,
|
|
36
|
-
className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`,
|
|
38
|
+
className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, {
|
|
39
|
+
'ncua-message-notification--block': isBlock
|
|
40
|
+
}, className),
|
|
37
41
|
role: "alert",
|
|
38
42
|
...rest,
|
|
39
43
|
children: _jsx("div", {
|
|
@@ -43,15 +47,17 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
43
47
|
children: [_jsx(MessageNotificationContent, {
|
|
44
48
|
title: title,
|
|
45
49
|
supportingText: supportingText,
|
|
50
|
+
supportTextLink: isBlock ? supportTextLink : undefined,
|
|
51
|
+
supportTextLinkTarget: supportTextLinkTarget,
|
|
46
52
|
icon: Icon || iconMap[validColor],
|
|
47
|
-
iconColor:
|
|
48
|
-
|
|
53
|
+
iconColor: featuredIconColor,
|
|
54
|
+
iconSize: isBlock ? 'sm' : 'md'
|
|
49
55
|
}), _jsx(MessageNotificationActions, {
|
|
50
56
|
actions: actions,
|
|
51
57
|
onClose: onClose,
|
|
52
58
|
onHidePermanently: onHidePermanently,
|
|
53
59
|
hidePermanentlyLabel: hidePermanentlyLabel,
|
|
54
|
-
closeIconColor:
|
|
60
|
+
closeIconColor: MESSAGE_CLOSE_ICON_COLOR[validColor]
|
|
55
61
|
})]
|
|
56
62
|
})
|
|
57
63
|
})
|
|
@@ -1,26 +1,34 @@
|
|
|
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
|
-
|
|
6
|
+
import { getLinkTargetAttributes } from './linkTarget';
|
|
7
|
+
import { NotificationActionButtons } from './NotificationActionButtons';
|
|
8
|
+
/** 아이콘 + 제목/서포팅 텍스트(또는 텍스트 링크) 영역. */
|
|
7
9
|
const MessageNotificationContent = _ref => {
|
|
8
10
|
let {
|
|
9
11
|
title,
|
|
10
12
|
supportingText,
|
|
13
|
+
supportTextLink,
|
|
14
|
+
supportTextLinkTarget,
|
|
11
15
|
icon,
|
|
12
16
|
iconColor,
|
|
13
|
-
|
|
17
|
+
iconSize
|
|
14
18
|
} = _ref;
|
|
15
19
|
const featuredIconProps = {
|
|
16
20
|
icon: icon || Pin02,
|
|
17
|
-
size:
|
|
21
|
+
size: iconSize,
|
|
18
22
|
color: iconColor,
|
|
19
23
|
theme: 'light-circle'
|
|
20
24
|
};
|
|
25
|
+
const supportingTextNode = supportingText && _jsx("span", {
|
|
26
|
+
className: "ncua-message-notification__supporting-text",
|
|
27
|
+
children: supportingText
|
|
28
|
+
});
|
|
21
29
|
return _jsxs("div", {
|
|
22
30
|
className: "ncua-message-notification__content-wrapper",
|
|
23
|
-
children: [
|
|
31
|
+
children: [_jsx(FeaturedIcon, {
|
|
24
32
|
...featuredIconProps,
|
|
25
33
|
className: "ncua-message-notification__icon"
|
|
26
34
|
}), _jsxs("div", {
|
|
@@ -28,10 +36,12 @@ const MessageNotificationContent = _ref => {
|
|
|
28
36
|
children: [_jsx("span", {
|
|
29
37
|
className: "ncua-message-notification__title",
|
|
30
38
|
children: title
|
|
31
|
-
}),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
}), supportTextLink && isSafeUrl(supportTextLink) && supportingTextNode ? _jsx("a", {
|
|
40
|
+
href: supportTextLink,
|
|
41
|
+
className: "ncua-message-notification__link",
|
|
42
|
+
...getLinkTargetAttributes(supportTextLinkTarget),
|
|
43
|
+
children: supportingTextNode
|
|
44
|
+
}) : supportingTextNode]
|
|
35
45
|
})]
|
|
36
46
|
});
|
|
37
47
|
};
|
|
@@ -55,12 +65,9 @@ const MessageNotificationActions = _ref2 => {
|
|
|
55
65
|
className: "ncua-message-notification__actions-container",
|
|
56
66
|
children: [hasActions && _jsx("div", {
|
|
57
67
|
className: "ncua-message-notification__actions",
|
|
58
|
-
children:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
label: action.label,
|
|
62
|
-
onClick: action?.onClick
|
|
63
|
-
}, `${action.label}-${action.hierarchy}`))
|
|
68
|
+
children: _jsx(NotificationActionButtons, {
|
|
69
|
+
actions: actions ?? []
|
|
70
|
+
})
|
|
64
71
|
}), hasFooter && _jsxs("div", {
|
|
65
72
|
className: "ncua-message-notification__footer-container",
|
|
66
73
|
children: [onHidePermanently && _jsx("button", {
|
|
@@ -13,6 +13,8 @@ const Notification = /*#__PURE__*/forwardRef(
|
|
|
13
13
|
color = 'neutral',
|
|
14
14
|
width,
|
|
15
15
|
supportTextLink,
|
|
16
|
+
supportTextLinkTarget,
|
|
17
|
+
variant,
|
|
16
18
|
...rest
|
|
17
19
|
} = _ref;
|
|
18
20
|
if (type === 'floating') {
|
|
@@ -26,6 +28,7 @@ const Notification = /*#__PURE__*/forwardRef(
|
|
|
26
28
|
return _jsx(FullWidthNotification, {
|
|
27
29
|
color: color,
|
|
28
30
|
supportTextLink: supportTextLink,
|
|
31
|
+
supportTextLinkTarget: supportTextLinkTarget,
|
|
29
32
|
...rest,
|
|
30
33
|
ref: ref
|
|
31
34
|
});
|
|
@@ -33,6 +36,9 @@ const Notification = /*#__PURE__*/forwardRef(
|
|
|
33
36
|
if (type === 'message') {
|
|
34
37
|
return _jsx(MessageNotification, {
|
|
35
38
|
color: color,
|
|
39
|
+
variant: variant,
|
|
40
|
+
supportTextLink: supportTextLink,
|
|
41
|
+
supportTextLinkTarget: supportTextLinkTarget,
|
|
36
42
|
...rest,
|
|
37
43
|
ref: ref
|
|
38
44
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
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 = _ref => {
|
|
9
|
+
let {
|
|
10
|
+
actions
|
|
11
|
+
} = _ref;
|
|
12
|
+
return _jsx(_Fragment, {
|
|
13
|
+
children: actions.map(action => _jsx(Button, {
|
|
14
|
+
size: "xs",
|
|
15
|
+
hierarchy: action.hierarchy || 'text',
|
|
16
|
+
label: action.label,
|
|
17
|
+
onClick: action.onClick
|
|
18
|
+
}, `${action.label}-${action.hierarchy}`))
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export { NotificationActionButtons };
|
|
@@ -0,0 +1,55 @@
|
|
|
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)) cleanup();
|
|
16
|
+
});
|
|
17
|
+
function mount(element) {
|
|
18
|
+
const container = document.createElement('div');
|
|
19
|
+
document.body.appendChild(container);
|
|
20
|
+
const root = createRoot(container);
|
|
21
|
+
act(() => {
|
|
22
|
+
root.render(element);
|
|
23
|
+
});
|
|
24
|
+
cleanups.push(() => {
|
|
25
|
+
act(() => root.unmount());
|
|
26
|
+
container.remove();
|
|
27
|
+
});
|
|
28
|
+
return container;
|
|
29
|
+
}
|
|
30
|
+
const IMPLEMENTATIONS = [['MessageNotification(block)', supportTextLink => mount(/*#__PURE__*/createElement(MessageNotification, {
|
|
31
|
+
title: '공지',
|
|
32
|
+
variant: 'block',
|
|
33
|
+
supportingText: '자세히 보기',
|
|
34
|
+
supportTextLink
|
|
35
|
+
})), '.ncua-message-notification__supporting-text'], ['FullWidthNotification', supportTextLink => mount(/*#__PURE__*/createElement(FullWidthNotification, {
|
|
36
|
+
title: '공지',
|
|
37
|
+
supportingText: '자세히 보기',
|
|
38
|
+
supportTextLink
|
|
39
|
+
})), '.ncua-full-width-notification__supporting-text']];
|
|
40
|
+
const UNSAFE = ['javascript:alert(1)', ' JavaScript:alert(1)', 'JAVASCRIPT:alert(1)', '\tjavascript:alert(1)', 'data:text/html,<script>alert(1)</script>'];
|
|
41
|
+
const SAFE = ['https://example.com/notice/1', 'http://example.com/notice/1', '/notice/1', 'notice/1'];
|
|
42
|
+
describe.each(IMPLEMENTATIONS)('%s — supportTextLink 프로토콜 검사', (_, render, supportingTextSelector) => {
|
|
43
|
+
it.each(UNSAFE)('unsafe %j → 링크 없이 텍스트만 렌더한다', url => {
|
|
44
|
+
const container = render(url);
|
|
45
|
+
expect(container.querySelector('a')).toBeNull();
|
|
46
|
+
expect(container.querySelector(supportingTextSelector)?.textContent).toBe('자세히 보기');
|
|
47
|
+
});
|
|
48
|
+
it.each(SAFE)('safe %j → 그대로 링크로 렌더한다', url => {
|
|
49
|
+
const container = render(url);
|
|
50
|
+
const anchors = container.querySelectorAll('a');
|
|
51
|
+
expect(anchors).toHaveLength(1);
|
|
52
|
+
expect(anchors[0].getAttribute('href')).toBe(url);
|
|
53
|
+
expect(anchors[0].querySelector(supportingTextSelector)?.textContent).toBe('자세히 보기');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
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' ? {
|
|
9
|
+
target: '_blank',
|
|
10
|
+
rel: 'noopener noreferrer'
|
|
11
|
+
} : undefined;
|
|
12
|
+
}
|