@adia-ai/web-components 0.6.24 → 0.6.25
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 +11 -0
- package/components/select/class.js +19 -6
- package/components/select/select.test.js +89 -0
- package/components/table/class.js +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.6.25] — 2026-05-22
|
|
4
|
+
|
|
5
|
+
### Fixed — `table-ui` Enter key activates focused body cell (FB-47)
|
|
6
|
+
|
|
7
|
+
- **Enter on a focused body cell now dispatches a click**, triggering the existing `cell-click` → `row-click` pipeline — the same event chain that fires on mouse click. Previously Enter was a no-op on body cells; only header cells triggered sort. Aligns with WAI-ARIA Grid Pattern §6.2.2. File: `components/table/class.js`.
|
|
8
|
+
|
|
9
|
+
### Fixed — `select-ui[multiple]` pre-initialization via `<option selected>` and `.value=` setter (FB-46)
|
|
10
|
+
|
|
11
|
+
- **`[multiple]` now correctly captures ALL `<option selected>` children** as the initial value (joined with commas), not just the first. The single-value `preSelected` accumulator was replaced with `preSelectedArr` that collects every selected option before setting `this.value = arr.join(',')`.
|
|
12
|
+
- **`#renderOptions()` and the aria-selected sync loop** now use a `Set` derived from `this.value.split(',')` for `[multiple]` mode, so every pre-selected option gets `aria-selected="true"` rendered. File: `components/select/class.js`.
|
|
13
|
+
|
|
3
14
|
## [0.6.24] — 2026-05-22
|
|
4
15
|
|
|
5
16
|
### Fixed — `slider-ui` label and suffix template snapshot trap (FB-45)
|
|
@@ -171,9 +171,14 @@ export class UISelect extends UIFormElement {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// Sync aria-selected when value changes
|
|
174
|
+
// §FB-46: [multiple] value is comma-separated — use Set membership.
|
|
174
175
|
if (this.#listbox) {
|
|
176
|
+
const selSet = this.multiple
|
|
177
|
+
? new Set(this.value.split(',').map((s) => s.trim()).filter(Boolean))
|
|
178
|
+
: null;
|
|
175
179
|
for (const opt of this.#listbox.querySelectorAll('[role="option"]')) {
|
|
176
|
-
const
|
|
180
|
+
const v = opt.getAttribute('data-value');
|
|
181
|
+
const selected = selSet ? selSet.has(v) : v === this.value;
|
|
177
182
|
if (selected) opt.setAttribute('aria-selected', 'true');
|
|
178
183
|
else opt.removeAttribute('aria-selected');
|
|
179
184
|
}
|
|
@@ -214,18 +219,19 @@ export class UISelect extends UIFormElement {
|
|
|
214
219
|
const unknownTags = new Set();
|
|
215
220
|
// FEEDBACK-36: capture the value of an <option selected> child so a
|
|
216
221
|
// declarative initial selection is honoured (native <select> parity).
|
|
217
|
-
|
|
222
|
+
// §FB-46: collect ALL selected options for [multiple] mode (not just first).
|
|
223
|
+
const preSelectedArr = [];
|
|
218
224
|
for (const child of this.children) {
|
|
219
225
|
if (child.tagName === 'OPTGROUP') {
|
|
220
226
|
const group = { label: child.label || child.getAttribute('label') || '', options: [] };
|
|
221
227
|
for (const opt of child.querySelectorAll('option')) {
|
|
222
228
|
group.options.push({ value: opt.value, label: opt.textContent.trim(), disabled: opt.disabled });
|
|
223
|
-
if (
|
|
229
|
+
if (opt.hasAttribute('selected')) preSelectedArr.push(opt.value);
|
|
224
230
|
}
|
|
225
231
|
this.#options.push(group);
|
|
226
232
|
} else if (child.tagName === 'OPTION') {
|
|
227
233
|
this.#options.push({ value: child.value, label: child.textContent.trim(), disabled: child.disabled });
|
|
228
|
-
if (
|
|
234
|
+
if (child.hasAttribute('selected')) preSelectedArr.push(child.value);
|
|
229
235
|
} else if (
|
|
230
236
|
// §225: skip [slot="display"] / [slot="listbox"] / [slot="action"] etc. — these are
|
|
231
237
|
// stamped by the component itself, not consumer-authored options. Anything other
|
|
@@ -239,7 +245,10 @@ export class UISelect extends UIFormElement {
|
|
|
239
245
|
// FEEDBACK-36: honour <option selected> as the initial value when the host
|
|
240
246
|
// carries no [value] — matches native <select> semantics. Guarded on an
|
|
241
247
|
// empty this.value so consumers who set [value] explicitly are unaffected.
|
|
242
|
-
|
|
248
|
+
// §FB-46: for [multiple], join all selected values with comma.
|
|
249
|
+
if (!this.value && preSelectedArr.length) {
|
|
250
|
+
this.value = this.multiple ? preSelectedArr.join(',') : preSelectedArr[0];
|
|
251
|
+
}
|
|
243
252
|
// §225 (v0.5.9): warn once per element when consumer authored non-<option> children.
|
|
244
253
|
if (unknownTags.size > 0 && !UISelect.#warnedNonOption.has(this)) {
|
|
245
254
|
UISelect.#warnedNonOption.add(this);
|
|
@@ -297,7 +306,11 @@ export class UISelect extends UIFormElement {
|
|
|
297
306
|
el.setAttribute('data-value', opt.value || '');
|
|
298
307
|
if (opt.icon) el.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
|
|
299
308
|
else el.textContent = opt.label;
|
|
300
|
-
|
|
309
|
+
// §FB-46: [multiple] value is comma-separated — use Set membership.
|
|
310
|
+
const selSet = this.multiple
|
|
311
|
+
? new Set(this.value.split(',').map((s) => s.trim()).filter(Boolean))
|
|
312
|
+
: null;
|
|
313
|
+
if (selSet ? selSet.has(opt.value) : opt.value === this.value) el.setAttribute('aria-selected', 'true');
|
|
301
314
|
if (opt.disabled) el.setAttribute('aria-disabled', 'true');
|
|
302
315
|
if (opt.action) el.dataset.action = opt.action;
|
|
303
316
|
el.__adiaOption = opt;
|
|
@@ -0,0 +1,89 @@
|
|
|
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-46 — <option selected> must capture ALL values in [multiple] mode
|
|
31
|
+
it('captures all <option selected> values as comma-separated for [multiple]', async () => {
|
|
32
|
+
const s = mount(`
|
|
33
|
+
<select-ui multiple>
|
|
34
|
+
<option value="a" selected>Alpha</option>
|
|
35
|
+
<option value="b">Beta</option>
|
|
36
|
+
<option value="c" selected>Gamma</option>
|
|
37
|
+
</select-ui>
|
|
38
|
+
`);
|
|
39
|
+
await tick();
|
|
40
|
+
// All selected values should be present (order-agnostic)
|
|
41
|
+
const parts = new Set(s.value.split(',').map(v => v.trim()));
|
|
42
|
+
expect(parts.has('a')).toBe(true);
|
|
43
|
+
expect(parts.has('c')).toBe(true);
|
|
44
|
+
expect(parts.has('b')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// §FB-46 — .value setter with comma-separated string must sync aria-selected
|
|
48
|
+
it('syncs aria-selected on all comma-separated values when .value is set', async () => {
|
|
49
|
+
const s = mount(`
|
|
50
|
+
<select-ui multiple open>
|
|
51
|
+
<option value="a">Alpha</option>
|
|
52
|
+
<option value="b">Beta</option>
|
|
53
|
+
<option value="c">Gamma</option>
|
|
54
|
+
</select-ui>
|
|
55
|
+
`);
|
|
56
|
+
await tick();
|
|
57
|
+
s.value = 'a,c';
|
|
58
|
+
await tick();
|
|
59
|
+
|
|
60
|
+
const opts = s.querySelectorAll('[role="option"]');
|
|
61
|
+
const selected = Array.from(opts)
|
|
62
|
+
.filter(o => o.getAttribute('aria-selected') === 'true')
|
|
63
|
+
.map(o => o.getAttribute('data-value'));
|
|
64
|
+
|
|
65
|
+
expect(selected).toContain('a');
|
|
66
|
+
expect(selected).toContain('c');
|
|
67
|
+
expect(selected).not.toContain('b');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Regression: single-select aria-selected must still work with the Set path
|
|
71
|
+
it('still sets aria-selected correctly for single-select after §FB-46 changes', async () => {
|
|
72
|
+
const s = mount(`
|
|
73
|
+
<select-ui open>
|
|
74
|
+
<option value="a">Alpha</option>
|
|
75
|
+
<option value="b">Beta</option>
|
|
76
|
+
</select-ui>
|
|
77
|
+
`);
|
|
78
|
+
await tick();
|
|
79
|
+
s.value = 'b';
|
|
80
|
+
await tick();
|
|
81
|
+
|
|
82
|
+
const opts = s.querySelectorAll('[role="option"]');
|
|
83
|
+
const selected = Array.from(opts)
|
|
84
|
+
.filter(o => o.getAttribute('aria-selected') === 'true')
|
|
85
|
+
.map(o => o.getAttribute('data-value'));
|
|
86
|
+
|
|
87
|
+
expect(selected).toEqual(['b']);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -1431,12 +1431,18 @@ export class UITable extends UIElement {
|
|
|
1431
1431
|
if (row < -1 || row >= totalRows) { handled = false; break; }
|
|
1432
1432
|
break;
|
|
1433
1433
|
case 'Enter':
|
|
1434
|
-
// If on header row, trigger sort
|
|
1435
1434
|
if (row === -1) {
|
|
1435
|
+
// Header row: trigger sort via the existing click path
|
|
1436
1436
|
const header = this.querySelector(':scope > [data-header]');
|
|
1437
1437
|
const cell = header?.children[col];
|
|
1438
1438
|
const sortKey = cell?.dataset.sortKey;
|
|
1439
1439
|
if (sortKey) cell.click();
|
|
1440
|
+
} else {
|
|
1441
|
+
// Body row: activate focused cell (WAI-ARIA Grid Pattern §6.2.2).
|
|
1442
|
+
// Delegates to the existing click → cell-click → row-click pipeline
|
|
1443
|
+
// so consumers get the same event regardless of mouse vs keyboard.
|
|
1444
|
+
const focused = this.querySelector('[data-focused]');
|
|
1445
|
+
if (focused) focused.click();
|
|
1440
1446
|
}
|
|
1441
1447
|
break;
|
|
1442
1448
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.25",
|
|
4
4
|
"description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|