@adia-ai/web-components 0.8.19 → 0.8.20

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/components/nav-group/nav-group.a2ui.json +5 -0
  3. package/components/nav-group/nav-group.class.js +14 -6
  4. package/components/nav-group/nav-group.css +5 -2
  5. package/components/nav-group/nav-group.d.ts +2 -0
  6. package/components/nav-group/nav-group.yaml +4 -0
  7. package/dist/theme-provider.min.js +1 -1
  8. package/dist/web-components.min.css +1 -1
  9. package/dist/web-components.min.js +2 -2
  10. package/dist/web-components.sheet.js +1 -1
  11. package/package.json +2 -1
  12. package/components/adia-mark/adia-mark.test.js +0 -69
  13. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  14. package/components/agent-artifact/agent-artifact.test.js +0 -61
  15. package/components/alert/alert.test.js +0 -364
  16. package/components/color-picker/color-picker.test.js +0 -96
  17. package/components/combobox/combobox.test.js +0 -256
  18. package/components/command/command.test.js +0 -99
  19. package/components/date-range-picker/date-range-picker.test.js +0 -387
  20. package/components/datetime-picker/datetime-picker.test.js +0 -454
  21. package/components/demo-toggle/demo-toggle.test.js +0 -102
  22. package/components/description-list/description-list.test.js +0 -72
  23. package/components/display-field/display-field.test.js +0 -115
  24. package/components/empty-state/empty-state.test.js +0 -77
  25. package/components/field/field.test.js +0 -260
  26. package/components/fields/fields.test.js +0 -88
  27. package/components/inline-message/inline-message.test.js +0 -203
  28. package/components/input/input.test.js +0 -335
  29. package/components/integration-card/integration-card.test.js +0 -318
  30. package/components/link/link.test.js +0 -143
  31. package/components/list/list.test.js +0 -106
  32. package/components/list-window/list-window.test.js +0 -303
  33. package/components/loading-overlay/loading-overlay.test.js +0 -257
  34. package/components/menu/menu.test.js +0 -176
  35. package/components/nav/nav.test.js +0 -428
  36. package/components/nav-group/nav-group.test.js +0 -67
  37. package/components/option-card/option-card.test.js +0 -32
  38. package/components/pagination/pagination.test.js +0 -195
  39. package/components/select/select.test.js +0 -481
  40. package/components/slider/slider.test.js +0 -202
  41. package/components/spinner/spinner.test.js +0 -272
  42. package/components/stat/stat.test.js +0 -108
  43. package/components/step-progress/step-progress.test.js +0 -129
  44. package/components/swatch/swatch.test.js +0 -563
  45. package/components/table/table.test.js +0 -479
  46. package/components/table-toolbar/table-toolbar.test.js +0 -152
  47. package/components/tag/tag.test.js +0 -137
  48. package/components/tags-input/tags-input.test.js +0 -477
  49. package/components/text/text.test.js +0 -202
  50. package/components/theme-provider/theme-provider.test.js +0 -94
  51. package/components/time-picker/time-picker.test.js +0 -287
  52. package/components/timeline/timeline.test.js +0 -31
  53. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  54. package/components/tree/tree.test.js +0 -312
  55. package/core/element.test.js +0 -389
  56. package/core/icons.test.js +0 -187
  57. package/core/logical-children.test.js +0 -54
  58. package/core/markdown.test.js +0 -47
  59. package/core/responsive.test.js +0 -121
  60. package/core/template.test.js +0 -611
  61. package/traits/active-state/active-state.test.js +0 -28
  62. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  63. package/traits/announcer/announcer.test.js +0 -268
  64. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  65. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  66. package/traits/confetti/confetti.test.js +0 -46
  67. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  68. package/traits/count-up/count-up.test.js +0 -54
  69. package/traits/declarative.test.js +0 -138
  70. package/traits/dirty-state/dirty-state.test.js +0 -45
  71. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  72. package/traits/draggable/draggable.test.js +0 -60
  73. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  74. package/traits/drop-target/drop-target.test.js +0 -241
  75. package/traits/droppable/droppable.test.js +0 -54
  76. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  77. package/traits/error-shake/error-shake.test.js +0 -114
  78. package/traits/fade-presence/fade-presence.test.js +0 -20
  79. package/traits/focus-restore/focus-restore.test.js +0 -202
  80. package/traits/focus-trap/focus-trap.test.js +0 -42
  81. package/traits/focusable/focusable.test.js +0 -53
  82. package/traits/glow-focus/glow-focus.test.js +0 -31
  83. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  84. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  85. package/traits/hotkey/hotkey.test.js +0 -61
  86. package/traits/hoverable/hoverable.test.js +0 -24
  87. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  88. package/traits/input-mask/input-mask.test.js +0 -151
  89. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  90. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  91. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  92. package/traits/layout-animation/layout-animation.test.js +0 -114
  93. package/traits/long-press/long-press.test.js +0 -244
  94. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  95. package/traits/noise-texture/noise-texture.test.js +0 -20
  96. package/traits/parallax/parallax.test.js +0 -26
  97. package/traits/portal/portal.test.js +0 -30
  98. package/traits/pressable/pressable.test.js +0 -73
  99. package/traits/resettable/resettable.test.js +0 -100
  100. package/traits/resizable/resizable.test.js +0 -39
  101. package/traits/resize-observer/resize-observer.test.js +0 -38
  102. package/traits/ripple/ripple.test.js +0 -32
  103. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  104. package/traits/scale-press/scale-press.test.js +0 -39
  105. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  106. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  107. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  108. package/traits/smoke.test.js +0 -111
  109. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  110. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  111. package/traits/spring-animate/spring-animate.test.js +0 -28
  112. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  113. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  114. package/traits/tossable/tossable.test.js +0 -31
  115. package/traits/traits-host.test.js +0 -73
  116. package/traits/typeahead/typeahead.test.js +0 -38
  117. package/traits/typewriter/typewriter.test.js +0 -47
  118. package/traits/validation/validation.test.js +0 -93
  119. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,176 +0,0 @@
