@ncds/ui-admin 1.8.22 → 1.8.23-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/cjs/src/components/navigation/bread-crumb/BreadCrumb.js +14 -6
- package/dist/cjs/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +207 -0
- 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 +14 -6
- package/dist/esm/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +204 -0
- 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 +3 -1
- package/dist/temp/src/components/navigation/bread-crumb/BreadCrumb.js +9 -5
- package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +1 -0
- package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +161 -0
- 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 +3 -1
- package/dist/types/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +1 -0
- package/dist/ui-admin/assets/styles/style.css +25 -19
- package/package.json +1 -1
|
@@ -4,11 +4,15 @@ import classNames from 'classnames';
|
|
|
4
4
|
const BreadcrumbItem = _ref => {
|
|
5
5
|
let {
|
|
6
6
|
href,
|
|
7
|
+
ariaLabel,
|
|
7
8
|
children
|
|
8
9
|
} = _ref;
|
|
9
10
|
const ElementType = href ? 'a' : 'span';
|
|
10
11
|
return _jsx(ElementType, {
|
|
11
12
|
href: href,
|
|
13
|
+
// <span> 은 generic role 이라 aria-label 이 무시될 수 있다. 링크는 이미 이름을 지원하므로 그대로 둔다
|
|
14
|
+
role: !href && ariaLabel ? 'img' : undefined,
|
|
15
|
+
"aria-label": ariaLabel,
|
|
12
16
|
className: "ncua-breadcrumb__item",
|
|
13
17
|
children: children
|
|
14
18
|
});
|
|
@@ -16,23 +20,27 @@ const BreadcrumbItem = _ref => {
|
|
|
16
20
|
const BreadCrumb = _ref2 => {
|
|
17
21
|
let {
|
|
18
22
|
items,
|
|
23
|
+
showHomeIcon = true,
|
|
19
24
|
className
|
|
20
25
|
} = _ref2;
|
|
21
26
|
if (!items || items.length === 0) return null;
|
|
22
27
|
const renderItem = (item, index) => {
|
|
23
28
|
const isLast = index === items.length - 1;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
height: 16,
|
|
27
|
-
color: "gray500"
|
|
28
|
-
}) : item.label;
|
|
29
|
+
// 아이콘 여부는 한 곳에서만 판정한다 — 콘텐츠·모디파이어·aria-label 이 모두 이 값을 따른다
|
|
30
|
+
const isHome = showHomeIcon && index === 0;
|
|
29
31
|
return _jsx("li", {
|
|
30
32
|
className: classNames('ncua-breadcrumb__list-item', {
|
|
33
|
+
'ncua-breadcrumb__list-item--home': isHome,
|
|
31
34
|
'ncua-breadcrumb__list-item--active': isLast
|
|
32
35
|
}),
|
|
33
36
|
children: _jsx(BreadcrumbItem, {
|
|
34
37
|
href: isLast ? undefined : item.href,
|
|
35
|
-
|
|
38
|
+
ariaLabel: isHome ? item.label : undefined,
|
|
39
|
+
children: isHome ? _jsx(Home01, {
|
|
40
|
+
width: 16,
|
|
41
|
+
height: 16,
|
|
42
|
+
color: "gray500"
|
|
43
|
+
}) : item.label
|
|
36
44
|
})
|
|
37
45
|
}, index);
|
|
38
46
|
};
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
});
|