@adia-ai/web-components 0.7.18 → 0.7.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 +10 -0
- package/components/field/field.class.js +31 -2
- package/components/field/field.test.js +20 -0
- package/components/icon/icon.class.js +32 -2
- package/components/icon/icon.js +11 -0
- package/dist/web-components.min.js +58 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.7.20] — 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`field-ui` (`components/field/`) — control binding survives junk siblings.** `#findControl()` bound the *first slot-less child* as the control; generated markup that lands a `text-ui` "label" or a `<small>` error inside `field-ui` (the 2026-06-10 gen-UI contact-form incident) stole the `id`/`for` wiring from the real input. Binding now prefers a known form-control tag (timing-independent), then a focusable-containing child, then the old first-slot-less fallback. Regression test with the exact incident markup. `dist/web-components.min.js` regenerated.
|
|
7
|
+
|
|
8
|
+
## [0.7.19] — 2026-06-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`icon-ui` — glyphs always paint on the standalone import path.** Importing `components/icon` directly (or a demo page loading only `./icon.js`) never loaded `core/icons-phosphor.js`, so the weight-loader map stayed empty and every `getIcon()` returned `''` — **silently** (the not-ready guard suppressed the warning): blank icons, zero console errors. Two fixes: `icon.js` now imports the Phosphor loader as a side-effect (the barrel paths already did), and `icon.class.js` gains a signal-reactive retry (`#rev` bumped by `whenIconRegistryReady`, aborted symmetrically in `disconnected()`) so elements that connect *after* the loaders install — e.g. fetched-and-injected demo markup — re-render once the registry is ready. Production (barrel-served) was never affected; the docs/demo single-component path was. Regression probe: `scripts/dev/icon-paint-probe.mjs`. `dist/web-components.min.js` (and the web-modules kitchen-sink bundle) regenerated to carry the fix.
|
|
12
|
+
|
|
3
13
|
## [0.7.18] — 2026-06-09
|
|
4
14
|
|
|
5
15
|
### Changed
|
|
@@ -246,12 +246,41 @@ export class UIField extends UIElement {
|
|
|
246
246
|
/** The default-slot control — first child without a `slot` attribute. */
|
|
247
247
|
#findControl() {
|
|
248
248
|
// wrapper-trap-ok: field control is always a single literal child (one <input>/<textarea>/<select>); interpolating the control via .map() is not a documented field pattern
|
|
249
|
+
//
|
|
250
|
+
// Prefer the first slot-less child that is actually a CONTROL (focusable
|
|
251
|
+
// itself or containing a focusable). Generated markup sometimes lands
|
|
252
|
+
// junk siblings inside field-ui (a text-ui "label", a <small> error —
|
|
253
|
+
// the gen-UI contact-form incident, 2026-06-10): first-slot-less-child
|
|
254
|
+
// bound the label as the control, stealing the id/for wiring from the
|
|
255
|
+
// real input. Fall back to the old first-slot-less rule so a not-yet-
|
|
256
|
+
// upgraded custom control (no focusable stamped yet) still binds.
|
|
257
|
+
let fallback = null;
|
|
258
|
+
let focusableHit = null;
|
|
249
259
|
for (const ch of this.children) {
|
|
250
|
-
if (
|
|
260
|
+
if (ch.hasAttribute('slot')) continue;
|
|
261
|
+
if (fallback === null) fallback = ch;
|
|
262
|
+
// Deterministic first: a known form-control tag wins regardless of
|
|
263
|
+
// upgrade timing (its internal focusable may not be stamped yet at
|
|
264
|
+
// field-ui's connect, and the childList observer can't see grandchild
|
|
265
|
+
// stamps).
|
|
266
|
+
if (UIField.#CONTROL_TAGS.has(ch.localName)) return ch;
|
|
267
|
+
if (focusableHit === null && this.#firstFocusable(ch)) focusableHit = ch;
|
|
251
268
|
}
|
|
252
|
-
return
|
|
269
|
+
return focusableHit ?? fallback;
|
|
253
270
|
}
|
|
254
271
|
|
|
272
|
+
/** Form-control tags field-ui binds to by name (timing-independent).
|
|
273
|
+
* Source: the form-bearing primitives (UIFormElement subclasses + native
|
|
274
|
+
* wrappers); extend when a new control primitive lands. */
|
|
275
|
+
static #CONTROL_TAGS = new Set([
|
|
276
|
+
'input-ui', 'textarea-ui', 'select-ui', 'combobox-ui', 'search-ui',
|
|
277
|
+
'check-ui', 'radio-ui', 'switch-ui', 'slider-ui', 'range-ui',
|
|
278
|
+
'otp-input-ui', 'tags-input-ui', 'chat-input-ui', 'autocomplete-input-ui',
|
|
279
|
+
'datetime-picker-ui', 'date-range-picker-ui', 'calendar-picker-ui',
|
|
280
|
+
'color-picker-ui', 'upload-ui', 'rating-ui', 'segmented-ui',
|
|
281
|
+
'input', 'textarea', 'select',
|
|
282
|
+
]);
|
|
283
|
+
|
|
255
284
|
/** First focusable descendant (or self) of `root`. Looks for native
|
|
256
285
|
* `<input>`, `<textarea>`, `<select>`, `<button>`, `[contenteditable]`,
|
|
257
286
|
* and any `[tabindex]` ≥ 0. Light DOM only. */
|
|
@@ -237,4 +237,24 @@ describe('field-ui', () => {
|
|
|
237
237
|
/:scope:has\((?:>\s*)?\[slot="hint"\]\s*,\s*(?:>\s*)?\[slot="error"\]\)\s*\{[^}]*row-gap:\s*var\(--field-gap[,)]/
|
|
238
238
|
);
|
|
239
239
|
});
|
|
240
|
+
|
|
241
|
+
it('binds the CONTROL (not junk label/error siblings) — the gen-UI contact-form incident', async () => {
|
|
242
|
+
// Generated markup sometimes lands a text-ui "label" + a <small> error
|
|
243
|
+
// INSIDE field-ui (the 2026-06-10 canvas incident). The old first-slot-less
|
|
244
|
+
// rule bound the text-ui as the control, stealing the id/for wiring.
|
|
245
|
+
const el = mount(`
|
|
246
|
+
<field-ui label="Full Name">
|
|
247
|
+
<text-ui variant="label" weight="semibold">Full Name</text-ui>
|
|
248
|
+
<input-ui type="text" name="name" placeholder="Enter your full name"></input-ui>
|
|
249
|
+
<small color="danger">Name is required</small>
|
|
250
|
+
</field-ui>
|
|
251
|
+
`);
|
|
252
|
+
await tick(); await tick();
|
|
253
|
+
const input = el.querySelector('input-ui');
|
|
254
|
+
const textUi = el.querySelector('text-ui');
|
|
255
|
+
const label = el.querySelector('[data-field-label]');
|
|
256
|
+
expect(input.id).toMatch(/^field-ctl-/); // the real control got the binding
|
|
257
|
+
expect(textUi.id || '').not.toMatch(/^field-ctl-/); // the junk label did NOT
|
|
258
|
+
expect(label.getAttribute('for')).toBe(input.id);
|
|
259
|
+
});
|
|
240
260
|
});
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { UIElement
|
|
15
|
-
import { getIcon } from '../../core/icons.js';
|
|
14
|
+
import { UIElement } from '../../core/element.js';
|
|
15
|
+
import { getIcon, whenIconRegistryReady } from '../../core/icons.js';
|
|
16
16
|
|
|
17
17
|
export class UIIcon extends UIElement {
|
|
18
18
|
static properties = {
|
|
@@ -23,12 +23,42 @@ export class UIIcon extends UIElement {
|
|
|
23
23
|
weight: { type: String, default: 'regular', reflect: true },
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// Incremented when the icon registry becomes ready so the reactive
|
|
27
|
+
// render() re-runs automatically after the loader map arrives.
|
|
28
|
+
// Declared as a private field (not a static property) to keep it out
|
|
29
|
+
// of the attribute reflection loop — it is a purely internal trigger.
|
|
30
|
+
#rev = this.signal(0);
|
|
31
|
+
|
|
32
|
+
// Abort controller for the registry-ready subscription. Cancelled on
|
|
33
|
+
// disconnectedCallback so elements removed before the registry resolves
|
|
34
|
+
// do not attempt a render on a detached host.
|
|
35
|
+
#readyCtrl = null;
|
|
36
|
+
|
|
26
37
|
connected() {
|
|
27
38
|
this.setAttribute('role', this.label ? 'img' : 'presentation');
|
|
28
39
|
if (!this.label) this.setAttribute('aria-hidden', 'true');
|
|
40
|
+
|
|
41
|
+
// Subscribe to the registry-ready promise ONCE per connect cycle.
|
|
42
|
+
// When installIconLoaders() fires (could be before or after connect),
|
|
43
|
+
// bump #rev so the reactive render() reruns and picks up the SVG.
|
|
44
|
+
// Guard: if the registry was already ready (e.g. warm page reload),
|
|
45
|
+
// the .then() fires in a microtask — still safe, just a no-op repaint.
|
|
46
|
+
this.#readyCtrl = new AbortController();
|
|
47
|
+
const ctrl = this.#readyCtrl;
|
|
48
|
+
whenIconRegistryReady.then(() => {
|
|
49
|
+
if (!ctrl.signal.aborted) this.#rev.value++;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
disconnected() {
|
|
54
|
+
this.#readyCtrl?.abort();
|
|
55
|
+
this.#readyCtrl = null;
|
|
29
56
|
}
|
|
30
57
|
|
|
31
58
|
render() {
|
|
59
|
+
// Read #rev so this render() is tracked as a dependent of the registry
|
|
60
|
+
// arrival. The value itself is not used — the read is the tracking hook.
|
|
61
|
+
void this.#rev.value;
|
|
32
62
|
if (this.label) this.setAttribute('aria-label', this.label);
|
|
33
63
|
const svg = getIcon(this.name, this.weight || 'regular');
|
|
34
64
|
if (svg && this.innerHTML !== svg) this.innerHTML = svg;
|
package/components/icon/icon.js
CHANGED
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
import { defineIfFree } from '../../core/register.js';
|
|
13
13
|
import { UIIcon } from './icon.class.js';
|
|
14
14
|
|
|
15
|
+
// Side-effect: wires the full @phosphor-icons/core asset tree into the icon
|
|
16
|
+
// registry so that any Phosphor name resolves without pre-registration.
|
|
17
|
+
// icon-ui is a glyph renderer — the icon loader is required for it to paint.
|
|
18
|
+
// The barrel (components/index.js) and core barrel both carry this import
|
|
19
|
+
// already; this import ensures standalone usage of the icon component
|
|
20
|
+
// (e.g. `import '@adia-ai/web-components/components/icon'`) also works.
|
|
21
|
+
// Apps with bundle-size SLOs that manage their own icon registration should
|
|
22
|
+
// import the class subpath directly and call installIconLoaders() themselves:
|
|
23
|
+
// import { UIIcon } from '@adia-ai/web-components/components/icon/class';
|
|
24
|
+
import '../../core/icons-phosphor.js';
|
|
25
|
+
|
|
15
26
|
defineIfFree('icon-ui', UIIcon);
|
|
16
27
|
|
|
17
28
|
export { UIIcon };
|