1
- /**
2
- * <menu-ui> behavioral tests.
3
- *
4
- * FEEDBACK-92 (2026-05-31): `#show()` collected items with a direct-child
5
- * `:scope >` query, so items rendered through the template engine's
6
- * `.map()` / `repeat()` (each wrapped in a `display:contents` <span>) were
7
- * skipped → the popover opened empty. The fix uses a descendant query
8
- * (mirroring `#hide()`). These tests pin the dynamic + static paths.
9
- */
10
-
11
- import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
12
- import { html, stamp, repeat } from '../../core/template.js';
13
-
14
- beforeAll(async () => {
15
- await import('../../core/element.js');
16
- await import('./menu.js');
17
- await import('../button/button.js');
18
- });
19
-
20
- const settle = () => new Promise((r) => setTimeout(r, 30));
21
-
22
- describe('<menu-ui> collects dynamically-rendered items (FEEDBACK-92)', () => {
23
- let host;
24
-
25
- beforeEach(() => {
26
- host = document.createElement('div');
27
- document.body.appendChild(host);
28
- });
29
- afterEach(() => host.remove());
30
-
31
- function popItems(menu) {
32
- const pop = menu.querySelector('[data-menu-popover]');
33
- return pop ? pop.querySelectorAll('menu-item-ui') : [];
34
- }
35
-
36
- it('populates the popover from `.map()`-rendered items (the bug)', async () => {
37
- const ITEMS = [{ id: 'a', text: 'Alpha' }, { id: 'b', text: 'Beta' }];
38
- // Render the menu (closed) the documented way — items via `.map()`. The
39
- // consumer opens it later via interaction, after the template has rendered.
40
- stamp(html`
41
- <menu-ui>
42
- <button-ui slot="trigger" text="Open"></button-ui>
43
- ${ITEMS.map((i) => html`<menu-item-ui .text=${i.text} .value=${i.id}></menu-item-ui>`)}
44
- </menu-ui>
45
- `, host);
46
- await settle();
47
-
48
- const menu = host.querySelector('menu-ui');
49
- // The exact bug condition: items exist under the host but are NOT direct
50
- // children — the template engine nested them inside display:contents spans,
51
- // which the old `:scope >` query in #show() could not see.
52
- expect(menu.querySelectorAll('menu-item-ui').length).toBe(2);
53
- expect(menu.querySelector(':scope > menu-item-ui')).toBeNull();
54
-
55
- menu.open = true; // user opens the menu post-render
56
- await settle();
57
-
58
- const items = popItems(menu);
59
- expect(items.length).toBe(2);
60
- expect([...items].map((el) => el.value)).toEqual(['a', 'b']);
61
- });
62
-
63
- it('populates the popover from `repeat()`-rendered items', async () => {
64
- const ITEMS = [{ id: 'x', text: 'Ex' }, { id: 'y', text: 'Why' }, { id: 'z', text: 'Zee' }];
65
- stamp(html`
66
- <menu-ui>
67
- <button-ui slot="trigger" text="Open"></button-ui>
68
- ${repeat(ITEMS, (i) => i.id, (i) => html`<menu-item-ui .text=${i.text} .value=${i.id}></menu-item-ui>`)}
69
- </menu-ui>
70
- `, host);
71
- await settle();
72
-
73
- const menu = host.querySelector('menu-ui');
74
- expect(menu.querySelector(':scope > menu-item-ui')).toBeNull(); // behind wrappers
75
- menu.open = true;
76
- await settle();
77
- expect(popItems(menu).length).toBe(3);
78
- });
79
-
80
- it('still populates from static literal children (no regression)', async () => {
81
- const menu = document.createElement('menu-ui');
82
- menu.innerHTML = `
83
- <button-ui slot="trigger" text="Open"></button-ui>
84
- <menu-item-ui text="Edit" value="edit"></menu-item-ui>
85
- <menu-divider-ui></menu-divider-ui>
86
- <menu-item-ui text="Delete" value="delete"></menu-item-ui>`;
87
- host.appendChild(menu);
88
- await settle();
89
- menu.open = true;
90
- await settle();
91
-
92
- expect(popItems(menu).length).toBe(2);
93
- const pop = menu.querySelector('[data-menu-popover]');
94
- expect(pop.querySelectorAll('menu-divider-ui').length).toBe(1);
95
- });
96
-
97
- it('does not absorb the trigger into the popover', async () => {
98
- const menu = document.createElement('menu-ui');
99
- menu.innerHTML = `
100
- <button-ui slot="trigger" text="Open"></button-ui>
101
- <menu-item-ui text="Edit" value="edit"></menu-item-ui>`;
102
- host.appendChild(menu);
103
- await settle();
104
- menu.open = true;
105
- await settle();
106
-
107
- const pop = menu.querySelector('[data-menu-popover]');
108
- expect(pop.querySelector('[slot="trigger"]')).toBeNull();
109
- expect(menu.querySelector(':scope > [slot="trigger"]')).not.toBeNull();
110
- });
111
-
112
- it('does not reorder items on a re-entrant #show() (open → re-render → still open)', async () => {
113
- const menu = document.createElement('menu-ui');
114
- menu.innerHTML = `
115
- <button-ui slot="trigger" text="Open"></button-ui>
116
- <menu-item-ui text="One" value="1"></menu-item-ui>
117
- <menu-item-ui text="Two" value="2"></menu-item-ui>`;
118
- host.appendChild(menu);
119
- await settle();
120
- menu.open = true;
121
- await settle();
122
- // Force another render() pass while still open (re-invokes #show()).
123
- menu.placement = 'top-start';
124
- await settle();
125
-
126
- const pop = menu.querySelector('[data-menu-popover]');
127
- const order = [...pop.querySelectorAll('menu-item-ui')].map((el) => el.value);
128
- expect(order).toEqual(['1', '2']);
129
- });
130
- });
131
-
132
- describe('<menu-label-ui> group heading', () => {
133
- let host;
134
- beforeEach(() => { host = document.createElement('div'); document.body.appendChild(host); });
135
- afterEach(() => host.remove());
136
-
137
- it('hoists labels into the popover, stamps [text], and marks them presentational', async () => {
138
- const menu = document.createElement('menu-ui');
139
- menu.innerHTML = `
140
- <button-ui slot="trigger" text="Open"></button-ui>
141
- <menu-label-ui text="Group A"></menu-label-ui>
142
- <menu-item-ui text="One" value="1"></menu-item-ui>
143
- <menu-divider-ui></menu-divider-ui>
144
- <menu-label-ui text="Group B"></menu-label-ui>
145
- <menu-item-ui text="Two" value="2"></menu-item-ui>`;
146
- host.appendChild(menu);
147
- await settle();
148
- menu.open = true;
149
- await settle();
150
-
151
- const pop = menu.querySelector('[data-menu-popover]');
152
- const labels = pop.querySelectorAll('menu-label-ui');
153
- expect(labels.length).toBe(2);
154
- expect(labels[0].getAttribute('role')).toBe('presentation');
155
- expect(labels[0].textContent.trim()).toBe('Group A');
156
- expect(labels[1].textContent.trim()).toBe('Group B');
157
- });
158
-
159
- it('is non-interactive — outside the menuitem set the roving focus queries', async () => {
160
- const menu = document.createElement('menu-ui');
161
- menu.innerHTML = `
162
- <button-ui slot="trigger" text="Open"></button-ui>
163
- <menu-label-ui text="Heading"></menu-label-ui>
164
- <menu-item-ui text="One" value="1"></menu-item-ui>`;
165
- host.appendChild(menu);
166
- await settle();
167
- menu.open = true;
168
- await settle();
169
-
170
- const pop = menu.querySelector('[data-menu-popover]');
171
- // A label is neither a menu-item-ui nor role="menuitem", so #enabledItems()
172
- // (a `menu-item-ui` query) never lands arrow-key focus on it.
173
- expect(pop.querySelector('menu-label-ui[role="menuitem"]')).toBeNull();
174
- expect(pop.querySelectorAll('menu-item-ui').length).toBe(1);
175
- });
176
- });
@@ -1,428 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import '../../core/element.js';
3
- import './nav.js';
4
- import '../nav-group/nav-group.js';
5
- import '../nav-item/nav-item.js';
6
-
7
- const tick = () => new Promise((r) => queueMicrotask(r));
8
-
9
- function mount(html) {
10
- const wrap = document.createElement('div');
11
- wrap.innerHTML = html;
12
- document.body.appendChild(wrap);
13
- return wrap.firstElementChild;
14
- }
15
-
16
- describe('nav-ui', () => {
17
- beforeEach(() => { document.body.innerHTML = ''; });
18
-
19
- it('registers nav-ui, nav-group-ui, and nav-item-ui as custom elements', () => {
20
- expect(customElements.get('nav-ui')).toBeDefined();
21
- expect(customElements.get('nav-group-ui')).toBeDefined();
22
- expect(customElements.get('nav-item-ui')).toBeDefined();
23
- });
24
-
25
- it('defaults to variant="primary" with role=navigation', () => {
26
- const nav = mount('<nav-ui></nav-ui>');
27
- expect(nav.variant).toBe('primary');
28
- expect(nav.getAttribute('role')).toBe('navigation');
29
- });
30
-
31
- it('reflects [variant="section"] from the property', () => {
32
- const nav = mount('<nav-ui variant="section"></nav-ui>');
33
- expect(nav.variant).toBe('section');
34
- expect(nav.getAttribute('variant')).toBe('section');
35
- });
36
-
37
- it('uses [heading] as aria-label and removes it when cleared', async () => {
38
- const nav = mount('<nav-ui heading="On this page"></nav-ui>');
39
- expect(nav.getAttribute('aria-label')).toBe('On this page');
40
- nav.heading = '';
41
- await tick();
42
- expect(nav.hasAttribute('aria-label')).toBe(false);
43
- });
44
-
45
- it('section variant skips the ResizeObserver wiring', () => {
46
- const primary = mount('<nav-ui></nav-ui>');
47
- const section = mount('<nav-ui variant="section"></nav-ui>');
48
- // The internal ResizeObserver is private (`#ro`); verify by the
49
- // observable surface — toggle() is a no-op on section, but the
50
- // primary nav can be flipped collapsed/uncollapsed.
51
- section.toggle();
52
- expect(section.collapsed).toBe(false);
53
- primary.toggle();
54
- expect(primary.collapsed).toBe(true);
55
- });
56
-
57
- it('select(item) sets [selected], clears prior selection, and bubbles nav-select', () => {
58
- const nav = mount(`
59
- <nav-ui>
60
- <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
61
- <nav-item-ui id="b" text="Bravo" value="/b" selected></nav-item-ui>
62
- </nav-ui>
63
- `);
64
- const a = nav.querySelector('#a');
65
- const b = nav.querySelector('#b');
66
-
67
- const events = [];
68
- nav.addEventListener('nav-select', (e) => events.push(e.detail));
69
-
70
- nav.select(a);
71
-
72
- expect(a.hasAttribute('selected')).toBe(true);
73
- expect(b.hasAttribute('selected')).toBe(false);
74
- expect(nav.selectedItem).toBe(a);
75
- expect(events).toHaveLength(1);
76
- expect(events[0]).toMatchObject({ item: a, text: 'Alpha', value: '/a' });
77
- });
78
-
79
- it('selectedItem getter returns null when no item is selected', () => {
80
- const nav = mount(`
81
- <nav-ui>
82
- <nav-item-ui text="Alpha" value="/a"></nav-item-ui>
83
- </nav-ui>
84
- `);
85
- expect(nav.selectedItem).toBeNull();
86
- });
87
-
88
- it('toggle() is a no-op on variant="section"', () => {
89
- const nav = mount('<nav-ui variant="section"></nav-ui>');
90
- nav.toggle();
91
- expect(nav.collapsed).toBe(false);
92
- nav.toggle();
93
- expect(nav.collapsed).toBe(false);
94
- });
95
-
96
- it('survives disconnect without throwing', () => {
97
- const nav = mount(`
98
- <nav-ui>
99
- <nav-item-ui text="Alpha"></nav-item-ui>
100
- </nav-ui>
101
- `);
102
- expect(() => nav.remove()).not.toThrow();
103
- });
104
- });
105
-
106
- describe('nav-item-ui', () => {
107
- beforeEach(() => { document.body.innerHTML = ''; });
108
-
109
- it('sets role=link and tabindex=0 by default', () => {
110
- const nav = mount(`
111
- <nav-ui>
112
- <nav-item-ui text="Alpha"></nav-item-ui>
113
- </nav-ui>
114
- `);
115
- const item = nav.querySelector('nav-item-ui');
116
- expect(item.getAttribute('role')).toBe('link');
117
- expect(item.getAttribute('tabindex')).toBe('0');
118
- });
119
-
120
- it('switches tabindex to -1 when [disabled]', async () => {
121
- const nav = mount(`
122
- <nav-ui>
123
- <nav-item-ui text="Alpha"></nav-item-ui>
124
- </nav-ui>
125
- `);
126
- const item = nav.querySelector('nav-item-ui');
127
- item.disabled = true;
128
- await tick();
129
- expect(item.getAttribute('tabindex')).toBe('-1');
130
- });
131
-
132
- it('click on the item calls parent nav-ui.select(this)', () => {
133
- const nav = mount(`
134
- <nav-ui>
135
- <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
136
- </nav-ui>
137
- `);
138
- const item = nav.querySelector('#a');
139
- const spy = vi.spyOn(nav, 'select');
140
-
141
- item.click();
142
-
143
- expect(spy).toHaveBeenCalledWith(item);
144
- });
145
-
146
- it('Enter key activates a nav-item-ui (same as click)', () => {
147
- const nav = mount(`
148
- <nav-ui>
149
- <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
150
- </nav-ui>
151
- `);
152
- const item = nav.querySelector('#a');
153
- const spy = vi.spyOn(nav, 'select');
154
-
155
- item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
156
-
157
- expect(spy).toHaveBeenCalledWith(item);
158
- });
159
-
160
- it('Space key activates a nav-item-ui', () => {
161
- const nav = mount(`
162
- <nav-ui>
163
- <nav-item-ui id="a" text="Alpha"></nav-item-ui>
164
- </nav-ui>
165
- `);
166
- const item = nav.querySelector('#a');
167
- const spy = vi.spyOn(nav, 'select');
168
-
169
- item.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
170
-
171
- expect(spy).toHaveBeenCalledWith(item);
172
- });
173
-
174
- it('disabled nav-item-ui does not activate on click or keyboard', () => {
175
- const nav = mount(`
176
- <nav-ui>
177
- <nav-item-ui id="a" text="Alpha" disabled></nav-item-ui>
178
- </nav-ui>
179
- `);
180
- const item = nav.querySelector('#a');
181
- const spy = vi.spyOn(nav, 'select');
182
-
183
- item.click();
184
- item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
185
-
186
- expect(spy).not.toHaveBeenCalled();
187
- });
188
-
189
- it('selected item carries aria-current="page"', async () => {
190
- const nav = mount(`
191
- <nav-ui>
192
- <nav-item-ui id="a" text="Alpha"></nav-item-ui>
193
- </nav-ui>
194
- `);
195
- const item = nav.querySelector('#a');
196
- nav.select(item);
197
- await tick();
198
- expect(item.getAttribute('aria-current')).toBe('page');
199
- });
200
-
201
- it('clearing selected removes aria-current', async () => {
202
- const nav = mount(`
203
- <nav-ui>
204
- <nav-item-ui id="a" text="Alpha"></nav-item-ui>
205
- <nav-item-ui id="b" text="Bravo"></nav-item-ui>
206
- </nav-ui>
207
- `);
208
- const a = nav.querySelector('#a');
209
- const b = nav.querySelector('#b');
210
- nav.select(a);
211
- await tick();
212
- expect(a.getAttribute('aria-current')).toBe('page');
213
- nav.select(b);
214
- await tick();
215
- expect(a.hasAttribute('aria-current')).toBe(false);
216
- expect(b.getAttribute('aria-current')).toBe('page');
217
- });
218
- });
219
-
220
- describe('nav-group-ui', () => {
221
- beforeEach(() => { document.body.innerHTML = ''; });
222
-
223
- it('auto-mints a header div with icon/text/caret slots', () => {
224
- const nav = mount(`
225
- <nav-ui>
226
- <nav-group-ui text="Settings" icon="gear">
227
- <nav-item-ui text="General"></nav-item-ui>
228
- </nav-group-ui>
229
- </nav-ui>
230
- `);
231
- const group = nav.querySelector('nav-group-ui');
232
- const header = group.querySelector(':scope > [slot="header"]');
233
- expect(header).not.toBeNull();
234
- expect(header.querySelector('[slot="text"]').textContent).toBe('Settings');
235
- expect(header.querySelector('icon-ui[slot="caret"]')).not.toBeNull();
236
- });
237
-
238
- it('renders [badge] in the header when set', () => {
239
- const nav = mount(`
240
- <nav-ui>
241
- <nav-group-ui text="Settings" badge="3"></nav-group-ui>
242
- </nav-ui>
243
- `);
244
- const group = nav.querySelector('nav-group-ui');
245
- const badge = group.querySelector('[slot="header"] [slot="badge"]');
246
- expect(badge).not.toBeNull();
247
- expect(badge.textContent).toBe('3');
248
- });
249
-
250
- it('header click toggles [open] when not collapsed (delegated through nav-ui)', async () => {
251
- const nav = mount(`
252
- <nav-ui>
253
- <nav-group-ui text="Settings"></nav-group-ui>
254
- </nav-ui>
255
- `);
256
- // happy-dom returns 0 for getBoundingClientRect width, which makes the
257
- // nav-ui treat itself as collapsed and route clicks to popover instead
258
- // of inline expand. Stub the rect so the inline-expand path fires.
259
- nav.getBoundingClientRect = () => ({ width: 240, height: 600, top: 0, left: 0, right: 240, bottom: 600, x: 0, y: 0 });
260
-
261
- const group = nav.querySelector('nav-group-ui');
262
- const header = group.querySelector(':scope > [slot="header"]');
263
- expect(group.open).toBe(false);
264
-
265
- header.click();
266
- await tick();
267
-
268
- expect(group.open).toBe(true);
269
- });
270
-
271
- it('keydown Enter on header is ignored when collapsible=false', () => {
272
- const nav = mount(`
273
- <nav-ui>
274
- <nav-group-ui text="Settings" open></nav-group-ui>
275
- </nav-ui>
276
- `);
277
- const group = nav.querySelector('nav-group-ui');
278
- group.collapsible = false;
279
-
280
- const header = group.querySelector(':scope > [slot="header"]');
281
- const events = [];
282
- group.addEventListener('group-toggle', (e) => events.push(e.detail));
283
-
284
- header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
285
-
286
- expect(events).toHaveLength(0);
287
- expect(group.open).toBe(true);
288
- });
289
-
290
- it('aria-expanded reflects the open state', async () => {
291
- const nav = mount(`
292
- <nav-ui>
293
- <nav-group-ui text="Settings"></nav-group-ui>
294
- </nav-ui>
295
- `);
296
- const group = nav.querySelector('nav-group-ui');
297
- expect(group.getAttribute('aria-expanded')).toBe('false');
298
- group.open = true;
299
- await tick();
300
- expect(group.getAttribute('aria-expanded')).toBe('true');
301
- });
302
-
303
- it('Enter on the header dispatches group-toggle when collapsible', () => {
304
- const nav = mount(`
305
- <nav-ui>
306
- <nav-group-ui text="Settings"></nav-group-ui>
307
- </nav-ui>
308
- `);
309
- const group = nav.querySelector('nav-group-ui');
310
- const header = group.querySelector(':scope > [slot="header"]');
311
-
312
- const events = [];
313
- group.addEventListener('group-toggle', (e) => events.push(e.detail));
314
-
315
- header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
316
-
317
- expect(events).toHaveLength(1);
318
- expect(events[0]).toMatchObject({ text: 'Settings', open: true });
319
- });
320
-
321
- it('respects an existing [slot="header"] instead of auto-minting one', () => {
322
- const nav = mount(`
323
- <nav-ui>
324
- <nav-group-ui text="Settings">
325
- <div slot="header" data-custom>my header</div>
326
- </nav-group-ui>
327
- </nav-ui>
328
- `);
329
- const group = nav.querySelector('nav-group-ui');
330
- const headers = group.querySelectorAll(':scope > [slot="header"]');
331
- expect(headers.length).toBe(1);
332
- expect(headers[0].dataset.custom).toBe('');
333
- });
334
-
335
- it('survives disconnect without throwing', () => {
336
- const nav = mount(`
337
- <nav-ui>
338
- <nav-group-ui text="Settings">
339
- <nav-item-ui text="General"></nav-item-ui>
340
- </nav-group-ui>
341
- </nav-ui>
342
- `);
343
- expect(() => nav.remove()).not.toThrow();
344
- });
345
-
346
- it('section-variant subnav stamps groups + items per the docs site pattern', () => {
347
- // Mirrors site/site.js subnav stamping for `subnav: true` sections:
348
- // dividers in the sitemap become nav-group-ui kickers; following items
349
- // become nav-item-ui children of the most recent group.
350
- const nav = mount(`
351
- <nav-ui variant="section" aria-label="Authentication">
352
- <nav-group-ui text="Sign in">
353
- <nav-item-ui text="Sign In" value="/auth/sign-in"></nav-item-ui>
354
- <nav-item-ui text="Password" value="/auth/sign-in/password"></nav-item-ui>
355
- </nav-group-ui>
356
- <nav-group-ui text="Sign up">
357
- <nav-item-ui text="Sign Up" value="/auth/sign-up"></nav-item-ui>
358
- </nav-group-ui>
359
- </nav-ui>
360
- `);
361
- const groups = nav.querySelectorAll(':scope > nav-group-ui');
362
- expect(groups.length).toBe(2);
363
- // Each group has its expected children attached (not stripped at mount).
364
- expect(groups[0].querySelectorAll('nav-item-ui').length).toBe(2);
365
- expect(groups[1].querySelectorAll('nav-item-ui').length).toBe(1);
366
- // Group headers are auto-minted with the kicker text.
367
- expect(groups[0].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign in');
368
- expect(groups[1].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign up');
369
- });
370
- });
371
-
372
- describe('section variant — explicit + cascade', () => {
373
- beforeEach(() => { document.body.innerHTML = ''; });
374
-
375
- it('nav-group-ui[variant="section"] reflects the property', () => {
376
- const group = mount('<nav-group-ui variant="section" text="Settings"></nav-group-ui>');
377
- expect(group.variant).toBe('section');
378
- expect(group.getAttribute('variant')).toBe('section');
379
- });
380
-
381
- it('nav-item-ui[variant="section"] reflects the property', () => {
382
- const item = mount('<nav-item-ui variant="section" text="General"></nav-item-ui>');
383
- expect(item.variant).toBe('section');
384
- expect(item.getAttribute('variant')).toBe('section');
385
- });
386
-
387
- it('nav-group-ui defaults to variant="" (empty) when not set', () => {
388
- const group = mount('<nav-group-ui text="Settings"></nav-group-ui>');
389
- expect(group.variant).toBe('');
390
- });
391
-
392
- it('nav-item-ui defaults to variant="" (empty) when not set', () => {
393
- const item = mount('<nav-item-ui text="General"></nav-item-ui>');
394
- expect(item.variant).toBe('');
395
- });
396
-
397
- it('cascade: nav-ui[variant="section"] does NOT mutate the child variant prop', () => {
398
- // The cascade is CSS-only — children keep their own (empty) variant.
399
- // CSS rules target `nav-ui[variant="section"] > x:not([variant])` so
400
- // the parent's variant only governs styling, not the JS state.
401
- const nav = mount(`
402
- <nav-ui variant="section">
403
- <nav-group-ui text="Settings">
404
- <nav-item-ui text="General"></nav-item-ui>
405
- </nav-group-ui>
406
- </nav-ui>
407
- `);
408
- const group = nav.querySelector('nav-group-ui');
409
- const item = nav.querySelector('nav-item-ui');
410
- expect(group.variant).toBe('');
411
- expect(item.variant).toBe('');
412
- });
413
-
414
- it('explicit variant on a child wins over the parent cascade', () => {
415
- // A child can opt out of the section cascade by setting its own
416
- // variant. The CSS rules use `:not([variant])` for the cascade path,
417
- // so an explicit non-section variant on the child suppresses kicker
418
- // styling and the child renders primary-style.
419
- const nav = mount(`
420
- <nav-ui variant="section">
421
- <nav-group-ui text="Override" variant="primary"></nav-group-ui>
422
- </nav-ui>
423
- `);
424
- const group = nav.querySelector('nav-group-ui');
425
- expect(group.variant).toBe('primary');
426
- expect(group.matches('[variant]')).toBe(true);
427
- });
428
- });
@@ -1,67 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import '../../core/element.js';
3
- import './nav-group.js';
4
-
5
- const tick = () => new Promise((r) => queueMicrotask(r));
6
- function mount(html) {
7
- const w = document.createElement('div');
8
- w.innerHTML = html;
9
- document.body.appendChild(w);
10
- return w.firstElementChild;
11
- }
12
-
13
- describe('nav-group-ui — header stamp-if-absent (wrapper trap)', () => {
14
- beforeEach(() => { document.body.innerHTML = ''; });
15
-
16
- it('stamps a header when the consumer provides none', async () => {
17
- const el = mount('<nav-group-ui text="Reports"></nav-group-ui>');
18
- await tick();
19
- expect(el.querySelectorAll('[slot="header"]').length).toBe(1);
20
- });
21
-
22
- // Regression (audit-wrapper-trap arm 2): a `.map()`/conditional-interpolated
23
- // [slot="header"] arrives nested in a display:contents wrapper. The old
24
- // `:scope > [slot="header"]` absence check missed it and stamped a DUPLICATE.
25
- it('does NOT stamp a duplicate when a WRAPPED consumer header is present', async () => {
26
- const el = mount(`<nav-group-ui text="Reports"><span style="display:contents" role="presentation"><div slot="header" data-test="h">Custom</div></span></nav-group-ui>`);
27
- await tick();
28
- const headers = el.querySelectorAll('[slot="header"]');
29
- expect(headers.length).toBe(1);
30
- expect(headers[0].dataset.test).toBe('h');
31
- });
32
- });
33
-
34
- describe('nav-group-ui — [open] drives the header icon weight', () => {
35
- beforeEach(() => { document.body.innerHTML = ''; });
36
-
37
- it('defaults the auto-stamped header icon to weight="regular"', async () => {
38
- const el = mount('<nav-group-ui text="Reports" icon="folder"></nav-group-ui>');
39
- await tick();
40
- const icon = el.querySelector('[slot="header"] [slot="icon"] icon-ui');
41
- expect(icon.getAttribute('weight')).toBe('regular');
42
- });
43
-
44
- it('switches the header icon to weight="fill" when [open] is set', async () => {
45
- const el = mount('<nav-group-ui text="Reports" icon="folder"></nav-group-ui>');
46
- await tick();
47
- el.open = true;
48
- await tick();
49
- const icon = el.querySelector('[slot="header"] [slot="icon"] icon-ui');
50
- expect(icon.getAttribute('weight')).toBe('fill');
51
- });
52
-
53
- it('reverts to weight="regular" when [open] is cleared', async () => {
54
- const el = mount('<nav-group-ui text="Reports" icon="folder" open></nav-group-ui>');
55
- await tick();
56
- el.open = false;
57
- await tick();
58
- const icon = el.querySelector('[slot="header"] [slot="icon"] icon-ui');
59
- expect(icon.getAttribute('weight')).toBe('regular');
60
- });
61
-
62
- it('is a no-op when the group has no icon (empty icon slot, no icon-ui)', async () => {
63
- const el = mount('<nav-group-ui text="Reports" open></nav-group-ui>');
64
- await tick();
65
- expect(el.querySelector('[slot="header"] [slot="icon"] icon-ui')).toBeNull();
66
- });
67
- });