@adia-ai/web-components 0.8.19 → 0.8.21
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/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- 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
|
-
});
|