@adia-ai/web-components 0.8.18 → 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 (126) hide show
  1. package/CHANGELOG.md +26 -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/components/select/select.a2ui.json +6 -1
  8. package/components/select/select.class.js +34 -7
  9. package/components/select/select.d.ts +2 -0
  10. package/components/select/select.yaml +16 -1
  11. package/dist/host.min.css +1 -1
  12. package/dist/host.sheet.js +1 -1
  13. package/dist/theme-provider.min.js +1 -1
  14. package/dist/web-components.min.css +1 -1
  15. package/dist/web-components.min.js +18 -18
  16. package/dist/web-components.sheet.js +1 -1
  17. package/package.json +2 -1
  18. package/styles/colors/semantics/core.css +10 -4
  19. package/components/adia-mark/adia-mark.test.js +0 -69
  20. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  21. package/components/agent-artifact/agent-artifact.test.js +0 -61
  22. package/components/alert/alert.test.js +0 -364
  23. package/components/color-picker/color-picker.test.js +0 -96
  24. package/components/combobox/combobox.test.js +0 -256
  25. package/components/command/command.test.js +0 -99
  26. package/components/date-range-picker/date-range-picker.test.js +0 -387
  27. package/components/datetime-picker/datetime-picker.test.js +0 -454
  28. package/components/demo-toggle/demo-toggle.test.js +0 -102
  29. package/components/description-list/description-list.test.js +0 -72
  30. package/components/display-field/display-field.test.js +0 -115
  31. package/components/empty-state/empty-state.test.js +0 -77
  32. package/components/field/field.test.js +0 -260
  33. package/components/fields/fields.test.js +0 -88
  34. package/components/inline-message/inline-message.test.js +0 -203
  35. package/components/input/input.test.js +0 -335
  36. package/components/integration-card/integration-card.test.js +0 -318
  37. package/components/link/link.test.js +0 -143
  38. package/components/list/list.test.js +0 -106
  39. package/components/list-window/list-window.test.js +0 -303
  40. package/components/loading-overlay/loading-overlay.test.js +0 -257
  41. package/components/menu/menu.test.js +0 -176
  42. package/components/nav/nav.test.js +0 -428
  43. package/components/nav-group/nav-group.test.js +0 -67
  44. package/components/option-card/option-card.test.js +0 -32
  45. package/components/pagination/pagination.test.js +0 -195
  46. package/components/select/select.test.js +0 -453
  47. package/components/slider/slider.test.js +0 -202
  48. package/components/spinner/spinner.test.js +0 -272
  49. package/components/stat/stat.test.js +0 -108
  50. package/components/step-progress/step-progress.test.js +0 -129
  51. package/components/swatch/swatch.test.js +0 -563
  52. package/components/table/table.test.js +0 -479
  53. package/components/table-toolbar/table-toolbar.test.js +0 -152
  54. package/components/tag/tag.test.js +0 -137
  55. package/components/tags-input/tags-input.test.js +0 -477
  56. package/components/text/text.test.js +0 -202
  57. package/components/theme-provider/theme-provider.test.js +0 -94
  58. package/components/time-picker/time-picker.test.js +0 -287
  59. package/components/timeline/timeline.test.js +0 -31
  60. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  61. package/components/tree/tree.test.js +0 -312
  62. package/core/element.test.js +0 -389
  63. package/core/icons.test.js +0 -187
  64. package/core/logical-children.test.js +0 -54
  65. package/core/markdown.test.js +0 -47
  66. package/core/responsive.test.js +0 -121
  67. package/core/template.test.js +0 -611
  68. package/traits/active-state/active-state.test.js +0 -28
  69. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  70. package/traits/announcer/announcer.test.js +0 -268
  71. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  72. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  73. package/traits/confetti/confetti.test.js +0 -46
  74. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  75. package/traits/count-up/count-up.test.js +0 -54
  76. package/traits/declarative.test.js +0 -138
  77. package/traits/dirty-state/dirty-state.test.js +0 -45
  78. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  79. package/traits/draggable/draggable.test.js +0 -60
  80. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  81. package/traits/drop-target/drop-target.test.js +0 -241
  82. package/traits/droppable/droppable.test.js +0 -54
  83. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  84. package/traits/error-shake/error-shake.test.js +0 -114
  85. package/traits/fade-presence/fade-presence.test.js +0 -20
  86. package/traits/focus-restore/focus-restore.test.js +0 -202
  87. package/traits/focus-trap/focus-trap.test.js +0 -42
  88. package/traits/focusable/focusable.test.js +0 -53
  89. package/traits/glow-focus/glow-focus.test.js +0 -31
  90. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  91. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  92. package/traits/hotkey/hotkey.test.js +0 -61
  93. package/traits/hoverable/hoverable.test.js +0 -24
  94. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  95. package/traits/input-mask/input-mask.test.js +0 -151
  96. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  97. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  98. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  99. package/traits/layout-animation/layout-animation.test.js +0 -114
  100. package/traits/long-press/long-press.test.js +0 -244
  101. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  102. package/traits/noise-texture/noise-texture.test.js +0 -20
  103. package/traits/parallax/parallax.test.js +0 -26
  104. package/traits/portal/portal.test.js +0 -30
  105. package/traits/pressable/pressable.test.js +0 -73
  106. package/traits/resettable/resettable.test.js +0 -100
  107. package/traits/resizable/resizable.test.js +0 -39
  108. package/traits/resize-observer/resize-observer.test.js +0 -38
  109. package/traits/ripple/ripple.test.js +0 -32
  110. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  111. package/traits/scale-press/scale-press.test.js +0 -39
  112. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  113. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  114. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  115. package/traits/smoke.test.js +0 -111
  116. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  117. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  118. package/traits/spring-animate/spring-animate.test.js +0 -28
  119. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  120. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  121. package/traits/tossable/tossable.test.js +0 -31
  122. package/traits/traits-host.test.js +0 -73
  123. package/traits/typeahead/typeahead.test.js +0 -38
  124. package/traits/typewriter/typewriter.test.js +0 -47
  125. package/traits/validation/validation.test.js +0 -93
  126. 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
- });