@ncds/ui-admin 1.8.23-alpha.1 → 1.8.24
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/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/cjs/src/components/navigation/bread-crumb/BreadCrumb.js +6 -14
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/esm/src/components/navigation/bread-crumb/BreadCrumb.js +6 -14
- package/dist/esm/src/generated/scoped-css.js +1 -1
- 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/navigation/bread-crumb/BreadCrumb.d.ts +1 -3
- package/dist/temp/src/components/navigation/bread-crumb/BreadCrumb.js +5 -9
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/types/src/components/navigation/bread-crumb/BreadCrumb.d.ts +1 -3
- package/dist/ui-admin/assets/styles/style.css +15 -13
- package/package.json +2 -2
- package/dist/cjs/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +0 -207
- package/dist/esm/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +0 -204
- package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +0 -1
- package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +0 -161
- package/dist/types/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +0 -1
|
@@ -16,4 +16,4 @@ type BadgeProps = {
|
|
|
16
16
|
};
|
|
17
17
|
declare const Badge: ({ label, type, color, className, leadingIcon, trailingIcon, size, }: BadgeProps) => import("react/jsx-runtime").JSX.Element;
|
|
18
18
|
export { Badge };
|
|
19
|
-
export type {
|
|
19
|
+
export type { BadgeType, BadgeColor, BadgeSize, BadgeProps };
|
|
@@ -4,10 +4,8 @@ interface BreadcrumbItemProps {
|
|
|
4
4
|
}
|
|
5
5
|
interface BreadcrumbProps {
|
|
6
6
|
items: BreadcrumbItemProps[];
|
|
7
|
-
/** 첫 항목을 홈 아이콘으로 렌더링할지 여부. false 이면 label 텍스트로 렌더링한다. */
|
|
8
|
-
showHomeIcon?: boolean;
|
|
9
7
|
className?: string;
|
|
10
8
|
}
|
|
11
|
-
declare const BreadCrumb: ({ items,
|
|
9
|
+
declare const BreadCrumb: ({ items, className }: BreadcrumbProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
12
10
|
export { BreadCrumb };
|
|
13
11
|
export type { BreadcrumbItemProps, BreadcrumbProps };
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
--shadow-xs-focused-3px-primary-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 3px #ffeaee;
|
|
68
68
|
--shadow-xs-focused-3px-gray-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 3px #f2f4f7;
|
|
69
69
|
--shadow-xs-focused-3px-error-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 3px #fee4e2;
|
|
70
|
+
--shadow-xs-focused-4px-primary-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 4px #ffeaee;
|
|
71
|
+
--shadow-xs-focused-4px-gray-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 4px #f2f4f7;
|
|
72
|
+
--shadow-xs-focused-4px-error-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 4px #fee4e2;
|
|
70
73
|
--shadow-sm: 0px 1px 3px 0px #1018281a, 0px 1px 2px 0px #1018280f;
|
|
71
74
|
--shadow-sm-focused-4px-primary-100: 0px 1px 2px 0px #1018280d, 0px 0px 0px 4px #ffeaee;
|
|
72
75
|
--shadow-sm-focused-4px-gray-100: 0px 1px 3px 0px #1018281a, 0px 1px 2px 0px #1018280f, 0px 0px 0px 4px #f2f4f7;
|
|
@@ -324,7 +327,7 @@ button {
|
|
|
324
327
|
background-color: var(--primary-red-450);
|
|
325
328
|
border: 1px solid var(--primary-red-500);
|
|
326
329
|
color: var(--base-white);
|
|
327
|
-
box-shadow: var(--shadow-
|
|
330
|
+
box-shadow: var(--shadow-xs-focused-4px-primary-100);
|
|
328
331
|
}
|
|
329
332
|
.ncua-btn--primary:disabled, .ncua-btn--primary.is-disable {
|
|
330
333
|
background-color: var(--primary-red-100);
|
|
@@ -370,7 +373,7 @@ button {
|
|
|
370
373
|
background-color: var(--base-white);
|
|
371
374
|
border: 1px solid var(--gray-200);
|
|
372
375
|
color: var(--gray-700);
|
|
373
|
-
box-shadow: var(--shadow-
|
|
376
|
+
box-shadow: var(--shadow-xs-focused-4px-gray-100);
|
|
374
377
|
}
|
|
375
378
|
.ncua-btn--secondary-gray:disabled, .ncua-btn--secondary-gray.is-disable {
|
|
376
379
|
background-color: var(--base-white);
|
|
@@ -956,7 +959,7 @@ button {
|
|
|
956
959
|
}
|
|
957
960
|
.ncua-checkbox-input.destructive :where(input):focus {
|
|
958
961
|
border-color: var(--primary-red-600);
|
|
959
|
-
box-shadow: var(--shadow-
|
|
962
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
960
963
|
}
|
|
961
964
|
.ncua-checkbox-input:hover {
|
|
962
965
|
border-color: var(--gray-600);
|
|
@@ -1737,7 +1740,7 @@ button {
|
|
|
1737
1740
|
border-color: var(--primary-red-600);
|
|
1738
1741
|
}
|
|
1739
1742
|
.ncua-input.destructive:where(:focus-within) .ncua-input__field {
|
|
1740
|
-
box-shadow: var(--shadow-
|
|
1743
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
1741
1744
|
}
|
|
1742
1745
|
.ncua-input.is-disabled .ncua-input__content > :not(button) {
|
|
1743
1746
|
background-color: var(--gray-50);
|
|
@@ -1927,7 +1930,7 @@ button {
|
|
|
1927
1930
|
border-color: var(--primary-red-600);
|
|
1928
1931
|
}
|
|
1929
1932
|
.ncua-input--textarea.destructive textarea:focus {
|
|
1930
|
-
box-shadow: var(--shadow-
|
|
1933
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
1931
1934
|
}
|
|
1932
1935
|
|
|
1933
1936
|
.ncua-input--textarea--xs textarea {
|
|
@@ -3189,7 +3192,7 @@ button {
|
|
|
3189
3192
|
}
|
|
3190
3193
|
.ncua-radio-input.destructive :where(input):focus {
|
|
3191
3194
|
border-color: var(--primary-red-600);
|
|
3192
|
-
box-shadow: var(--shadow-
|
|
3195
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
3193
3196
|
}
|
|
3194
3197
|
|
|
3195
3198
|
.ncua-radio-field {
|
|
@@ -3691,7 +3694,7 @@ button {
|
|
|
3691
3694
|
border-color: var(--primary-red-600);
|
|
3692
3695
|
}
|
|
3693
3696
|
.ncua-select.destructive .ncua-select__content:focus-within {
|
|
3694
|
-
box-shadow: var(--shadow-
|
|
3697
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
3695
3698
|
}
|
|
3696
3699
|
.ncua-select--xs .ncua-select__content {
|
|
3697
3700
|
height: var(--select-height-xs);
|
|
@@ -4032,7 +4035,7 @@ button {
|
|
|
4032
4035
|
border-color: var(--primary-red-600);
|
|
4033
4036
|
}
|
|
4034
4037
|
.ncua-selectbox.destructive .ncua-selectbox__content:focus {
|
|
4035
|
-
box-shadow: var(--shadow-
|
|
4038
|
+
box-shadow: var(--shadow-xs-focused-4px-error-100);
|
|
4036
4039
|
}
|
|
4037
4040
|
.ncua-selectbox--xs .ncua-selectbox__content {
|
|
4038
4041
|
height: var(--select-height-xs);
|
|
@@ -5194,7 +5197,7 @@ button {
|
|
|
5194
5197
|
font-weight: var(--font-weights-commerce-sans-1);
|
|
5195
5198
|
cursor: pointer;
|
|
5196
5199
|
}
|
|
5197
|
-
.ncua-breadcrumb__list-item
|
|
5200
|
+
.ncua-breadcrumb__list-item:first-child {
|
|
5198
5201
|
height: 24px;
|
|
5199
5202
|
padding: 4px;
|
|
5200
5203
|
}
|
|
@@ -5325,16 +5328,15 @@ button {
|
|
|
5325
5328
|
justify-content: center;
|
|
5326
5329
|
color: var(--pink-600);
|
|
5327
5330
|
background-color: var(--pink-200);
|
|
5331
|
+
border-radius: 5px;
|
|
5328
5332
|
}
|
|
5329
5333
|
.ncua-badge--new-badge-sm {
|
|
5330
5334
|
width: 16px;
|
|
5331
5335
|
height: 16px;
|
|
5332
|
-
border-radius: 3px;
|
|
5333
5336
|
}
|
|
5334
5337
|
.ncua-badge--new-badge-md {
|
|
5335
5338
|
width: 20px;
|
|
5336
5339
|
height: 20px;
|
|
5337
|
-
border-radius: 4px;
|
|
5338
5340
|
}
|
|
5339
5341
|
|
|
5340
5342
|
.ncua-badge-group {
|
|
@@ -6596,7 +6598,7 @@ button {
|
|
|
6596
6598
|
bottom: 0;
|
|
6597
6599
|
width: 3px;
|
|
6598
6600
|
background: var(--base-black);
|
|
6599
|
-
z-index:
|
|
6601
|
+
z-index: 1;
|
|
6600
6602
|
}
|
|
6601
6603
|
.ncua-table__row--warning.ncua-table__row--selected:nth-child(n) {
|
|
6602
6604
|
background: var(--orange-50);
|
|
@@ -6741,7 +6743,7 @@ button {
|
|
|
6741
6743
|
.ncua-table--fixed-header .ncua-table__header-cell {
|
|
6742
6744
|
position: sticky;
|
|
6743
6745
|
top: 0;
|
|
6744
|
-
z-index:
|
|
6746
|
+
z-index: 10;
|
|
6745
6747
|
padding: 0 var(--spacing-m);
|
|
6746
6748
|
height: var(--ncua-table-header-height);
|
|
6747
6749
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ncds/ui-admin",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.24",
|
|
4
4
|
"description": "nhn-commerce의 어드민 디자인 시스템입니다.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"barrel": "node barrel.js",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.0",
|
|
79
79
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
80
80
|
"@atlaskit/pragmatic-drag-and-drop-react-accessibility": "1.1.4",
|
|
81
|
-
"@ncds/ui-admin-icon": "0.1.
|
|
81
|
+
"@ncds/ui-admin-icon": "0.1.13",
|
|
82
82
|
"classnames": "2.5.1",
|
|
83
83
|
"dompurify": "3.4.12",
|
|
84
84
|
"flatpickr": "4.6.13",
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var _react = require("react");
|
|
4
|
-
var _client = require("react-dom/client");
|
|
5
|
-
var _testUtils = require("react-dom/test-utils");
|
|
6
|
-
var _vitest = require("vitest");
|
|
7
|
-
var _BreadCrumb = require("../BreadCrumb");
|
|
8
|
-
// @vitest-environment jsdom
|
|
9
|
-
|
|
10
|
-
const LIST_ITEM = '.ncua-breadcrumb__list-item';
|
|
11
|
-
const HOME_MODIFIER = 'ncua-breadcrumb__list-item--home';
|
|
12
|
-
const ACTIVE_MODIFIER = 'ncua-breadcrumb__list-item--active';
|
|
13
|
-
const ITEM = '.ncua-breadcrumb__item';
|
|
14
|
-
/** DocBreadcrumbs(유일한 사내 호출부)와 같은 형태의 입력 — AC-7 회귀 기준 */
|
|
15
|
-
const DOC_ITEMS = [{
|
|
16
|
-
href: '/',
|
|
17
|
-
label: '홈'
|
|
18
|
-
}, {
|
|
19
|
-
href: '/docs',
|
|
20
|
-
label: '문서'
|
|
21
|
-
}, {
|
|
22
|
-
label: '현재 페이지'
|
|
23
|
-
}];
|
|
24
|
-
let root = null;
|
|
25
|
-
let container = null;
|
|
26
|
-
function render(props) {
|
|
27
|
-
if (!container) {
|
|
28
|
-
container = document.createElement('div');
|
|
29
|
-
document.body.appendChild(container);
|
|
30
|
-
root = (0, _client.createRoot)(container);
|
|
31
|
-
}
|
|
32
|
-
(0, _testUtils.act)(() => {
|
|
33
|
-
root?.render(/*#__PURE__*/(0, _react.createElement)(_BreadCrumb.BreadCrumb, props));
|
|
34
|
-
});
|
|
35
|
-
return container;
|
|
36
|
-
}
|
|
37
|
-
(0, _vitest.afterEach)(() => {
|
|
38
|
-
if (root && container) {
|
|
39
|
-
(0, _testUtils.act)(() => root?.unmount());
|
|
40
|
-
container.remove();
|
|
41
|
-
}
|
|
42
|
-
root = null;
|
|
43
|
-
container = null;
|
|
44
|
-
});
|
|
45
|
-
const listItems = el => Array.from(el.querySelectorAll(LIST_ITEM));
|
|
46
|
-
/** <li> 안에서 실제 포커스를 받는 요소(<a> 또는 <span>) */
|
|
47
|
-
const itemOf = li => li.querySelector(ITEM);
|
|
48
|
-
(0, _vitest.describe)('BreadCrumb — showHomeIcon 아이콘/라벨 분기', () => {
|
|
49
|
-
(0, _vitest.it)('AC-1: showHomeIcon 미지정이면 첫 항목을 홈 아이콘으로 렌더한다', () => {
|
|
50
|
-
// Given/When: showHomeIcon 을 넘기지 않고 렌더하면
|
|
51
|
-
const el = render({
|
|
52
|
-
items: DOC_ITEMS
|
|
53
|
-
});
|
|
54
|
-
// Then: 첫 항목은 --home 모디파이어를 달고 svg 를 품으며, label 텍스트는 노출되지 않는다
|
|
55
|
-
const [first] = listItems(el);
|
|
56
|
-
(0, _vitest.expect)(first.classList.contains(HOME_MODIFIER)).toBe(true);
|
|
57
|
-
(0, _vitest.expect)(first.querySelector('svg')).not.toBeNull();
|
|
58
|
-
(0, _vitest.expect)(first.textContent).not.toContain(DOC_ITEMS[0].label);
|
|
59
|
-
});
|
|
60
|
-
(0, _vitest.it)('AC-2: showHomeIcon={false} 이면 첫 항목을 label 텍스트로 렌더한다', () => {
|
|
61
|
-
// Given/When: 라벨 모드로 렌더하면
|
|
62
|
-
const el = render({
|
|
63
|
-
items: DOC_ITEMS,
|
|
64
|
-
showHomeIcon: false
|
|
65
|
-
});
|
|
66
|
-
// Then: 아이콘 대신 label 텍스트가 나오고 --home 은 붙지 않는다
|
|
67
|
-
const [first] = listItems(el);
|
|
68
|
-
(0, _vitest.expect)(first.classList.contains(HOME_MODIFIER)).toBe(false);
|
|
69
|
-
(0, _vitest.expect)(first.querySelector('svg')).toBeNull();
|
|
70
|
-
(0, _vitest.expect)(first.textContent).toBe(DOC_ITEMS[0].label);
|
|
71
|
-
});
|
|
72
|
-
(0, _vitest.it)('AC-3: 홈 아이콘 항목의 링크에 label 값이 aria-label 로 부여된다', () => {
|
|
73
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
74
|
-
const el = render({
|
|
75
|
-
items: DOC_ITEMS
|
|
76
|
-
});
|
|
77
|
-
// Then: 접근 가능한 이름은 <li> 가 아니라 포커스를 받는 <a> 에 붙는다
|
|
78
|
-
const [first] = listItems(el);
|
|
79
|
-
const anchor = itemOf(first);
|
|
80
|
-
(0, _vitest.expect)(anchor?.tagName).toBe('A');
|
|
81
|
-
(0, _vitest.expect)(anchor?.getAttribute('aria-label')).toBe(DOC_ITEMS[0].label);
|
|
82
|
-
});
|
|
83
|
-
(0, _vitest.it)('AC-4(라벨 모드): 어떤 항목에도 --home 과 aria-label 이 붙지 않는다', () => {
|
|
84
|
-
// Given/When: 라벨 모드로 렌더하면
|
|
85
|
-
const el = render({
|
|
86
|
-
items: DOC_ITEMS,
|
|
87
|
-
showHomeIcon: false
|
|
88
|
-
});
|
|
89
|
-
// Then: --home 은 전 항목에서 사라지고, 첫 항목의 aria-label 속성 자체가 출력되지 않는다
|
|
90
|
-
const items = listItems(el);
|
|
91
|
-
(0, _vitest.expect)(items.every(li => !li.classList.contains(HOME_MODIFIER))).toBe(true);
|
|
92
|
-
(0, _vitest.expect)(itemOf(items[0])?.hasAttribute('aria-label')).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
(0, _vitest.it)('AC-4(아이콘 모드): --home 은 첫 항목에만 붙는다', () => {
|
|
95
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
96
|
-
const el = render({
|
|
97
|
-
items: DOC_ITEMS
|
|
98
|
-
});
|
|
99
|
-
// Then: 홈 아이콘인 첫 항목에만 붙고 나머지에는 붙지 않는다
|
|
100
|
-
const items = listItems(el);
|
|
101
|
-
(0, _vitest.expect)(items.filter(li => li.classList.contains(HOME_MODIFIER))).toHaveLength(1);
|
|
102
|
-
(0, _vitest.expect)(items[0].classList.contains(HOME_MODIFIER)).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
(0, _vitest.it)('AC-5: 마지막 항목은 showHomeIcon 과 무관하게 --active 를 달고 링크가 아니다', () => {
|
|
105
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
106
|
-
const iconMode = listItems(render({
|
|
107
|
-
items: DOC_ITEMS
|
|
108
|
-
}));
|
|
109
|
-
const iconLast = iconMode[iconMode.length - 1];
|
|
110
|
-
// Then: --active 가 붙고 <span> 이라 클릭 이동이 없다
|
|
111
|
-
(0, _vitest.expect)(iconLast.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
112
|
-
(0, _vitest.expect)(itemOf(iconLast)?.tagName).toBe('SPAN');
|
|
113
|
-
// When: 라벨 모드로 다시 렌더해도
|
|
114
|
-
const labelMode = listItems(render({
|
|
115
|
-
items: DOC_ITEMS,
|
|
116
|
-
showHomeIcon: false
|
|
117
|
-
}));
|
|
118
|
-
const labelLast = labelMode[labelMode.length - 1];
|
|
119
|
-
// Then: 판정은 showHomeIcon 과 독립이다
|
|
120
|
-
(0, _vitest.expect)(labelLast.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
121
|
-
(0, _vitest.expect)(itemOf(labelLast)?.tagName).toBe('SPAN');
|
|
122
|
-
});
|
|
123
|
-
(0, _vitest.it)('AC-6: 단일 항목이면 첫 항목이 곧 마지막 항목으로 처리된다', () => {
|
|
124
|
-
// Given/When: 항목이 하나뿐인 배열을 아이콘 모드로 렌더하면
|
|
125
|
-
const single = [{
|
|
126
|
-
href: '/',
|
|
127
|
-
label: '홈'
|
|
128
|
-
}];
|
|
129
|
-
const [iconOnly] = listItems(render({
|
|
130
|
-
items: single
|
|
131
|
-
}));
|
|
132
|
-
// Then: --home 과 --active 가 동시에 붙고 링크가 아니다
|
|
133
|
-
(0, _vitest.expect)(iconOnly.classList.contains(HOME_MODIFIER)).toBe(true);
|
|
134
|
-
(0, _vitest.expect)(iconOnly.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
135
|
-
(0, _vitest.expect)(itemOf(iconOnly)?.tagName).toBe('SPAN');
|
|
136
|
-
(0, _vitest.expect)(iconOnly.querySelector('svg')).not.toBeNull();
|
|
137
|
-
// When: 같은 배열을 라벨 모드로 렌더하면
|
|
138
|
-
const [labelOnly] = listItems(render({
|
|
139
|
-
items: single,
|
|
140
|
-
showHomeIcon: false
|
|
141
|
-
}));
|
|
142
|
-
// Then: --active 만 붙고 label 텍스트가 나온다
|
|
143
|
-
(0, _vitest.expect)(labelOnly.classList.contains(HOME_MODIFIER)).toBe(false);
|
|
144
|
-
(0, _vitest.expect)(labelOnly.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
145
|
-
(0, _vitest.expect)(labelOnly.textContent).toBe(single[0].label);
|
|
146
|
-
});
|
|
147
|
-
(0, _vitest.it)('AC-7: 기존 호출부 입력의 렌더 결과는 --home·aria-label 추가분 외에 변하지 않는다', () => {
|
|
148
|
-
// Given/When: DocBreadcrumbs 와 같은 입력을 showHomeIcon 미지정으로 렌더하면
|
|
149
|
-
const el = render({
|
|
150
|
-
items: DOC_ITEMS
|
|
151
|
-
});
|
|
152
|
-
const items = listItems(el);
|
|
153
|
-
// Then: 요소 종류·텍스트·href 는 변경 전과 같고, 첫 항목 svg 도 그대로다
|
|
154
|
-
(0, _vitest.expect)(items).toHaveLength(DOC_ITEMS.length);
|
|
155
|
-
(0, _vitest.expect)(items.map(li => itemOf(li)?.tagName)).toEqual(['A', 'A', 'SPAN']);
|
|
156
|
-
(0, _vitest.expect)(items.map(li => itemOf(li)?.getAttribute('href'))).toEqual(['/', '/docs', null]);
|
|
157
|
-
(0, _vitest.expect)(items.map(li => li.textContent)).toEqual(['', DOC_ITEMS[1].label, DOC_ITEMS[2].label]);
|
|
158
|
-
(0, _vitest.expect)(items[0].querySelector('svg')).not.toBeNull();
|
|
159
|
-
// Then: <li> 클래스는 기존 베이스/--active 를 그대로 유지하고, 늘어난 것은 --home 뿐이다
|
|
160
|
-
(0, _vitest.expect)(items.map(li => Array.from(li.classList).sort())).toEqual([['ncua-breadcrumb__list-item', HOME_MODIFIER].sort(), ['ncua-breadcrumb__list-item'], ['ncua-breadcrumb__list-item', ACTIVE_MODIFIER].sort()]);
|
|
161
|
-
// Then: <a>/<span> 에 늘어난 속성은 첫 항목의 aria-label 뿐이다
|
|
162
|
-
(0, _vitest.expect)(itemOf(items[1])?.hasAttribute('aria-label')).toBe(false);
|
|
163
|
-
(0, _vitest.expect)(itemOf(items[2])?.hasAttribute('aria-label')).toBe(false);
|
|
164
|
-
});
|
|
165
|
-
(0, _vitest.it)('비링크 홈 항목에는 role="img" 를 부여해 aria-label 이 무시되지 않게 한다', () => {
|
|
166
|
-
// Given/When: 첫 항목에 href 가 없어 <span> 으로 렌더되는 입력(meta usageExamples.default 형태)을 아이콘 모드로 렌더하면
|
|
167
|
-
const el = render({
|
|
168
|
-
items: [{
|
|
169
|
-
label: '대시보드'
|
|
170
|
-
}, {
|
|
171
|
-
href: '/products',
|
|
172
|
-
label: '상품'
|
|
173
|
-
}, {
|
|
174
|
-
label: '상품 상세'
|
|
175
|
-
}]
|
|
176
|
-
});
|
|
177
|
-
// Then: generic role 의 <span> 은 aria-label 을 노출하지 못하므로 role="img" 로 이름을 살린다
|
|
178
|
-
const [first] = listItems(el);
|
|
179
|
-
const home = itemOf(first);
|
|
180
|
-
(0, _vitest.expect)(home?.tagName).toBe('SPAN');
|
|
181
|
-
(0, _vitest.expect)(home?.getAttribute('role')).toBe('img');
|
|
182
|
-
(0, _vitest.expect)(home?.getAttribute('aria-label')).toBe('대시보드');
|
|
183
|
-
});
|
|
184
|
-
(0, _vitest.it)('링크인 홈 항목에는 role 을 부여하지 않는다', () => {
|
|
185
|
-
// Given/When: 첫 항목에 href 가 있어 <a> 로 렌더되는 입력을 아이콘 모드로 렌더하면
|
|
186
|
-
const el = render({
|
|
187
|
-
items: DOC_ITEMS
|
|
188
|
-
});
|
|
189
|
-
// Then: 링크는 이미 접근 가능한 이름을 지원하므로 role 을 덧씌우지 않는다 (ADR-3)
|
|
190
|
-
const items = listItems(el);
|
|
191
|
-
const home = itemOf(items[0]);
|
|
192
|
-
(0, _vitest.expect)(home?.tagName).toBe('A');
|
|
193
|
-
(0, _vitest.expect)(home?.hasAttribute('role')).toBe(false);
|
|
194
|
-
// Then: 홈이 아닌 항목에도 role 은 출력되지 않는다
|
|
195
|
-
(0, _vitest.expect)(items.slice(1).every(li => !itemOf(li)?.hasAttribute('role'))).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
(0, _vitest.it)('경계: items 가 빈 배열이면 아무것도 렌더하지 않는다', () => {
|
|
198
|
-
// Given/When: 빈 배열을 양쪽 모드로 렌더하면
|
|
199
|
-
const el = render({
|
|
200
|
-
items: [],
|
|
201
|
-
showHomeIcon: false
|
|
202
|
-
});
|
|
203
|
-
// Then: <nav> 자체가 만들어지지 않는다 (기존 동작 유지)
|
|
204
|
-
(0, _vitest.expect)(el.querySelector('.ncua-breadcrumb')).toBeNull();
|
|
205
|
-
(0, _vitest.expect)(el.innerHTML).toBe('');
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,204 +0,0 @@
|
|
|
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 { BreadCrumb } from '../BreadCrumb';
|
|
7
|
-
const LIST_ITEM = '.ncua-breadcrumb__list-item';
|
|
8
|
-
const HOME_MODIFIER = 'ncua-breadcrumb__list-item--home';
|
|
9
|
-
const ACTIVE_MODIFIER = 'ncua-breadcrumb__list-item--active';
|
|
10
|
-
const ITEM = '.ncua-breadcrumb__item';
|
|
11
|
-
/** DocBreadcrumbs(유일한 사내 호출부)와 같은 형태의 입력 — AC-7 회귀 기준 */
|
|
12
|
-
const DOC_ITEMS = [{
|
|
13
|
-
href: '/',
|
|
14
|
-
label: '홈'
|
|
15
|
-
}, {
|
|
16
|
-
href: '/docs',
|
|
17
|
-
label: '문서'
|
|
18
|
-
}, {
|
|
19
|
-
label: '현재 페이지'
|
|
20
|
-
}];
|
|
21
|
-
let root = null;
|
|
22
|
-
let container = null;
|
|
23
|
-
function render(props) {
|
|
24
|
-
if (!container) {
|
|
25
|
-
container = document.createElement('div');
|
|
26
|
-
document.body.appendChild(container);
|
|
27
|
-
root = createRoot(container);
|
|
28
|
-
}
|
|
29
|
-
act(() => {
|
|
30
|
-
root?.render(/*#__PURE__*/createElement(BreadCrumb, props));
|
|
31
|
-
});
|
|
32
|
-
return container;
|
|
33
|
-
}
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
if (root && container) {
|
|
36
|
-
act(() => root?.unmount());
|
|
37
|
-
container.remove();
|
|
38
|
-
}
|
|
39
|
-
root = null;
|
|
40
|
-
container = null;
|
|
41
|
-
});
|
|
42
|
-
const listItems = el => Array.from(el.querySelectorAll(LIST_ITEM));
|
|
43
|
-
/** <li> 안에서 실제 포커스를 받는 요소(<a> 또는 <span>) */
|
|
44
|
-
const itemOf = li => li.querySelector(ITEM);
|
|
45
|
-
describe('BreadCrumb — showHomeIcon 아이콘/라벨 분기', () => {
|
|
46
|
-
it('AC-1: showHomeIcon 미지정이면 첫 항목을 홈 아이콘으로 렌더한다', () => {
|
|
47
|
-
// Given/When: showHomeIcon 을 넘기지 않고 렌더하면
|
|
48
|
-
const el = render({
|
|
49
|
-
items: DOC_ITEMS
|
|
50
|
-
});
|
|
51
|
-
// Then: 첫 항목은 --home 모디파이어를 달고 svg 를 품으며, label 텍스트는 노출되지 않는다
|
|
52
|
-
const [first] = listItems(el);
|
|
53
|
-
expect(first.classList.contains(HOME_MODIFIER)).toBe(true);
|
|
54
|
-
expect(first.querySelector('svg')).not.toBeNull();
|
|
55
|
-
expect(first.textContent).not.toContain(DOC_ITEMS[0].label);
|
|
56
|
-
});
|
|
57
|
-
it('AC-2: showHomeIcon={false} 이면 첫 항목을 label 텍스트로 렌더한다', () => {
|
|
58
|
-
// Given/When: 라벨 모드로 렌더하면
|
|
59
|
-
const el = render({
|
|
60
|
-
items: DOC_ITEMS,
|
|
61
|
-
showHomeIcon: false
|
|
62
|
-
});
|
|
63
|
-
// Then: 아이콘 대신 label 텍스트가 나오고 --home 은 붙지 않는다
|
|
64
|
-
const [first] = listItems(el);
|
|
65
|
-
expect(first.classList.contains(HOME_MODIFIER)).toBe(false);
|
|
66
|
-
expect(first.querySelector('svg')).toBeNull();
|
|
67
|
-
expect(first.textContent).toBe(DOC_ITEMS[0].label);
|
|
68
|
-
});
|
|
69
|
-
it('AC-3: 홈 아이콘 항목의 링크에 label 값이 aria-label 로 부여된다', () => {
|
|
70
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
71
|
-
const el = render({
|
|
72
|
-
items: DOC_ITEMS
|
|
73
|
-
});
|
|
74
|
-
// Then: 접근 가능한 이름은 <li> 가 아니라 포커스를 받는 <a> 에 붙는다
|
|
75
|
-
const [first] = listItems(el);
|
|
76
|
-
const anchor = itemOf(first);
|
|
77
|
-
expect(anchor?.tagName).toBe('A');
|
|
78
|
-
expect(anchor?.getAttribute('aria-label')).toBe(DOC_ITEMS[0].label);
|
|
79
|
-
});
|
|
80
|
-
it('AC-4(라벨 모드): 어떤 항목에도 --home 과 aria-label 이 붙지 않는다', () => {
|
|
81
|
-
// Given/When: 라벨 모드로 렌더하면
|
|
82
|
-
const el = render({
|
|
83
|
-
items: DOC_ITEMS,
|
|
84
|
-
showHomeIcon: false
|
|
85
|
-
});
|
|
86
|
-
// Then: --home 은 전 항목에서 사라지고, 첫 항목의 aria-label 속성 자체가 출력되지 않는다
|
|
87
|
-
const items = listItems(el);
|
|
88
|
-
expect(items.every(li => !li.classList.contains(HOME_MODIFIER))).toBe(true);
|
|
89
|
-
expect(itemOf(items[0])?.hasAttribute('aria-label')).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
it('AC-4(아이콘 모드): --home 은 첫 항목에만 붙는다', () => {
|
|
92
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
93
|
-
const el = render({
|
|
94
|
-
items: DOC_ITEMS
|
|
95
|
-
});
|
|
96
|
-
// Then: 홈 아이콘인 첫 항목에만 붙고 나머지에는 붙지 않는다
|
|
97
|
-
const items = listItems(el);
|
|
98
|
-
expect(items.filter(li => li.classList.contains(HOME_MODIFIER))).toHaveLength(1);
|
|
99
|
-
expect(items[0].classList.contains(HOME_MODIFIER)).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
it('AC-5: 마지막 항목은 showHomeIcon 과 무관하게 --active 를 달고 링크가 아니다', () => {
|
|
102
|
-
// Given/When: 아이콘 모드로 렌더하면
|
|
103
|
-
const iconMode = listItems(render({
|
|
104
|
-
items: DOC_ITEMS
|
|
105
|
-
}));
|
|
106
|
-
const iconLast = iconMode[iconMode.length - 1];
|
|
107
|
-
// Then: --active 가 붙고 <span> 이라 클릭 이동이 없다
|
|
108
|
-
expect(iconLast.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
109
|
-
expect(itemOf(iconLast)?.tagName).toBe('SPAN');
|
|
110
|
-
// When: 라벨 모드로 다시 렌더해도
|
|
111
|
-
const labelMode = listItems(render({
|
|
112
|
-
items: DOC_ITEMS,
|
|
113
|
-
showHomeIcon: false
|
|
114
|
-
}));
|
|
115
|
-
const labelLast = labelMode[labelMode.length - 1];
|
|
116
|
-
// Then: 판정은 showHomeIcon 과 독립이다
|
|
117
|
-
expect(labelLast.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
118
|
-
expect(itemOf(labelLast)?.tagName).toBe('SPAN');
|
|
119
|
-
});
|
|
120
|
-
it('AC-6: 단일 항목이면 첫 항목이 곧 마지막 항목으로 처리된다', () => {
|
|
121
|
-
// Given/When: 항목이 하나뿐인 배열을 아이콘 모드로 렌더하면
|
|
122
|
-
const single = [{
|
|
123
|
-
href: '/',
|
|
124
|
-
label: '홈'
|
|
125
|
-
}];
|
|
126
|
-
const [iconOnly] = listItems(render({
|
|
127
|
-
items: single
|
|
128
|
-
}));
|
|
129
|
-
// Then: --home 과 --active 가 동시에 붙고 링크가 아니다
|
|
130
|
-
expect(iconOnly.classList.contains(HOME_MODIFIER)).toBe(true);
|
|
131
|
-
expect(iconOnly.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
132
|
-
expect(itemOf(iconOnly)?.tagName).toBe('SPAN');
|
|
133
|
-
expect(iconOnly.querySelector('svg')).not.toBeNull();
|
|
134
|
-
// When: 같은 배열을 라벨 모드로 렌더하면
|
|
135
|
-
const [labelOnly] = listItems(render({
|
|
136
|
-
items: single,
|
|
137
|
-
showHomeIcon: false
|
|
138
|
-
}));
|
|
139
|
-
// Then: --active 만 붙고 label 텍스트가 나온다
|
|
140
|
-
expect(labelOnly.classList.contains(HOME_MODIFIER)).toBe(false);
|
|
141
|
-
expect(labelOnly.classList.contains(ACTIVE_MODIFIER)).toBe(true);
|
|
142
|
-
expect(labelOnly.textContent).toBe(single[0].label);
|
|
143
|
-
});
|
|
144
|
-
it('AC-7: 기존 호출부 입력의 렌더 결과는 --home·aria-label 추가분 외에 변하지 않는다', () => {
|
|
145
|
-
// Given/When: DocBreadcrumbs 와 같은 입력을 showHomeIcon 미지정으로 렌더하면
|
|
146
|
-
const el = render({
|
|
147
|
-
items: DOC_ITEMS
|
|
148
|
-
});
|
|
149
|
-
const items = listItems(el);
|
|
150
|
-
// Then: 요소 종류·텍스트·href 는 변경 전과 같고, 첫 항목 svg 도 그대로다
|
|
151
|
-
expect(items).toHaveLength(DOC_ITEMS.length);
|
|
152
|
-
expect(items.map(li => itemOf(li)?.tagName)).toEqual(['A', 'A', 'SPAN']);
|
|
153
|
-
expect(items.map(li => itemOf(li)?.getAttribute('href'))).toEqual(['/', '/docs', null]);
|
|
154
|
-
expect(items.map(li => li.textContent)).toEqual(['', DOC_ITEMS[1].label, DOC_ITEMS[2].label]);
|
|
155
|
-
expect(items[0].querySelector('svg')).not.toBeNull();
|
|
156
|
-
// Then: <li> 클래스는 기존 베이스/--active 를 그대로 유지하고, 늘어난 것은 --home 뿐이다
|
|
157
|
-
expect(items.map(li => Array.from(li.classList).sort())).toEqual([['ncua-breadcrumb__list-item', HOME_MODIFIER].sort(), ['ncua-breadcrumb__list-item'], ['ncua-breadcrumb__list-item', ACTIVE_MODIFIER].sort()]);
|
|
158
|
-
// Then: <a>/<span> 에 늘어난 속성은 첫 항목의 aria-label 뿐이다
|
|
159
|
-
expect(itemOf(items[1])?.hasAttribute('aria-label')).toBe(false);
|
|
160
|
-
expect(itemOf(items[2])?.hasAttribute('aria-label')).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
it('비링크 홈 항목에는 role="img" 를 부여해 aria-label 이 무시되지 않게 한다', () => {
|
|
163
|
-
// Given/When: 첫 항목에 href 가 없어 <span> 으로 렌더되는 입력(meta usageExamples.default 형태)을 아이콘 모드로 렌더하면
|
|
164
|
-
const el = render({
|
|
165
|
-
items: [{
|
|
166
|
-
label: '대시보드'
|
|
167
|
-
}, {
|
|
168
|
-
href: '/products',
|
|
169
|
-
label: '상품'
|
|
170
|
-
}, {
|
|
171
|
-
label: '상품 상세'
|
|
172
|
-
}]
|
|
173
|
-
});
|
|
174
|
-
// Then: generic role 의 <span> 은 aria-label 을 노출하지 못하므로 role="img" 로 이름을 살린다
|
|
175
|
-
const [first] = listItems(el);
|
|
176
|
-
const home = itemOf(first);
|
|
177
|
-
expect(home?.tagName).toBe('SPAN');
|
|
178
|
-
expect(home?.getAttribute('role')).toBe('img');
|
|
179
|
-
expect(home?.getAttribute('aria-label')).toBe('대시보드');
|
|
180
|
-
});
|
|
181
|
-
it('링크인 홈 항목에는 role 을 부여하지 않는다', () => {
|
|
182
|
-
// Given/When: 첫 항목에 href 가 있어 <a> 로 렌더되는 입력을 아이콘 모드로 렌더하면
|
|
183
|
-
const el = render({
|
|
184
|
-
items: DOC_ITEMS
|
|
185
|
-
});
|
|
186
|
-
// Then: 링크는 이미 접근 가능한 이름을 지원하므로 role 을 덧씌우지 않는다 (ADR-3)
|
|
187
|
-
const items = listItems(el);
|
|
188
|
-
const home = itemOf(items[0]);
|
|
189
|
-
expect(home?.tagName).toBe('A');
|
|
190
|
-
expect(home?.hasAttribute('role')).toBe(false);
|
|
191
|
-
// Then: 홈이 아닌 항목에도 role 은 출력되지 않는다
|
|
192
|
-
expect(items.slice(1).every(li => !itemOf(li)?.hasAttribute('role'))).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
it('경계: items 가 빈 배열이면 아무것도 렌더하지 않는다', () => {
|
|
195
|
-
// Given/When: 빈 배열을 양쪽 모드로 렌더하면
|
|
196
|
-
const el = render({
|
|
197
|
-
items: [],
|
|
198
|
-
showHomeIcon: false
|
|
199
|
-
});
|
|
200
|
-
// Then: <nav> 자체가 만들어지지 않는다 (기존 동작 유지)
|
|
201
|
-
expect(el.querySelector('.ncua-breadcrumb')).toBeNull();
|
|
202
|
-
expect(el.innerHTML).toBe('');
|
|
203
|
-
});
|
|
204
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|