@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.
- package/CHANGELOG.md +26 -0
- 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 +6 -1
- package/components/select/select.class.js +34 -7
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +16 -1
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +18 -18
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/styles/colors/semantics/core.css +10 -4
- 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 -453
- 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,32 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './option-card.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('option-card-ui — heading/description stamp-if-absent (wrapper trap)', () => {
|
|
14
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
15
|
-
|
|
16
|
-
it('stamps heading + description from attrs when no slots are provided', async () => {
|
|
17
|
-
const el = mount('<option-card-ui heading="Pro" description="Best value"></option-card-ui>');
|
|
18
|
-
await tick();
|
|
19
|
-
expect(el.querySelectorAll('[slot="heading"]').length).toBe(1);
|
|
20
|
-
expect(el.querySelectorAll('[slot="description"]').length).toBe(1);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Regression (audit-wrapper-trap arm 2): a WRAPPED consumer [slot="heading"]
|
|
24
|
-
// override must suppress the attr-stamp, not collide with it.
|
|
25
|
-
it('does NOT stamp a duplicate heading when a WRAPPED consumer heading is present', async () => {
|
|
26
|
-
const el = mount(`<option-card-ui heading="Pro"><span style="display:contents" role="presentation"><h3 slot="heading" data-test="h">Custom</h3></span></option-card-ui>`);
|
|
27
|
-
await tick();
|
|
28
|
-
const headings = el.querySelectorAll('[slot="heading"]');
|
|
29
|
-
expect(headings.length).toBe(1);
|
|
30
|
-
expect(headings[0].dataset.test).toBe('h');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pagination-ui — regression guards for the v0.6.36 native-primitive
|
|
3
|
-
* leak fix and the universal [size] system threading.
|
|
4
|
-
*
|
|
5
|
-
* Before the fix, every page / prev / next item was a raw <button>,
|
|
6
|
-
* and pagination hardcoded `--a-size-sm` for its button cells (no
|
|
7
|
-
* [size] prop, no threading to the substrate's 24/30/36 px size
|
|
8
|
-
* system). After the fix, every item is a <button-ui> whose `size`
|
|
9
|
-
* attribute mirrors the host's, and active items render as
|
|
10
|
-
* `variant="primary"` so the filled-accent state comes from
|
|
11
|
-
* button-ui's primary surface matrix.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { dirname, resolve } from 'node:path';
|
|
18
|
-
import '../../core/element.js';
|
|
19
|
-
import './pagination.js';
|
|
20
|
-
import '../button/button.js';
|
|
21
|
-
|
|
22
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const CLASS_JS = readFileSync(resolve(HERE, 'pagination.class.js'), 'utf8');
|
|
24
|
-
|
|
25
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
26
|
-
|
|
27
|
-
function mount(html) {
|
|
28
|
-
const wrap = document.createElement('div');
|
|
29
|
-
wrap.innerHTML = html;
|
|
30
|
-
document.body.appendChild(wrap);
|
|
31
|
-
return wrap.firstElementChild;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
35
|
-
|
|
36
|
-
// ── Source-grep contract guards ─────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
describe('pagination-ui — no native <button> leak', () => {
|
|
39
|
-
it('class.js creates <button-ui> elements (not raw <button>)', () => {
|
|
40
|
-
expect(CLASS_JS).toMatch(/createElement\(['"]button-ui['"]\)/);
|
|
41
|
-
// Negative — guard against accidental revert to raw <button>.
|
|
42
|
-
expect(CLASS_JS).not.toMatch(/createElement\(['"]button['"]\)/);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('declares caret-left + caret-right in static requiredIcons', () => {
|
|
46
|
-
expect(CLASS_JS).toMatch(/static\s+requiredIcons\s*=\s*\[[^\]]*['"]caret-left['"]/);
|
|
47
|
-
expect(CLASS_JS).toMatch(/static\s+requiredIcons\s*=\s*\[[^\]]*['"]caret-right['"]/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('declares size prop in static properties (universal [size] thread-through)', () => {
|
|
51
|
-
expect(CLASS_JS).toMatch(/static\s+properties\s*=[\s\S]*?size:\s*\{\s*type:\s*String/);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ── DOM contract guards ─────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
describe('pagination-ui — DOM composition', () => {
|
|
58
|
-
it('stamps <button-ui> for every page / prev / next cell', async () => {
|
|
59
|
-
const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
|
|
60
|
-
await tick();
|
|
61
|
-
const buttons = el.querySelectorAll('button-ui');
|
|
62
|
-
expect(buttons.length).toBeGreaterThan(0);
|
|
63
|
-
// No raw <button> children — confirms the native-leak is closed at runtime.
|
|
64
|
-
expect(el.querySelectorAll(':scope button:not(button-ui)').length).toBe(0);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('threads size attribute through to every nested button-ui', async () => {
|
|
68
|
-
const el = mount('<pagination-ui page="2" total="5" size="lg"></pagination-ui>');
|
|
69
|
-
await tick();
|
|
70
|
-
const buttons = el.querySelectorAll('button-ui');
|
|
71
|
-
expect(buttons.length).toBeGreaterThan(0);
|
|
72
|
-
for (const btn of buttons) {
|
|
73
|
-
expect(btn.getAttribute('size')).toBe('lg');
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('defaults to size="md" (aligned with button-ui default)', async () => {
|
|
78
|
-
const el = mount('<pagination-ui page="2" total="5"></pagination-ui>');
|
|
79
|
-
await tick();
|
|
80
|
-
const buttons = el.querySelectorAll('button-ui');
|
|
81
|
-
expect(buttons.length).toBeGreaterThan(0);
|
|
82
|
-
for (const btn of buttons) {
|
|
83
|
-
expect(btn.getAttribute('size')).toBe('md');
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('renders the active page as variant="primary" (button-ui token chain)', async () => {
|
|
88
|
-
const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
|
|
89
|
-
await tick();
|
|
90
|
-
const active = el.querySelector('button-ui[data-active]');
|
|
91
|
-
expect(active).toBeTruthy();
|
|
92
|
-
expect(active.getAttribute('variant')).toBe('primary');
|
|
93
|
-
expect(active.getAttribute('aria-current')).toBe('page');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('renders prev / next as ghost (default variant) or outline (button variant)', async () => {
|
|
97
|
-
const elDefault = mount('<pagination-ui page="3" total="10"></pagination-ui>');
|
|
98
|
-
await tick();
|
|
99
|
-
expect(elDefault.querySelector('button-ui[data-prev]').getAttribute('variant')).toBe('ghost');
|
|
100
|
-
expect(elDefault.querySelector('button-ui[data-next]').getAttribute('variant')).toBe('ghost');
|
|
101
|
-
|
|
102
|
-
const elButton = mount('<pagination-ui page="3" total="10" variant="button"></pagination-ui>');
|
|
103
|
-
await tick();
|
|
104
|
-
expect(elButton.querySelector('button-ui[data-prev]').getAttribute('variant')).toBe('outline');
|
|
105
|
-
expect(elButton.querySelector('button-ui[data-next]').getAttribute('variant')).toBe('outline');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('uses icon="caret-left" / "caret-right" on prev / next (not glyph text)', async () => {
|
|
109
|
-
const el = mount('<pagination-ui page="3" total="10"></pagination-ui>');
|
|
110
|
-
await tick();
|
|
111
|
-
expect(el.querySelector('button-ui[data-prev]').getAttribute('icon')).toBe('caret-left');
|
|
112
|
-
expect(el.querySelector('button-ui[data-next]').getAttribute('icon')).toBe('caret-right');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ── Width invariance: compact-mode cell count is constant across pages ──
|
|
117
|
-
//
|
|
118
|
-
// Compact width W = 2*siblings + 5 (= 7 at siblings=1). When the current
|
|
119
|
-
// page advances by one, the row's visible cell count must NOT change —
|
|
120
|
-
// otherwise the layout wobbles. For total ≤ W, we show every page and
|
|
121
|
-
// skip compact mode entirely (no point compressing if it doesn't save
|
|
122
|
-
// slots — also wobble-free since every page just highlights a different
|
|
123
|
-
// constant-width row).
|
|
124
|
-
|
|
125
|
-
describe('pagination-ui — compact-mode width invariance', () => {
|
|
126
|
-
function pageValues(el) {
|
|
127
|
-
return [...el.querySelectorAll('button-ui[data-page]')].map((b) => Number(b.dataset.value));
|
|
128
|
-
}
|
|
129
|
-
function cellCount(el) {
|
|
130
|
-
return el.querySelectorAll('button-ui[data-page], [data-ellipsis]').length;
|
|
131
|
-
}
|
|
132
|
-
function hasEllipsis(el) {
|
|
133
|
-
return el.querySelectorAll('[data-ellipsis]').length > 0;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
it('total=5 siblings=1 → never compacts (shows 1-5 every page)', async () => {
|
|
137
|
-
for (const page of [1, 2, 3, 4, 5]) {
|
|
138
|
-
const el = mount(`<pagination-ui page="${page}" total="5" siblings="1"></pagination-ui>`);
|
|
139
|
-
await tick();
|
|
140
|
-
expect(pageValues(el)).toEqual([1, 2, 3, 4, 5]);
|
|
141
|
-
expect(hasEllipsis(el)).toBe(false);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('total=10 siblings=1 → exactly 7 cells on every page (no wobble)', async () => {
|
|
146
|
-
for (const page of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
|
|
147
|
-
const el = mount(`<pagination-ui page="${page}" total="10" siblings="1"></pagination-ui>`);
|
|
148
|
-
await tick();
|
|
149
|
-
expect(cellCount(el)).toBe(7);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('total=10 page=3 siblings=1 → near-start: [1,2,3,4,5,…,10]', async () => {
|
|
154
|
-
const el = mount('<pagination-ui page="3" total="10" siblings="1"></pagination-ui>');
|
|
155
|
-
await tick();
|
|
156
|
-
expect(pageValues(el)).toEqual([1, 2, 3, 4, 5, 10]);
|
|
157
|
-
expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('total=10 page=4 siblings=1 → still near-start: [1,2,3,4,5,…,10] (same as page=3)', async () => {
|
|
161
|
-
const el = mount('<pagination-ui page="4" total="10" siblings="1"></pagination-ui>');
|
|
162
|
-
await tick();
|
|
163
|
-
expect(pageValues(el)).toEqual([1, 2, 3, 4, 5, 10]);
|
|
164
|
-
expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('total=10 page=5 siblings=1 → middle: [1,…,4,5,6,…,10]', async () => {
|
|
168
|
-
const el = mount('<pagination-ui page="5" total="10" siblings="1"></pagination-ui>');
|
|
169
|
-
await tick();
|
|
170
|
-
expect(pageValues(el)).toEqual([1, 4, 5, 6, 10]);
|
|
171
|
-
expect(el.querySelectorAll('[data-ellipsis]').length).toBe(2);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('total=10 page=7 siblings=1 → near-end: [1,…,6,7,8,9,10]', async () => {
|
|
175
|
-
const el = mount('<pagination-ui page="7" total="10" siblings="1"></pagination-ui>');
|
|
176
|
-
await tick();
|
|
177
|
-
expect(pageValues(el)).toEqual([1, 6, 7, 8, 9, 10]);
|
|
178
|
-
expect(el.querySelectorAll('[data-ellipsis]').length).toBe(1);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('total=20 page=10 siblings=1 → middle: [1,…,9,10,11,…,20]', async () => {
|
|
182
|
-
const el = mount('<pagination-ui page="10" total="20" siblings="1"></pagination-ui>');
|
|
183
|
-
await tick();
|
|
184
|
-
expect(pageValues(el)).toEqual([1, 9, 10, 11, 20]);
|
|
185
|
-
expect(el.querySelectorAll('[data-ellipsis]').length).toBe(2);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('total=20 siblings=2 → exactly 9 cells on every page', async () => {
|
|
189
|
-
for (const page of [1, 5, 10, 15, 20]) {
|
|
190
|
-
const el = mount(`<pagination-ui page="${page}" total="20" siblings="2"></pagination-ui>`);
|
|
191
|
-
await tick();
|
|
192
|
-
expect(cellCount(el)).toBe(9);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
});
|
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './select.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('select-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
// §FEEDBACK-36 — declarative <option selected> initial value (single-select)
|
|
18
|
-
it('honours the first <option selected> as initial value for single-select', async () => {
|
|
19
|
-
const s = mount(`
|
|
20
|
-
<select-ui>
|
|
21
|
-
<option value="a">Alpha</option>
|
|
22
|
-
<option value="b" selected>Beta</option>
|
|
23
|
-
<option value="c">Gamma</option>
|
|
24
|
-
</select-ui>
|
|
25
|
-
`);
|
|
26
|
-
await tick();
|
|
27
|
-
expect(s.value).toBe('b');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// §FB-98 — <option> children nested in the template engine's display:contents
|
|
31
|
-
// wrapper spans (.map()/repeat()) must be parsed, not just static literals.
|
|
32
|
-
it('FB-98: parses options nested in display:contents/role=presentation wrappers (present at connect)', async () => {
|
|
33
|
-
const s = mount(`
|
|
34
|
-
<select-ui value="a">
|
|
35
|
-
<span style="display:contents" role="presentation">
|
|
36
|
-
<span style="display:contents" role="presentation"><option value="a">Alpha</option></span>
|
|
37
|
-
<span style="display:contents" role="presentation"><option value="b">Beta</option></span>
|
|
38
|
-
<span style="display:contents" role="presentation"><option value="c">Gamma</option></span>
|
|
39
|
-
</span>
|
|
40
|
-
</select-ui>
|
|
41
|
-
`);
|
|
42
|
-
await tick();
|
|
43
|
-
expect(s.querySelectorAll('[role="option"]').length).toBe(3);
|
|
44
|
-
expect(s.options.map((o) => o.value)).toEqual(['a', 'b', 'c']);
|
|
45
|
-
expect(s.value).toBe('a');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// §FB-98 — interpolated options that ARRIVE after connect (the template's
|
|
49
|
-
// update() pass) are picked up by the re-parse MutationObserver.
|
|
50
|
-
it('FB-98: re-parses wrapped options that arrive after connect (observer)', async () => {
|
|
51
|
-
const s = mount(`<select-ui></select-ui>`);
|
|
52
|
-
await tick();
|
|
53
|
-
expect(s.querySelectorAll('[role="option"]').length).toBe(0);
|
|
54
|
-
const wrap = document.createElement('span');
|
|
55
|
-
wrap.style.display = 'contents';
|
|
56
|
-
wrap.setAttribute('role', 'presentation');
|
|
57
|
-
wrap.innerHTML = `<option value="x">X</option><option value="y">Y</option>`;
|
|
58
|
-
s.appendChild(wrap);
|
|
59
|
-
await tick(); await tick(); // mutation → observer microtask → re-parse + render
|
|
60
|
-
expect(s.querySelectorAll('[role="option"]').length).toBe(2);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// §FB-46 — <option selected> must capture ALL values in [multiple] mode
|
|
64
|
-
it('captures all <option selected> values as comma-separated for [multiple]', async () => {
|
|
65
|
-
const s = mount(`
|
|
66
|
-
<select-ui multiple>
|
|
67
|
-
<option value="a" selected>Alpha</option>
|
|
68
|
-
<option value="b">Beta</option>
|
|
69
|
-
<option value="c" selected>Gamma</option>
|
|
70
|
-
</select-ui>
|
|
71
|
-
`);
|
|
72
|
-
await tick();
|
|
73
|
-
// All selected values should be present (order-agnostic)
|
|
74
|
-
const parts = new Set(s.value.split(',').map(v => v.trim()));
|
|
75
|
-
expect(parts.has('a')).toBe(true);
|
|
76
|
-
expect(parts.has('c')).toBe(true);
|
|
77
|
-
expect(parts.has('b')).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// §FB-46 — .value setter with comma-separated string must sync aria-selected
|
|
81
|
-
it('syncs aria-selected on all comma-separated values when .value is set', async () => {
|
|
82
|
-
const s = mount(`
|
|
83
|
-
<select-ui multiple open>
|
|
84
|
-
<option value="a">Alpha</option>
|
|
85
|
-
<option value="b">Beta</option>
|
|
86
|
-
<option value="c">Gamma</option>
|
|
87
|
-
</select-ui>
|
|
88
|
-
`);
|
|
89
|
-
await tick();
|
|
90
|
-
s.value = 'a,c';
|
|
91
|
-
await tick();
|
|
92
|
-
|
|
93
|
-
const opts = s.querySelectorAll('[role="option"]');
|
|
94
|
-
const selected = Array.from(opts)
|
|
95
|
-
.filter(o => o.getAttribute('aria-selected') === 'true')
|
|
96
|
-
.map(o => o.getAttribute('data-value'));
|
|
97
|
-
|
|
98
|
-
expect(selected).toContain('a');
|
|
99
|
-
expect(selected).toContain('c');
|
|
100
|
-
expect(selected).not.toContain('b');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Regression: single-select aria-selected must still work with the Set path
|
|
104
|
-
it('still sets aria-selected correctly for single-select after §FB-46 changes', async () => {
|
|
105
|
-
const s = mount(`
|
|
106
|
-
<select-ui open>
|
|
107
|
-
<option value="a">Alpha</option>
|
|
108
|
-
<option value="b">Beta</option>
|
|
109
|
-
</select-ui>
|
|
110
|
-
`);
|
|
111
|
-
await tick();
|
|
112
|
-
s.value = 'b';
|
|
113
|
-
await tick();
|
|
114
|
-
|
|
115
|
-
const opts = s.querySelectorAll('[role="option"]');
|
|
116
|
-
const selected = Array.from(opts)
|
|
117
|
-
.filter(o => o.getAttribute('aria-selected') === 'true')
|
|
118
|
-
.map(o => o.getAttribute('data-value'));
|
|
119
|
-
|
|
120
|
-
expect(selected).toEqual(['b']);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// ── SPEC-040 (multi-select) ──
|
|
124
|
-
|
|
125
|
-
it('multi-select: [multiple] sets data-multi-chips and renders tag-ui chips per selected value', async () => {
|
|
126
|
-
const s = mount(`
|
|
127
|
-
<select-ui multiple value="a,c">
|
|
128
|
-
<option value="a">Alpha</option>
|
|
129
|
-
<option value="b">Beta</option>
|
|
130
|
-
<option value="c">Gamma</option>
|
|
131
|
-
</select-ui>
|
|
132
|
-
`);
|
|
133
|
-
await tick();
|
|
134
|
-
expect(s.hasAttribute('data-multi-chips')).toBe(true);
|
|
135
|
-
const chips = s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui');
|
|
136
|
-
expect(chips.length).toBe(2);
|
|
137
|
-
const chipValues = Array.from(chips).map((c) => c.dataset.chipValue);
|
|
138
|
-
expect(chipValues).toContain('a');
|
|
139
|
-
expect(chipValues).toContain('c');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('multi-select: aria-multiselectable=true is set on the listbox', async () => {
|
|
143
|
-
const s = mount(`
|
|
144
|
-
<select-ui multiple open>
|
|
145
|
-
<option value="a">Alpha</option>
|
|
146
|
-
<option value="b">Beta</option>
|
|
147
|
-
</select-ui>
|
|
148
|
-
`);
|
|
149
|
-
await tick();
|
|
150
|
-
const lb = s.querySelector('[slot="listbox"]');
|
|
151
|
-
expect(lb).toBeTruthy();
|
|
152
|
-
expect(lb.getAttribute('aria-multiselectable')).toBe('true');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('multi-select: clicking an option toggles selection without closing the popover', async () => {
|
|
156
|
-
const s = mount(`
|
|
157
|
-
<select-ui multiple open>
|
|
158
|
-
<option value="a">Alpha</option>
|
|
159
|
-
<option value="b">Beta</option>
|
|
160
|
-
<option value="c">Gamma</option>
|
|
161
|
-
</select-ui>
|
|
162
|
-
`);
|
|
163
|
-
await tick();
|
|
164
|
-
const opts = s.querySelectorAll('[role="option"]');
|
|
165
|
-
const optA = Array.from(opts).find((o) => o.getAttribute('data-value') === 'a');
|
|
166
|
-
optA.click();
|
|
167
|
-
await tick();
|
|
168
|
-
expect(s.open).toBe(true); // still open
|
|
169
|
-
expect(s.value).toBe('a');
|
|
170
|
-
|
|
171
|
-
const optC = Array.from(s.querySelectorAll('[role="option"]')).find((o) => o.getAttribute('data-value') === 'c');
|
|
172
|
-
optC.click();
|
|
173
|
-
await tick();
|
|
174
|
-
expect(s.open).toBe(true);
|
|
175
|
-
const parts = new Set(s.value.split(','));
|
|
176
|
-
expect(parts.has('a')).toBe(true);
|
|
177
|
-
expect(parts.has('c')).toBe(true);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('multi-select: Backspace from the trigger removes the last chip', async () => {
|
|
181
|
-
const s = mount(`
|
|
182
|
-
<select-ui multiple value="a,b">
|
|
183
|
-
<option value="a">Alpha</option>
|
|
184
|
-
<option value="b">Beta</option>
|
|
185
|
-
</select-ui>
|
|
186
|
-
`);
|
|
187
|
-
await tick();
|
|
188
|
-
const ev = new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true });
|
|
189
|
-
s.dispatchEvent(ev);
|
|
190
|
-
await tick();
|
|
191
|
-
expect(s.value).toBe('a');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('multi-select: toggle(id) is a public API', async () => {
|
|
195
|
-
const s = mount(`
|
|
196
|
-
<select-ui multiple>
|
|
197
|
-
<option value="a">Alpha</option>
|
|
198
|
-
<option value="b">Beta</option>
|
|
199
|
-
</select-ui>
|
|
200
|
-
`);
|
|
201
|
-
await tick();
|
|
202
|
-
s.toggle('a');
|
|
203
|
-
await tick();
|
|
204
|
-
expect(s.value).toBe('a');
|
|
205
|
-
s.toggle('a');
|
|
206
|
-
await tick();
|
|
207
|
-
expect(s.value).toBe('');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('multi-select: [max] suppresses further selection and fires invalid', async () => {
|
|
211
|
-
const s = mount(`
|
|
212
|
-
<select-ui multiple max="2" value="a,b">
|
|
213
|
-
<option value="a">Alpha</option>
|
|
214
|
-
<option value="b">Beta</option>
|
|
215
|
-
<option value="c">Gamma</option>
|
|
216
|
-
</select-ui>
|
|
217
|
-
`);
|
|
218
|
-
await tick();
|
|
219
|
-
let invalidFired = null;
|
|
220
|
-
s.addEventListener('invalid', (e) => { invalidFired = e.detail; });
|
|
221
|
-
s.toggle('c');
|
|
222
|
-
await tick();
|
|
223
|
-
expect(invalidFired).toBeTruthy();
|
|
224
|
-
expect(invalidFired.reason).toBe('max');
|
|
225
|
-
expect(s.value).toBe('a,b'); // unchanged
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('multi-select: clear() empties value and fires change with removed array', async () => {
|
|
229
|
-
const s = mount(`
|
|
230
|
-
<select-ui multiple value="a,b">
|
|
231
|
-
<option value="a">Alpha</option>
|
|
232
|
-
<option value="b">Beta</option>
|
|
233
|
-
</select-ui>
|
|
234
|
-
`);
|
|
235
|
-
await tick();
|
|
236
|
-
let changed = null;
|
|
237
|
-
s.addEventListener('change', (e) => { changed = e.detail; });
|
|
238
|
-
s.clear();
|
|
239
|
-
await tick();
|
|
240
|
-
expect(s.value).toBe('');
|
|
241
|
-
expect(changed.removed).toEqual(['a', 'b']);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('multi-select: selectAllOptions() selects every non-disabled option', async () => {
|
|
245
|
-
const s = mount(`
|
|
246
|
-
<select-ui multiple>
|
|
247
|
-
<option value="a">Alpha</option>
|
|
248
|
-
<option value="b">Beta</option>
|
|
249
|
-
<option value="c" disabled>Gamma</option>
|
|
250
|
-
</select-ui>
|
|
251
|
-
`);
|
|
252
|
-
await tick();
|
|
253
|
-
s.selectAllOptions();
|
|
254
|
-
await tick();
|
|
255
|
-
const parts = new Set(s.value.split(','));
|
|
256
|
-
expect(parts.has('a')).toBe(true);
|
|
257
|
-
expect(parts.has('b')).toBe(true);
|
|
258
|
-
expect(parts.has('c')).toBe(false); // disabled
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('multi-select: [max-chips] shows "+N more" pill when exceeded', async () => {
|
|
262
|
-
const s = mount(`
|
|
263
|
-
<select-ui multiple max-chips="2" value="a,b,c,d">
|
|
264
|
-
<option value="a">Alpha</option>
|
|
265
|
-
<option value="b">Beta</option>
|
|
266
|
-
<option value="c">Gamma</option>
|
|
267
|
-
<option value="d">Delta</option>
|
|
268
|
-
</select-ui>
|
|
269
|
-
`);
|
|
270
|
-
await tick();
|
|
271
|
-
const chips = s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui');
|
|
272
|
-
expect(chips.length).toBe(2);
|
|
273
|
-
const more = s.querySelector('[slot="trigger"] [data-more]');
|
|
274
|
-
expect(more).toBeTruthy();
|
|
275
|
-
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
276
|
-
expect(more.tagName).toBe('BUTTON-UI');
|
|
277
|
-
expect(more.getAttribute('text')).toBe('+2 more');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('multi-select: [clearable] reveals the clear-all button when chips present', async () => {
|
|
281
|
-
const s = mount(`
|
|
282
|
-
<select-ui multiple clearable value="a">
|
|
283
|
-
<option value="a">Alpha</option>
|
|
284
|
-
<option value="b">Beta</option>
|
|
285
|
-
</select-ui>
|
|
286
|
-
`);
|
|
287
|
-
await tick();
|
|
288
|
-
const clearBtn = s.querySelector('[data-clear-all]');
|
|
289
|
-
expect(clearBtn).toBeTruthy();
|
|
290
|
-
expect(clearBtn.hasAttribute('hidden')).toBe(false);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('multi-select: [select-all] renders the bulk-action header', async () => {
|
|
294
|
-
const s = mount(`
|
|
295
|
-
<select-ui multiple select-all open>
|
|
296
|
-
<option value="a">Alpha</option>
|
|
297
|
-
<option value="b">Beta</option>
|
|
298
|
-
</select-ui>
|
|
299
|
-
`);
|
|
300
|
-
await tick();
|
|
301
|
-
const header = s.querySelector('[slot="listbox"] [data-select-all]');
|
|
302
|
-
expect(header).toBeTruthy();
|
|
303
|
-
const btn = header.querySelector('[data-select-all-btn]');
|
|
304
|
-
expect(btn).toBeTruthy();
|
|
305
|
-
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
306
|
-
expect(btn.tagName).toBe('BUTTON-UI');
|
|
307
|
-
expect(btn.getAttribute('text')).toBe('Select all');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('multi-select: chip remove event empties that selection from value', async () => {
|
|
311
|
-
const s = mount(`
|
|
312
|
-
<select-ui multiple value="a,b">
|
|
313
|
-
<option value="a">Alpha</option>
|
|
314
|
-
<option value="b">Beta</option>
|
|
315
|
-
</select-ui>
|
|
316
|
-
`);
|
|
317
|
-
await tick();
|
|
318
|
-
const chipA = s.querySelector('tag-ui[data-chip-value="a"]');
|
|
319
|
-
expect(chipA).toBeTruthy();
|
|
320
|
-
// Simulate tag-ui's remove event (bubbles, with detail.text)
|
|
321
|
-
chipA.dispatchEvent(new CustomEvent('remove', {
|
|
322
|
-
bubbles: true,
|
|
323
|
-
detail: { text: 'Alpha', value: 'Alpha' },
|
|
324
|
-
}));
|
|
325
|
-
await tick();
|
|
326
|
-
expect(s.value).toBe('b');
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// §FB-88 — placeholder-only select derives an accessible name (WCAG 4.1.2),
|
|
330
|
-
// mirroring input-ui, without clobbering a real label.
|
|
331
|
-
describe('accessible name from placeholder (FB-88)', () => {
|
|
332
|
-
it('names a placeholder-only select from its placeholder', async () => {
|
|
333
|
-
const s = mount(`<select-ui placeholder="Status"></select-ui>`);
|
|
334
|
-
await tick();
|
|
335
|
-
expect(s.getAttribute('aria-label')).toBe('Status');
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('does NOT derive a name from the default "Select..." placeholder', async () => {
|
|
339
|
-
const s = mount(`<select-ui></select-ui>`);
|
|
340
|
-
await tick();
|
|
341
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('preserves an author-supplied aria-label (override wins over placeholder)', async () => {
|
|
345
|
-
const s = mount(`<select-ui aria-label="Filter by status" placeholder="Status"></select-ui>`);
|
|
346
|
-
await tick();
|
|
347
|
-
expect(s.getAttribute('aria-label')).toBe('Filter by status');
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('does not set aria-label when a visible label is present', async () => {
|
|
351
|
-
const s = mount(`<select-ui label="Status" placeholder="Pick one"></select-ui>`);
|
|
352
|
-
await tick();
|
|
353
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('does not set aria-label when aria-labelledby is present (e.g. field-ui)', async () => {
|
|
357
|
-
const s = mount(`<select-ui aria-labelledby="ext-label" placeholder="Pick one"></select-ui>`);
|
|
358
|
-
await tick();
|
|
359
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// gh#339 — mark (adia-mark-ui) as a leading visual, alongside icon/avatar.
|
|
364
|
-
describe('mark (gh#339)', () => {
|
|
365
|
-
it('renders <adia-mark-ui> in the listbox row for a declarative <option mark>', async () => {
|
|
366
|
-
const s = mount(`
|
|
367
|
-
<select-ui placeholder="Choose…">
|
|
368
|
-
<option value="adia" mark>Adia</option>
|
|
369
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
370
|
-
</select-ui>
|
|
371
|
-
`);
|
|
372
|
-
await tick();
|
|
373
|
-
const row = s.querySelector('[role="option"][data-value="adia"]');
|
|
374
|
-
expect(row.querySelector('adia-mark-ui')).not.toBeNull();
|
|
375
|
-
expect(s.options.find((o) => o.value === 'adia').mark).toBe(true);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('reflects the SELECTED option\'s mark in the trigger leading slot', async () => {
|
|
379
|
-
const s = mount(`
|
|
380
|
-
<select-ui value="adia">
|
|
381
|
-
<option value="adia" mark>Adia</option>
|
|
382
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
383
|
-
</select-ui>
|
|
384
|
-
`);
|
|
385
|
-
await tick();
|
|
386
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
387
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('mark wins over avatar and icon when more than one is set on the same option', async () => {
|
|
391
|
-
const s = mount(`<select-ui value="a"></select-ui>`);
|
|
392
|
-
await tick();
|
|
393
|
-
s.options = [{ value: 'a', label: 'A', mark: true, avatar: 'https://example.test/a.png', icon: 'star' }];
|
|
394
|
-
await tick();
|
|
395
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
396
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('switching the SELECTED option from mark to avatar swaps the trigger leading element (not just its attributes)', async () => {
|
|
400
|
-
const s = mount(`
|
|
401
|
-
<select-ui value="adia">
|
|
402
|
-
<option value="adia" mark>Adia</option>
|
|
403
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
404
|
-
</select-ui>
|
|
405
|
-
`);
|
|
406
|
-
await tick();
|
|
407
|
-
expect(s.querySelector('[slot="trigger"] > [data-select-leading]').tagName).toBe('ADIA-MARK-UI');
|
|
408
|
-
s.value = 'acme';
|
|
409
|
-
await tick();
|
|
410
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
411
|
-
expect(leading.tagName).toBe('IMG');
|
|
412
|
-
expect(leading.getAttribute('src')).toBe('https://example.test/acme.png');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('a host-level [mark] renders the brand mark as a static leading visual when no option overrides it', async () => {
|
|
416
|
-
const s = mount(`
|
|
417
|
-
<select-ui mark value="adia">
|
|
418
|
-
<option value="adia">Adia</option>
|
|
419
|
-
</select-ui>
|
|
420
|
-
`);
|
|
421
|
-
await tick();
|
|
422
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
423
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('renders <adia-mark-ui> in a listbox ROW under [multiple] mode too (shares #optionLeadHTML with single-select)', async () => {
|
|
427
|
-
const s = mount(`
|
|
428
|
-
<select-ui multiple value="adia">
|
|
429
|
-
<option value="adia" mark>Adia</option>
|
|
430
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
431
|
-
</select-ui>
|
|
432
|
-
`);
|
|
433
|
-
await tick();
|
|
434
|
-
const row = s.querySelector('[role="option"][data-value="adia"]');
|
|
435
|
-
expect(row.querySelector('adia-mark-ui')).not.toBeNull();
|
|
436
|
-
// Multi-select shows chips, not a leading visual, in the TRIGGER — mark
|
|
437
|
-
// is a correct no-op there (#syncLeading's `if (this.multiple) return`).
|
|
438
|
-
expect(s.querySelector('[slot="trigger"] > [data-select-leading]')).toBeNull();
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('mark set via the .options property (not just the declarative attribute) is honoured', async () => {
|
|
442
|
-
const s = mount(`<select-ui value="adia"></select-ui>`);
|
|
443
|
-
await tick();
|
|
444
|
-
s.options = [
|
|
445
|
-
{ value: 'adia', label: 'Adia', mark: true },
|
|
446
|
-
{ value: 'acme', label: 'Acme', avatar: 'https://example.test/acme.png' },
|
|
447
|
-
];
|
|
448
|
-
await tick();
|
|
449
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
450
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
});
|