@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,481 +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: [summary-label] renders a text summary instead of chips (gh#442)', async () => {
|
|
143
|
-
const s = mount(`
|
|
144
|
-
<select-ui multiple summary-label="Organizations" value="a,c">
|
|
145
|
-
<option value="a">Riverside Health System</option>
|
|
146
|
-
<option value="b">Bay Area Health Group</option>
|
|
147
|
-
<option value="c">Greenfield Medical Group</option>
|
|
148
|
-
</select-ui>
|
|
149
|
-
`);
|
|
150
|
-
await tick();
|
|
151
|
-
expect(s.hasAttribute('data-multi-chips')).toBe(false);
|
|
152
|
-
expect(s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui').length).toBe(0);
|
|
153
|
-
expect(s.querySelector('[slot="display"]').textContent).toBe('Organizations (2)');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('multi-select summary: one selection shows its own label; none shows the placeholder (gh#442)', async () => {
|
|
157
|
-
const s = mount(`
|
|
158
|
-
<select-ui multiple summary-label="Organizations" placeholder="All organizations" value="a">
|
|
159
|
-
<option value="a">Riverside Health System</option>
|
|
160
|
-
<option value="b">Bay Area Health Group</option>
|
|
161
|
-
</select-ui>
|
|
162
|
-
`);
|
|
163
|
-
await tick();
|
|
164
|
-
expect(s.querySelector('[slot="display"]').textContent).toBe('Riverside Health System');
|
|
165
|
-
s.value = '';
|
|
166
|
-
await tick();
|
|
167
|
-
expect(s.querySelector('[slot="display"]').textContent).toBe('All organizations');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('multi-select: aria-multiselectable=true is set on the listbox', async () => {
|
|
171
|
-
const s = mount(`
|
|
172
|
-
<select-ui multiple open>
|
|
173
|
-
<option value="a">Alpha</option>
|
|
174
|
-
<option value="b">Beta</option>
|
|
175
|
-
</select-ui>
|
|
176
|
-
`);
|
|
177
|
-
await tick();
|
|
178
|
-
const lb = s.querySelector('[slot="listbox"]');
|
|
179
|
-
expect(lb).toBeTruthy();
|
|
180
|
-
expect(lb.getAttribute('aria-multiselectable')).toBe('true');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('multi-select: clicking an option toggles selection without closing the popover', async () => {
|
|
184
|
-
const s = mount(`
|
|
185
|
-
<select-ui multiple open>
|
|
186
|
-
<option value="a">Alpha</option>
|
|
187
|
-
<option value="b">Beta</option>
|
|
188
|
-
<option value="c">Gamma</option>
|
|
189
|
-
</select-ui>
|
|
190
|
-
`);
|
|
191
|
-
await tick();
|
|
192
|
-
const opts = s.querySelectorAll('[role="option"]');
|
|
193
|
-
const optA = Array.from(opts).find((o) => o.getAttribute('data-value') === 'a');
|
|
194
|
-
optA.click();
|
|
195
|
-
await tick();
|
|
196
|
-
expect(s.open).toBe(true); // still open
|
|
197
|
-
expect(s.value).toBe('a');
|
|
198
|
-
|
|
199
|
-
const optC = Array.from(s.querySelectorAll('[role="option"]')).find((o) => o.getAttribute('data-value') === 'c');
|
|
200
|
-
optC.click();
|
|
201
|
-
await tick();
|
|
202
|
-
expect(s.open).toBe(true);
|
|
203
|
-
const parts = new Set(s.value.split(','));
|
|
204
|
-
expect(parts.has('a')).toBe(true);
|
|
205
|
-
expect(parts.has('c')).toBe(true);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('multi-select: Backspace from the trigger removes the last chip', async () => {
|
|
209
|
-
const s = mount(`
|
|
210
|
-
<select-ui multiple value="a,b">
|
|
211
|
-
<option value="a">Alpha</option>
|
|
212
|
-
<option value="b">Beta</option>
|
|
213
|
-
</select-ui>
|
|
214
|
-
`);
|
|
215
|
-
await tick();
|
|
216
|
-
const ev = new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true });
|
|
217
|
-
s.dispatchEvent(ev);
|
|
218
|
-
await tick();
|
|
219
|
-
expect(s.value).toBe('a');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('multi-select: toggle(id) is a public API', async () => {
|
|
223
|
-
const s = mount(`
|
|
224
|
-
<select-ui multiple>
|
|
225
|
-
<option value="a">Alpha</option>
|
|
226
|
-
<option value="b">Beta</option>
|
|
227
|
-
</select-ui>
|
|
228
|
-
`);
|
|
229
|
-
await tick();
|
|
230
|
-
s.toggle('a');
|
|
231
|
-
await tick();
|
|
232
|
-
expect(s.value).toBe('a');
|
|
233
|
-
s.toggle('a');
|
|
234
|
-
await tick();
|
|
235
|
-
expect(s.value).toBe('');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('multi-select: [max] suppresses further selection and fires invalid', async () => {
|
|
239
|
-
const s = mount(`
|
|
240
|
-
<select-ui multiple max="2" value="a,b">
|
|
241
|
-
<option value="a">Alpha</option>
|
|
242
|
-
<option value="b">Beta</option>
|
|
243
|
-
<option value="c">Gamma</option>
|
|
244
|
-
</select-ui>
|
|
245
|
-
`);
|
|
246
|
-
await tick();
|
|
247
|
-
let invalidFired = null;
|
|
248
|
-
s.addEventListener('invalid', (e) => { invalidFired = e.detail; });
|
|
249
|
-
s.toggle('c');
|
|
250
|
-
await tick();
|
|
251
|
-
expect(invalidFired).toBeTruthy();
|
|
252
|
-
expect(invalidFired.reason).toBe('max');
|
|
253
|
-
expect(s.value).toBe('a,b'); // unchanged
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('multi-select: clear() empties value and fires change with removed array', async () => {
|
|
257
|
-
const s = mount(`
|
|
258
|
-
<select-ui multiple value="a,b">
|
|
259
|
-
<option value="a">Alpha</option>
|
|
260
|
-
<option value="b">Beta</option>
|
|
261
|
-
</select-ui>
|
|
262
|
-
`);
|
|
263
|
-
await tick();
|
|
264
|
-
let changed = null;
|
|
265
|
-
s.addEventListener('change', (e) => { changed = e.detail; });
|
|
266
|
-
s.clear();
|
|
267
|
-
await tick();
|
|
268
|
-
expect(s.value).toBe('');
|
|
269
|
-
expect(changed.removed).toEqual(['a', 'b']);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('multi-select: selectAllOptions() selects every non-disabled option', async () => {
|
|
273
|
-
const s = mount(`
|
|
274
|
-
<select-ui multiple>
|
|
275
|
-
<option value="a">Alpha</option>
|
|
276
|
-
<option value="b">Beta</option>
|
|
277
|
-
<option value="c" disabled>Gamma</option>
|
|
278
|
-
</select-ui>
|
|
279
|
-
`);
|
|
280
|
-
await tick();
|
|
281
|
-
s.selectAllOptions();
|
|
282
|
-
await tick();
|
|
283
|
-
const parts = new Set(s.value.split(','));
|
|
284
|
-
expect(parts.has('a')).toBe(true);
|
|
285
|
-
expect(parts.has('b')).toBe(true);
|
|
286
|
-
expect(parts.has('c')).toBe(false); // disabled
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('multi-select: [max-chips] shows "+N more" pill when exceeded', async () => {
|
|
290
|
-
const s = mount(`
|
|
291
|
-
<select-ui multiple max-chips="2" value="a,b,c,d">
|
|
292
|
-
<option value="a">Alpha</option>
|
|
293
|
-
<option value="b">Beta</option>
|
|
294
|
-
<option value="c">Gamma</option>
|
|
295
|
-
<option value="d">Delta</option>
|
|
296
|
-
</select-ui>
|
|
297
|
-
`);
|
|
298
|
-
await tick();
|
|
299
|
-
const chips = s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui');
|
|
300
|
-
expect(chips.length).toBe(2);
|
|
301
|
-
const more = s.querySelector('[slot="trigger"] [data-more]');
|
|
302
|
-
expect(more).toBeTruthy();
|
|
303
|
-
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
304
|
-
expect(more.tagName).toBe('BUTTON-UI');
|
|
305
|
-
expect(more.getAttribute('text')).toBe('+2 more');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('multi-select: [clearable] reveals the clear-all button when chips present', async () => {
|
|
309
|
-
const s = mount(`
|
|
310
|
-
<select-ui multiple clearable value="a">
|
|
311
|
-
<option value="a">Alpha</option>
|
|
312
|
-
<option value="b">Beta</option>
|
|
313
|
-
</select-ui>
|
|
314
|
-
`);
|
|
315
|
-
await tick();
|
|
316
|
-
const clearBtn = s.querySelector('[data-clear-all]');
|
|
317
|
-
expect(clearBtn).toBeTruthy();
|
|
318
|
-
expect(clearBtn.hasAttribute('hidden')).toBe(false);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('multi-select: [select-all] renders the bulk-action header', async () => {
|
|
322
|
-
const s = mount(`
|
|
323
|
-
<select-ui multiple select-all open>
|
|
324
|
-
<option value="a">Alpha</option>
|
|
325
|
-
<option value="b">Beta</option>
|
|
326
|
-
</select-ui>
|
|
327
|
-
`);
|
|
328
|
-
await tick();
|
|
329
|
-
const header = s.querySelector('[slot="listbox"] [data-select-all]');
|
|
330
|
-
expect(header).toBeTruthy();
|
|
331
|
-
const btn = header.querySelector('[data-select-all-btn]');
|
|
332
|
-
expect(btn).toBeTruthy();
|
|
333
|
-
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
334
|
-
expect(btn.tagName).toBe('BUTTON-UI');
|
|
335
|
-
expect(btn.getAttribute('text')).toBe('Select all');
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('multi-select: chip remove event empties that selection from value', async () => {
|
|
339
|
-
const s = mount(`
|
|
340
|
-
<select-ui multiple value="a,b">
|
|
341
|
-
<option value="a">Alpha</option>
|
|
342
|
-
<option value="b">Beta</option>
|
|
343
|
-
</select-ui>
|
|
344
|
-
`);
|
|
345
|
-
await tick();
|
|
346
|
-
const chipA = s.querySelector('tag-ui[data-chip-value="a"]');
|
|
347
|
-
expect(chipA).toBeTruthy();
|
|
348
|
-
// Simulate tag-ui's remove event (bubbles, with detail.text)
|
|
349
|
-
chipA.dispatchEvent(new CustomEvent('remove', {
|
|
350
|
-
bubbles: true,
|
|
351
|
-
detail: { text: 'Alpha', value: 'Alpha' },
|
|
352
|
-
}));
|
|
353
|
-
await tick();
|
|
354
|
-
expect(s.value).toBe('b');
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// §FB-88 — placeholder-only select derives an accessible name (WCAG 4.1.2),
|
|
358
|
-
// mirroring input-ui, without clobbering a real label.
|
|
359
|
-
describe('accessible name from placeholder (FB-88)', () => {
|
|
360
|
-
it('names a placeholder-only select from its placeholder', async () => {
|
|
361
|
-
const s = mount(`<select-ui placeholder="Status"></select-ui>`);
|
|
362
|
-
await tick();
|
|
363
|
-
expect(s.getAttribute('aria-label')).toBe('Status');
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('does NOT derive a name from the default "Select..." placeholder', async () => {
|
|
367
|
-
const s = mount(`<select-ui></select-ui>`);
|
|
368
|
-
await tick();
|
|
369
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('preserves an author-supplied aria-label (override wins over placeholder)', async () => {
|
|
373
|
-
const s = mount(`<select-ui aria-label="Filter by status" placeholder="Status"></select-ui>`);
|
|
374
|
-
await tick();
|
|
375
|
-
expect(s.getAttribute('aria-label')).toBe('Filter by status');
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('does not set aria-label when a visible label is present', async () => {
|
|
379
|
-
const s = mount(`<select-ui label="Status" placeholder="Pick one"></select-ui>`);
|
|
380
|
-
await tick();
|
|
381
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('does not set aria-label when aria-labelledby is present (e.g. field-ui)', async () => {
|
|
385
|
-
const s = mount(`<select-ui aria-labelledby="ext-label" placeholder="Pick one"></select-ui>`);
|
|
386
|
-
await tick();
|
|
387
|
-
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// gh#339 — mark (adia-mark-ui) as a leading visual, alongside icon/avatar.
|
|
392
|
-
describe('mark (gh#339)', () => {
|
|
393
|
-
it('renders <adia-mark-ui> in the listbox row for a declarative <option mark>', async () => {
|
|
394
|
-
const s = mount(`
|
|
395
|
-
<select-ui placeholder="Choose…">
|
|
396
|
-
<option value="adia" mark>Adia</option>
|
|
397
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
398
|
-
</select-ui>
|
|
399
|
-
`);
|
|
400
|
-
await tick();
|
|
401
|
-
const row = s.querySelector('[role="option"][data-value="adia"]');
|
|
402
|
-
expect(row.querySelector('adia-mark-ui')).not.toBeNull();
|
|
403
|
-
expect(s.options.find((o) => o.value === 'adia').mark).toBe(true);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('reflects the SELECTED option\'s mark in the trigger leading slot', async () => {
|
|
407
|
-
const s = mount(`
|
|
408
|
-
<select-ui value="adia">
|
|
409
|
-
<option value="adia" mark>Adia</option>
|
|
410
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
411
|
-
</select-ui>
|
|
412
|
-
`);
|
|
413
|
-
await tick();
|
|
414
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
415
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('mark wins over avatar and icon when more than one is set on the same option', async () => {
|
|
419
|
-
const s = mount(`<select-ui value="a"></select-ui>`);
|
|
420
|
-
await tick();
|
|
421
|
-
s.options = [{ value: 'a', label: 'A', mark: true, avatar: 'https://example.test/a.png', icon: 'star' }];
|
|
422
|
-
await tick();
|
|
423
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
424
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it('switching the SELECTED option from mark to avatar swaps the trigger leading element (not just its attributes)', async () => {
|
|
428
|
-
const s = mount(`
|
|
429
|
-
<select-ui value="adia">
|
|
430
|
-
<option value="adia" mark>Adia</option>
|
|
431
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
432
|
-
</select-ui>
|
|
433
|
-
`);
|
|
434
|
-
await tick();
|
|
435
|
-
expect(s.querySelector('[slot="trigger"] > [data-select-leading]').tagName).toBe('ADIA-MARK-UI');
|
|
436
|
-
s.value = 'acme';
|
|
437
|
-
await tick();
|
|
438
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
439
|
-
expect(leading.tagName).toBe('IMG');
|
|
440
|
-
expect(leading.getAttribute('src')).toBe('https://example.test/acme.png');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('a host-level [mark] renders the brand mark as a static leading visual when no option overrides it', async () => {
|
|
444
|
-
const s = mount(`
|
|
445
|
-
<select-ui mark value="adia">
|
|
446
|
-
<option value="adia">Adia</option>
|
|
447
|
-
</select-ui>
|
|
448
|
-
`);
|
|
449
|
-
await tick();
|
|
450
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
451
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it('renders <adia-mark-ui> in a listbox ROW under [multiple] mode too (shares #optionLeadHTML with single-select)', async () => {
|
|
455
|
-
const s = mount(`
|
|
456
|
-
<select-ui multiple value="adia">
|
|
457
|
-
<option value="adia" mark>Adia</option>
|
|
458
|
-
<option value="acme" avatar="https://example.test/acme.png">Acme</option>
|
|
459
|
-
</select-ui>
|
|
460
|
-
`);
|
|
461
|
-
await tick();
|
|
462
|
-
const row = s.querySelector('[role="option"][data-value="adia"]');
|
|
463
|
-
expect(row.querySelector('adia-mark-ui')).not.toBeNull();
|
|
464
|
-
// Multi-select shows chips, not a leading visual, in the TRIGGER — mark
|
|
465
|
-
// is a correct no-op there (#syncLeading's `if (this.multiple) return`).
|
|
466
|
-
expect(s.querySelector('[slot="trigger"] > [data-select-leading]')).toBeNull();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('mark set via the .options property (not just the declarative attribute) is honoured', async () => {
|
|
470
|
-
const s = mount(`<select-ui value="adia"></select-ui>`);
|
|
471
|
-
await tick();
|
|
472
|
-
s.options = [
|
|
473
|
-
{ value: 'adia', label: 'Adia', mark: true },
|
|
474
|
-
{ value: 'acme', label: 'Acme', avatar: 'https://example.test/acme.png' },
|
|
475
|
-
];
|
|
476
|
-
await tick();
|
|
477
|
-
const leading = s.querySelector('[slot="trigger"] > [data-select-leading]');
|
|
478
|
-
expect(leading.tagName).toBe('ADIA-MARK-UI');
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './slider.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('slider-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
// §330 (v0.5.16) — slider iOS-style pill redesign. Thumb position no
|
|
18
|
-
// longer set via thumb.style.left; instead the host gets a CSS custom
|
|
19
|
-
// property `--slider-pct` (fraction 0.0–1.0) and CSS calc() composes
|
|
20
|
-
// the full geometry: `fill_width(p) = t + p·(W−t)`, where t = thumb
|
|
21
|
-
// width and W = track width. Test asserts the CSS variable carries
|
|
22
|
-
// the expected fraction (50% → 0.5).
|
|
23
|
-
const pct = (s) => s.style.getPropertyValue('--slider-pct').trim();
|
|
24
|
-
|
|
25
|
-
it('renders host --slider-pct for initial value', async () => {
|
|
26
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
27
|
-
await tick();
|
|
28
|
-
expect(pct(s)).toBe('0.5');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Property reactivity contract — locks in the behavior that consumer
|
|
32
|
-
// feedback (FEEDBACK-adia-packages.md §5, 2026-05-12) incorrectly
|
|
33
|
-
// claimed was broken. UIElement's signal-backed property setters
|
|
34
|
-
// trigger the host's render effect on every property change; slider-ui's
|
|
35
|
-
// render() reads this.value and writes --slider-pct on the host.
|
|
36
|
-
it('updates --slider-pct when .value is set programmatically', async () => {
|
|
37
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
38
|
-
await tick();
|
|
39
|
-
expect(pct(s)).toBe('0.5');
|
|
40
|
-
|
|
41
|
-
s.value = 75;
|
|
42
|
-
await tick();
|
|
43
|
-
expect(pct(s)).toBe('0.75');
|
|
44
|
-
|
|
45
|
-
s.value = 10;
|
|
46
|
-
await tick();
|
|
47
|
-
expect(pct(s)).toBe('0.1');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('updates --slider-pct when attribute is set imperatively', async () => {
|
|
51
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
52
|
-
await tick();
|
|
53
|
-
|
|
54
|
-
s.setAttribute('value', '25');
|
|
55
|
-
await tick();
|
|
56
|
-
expect(pct(s)).toBe('0.25');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('updates [slot="value"] readout text when .value changes', async () => {
|
|
60
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
61
|
-
await tick();
|
|
62
|
-
const readout = s.querySelector('[slot="value"]');
|
|
63
|
-
expect(readout.textContent).toBe('50');
|
|
64
|
-
|
|
65
|
-
s.value = 88;
|
|
66
|
-
await tick();
|
|
67
|
-
expect(readout.textContent).toBe('88');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('emits CustomEvent("change") with detail.value on keyboard step', async () => {
|
|
71
|
-
const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
|
|
72
|
-
await tick();
|
|
73
|
-
|
|
74
|
-
let captured = null;
|
|
75
|
-
s.addEventListener('change', (e) => { captured = e; });
|
|
76
|
-
|
|
77
|
-
s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
78
|
-
await tick();
|
|
79
|
-
|
|
80
|
-
expect(captured).not.toBeNull();
|
|
81
|
-
expect(captured).toBeInstanceOf(CustomEvent);
|
|
82
|
-
expect(captured.detail).toEqual({ value: 51 });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('emits CustomEvent("input") with detail.value on internal value change', async () => {
|
|
86
|
-
const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
|
|
87
|
-
await tick();
|
|
88
|
-
|
|
89
|
-
let captured = null;
|
|
90
|
-
s.addEventListener('input', (e) => { captured = e; });
|
|
91
|
-
|
|
92
|
-
s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
|
93
|
-
await tick();
|
|
94
|
-
|
|
95
|
-
expect(captured).not.toBeNull();
|
|
96
|
-
expect(captured.detail).toEqual({ value: 51 });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('reflects value to [aria-valuenow] for screen readers', async () => {
|
|
100
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
101
|
-
await tick();
|
|
102
|
-
expect(s.getAttribute('aria-valuenow')).toBe('50');
|
|
103
|
-
|
|
104
|
-
s.value = 80;
|
|
105
|
-
await tick();
|
|
106
|
-
expect(s.getAttribute('aria-valuenow')).toBe('80');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// §FB-45 (color-app, 2026-05-22) — template engine snapshot trap.
|
|
110
|
-
// connected() stamped label/suffix once; render() didn't re-sync them.
|
|
111
|
-
// Template engine writes {{p:N}} first, then resolves to the real value —
|
|
112
|
-
// simulated here by setAttribute('label', placeholder) then setting the
|
|
113
|
-
// real value. Both spans must reflect the final value, not the placeholder.
|
|
114
|
-
it('re-syncs [slot="label"] when label attribute changes after connection', async () => {
|
|
115
|
-
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
116
|
-
await tick();
|
|
117
|
-
// No label initially — span absent
|
|
118
|
-
expect(s.querySelector('[slot="label"]')).toBeNull();
|
|
119
|
-
|
|
120
|
-
// Simulate template engine writing placeholder then real value
|
|
121
|
-
s.setAttribute('label', '{{p:7}}');
|
|
122
|
-
await tick();
|
|
123
|
-
s.setAttribute('label', 'Volume');
|
|
124
|
-
await tick();
|
|
125
|
-
|
|
126
|
-
const labelEl = s.querySelector('[slot="label"]');
|
|
127
|
-
expect(labelEl).not.toBeNull();
|
|
128
|
-
expect(labelEl.textContent).toBe('Volume');
|
|
129
|
-
expect(s.getAttribute('aria-label')).toBe('Volume');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('re-syncs [slot="suffix"] when suffix attribute changes after connection', async () => {
|
|
133
|
-
const s = mount('<slider-ui value="50" min="0" max="100" label="Volume" suffix="%"></slider-ui>');
|
|
134
|
-
await tick();
|
|
135
|
-
const suffixEl = s.querySelector('[slot="suffix"]');
|
|
136
|
-
expect(suffixEl).not.toBeNull();
|
|
137
|
-
expect(suffixEl.textContent).toBe('%');
|
|
138
|
-
|
|
139
|
-
// Simulate placeholder → resolved suffix
|
|
140
|
-
s.setAttribute('suffix', '{{p:8}}');
|
|
141
|
-
await tick();
|
|
142
|
-
s.setAttribute('suffix', 'rem');
|
|
143
|
-
await tick();
|
|
144
|
-
|
|
145
|
-
expect(s.querySelector('[slot="suffix"]').textContent).toBe('rem');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// ─── Dual-thumb mode ────────────────────────────────────────────────
|
|
149
|
-
// v1.9.x: dual-thumb range slider lives on slider-ui[dual] + [lower-value]/[upper-value].
|
|
150
|
-
// Fill spans BETWEEN the two thumb centers; thumbs are positioned via
|
|
151
|
-
// --slider-pct-lower / --slider-pct-upper CSS variables.
|
|
152
|
-
const pctLower = (s) => s.style.getPropertyValue('--slider-pct-lower').trim();
|
|
153
|
-
const pctUpper = (s) => s.style.getPropertyValue('--slider-pct-upper').trim();
|
|
154
|
-
|
|
155
|
-
it('dual mode: writes --slider-pct-lower and --slider-pct-upper from lower-value / upper-value', async () => {
|
|
156
|
-
const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
|
|
157
|
-
await tick();
|
|
158
|
-
expect(pctLower(s)).toBe('0.2');
|
|
159
|
-
expect(pctUpper(s)).toBe('0.8');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('dual mode: stamps two thumb elements with data-thumb="lower" and data-thumb="upper"', async () => {
|
|
163
|
-
const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
|
|
164
|
-
await tick();
|
|
165
|
-
const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
|
|
166
|
-
const upperThumb = s.querySelector('[slot="thumb"][data-thumb="upper"]');
|
|
167
|
-
expect(lowerThumb).not.toBeNull();
|
|
168
|
-
expect(upperThumb).not.toBeNull();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('dual mode: change event detail carries {lower, upper}', async () => {
|
|
172
|
-
const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
|
|
173
|
-
await tick();
|
|
174
|
-
let captured = null;
|
|
175
|
-
s.addEventListener('change', (e) => { captured = e; });
|
|
176
|
-
// Focus the lower thumb explicitly so the keyboard handler targets it
|
|
177
|
-
const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
|
|
178
|
-
lowerThumb.focus();
|
|
179
|
-
lowerThumb.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
180
|
-
await tick();
|
|
181
|
-
expect(captured).not.toBeNull();
|
|
182
|
-
expect(captured.detail).toEqual({ lower: 21, upper: 80 });
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('dual mode: lower setter clamps DOWN to upper when set too high', async () => {
|
|
186
|
-
const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
|
|
187
|
-
await tick();
|
|
188
|
-
s.lowerValue = 90; // > upperValue (80) → constraint clamps lower DOWN to upper
|
|
189
|
-
await tick();
|
|
190
|
-
expect(s.lowerValue).toBe(80);
|
|
191
|
-
expect(s.upperValue).toBe(80); // upper unchanged
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('dual mode: upper setter clamps UP to lower when set too low', async () => {
|
|
195
|
-
const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
|
|
196
|
-
await tick();
|
|
197
|
-
s.upperValue = 10; // < lowerValue (20) → constraint clamps upper UP to lower
|
|
198
|
-
await tick();
|
|
199
|
-
expect(s.upperValue).toBe(20);
|
|
200
|
-
expect(s.lowerValue).toBe(20); // lower unchanged
|
|
201
|
-
});
|
|
202
|
-
});
|