@adia-ai/web-components 0.6.43 → 0.6.45
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 +25 -0
- package/components/canvas/canvas.js +76 -9
- package/components/select/select.class.js +35 -0
- package/components/select/select.test.js +34 -0
- package/dist/web-components.min.js +107 -107
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.6.45] — 2026-05-29
|
|
4
|
+
|
|
5
|
+
### Fixed — `<canvas-ui>` lazy `a2ui-root` import uses a bare specifier (FEEDBACK-85, follow-up to FB-81)
|
|
6
|
+
|
|
7
|
+
- **`components/canvas/canvas.js`** — the FB-81 lazy import used a *relative* cross-package path (`../../../web-modules/runtime/a2ui-root/a2ui-root.js`). That resolves from the real `node_modules` location but breaks once Vite pre-bundles `web-components` into `node_modules/.vite/deps/` — `vite:import-analysis` fails to resolve the relative path (even with `/* @vite-ignore */`) and aborts dep optimization, so the app won't boot (blank page, Vite overlay only). Switched to a **bare specifier** `import('@adia-ai/web-modules/runtime/a2ui-root')`, which resolves identically whether `web-components` is pre-bundled or not (via web-modules' `./runtime/*` export). **`@adia-ai/web-modules` is now declared as an `optionalDependency`** (`^0.6.0`) so the specifier resolves under any bundler; the import stays dynamic + `.catch()`-guarded, so it's loaded only when `<canvas-ui>` is used and degrades gracefully if web-modules is genuinely absent. Lockfile regen (`npm install --package-lock-only`) required at the release cut.
|
|
8
|
+
|
|
9
|
+
### Fixed — `<select-ui>` derives an accessible name from `placeholder` (FEEDBACK-88)
|
|
10
|
+
|
|
11
|
+
- **`components/select/select.class.js`** — a placeholder-only `<select-ui placeholder="Status">` (no `label`/`aria-label`/`<field-ui>`) had **no accessible name** (WCAG 4.1.2 fail), while the sibling `<input-ui>` names itself from its placeholder. Added `#syncAccessibleName()` (run in `render()`) mirroring input-ui: derive `aria-label` from `placeholder` when the select is otherwise unnamed. **Guards** (a placeholder-only fix must not clobber a real label): an author `aria-label`, a visible `label`, or `aria-labelledby` always win; the **default `'Select...'` prompt is never used as a name** — otherwise every `<field-ui>`-wrapped select (named via a sibling `<label for>`, which `aria-label` would override) would be renamed to the generic prompt. A `#placeholderNamed` flag tracks the derived name so re-renders refresh/clear it without touching an author's aria-label. +5 unit tests. Known residual (matches input-ui): a `<field-ui>` + a *custom* placeholder on the same select still yields an aria-label from the placeholder — an unusual author combo.
|
|
12
|
+
|
|
13
|
+
### Maintenance
|
|
14
|
+
|
|
15
|
+
- **`package.json`** — `@adia-ai/web-modules` added as an `optionalDependency` (`^0.6.0`) for the FB-85 bare-specifier resolution; lockfile regenerated.
|
|
16
|
+
- **`dist/web-components.min.js` + `dist/icons-manifest.js`** — bundle rebuild reflecting the `canvas.js` + `select.class.js` fixes above.
|
|
17
|
+
|
|
18
|
+
## [0.6.44] — 2026-05-28
|
|
19
|
+
|
|
20
|
+
### Fixed — `<canvas-ui>` no longer hard-requires `@adia-ai/web-modules` at build time (FEEDBACK-81)
|
|
21
|
+
|
|
22
|
+
- **`components/canvas/canvas.js`** — the A2UI renderer `<a2ui-root>` (which lives in `@adia-ai/web-modules`) was a top-level *static* import. Because `@adia-ai/web-components` declares no dependency on `@adia-ai/web-modules`, the default barrel `import '@adia-ai/web-components'` (which registers `<canvas-ui>`) failed to build for primitives-only consumers who omitted `web-modules` — a hard Vite/rolldown `UNRESOLVED_IMPORT … web-modules/runtime/a2ui-root`. The import is now lazy + guarded: `#ensureRenderer()` runs a `/* @vite-ignore */` dynamic import on `connected()`, and a missing `web-modules` degrades to a one-time console warning instead of breaking the build. Consumers that have `web-modules` keep the prior synchronous render path (fast-path when `<a2ui-root>` is already defined); `process` / `processAll` / `reset` / version-restore buffer until the renderer is ready, preserving call order. The primitives barrel is now self-contained.
|
|
23
|
+
|
|
24
|
+
### Maintenance
|
|
25
|
+
|
|
26
|
+
- **`dist/web-components.min.js` + `dist/icons-manifest.js`** — bundle rebuild reflecting the `components/canvas/canvas.js` decouple above.
|
|
27
|
+
|
|
3
28
|
## [0.6.43] — 2026-05-27
|
|
4
29
|
|
|
5
30
|
### Added — `<list-ui>` `[contained]` prop declared in yaml
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { UIElement } from '../../core/element.js';
|
|
2
|
-
|
|
2
|
+
// NOTE: the A2UI renderer (<a2ui-root>) lives in @adia-ai/web-modules and is
|
|
3
|
+
// loaded lazily + guarded inside #ensureRenderer() (kicked off from connected()).
|
|
4
|
+
// Two constraints shape this import:
|
|
5
|
+
// • NOT static (FEEDBACK-81): a static cross-package import hard-fails the build
|
|
6
|
+
// for primitives-only consumers who never use <canvas-ui>. Hence lazy import().
|
|
7
|
+
// • BARE specifier, not a relative path (FEEDBACK-85): a relative
|
|
8
|
+
// '../../../web-modules/…' breaks Vite dep pre-bundling — once web-components is
|
|
9
|
+
// flattened into node_modules/.vite/deps/, the relative path no longer resolves
|
|
10
|
+
// and import-analysis aborts (even with @vite-ignore). A bare specifier resolves
|
|
11
|
+
// identically whether pre-bundled or not. @adia-ai/web-modules is declared as an
|
|
12
|
+
// optionalDependency so it resolves; the .catch() below degrades gracefully if
|
|
13
|
+
// it is genuinely absent at runtime.
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* <canvas-ui> — A2UI rendering surface.
|
|
@@ -25,6 +36,10 @@ export class UICanvas extends UIElement {
|
|
|
25
36
|
#historyIndex = -1;
|
|
26
37
|
static MAX_HISTORY = 10;
|
|
27
38
|
|
|
39
|
+
// FB-81: lazy/guarded <a2ui-root> loader state.
|
|
40
|
+
#rendererReady = null; // Promise<boolean> | null
|
|
41
|
+
static #warnedMissingRenderer = false;
|
|
42
|
+
|
|
28
43
|
#onKeydown = (e) => {
|
|
29
44
|
if (e.target.closest('input, textarea, [contenteditable]')) return;
|
|
30
45
|
if (!this.contains(document.activeElement) && document.activeElement !== document.body) return;
|
|
@@ -66,6 +81,8 @@ export class UICanvas extends UIElement {
|
|
|
66
81
|
this.addEventListener('input', this.#onInput);
|
|
67
82
|
// Forward a2ui-retry from wired error cards so consumers can retry generation
|
|
68
83
|
this.addEventListener('a2ui-retry', this.#onRetry);
|
|
84
|
+
// Kick off the lazy renderer load now that a canvas is in the DOM.
|
|
85
|
+
this.#ensureRenderer();
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
disconnected() {
|
|
@@ -79,6 +96,53 @@ export class UICanvas extends UIElement {
|
|
|
79
96
|
this.#bound = false;
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Lazily load the A2UI renderer (<a2ui-root>) from @adia-ai/web-modules.
|
|
101
|
+
* Resolves true once the element is defined, false if web-modules is not
|
|
102
|
+
* installed (primitives-only consumers). Idempotent — the import runs once.
|
|
103
|
+
* Guarded so a missing sibling package degrades to a one-time warning
|
|
104
|
+
* instead of a hard build/runtime failure (FEEDBACK-81).
|
|
105
|
+
*/
|
|
106
|
+
#ensureRenderer() {
|
|
107
|
+
if (this.#rendererReady) return this.#rendererReady;
|
|
108
|
+
if (customElements.get('a2ui-root')) {
|
|
109
|
+
this.#rendererReady = Promise.resolve(true);
|
|
110
|
+
return this.#rendererReady;
|
|
111
|
+
}
|
|
112
|
+
this.#rendererReady = import('@adia-ai/web-modules/runtime/a2ui-root')
|
|
113
|
+
.then(() => customElements.whenDefined('a2ui-root'))
|
|
114
|
+
.then(() => true)
|
|
115
|
+
.catch((err) => {
|
|
116
|
+
if (!UICanvas.#warnedMissingRenderer) {
|
|
117
|
+
UICanvas.#warnedMissingRenderer = true;
|
|
118
|
+
console.warn(
|
|
119
|
+
'[canvas-ui] A2UI renderer <a2ui-root> could not be loaded — install ' +
|
|
120
|
+
'@adia-ai/web-modules to render generated UI in <canvas-ui>. Apps that ' +
|
|
121
|
+
'use only primitives (and never <canvas-ui>) can ignore this.',
|
|
122
|
+
err,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
});
|
|
127
|
+
return this.#rendererReady;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run `fn(rootEl)` against the <a2ui-root> once it is available. Synchronous
|
|
132
|
+
* fast-path when the renderer is already upgraded (preserves prior timing for
|
|
133
|
+
* consumers that have @adia-ai/web-modules); otherwise defers until the lazy
|
|
134
|
+
* import resolves, preserving call order. No-op when web-modules is absent.
|
|
135
|
+
*/
|
|
136
|
+
#whenRoot(fn) {
|
|
137
|
+
if (this.#rootEl && typeof this.#rootEl.process === 'function') {
|
|
138
|
+
fn(this.#rootEl);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#ensureRenderer().then((ok) => {
|
|
142
|
+
if (ok && this.#rootEl && typeof this.#rootEl.process === 'function') fn(this.#rootEl);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
82
146
|
render() {
|
|
83
147
|
if (this.#bound) return;
|
|
84
148
|
this.#bound = true;
|
|
@@ -104,18 +168,19 @@ export class UICanvas extends UIElement {
|
|
|
104
168
|
|
|
105
169
|
/** Process a single A2UI message. */
|
|
106
170
|
process(message) {
|
|
107
|
-
this.#
|
|
171
|
+
this.#whenRoot((root) => root.process(message));
|
|
108
172
|
}
|
|
109
173
|
|
|
110
174
|
/** Process an array of A2UI messages. */
|
|
111
175
|
processAll(messages) {
|
|
112
|
-
|
|
113
|
-
|
|
176
|
+
this.#whenRoot((root) => {
|
|
177
|
+
for (const msg of messages) root.process(msg);
|
|
178
|
+
});
|
|
114
179
|
}
|
|
115
180
|
|
|
116
181
|
/** Reset the canvas. */
|
|
117
182
|
reset() {
|
|
118
|
-
this.#
|
|
183
|
+
this.#whenRoot((root) => root.reset());
|
|
119
184
|
}
|
|
120
185
|
|
|
121
186
|
/** Return formatted innerHTML of the a2ui-root. */
|
|
@@ -155,10 +220,12 @@ export class UICanvas extends UIElement {
|
|
|
155
220
|
|
|
156
221
|
#restoreVersion() {
|
|
157
222
|
const messages = this.#history[this.#historyIndex];
|
|
158
|
-
this.#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
this.#whenRoot((root) => {
|
|
224
|
+
root.reset();
|
|
225
|
+
if (messages) {
|
|
226
|
+
for (const msg of messages) root.process(msg);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
162
229
|
}
|
|
163
230
|
|
|
164
231
|
// ── Private ──
|
|
@@ -324,6 +324,37 @@ export class UISelect extends UIFormElement {
|
|
|
324
324
|
else this.selectAllOptions();
|
|
325
325
|
};
|
|
326
326
|
|
|
327
|
+
// FB-88: tracks whether the host's current aria-label was derived from
|
|
328
|
+
// `placeholder` (vs author-supplied), so re-renders refresh/clear OUR name
|
|
329
|
+
// without ever clobbering an author's aria-label.
|
|
330
|
+
#placeholderNamed = false;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Derive an accessible name from `placeholder` when the select is otherwise
|
|
334
|
+
* unnamed — mirrors input-ui so a placeholder-only select (the idiomatic
|
|
335
|
+
* filter-bar shape) still has a name for assistive tech (WCAG 4.1.2, FB-88).
|
|
336
|
+
*
|
|
337
|
+
* Guards (a placeholder-only fix must NOT clobber a real label):
|
|
338
|
+
* • an author `aria-label`, a visible `label`, or an `aria-labelledby` wins;
|
|
339
|
+
* • the default 'Select...' prompt is NOT used as a name — otherwise every
|
|
340
|
+
* field-ui-wrapped select (named by a sibling `<label for>`, which aria-label
|
|
341
|
+
* would override) would be renamed to the generic prompt.
|
|
342
|
+
*/
|
|
343
|
+
#syncAccessibleName() {
|
|
344
|
+
const DEFAULT_PLACEHOLDER = 'Select...'; // matches static properties.placeholder.default
|
|
345
|
+
const authoredAria = this.hasAttribute('aria-label') && !this.#placeholderNamed;
|
|
346
|
+
const labeledElsewhere = !!this.label || this.hasAttribute('aria-labelledby');
|
|
347
|
+
const namable = !authoredAria && !labeledElsewhere
|
|
348
|
+
&& this.placeholder && this.placeholder !== DEFAULT_PLACEHOLDER;
|
|
349
|
+
if (namable) {
|
|
350
|
+
this.setAttribute('aria-label', this.placeholder);
|
|
351
|
+
this.#placeholderNamed = true;
|
|
352
|
+
} else if (this.#placeholderNamed) {
|
|
353
|
+
this.removeAttribute('aria-label');
|
|
354
|
+
this.#placeholderNamed = false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
connected() {
|
|
328
359
|
super.connected();
|
|
329
360
|
this.setAttribute('role', 'combobox');
|
|
@@ -349,6 +380,10 @@ export class UISelect extends UIFormElement {
|
|
|
349
380
|
if (this.multiple) this.setAttribute('data-multi-chips', '');
|
|
350
381
|
else this.removeAttribute('data-multi-chips');
|
|
351
382
|
|
|
383
|
+
// A11y (FB-88): name a placeholder-only select from its placeholder, like
|
|
384
|
+
// input-ui, so it isn't unnamed for assistive tech (WCAG 4.1.2).
|
|
385
|
+
this.#syncAccessibleName();
|
|
386
|
+
|
|
352
387
|
// Stamp default trigger if none provided
|
|
353
388
|
if (!this.querySelector('[slot="trigger"]')) {
|
|
354
389
|
// Detach listbox before innerHTML wipe so it isn't destroyed
|
|
@@ -288,4 +288,38 @@ describe('select-ui', () => {
|
|
|
288
288
|
await tick();
|
|
289
289
|
expect(s.value).toBe('b');
|
|
290
290
|
});
|
|
291
|
+
|
|
292
|
+
// §FB-88 — placeholder-only select derives an accessible name (WCAG 4.1.2),
|
|
293
|
+
// mirroring input-ui, without clobbering a real label.
|
|
294
|
+
describe('accessible name from placeholder (FB-88)', () => {
|
|
295
|
+
it('names a placeholder-only select from its placeholder', async () => {
|
|
296
|
+
const s = mount(`<select-ui placeholder="Status"></select-ui>`);
|
|
297
|
+
await tick();
|
|
298
|
+
expect(s.getAttribute('aria-label')).toBe('Status');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('does NOT derive a name from the default "Select..." placeholder', async () => {
|
|
302
|
+
const s = mount(`<select-ui></select-ui>`);
|
|
303
|
+
await tick();
|
|
304
|
+
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('preserves an author-supplied aria-label (override wins over placeholder)', async () => {
|
|
308
|
+
const s = mount(`<select-ui aria-label="Filter by status" placeholder="Status"></select-ui>`);
|
|
309
|
+
await tick();
|
|
310
|
+
expect(s.getAttribute('aria-label')).toBe('Filter by status');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('does not set aria-label when a visible label is present', async () => {
|
|
314
|
+
const s = mount(`<select-ui label="Status" placeholder="Pick one"></select-ui>`);
|
|
315
|
+
await tick();
|
|
316
|
+
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('does not set aria-label when aria-labelledby is present (e.g. field-ui)', async () => {
|
|
320
|
+
const s = mount(`<select-ui aria-labelledby="ext-label" placeholder="Pick one"></select-ui>`);
|
|
321
|
+
await tick();
|
|
322
|
+
expect(s.hasAttribute('aria-label')).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
291
325
|
});
|