@ncds/ui-admin 1.8.23 → 1.8.25-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.
@@ -16,7 +16,7 @@ const BADGE_ICON_SIZE = 12;
16
16
  /** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
17
17
  const NEW_BADGE_ICON_SIZE = {
18
18
  sm: 12,
19
- md: 16
19
+ md: 14
20
20
  };
21
21
  const Badge = _ref => {
22
22
  let {
@@ -11,11 +11,15 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
11
11
  const BreadcrumbItem = _ref => {
12
12
  let {
13
13
  href,
14
+ ariaLabel,
14
15
  children
15
16
  } = _ref;
16
17
  const ElementType = href ? 'a' : 'span';
17
18
  return (0, _jsxRuntime.jsx)(ElementType, {
18
19
  href: href,
20
+ // <span> 은 generic role 이라 aria-label 이 무시될 수 있다. 링크는 이미 이름을 지원하므로 그대로 둔다
21
+ role: !href && ariaLabel ? 'img' : undefined,
22
+ "aria-label": ariaLabel,
19
23
  className: "ncua-breadcrumb__item",
20
24
  children: children
21
25
  });
@@ -23,23 +27,27 @@ const BreadcrumbItem = _ref => {
23
27
  const BreadCrumb = _ref2 => {
24
28
  let {
25
29
  items,
30
+ showHomeIcon = true,
26
31
  className
27
32
  } = _ref2;
28
33
  if (!items || items.length === 0) return null;
29
34
  const renderItem = (item, index) => {
30
35
  const isLast = index === items.length - 1;
31
- const content = index === 0 ? (0, _jsxRuntime.jsx)(_uiAdminIcon.Home01, {
32
- width: 16,
33
- height: 16,
34
- color: "gray500"
35
- }) : item.label;
36
+ // 아이콘 여부는 한 곳에서만 판정한다 — 콘텐츠·모디파이어·aria-label 이 모두 이 값을 따른다
37
+ const isHome = showHomeIcon && index === 0;
36
38
  return (0, _jsxRuntime.jsx)("li", {
37
39
  className: (0, _classnames.default)('ncua-breadcrumb__list-item', {
40
+ 'ncua-breadcrumb__list-item--home': isHome,
38
41
  'ncua-breadcrumb__list-item--active': isLast
39
42
  }),
40
43
  children: (0, _jsxRuntime.jsx)(BreadcrumbItem, {
41
44
  href: isLast ? undefined : item.href,
42
- children: content
45
+ ariaLabel: isHome ? item.label : undefined,
46
+ children: isHome ? (0, _jsxRuntime.jsx)(_uiAdminIcon.Home01, {
47
+ width: 16,
48
+ height: 16,
49
+ color: "gray500"
50
+ }) : item.label
43
51
  })
44
52
  }, index);
45
53
  };
@@ -0,0 +1,207 @@
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
+ });