@adia-ai/web-components 0.8.3 → 0.8.5
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 +33 -0
- package/USAGE.md +60 -25
- package/components/accordion/accordion.css +5 -0
- package/components/adia-mark/adia-mark.a2ui.json +123 -0
- package/components/adia-mark/adia-mark.class.js +64 -0
- package/components/adia-mark/adia-mark.css +48 -0
- package/components/adia-mark/adia-mark.d.ts +20 -0
- package/components/adia-mark/adia-mark.examples.md +26 -0
- package/components/adia-mark/adia-mark.js +17 -0
- package/components/adia-mark/adia-mark.test.js +69 -0
- package/components/adia-mark/adia-mark.yaml +121 -0
- package/components/agent-artifact/agent-artifact.css +1 -0
- package/components/agent-questions/agent-questions.a2ui.json +2 -1
- package/components/agent-questions/agent-questions.class.js +96 -53
- package/components/agent-questions/agent-questions.css +27 -106
- package/components/agent-questions/agent-questions.yaml +1 -0
- package/components/agent-reasoning/agent-reasoning.css +9 -5
- package/components/button/button.class.js +9 -2
- package/components/button/button.css +1 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +3 -1
- package/components/calendar-grid/calendar-grid.class.js +11 -6
- package/components/calendar-grid/calendar-grid.css +42 -20
- package/components/calendar-grid/calendar-grid.yaml +5 -0
- package/components/calendar-picker/calendar-picker.a2ui.json +3 -1
- package/components/calendar-picker/calendar-picker.class.js +11 -6
- package/components/calendar-picker/calendar-picker.css +41 -19
- package/components/calendar-picker/calendar-picker.yaml +5 -0
- package/components/chart/chart.class.js +17 -14
- package/components/chat-thread/chat-input.a2ui.json +3 -1
- package/components/chat-thread/chat-input.css +12 -13
- package/components/chat-thread/chat-input.js +6 -2
- package/components/chat-thread/chat-input.yaml +3 -0
- package/components/check/check.css +1 -1
- package/components/color-input/color-input.class.js +8 -5
- package/components/color-picker/color-picker.class.js +6 -3
- package/components/combobox/combobox.a2ui.json +3 -1
- package/components/combobox/combobox.class.js +1 -3
- package/components/combobox/combobox.css +9 -12
- package/components/combobox/combobox.yaml +2 -0
- package/components/date-range-picker/date-range-picker.css +1 -0
- package/components/field/field.class.js +10 -7
- package/components/fields/fields.class.js +15 -12
- package/components/icon/icon.class.js +9 -5
- package/components/index.js +1 -0
- package/components/input/input.css +6 -2
- package/components/nav/nav.class.js +2 -1
- package/components/nav-group/nav-group.css +5 -6
- package/components/noodles/noodles.class.js +14 -10
- package/components/option-card/option-card.css +1 -0
- package/components/page/page.class.js +3 -2
- package/components/radio/radio.css +1 -1
- package/components/range/range.css +2 -0
- package/components/select/select.a2ui.json +2 -1
- package/components/select/select.class.js +12 -11
- package/components/select/select.css +29 -40
- package/components/select/select.test.js +6 -2
- package/components/select/select.yaml +1 -0
- package/components/swatch/swatch.a2ui.json +3 -1
- package/components/swatch/swatch.class.js +41 -32
- package/components/swatch/swatch.css +13 -18
- package/components/swatch/swatch.yaml +2 -0
- package/components/swiper/swiper.class.js +4 -1
- package/components/swiper/swiper.css +13 -4
- package/components/switch/switch.css +1 -1
- package/components/tag/tag.a2ui.json +4 -1
- package/components/tag/tag.class.js +3 -1
- package/components/tag/tag.css +13 -15
- package/components/tag/tag.yaml +3 -0
- package/components/tags-input/tags-input.css +1 -0
- package/components/textarea/textarea.css +2 -0
- package/components/time-picker/time-picker.css +2 -0
- package/components/timeline/timeline.a2ui.json +2 -1
- package/components/timeline/timeline.class.js +7 -8
- package/components/timeline/timeline.css +14 -26
- package/components/timeline/timeline.yaml +1 -0
- package/components/toggle-group/toggle-group.css +1 -0
- package/components/toolbar/toolbar.class.js +11 -6
- package/components/tree/tree.class.js +5 -2
- package/components/tree/tree.css +5 -2
- package/components/upload/upload.css +1 -0
- package/core/data-stream.js +21 -15
- package/core/element.js +36 -1
- package/core/element.test.js +67 -0
- package/core/icons-phosphor.js +86 -22
- package/core/icons.js +22 -8
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +109 -108
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -5
- package/styles/colors/semantics/features.css +6 -1
- package/styles/components.css +1 -0
- package/traits/error-shake/error-shake.js +23 -17
- package/traits/fade-presence/fade-presence.js +19 -15
- package/traits/success-checkmark/success-checkmark.js +21 -16
|
@@ -191,7 +191,16 @@
|
|
|
191
191
|
`--swiper-dot-size` for idle dots and `--swiper-dot-size-active`
|
|
192
192
|
for the active one (one space token larger). Padding is computed
|
|
193
193
|
from the size so the dot stays centered in the hitbox. */
|
|
194
|
-
:scope [data-swiper-dots] > button {
|
|
194
|
+
:scope [data-swiper-dots] > button-ui {
|
|
195
|
+
/* Stamped <button-ui variant="ghost" role="tab"> (gh issue 276 wave 4).
|
|
196
|
+
The dot look is bespoke (content-box clip keeps a 1rem hitbox around
|
|
197
|
+
a --swiper-dot-size circle) — these host declarations out-specify
|
|
198
|
+
button-ui's token-driven chrome; the ghost variant keeps its own
|
|
199
|
+
hover fill out of the way. */
|
|
200
|
+
/* button-ui floors min-width/height at --button-height — point it at
|
|
201
|
+
the VISIBLE circle so the padding (not the min-floor) builds the
|
|
202
|
+
1rem hitbox, exactly like the native dot did. */
|
|
203
|
+
--button-height: var(--swiper-dot-size);
|
|
195
204
|
width: var(--swiper-dot-size);
|
|
196
205
|
height: var(--swiper-dot-size);
|
|
197
206
|
border-radius: var(--swiper-dot-radius);
|
|
@@ -208,18 +217,18 @@
|
|
|
208
217
|
`background-clip: content-box` from the base rule isn't reset to
|
|
209
218
|
its initial `border-box` value — the colored area would otherwise
|
|
210
219
|
fill the full 1rem hitbox and turn into a huge circle. */
|
|
211
|
-
:scope [data-swiper-dots] > button:hover {
|
|
220
|
+
:scope [data-swiper-dots] > button-ui:hover {
|
|
212
221
|
background-color: var(--swiper-dot-bg-active);
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
/* Focus ring is rendered INSIDE the padding area (inset shadow), so
|
|
216
225
|
the dot's visual footprint stays the size of every other dot. */
|
|
217
|
-
:scope [data-swiper-dots] > button:focus-visible {
|
|
226
|
+
:scope [data-swiper-dots] > button-ui:focus-visible {
|
|
218
227
|
outline: none;
|
|
219
228
|
box-shadow: inset 0 0 0 var(--a-focus-width) var(--a-focus-color);
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
:scope [data-swiper-dots] > button[aria-current="true"] {
|
|
231
|
+
:scope [data-swiper-dots] > button-ui[aria-current="true"] {
|
|
223
232
|
background-color: var(--swiper-dot-bg-active);
|
|
224
233
|
width: var(--swiper-dot-size-active);
|
|
225
234
|
height: var(--swiper-dot-size-active);
|
|
@@ -130,7 +130,7 @@ switch-ui[checked] [slot="thumb"] {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
:scope:focus-visible { outline: none; }
|
|
133
|
-
:scope:focus-visible [slot="track"] { box-shadow: var(--switch-focus-ring); }
|
|
133
|
+
:scope:focus-visible [slot="track"] { box-shadow: var(--switch-focus-ring); border-color: transparent; }
|
|
134
134
|
|
|
135
135
|
/* Size handled by universal [size] attribute system. */
|
|
136
136
|
}
|
|
@@ -55,7 +55,9 @@ export class UITag extends UIElement {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
static parts = {
|
|
58
|
-
|
|
58
|
+
// button-ui, not a native <button> (gh issue 276) — the icon slots in
|
|
59
|
+
// explicitly to keep its bold weight (the icon= prop stamps regular).
|
|
60
|
+
dismiss: '<button-ui slot="dismiss" variant="ghost" size="xs" aria-label="Remove"><icon-ui slot="icon" name="x" weight="bold"></icon-ui></button-ui>',
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
static template = () => null;
|
package/components/tag/tag.css
CHANGED
|
@@ -196,6 +196,7 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
196
196
|
:scope:focus-visible {
|
|
197
197
|
outline: none;
|
|
198
198
|
box-shadow: var(--tag-focus-ring);
|
|
199
|
+
border-color: transparent;
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
/* ── Slotted icons (leading) ──
|
|
@@ -222,28 +223,25 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
222
223
|
gap: var(--tag-gap-icon, 0.2em);
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
/* ── Dismiss button
|
|
226
|
+
/* ── Dismiss button — a stamped <button-ui variant="ghost" size="xs">
|
|
227
|
+
(gh issue 276: no native <button> in stamped DOM); geometry + hover
|
|
228
|
+
ride button token re-points, chrome is button-ui's own. */
|
|
226
229
|
[slot="dismiss"] {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
padding: 0;
|
|
233
|
-
border: none;
|
|
234
|
-
background: none;
|
|
235
|
-
cursor: pointer;
|
|
236
|
-
border-radius: var(--tag-dismiss-radius);
|
|
237
|
-
color: var(--tag-dismiss-fg);
|
|
238
|
-
opacity: var(--tag-dismiss-opacity);
|
|
230
|
+
--button-height: var(--tag-dismiss-size);
|
|
231
|
+
--button-radius: var(--tag-dismiss-radius);
|
|
232
|
+
--button-fg: var(--tag-dismiss-fg);
|
|
233
|
+
--button-bg-ghost-hover: var(--tag-dismiss-bg-hover);
|
|
234
|
+
--button-fg-ghost-hover: var(--tag-dismiss-fg);
|
|
239
235
|
--a-icon-size: 0.875rem;
|
|
236
|
+
|
|
237
|
+
opacity: var(--tag-dismiss-opacity);
|
|
240
238
|
order: 1; /* push dismiss to end so layout reads [text] [×] */
|
|
241
|
-
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
transition: opacity var(--tag-duration) var(--tag-easing);
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
[slot="dismiss"]:hover {
|
|
245
244
|
opacity: var(--tag-dismiss-opacity-hover);
|
|
246
|
-
background: var(--tag-dismiss-bg-hover);
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
/* ── Disabled ── */
|
package/components/tag/tag.yaml
CHANGED
|
@@ -16,6 +16,9 @@ description: |
|
|
|
16
16
|
markers — badge-ui has no remove event. For navigation grouping
|
|
17
17
|
use <nav-group-ui>; for inline command actions use
|
|
18
18
|
<action-list-ui>.
|
|
19
|
+
composes:
|
|
20
|
+
- button-ui # dismiss affordance (gh issue 276 — no native <button> in stamped DOM)
|
|
21
|
+
- icon-ui # the dismiss x glyph, slotted into the button-ui
|
|
19
22
|
props:
|
|
20
23
|
disabled:
|
|
21
24
|
description: Disables interaction and dims the tag.
|
|
@@ -96,10 +96,12 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
96
96
|
outline: none;
|
|
97
97
|
box-shadow: var(--textarea-focus-ring);
|
|
98
98
|
color: var(--textarea-fg-hover);
|
|
99
|
+
border-color: transparent;
|
|
99
100
|
}
|
|
100
101
|
:scope[aria-invalid="true"]:not([disabled]) [slot="text"]:focus,
|
|
101
102
|
:scope[error]:not([disabled]) [slot="text"]:focus {
|
|
102
103
|
box-shadow: var(--textarea-focus-ring-invalid);
|
|
104
|
+
border-color: transparent;
|
|
103
105
|
}
|
|
104
106
|
:scope:not([disabled]) [slot="text"]:focus + [slot="label"],
|
|
105
107
|
:scope:not([disabled]):focus-within [slot="label"] {
|
|
@@ -107,11 +107,13 @@ time-picker-ui:not([disabled]):hover {
|
|
|
107
107
|
Canonical L3 ring; consumed via --time-picker-focus-ring (aliases --a-focus-ring). */
|
|
108
108
|
time-picker-ui:not([disabled]):focus-within {
|
|
109
109
|
box-shadow: var(--time-picker-focus-ring);
|
|
110
|
+
border-color: transparent;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
time-picker-ui[aria-invalid="true"]:not([disabled]):focus-within,
|
|
113
114
|
time-picker-ui[error]:not([disabled]):focus-within {
|
|
114
115
|
box-shadow: var(--time-picker-focus-ring-invalid);
|
|
116
|
+
border-color: transparent;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/* Disabled / readonly affordance */
|
|
@@ -154,8 +154,10 @@ export class UITimelineItem extends UIElement {
|
|
|
154
154
|
// Toggle caret lives on the row — we stamp a button once
|
|
155
155
|
let toggle = this.querySelector(':scope > [data-timeline-toggle]');
|
|
156
156
|
if (!toggle) {
|
|
157
|
-
|
|
158
|
-
toggle
|
|
157
|
+
// button-ui, not a native <button> (gh issue 276).
|
|
158
|
+
toggle = document.createElement('button-ui');
|
|
159
|
+
toggle.setAttribute('variant', 'ghost');
|
|
160
|
+
toggle.setAttribute('size', 'xs');
|
|
159
161
|
toggle.setAttribute('data-timeline-toggle', '');
|
|
160
162
|
toggle.setAttribute('aria-label', 'Toggle details');
|
|
161
163
|
toggle.addEventListener('click', (e) => {
|
|
@@ -168,12 +170,9 @@ export class UITimelineItem extends UIElement {
|
|
|
168
170
|
});
|
|
169
171
|
this.appendChild(toggle);
|
|
170
172
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
caret.setAttribute('color', 'muted');
|
|
175
|
-
caret.setAttribute('size', 'sm');
|
|
176
|
-
toggle.appendChild(caret);
|
|
173
|
+
// The caret rides button-ui's icon prop — flipping the attribute
|
|
174
|
+
// re-stamps the glyph; color/size are CSS token re-points.
|
|
175
|
+
toggle.setAttribute('icon', this.#expanded ? 'caret-down' : 'caret-right');
|
|
177
176
|
} else {
|
|
178
177
|
this.querySelector(':scope > [data-timeline-toggle]')?.remove();
|
|
179
178
|
body?.remove();
|
|
@@ -362,32 +362,22 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
|
|
|
362
362
|
|
|
363
363
|
/* ═══════ Toggle caret (outcomes) ═══════ */
|
|
364
364
|
|
|
365
|
+
/* A stamped <button-ui variant="ghost" size="xs" icon="caret-*">
|
|
366
|
+
(gh issue 276: no native <button> in stamped DOM). Geometry/colors
|
|
367
|
+
ride button token re-points; focus ring + hover are button-ui's own. */
|
|
365
368
|
:scope > [data-timeline-toggle] {
|
|
366
369
|
position: absolute;
|
|
367
370
|
inset-inline-end: 0;
|
|
368
|
-
/* Place the caret's icon-center on row 1's optical center
|
|
369
|
-
|
|
370
|
-
sits that much lower inside the box; we subtract that so the
|
|
371
|
-
icon (not the box) lines up with the time-slot text center. */
|
|
371
|
+
/* Place the caret's icon-center on row 1's optical center (the box
|
|
372
|
+
is --button-height tall around a 1em-line icon). */
|
|
372
373
|
top: calc((1.4em - 1em) / 2 - var(--a-space-0-5));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
display: inline-flex;
|
|
381
|
-
align-items: center;
|
|
382
|
-
justify-content: center;
|
|
383
|
-
line-height: 0;
|
|
384
|
-
transition: background var(--timeline-item-duration) var(--timeline-item-easing),
|
|
385
|
-
color var(--timeline-item-duration) var(--timeline-item-easing);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
:scope > [data-timeline-toggle]:hover {
|
|
389
|
-
background: var(--a-bg-subtle);
|
|
390
|
-
color: var(--timeline-item-label-fg, var(--md-sys-color-neutral-on-surface));
|
|
374
|
+
--button-height: calc(1em + var(--a-space-1));
|
|
375
|
+
--button-radius: var(--a-radius-sm);
|
|
376
|
+
--button-fg: var(--timeline-item-label-fg-muted, var(--a-fg-muted));
|
|
377
|
+
--button-fg-ghost-hover: var(--timeline-item-label-fg, var(--md-sys-color-neutral-on-surface));
|
|
378
|
+
--button-bg-ghost-hover: var(--a-bg-subtle);
|
|
379
|
+
/* It IS a caret — universal caret size (ruling 2026-07-16). */
|
|
380
|
+
--a-icon-size: var(--a-caret-size);
|
|
391
381
|
}
|
|
392
382
|
|
|
393
383
|
/* Reserve room so the time isn't overlapped by the caret */
|
|
@@ -395,10 +385,8 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
|
|
|
395
385
|
margin-inline-end: var(--timeline-item-toggle-time-margin);
|
|
396
386
|
}
|
|
397
387
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
box-shadow: var(--a-focus-ring);
|
|
401
|
-
}
|
|
388
|
+
/* (Focus ring is button-ui's own — the custom :focus-visible rule the
|
|
389
|
+
native button needed is gone with it, gh issue 276.) */
|
|
402
390
|
}
|
|
403
391
|
|
|
404
392
|
/* When size=sm on the parent, ring is 1.5px so the small dot still looks crisp.
|
|
@@ -12,6 +12,7 @@ description: "Timeline and step wizard. Two modes: timeline (per-item state) and
|
|
|
12
12
|
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
13
13
|
composes:
|
|
14
14
|
- icon-ui
|
|
15
|
+
- button-ui # outcomes toggle (gh issue 276 — no native <button> in stamped DOM)
|
|
15
16
|
props:
|
|
16
17
|
orientation:
|
|
17
18
|
description: Layout direction
|
|
@@ -114,12 +114,17 @@ export class UIToolbar extends UIElement {
|
|
|
114
114
|
connected() {
|
|
115
115
|
this.setAttribute('role', 'toolbar');
|
|
116
116
|
this.#ensureSpillover();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
// gh#285 — SSR DOM shims (linkedom) have neither observer global.
|
|
118
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
119
|
+
this.#ro = new ResizeObserver(() => this.#queueReflow());
|
|
120
|
+
this.#ro.observe(this);
|
|
121
|
+
}
|
|
122
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
123
|
+
this.#mo = new MutationObserver(() => {
|
|
124
|
+
if (this.#measuring) return;
|
|
125
|
+
this.#queueReflow();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
123
128
|
this.#moObserve();
|
|
124
129
|
this.#queueReflow();
|
|
125
130
|
}
|
|
@@ -324,8 +324,11 @@ export class UITreeItem extends UIElement {
|
|
|
324
324
|
// role=button child of the group/tree (axe `aria-required-children`). Adopt
|
|
325
325
|
// what's present now, then observe for the late arrivals.
|
|
326
326
|
this.#adoptSlotted();
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
|
|
328
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
329
|
+
this.#slotObserver = new MutationObserver(() => this.#adoptSlotted());
|
|
330
|
+
this.#slotObserver.observe(this, { childList: true, subtree: true });
|
|
331
|
+
}
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
#stamp() {
|
package/components/tree/tree.css
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
--tree-row-gap: var(--a-space-1);
|
|
8
8
|
--tree-actions-gap: var(--a-space-0-5);
|
|
9
9
|
--tree-indent: var(--a-space-4);
|
|
10
|
-
|
|
10
|
+
/* The universal caret token (14px default) — sidebar carets read one
|
|
11
|
+
size across nav-group / tree / accordion (ruling 2026-07-16); the
|
|
12
|
+
old --a-space-2 read made tree carets an 8px outlier. */
|
|
13
|
+
--tree-caret-size: var(--a-caret-size);
|
|
11
14
|
--tree-icon-size: var(--a-space-3);
|
|
12
15
|
|
|
13
16
|
/* ── Typography ── */
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
--tree-item-row-gap: var(--tree-row-gap, var(--a-space-1));
|
|
48
51
|
--tree-item-actions-gap: var(--tree-actions-gap, var(--a-space-0-5));
|
|
49
52
|
--tree-item-indent: var(--tree-indent, var(--a-space-4));
|
|
50
|
-
--tree-item-caret-size: var(--tree-caret-size, var(--a-
|
|
53
|
+
--tree-item-caret-size: var(--tree-caret-size, var(--a-caret-size));
|
|
51
54
|
--tree-item-icon-size: var(--tree-icon-size, var(--a-space-3));
|
|
52
55
|
|
|
53
56
|
--tree-item-fg: var(--tree-fg, var(--md-sys-color-neutral-on-surface));
|
package/core/data-stream.js
CHANGED
|
@@ -453,24 +453,30 @@ function visitSubtree(root, fn) {
|
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
456
|
+
// gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
|
|
457
|
+
// Constructed lazily so import-time evaluation never throws; bootstrap()
|
|
458
|
+
// no-ops the document-level watch when the global is missing (existing
|
|
459
|
+
// stream elements found via querySelectorAll still get started once).
|
|
460
|
+
const observer = typeof MutationObserver !== 'undefined'
|
|
461
|
+
? new MutationObserver((mutations) => {
|
|
462
|
+
for (const m of mutations) {
|
|
463
|
+
if (m.type === 'attributes' && ATTR_FILTER.includes(m.attributeName)) {
|
|
464
|
+
const el = m.target;
|
|
465
|
+
if (isStreamingEl(el)) start(el);
|
|
466
|
+
else stop(el);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (m.type === 'childList') {
|
|
470
|
+
m.addedNodes.forEach(n => visitSubtree(n, start));
|
|
471
|
+
m.removedNodes.forEach(n => visitSubtree(n, stop));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
: null;
|
|
470
476
|
|
|
471
477
|
function bootstrap() {
|
|
472
478
|
if (typeof document === 'undefined') return;
|
|
473
|
-
observer
|
|
479
|
+
observer?.observe(document.documentElement, {
|
|
474
480
|
childList: true, subtree: true,
|
|
475
481
|
attributes: true, attributeFilter: ATTR_FILTER,
|
|
476
482
|
});
|
package/core/element.js
CHANGED
|
@@ -64,6 +64,26 @@ function installProps(el, props) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// A minimal no-op ElementInternals shim for environments with no real
|
|
68
|
+
// implementation (linkedom and other DOM-shim SSR passes — gh#285). Real
|
|
69
|
+
// form-participation/validation behavior is meaningless server-side (no
|
|
70
|
+
// live user interaction to validate); the shim exists so the ~12
|
|
71
|
+
// call sites across the framework that read `this.internals.setValidity(…)`
|
|
72
|
+
// / `.setFormValue(…)` / `.validity` etc. keep working as inert no-ops
|
|
73
|
+
// instead of throwing on a missing method. Surface matches exactly what
|
|
74
|
+
// the framework's own consumers call (grepped, not guessed).
|
|
75
|
+
const NOOP_INTERNALS = Object.freeze({
|
|
76
|
+
setFormValue() {},
|
|
77
|
+
setValidity() {},
|
|
78
|
+
checkValidity() { return true; },
|
|
79
|
+
reportValidity() { return true; },
|
|
80
|
+
get form() { return null; },
|
|
81
|
+
get labels() { return []; },
|
|
82
|
+
get validity() { return {}; },
|
|
83
|
+
get validationMessage() { return ''; },
|
|
84
|
+
get willValidate() { return false; },
|
|
85
|
+
});
|
|
86
|
+
|
|
67
87
|
function reflect(el, a, v, t) {
|
|
68
88
|
t === Boolean
|
|
69
89
|
? v ? el.setAttribute(a, '') : el.removeAttribute(a)
|
|
@@ -78,6 +98,11 @@ function adoptStyles(ctor) {
|
|
|
78
98
|
ctor._sa = true;
|
|
79
99
|
const sheets = ctor.styles;
|
|
80
100
|
if (!sheets) return;
|
|
101
|
+
// gh#285 — linkedom's document has no adoptedStyleSheets at all; SSR
|
|
102
|
+
// has no rendered stylesheet to adopt into anyway (styles matter for
|
|
103
|
+
// paint, which doesn't happen server-side), so skipping is a correct
|
|
104
|
+
// no-op, not a degraded feature.
|
|
105
|
+
if (!('adoptedStyleSheets' in document)) return;
|
|
81
106
|
const list = Array.isArray(sheets) ? sheets : [sheets];
|
|
82
107
|
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...list.filter(s => !document.adoptedStyleSheets.includes(s))];
|
|
83
108
|
}
|
|
@@ -114,7 +139,17 @@ export class UIElement extends HTMLElement {
|
|
|
114
139
|
|
|
115
140
|
constructor() {
|
|
116
141
|
super();
|
|
117
|
-
|
|
142
|
+
// gh#285 — linkedom (and other DOM-shim SSR passes) implement no
|
|
143
|
+
// ElementInternals; an unconditional attachInternals() call threw
|
|
144
|
+
// HERE, before ANY subclass code ran — the single highest-leverage
|
|
145
|
+
// crash site in the whole framework under SSR. Feature-detect and
|
|
146
|
+
// fall back to a no-op shim (form participation is meaningless
|
|
147
|
+
// server-side) rather than leaving `this.internals` undefined,
|
|
148
|
+
// which would just move the crash to every consumer's first
|
|
149
|
+
// `.setValidity(…)` call instead of fixing it.
|
|
150
|
+
this.internals = typeof this.attachInternals === 'function'
|
|
151
|
+
? this.attachInternals()
|
|
152
|
+
: NOOP_INTERNALS;
|
|
118
153
|
installProps(this, this.constructor.properties);
|
|
119
154
|
}
|
|
120
155
|
|
package/core/element.test.js
CHANGED
|
@@ -249,3 +249,70 @@ describe('UIElement — addTrait', () => {
|
|
|
249
249
|
expect(disconnects).toBe(1);
|
|
250
250
|
});
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
// gh#285 — SSR DOM shims (linkedom et al.) implement no ElementInternals
|
|
254
|
+
// and no document.adoptedStyleSheets. These tests simulate that absence
|
|
255
|
+
// directly (deleting the real APIs) rather than trusting a mock — the
|
|
256
|
+
// bug was an unconditional call crashing the CONSTRUCTOR, before any
|
|
257
|
+
// subclass code ran, so the only real proof is exercising construction
|
|
258
|
+
// with the API genuinely gone.
|
|
259
|
+
describe('UIElement — SSR browser-API absence (gh#285)', () => {
|
|
260
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
261
|
+
|
|
262
|
+
it('constructs without throwing when attachInternals does not exist', () => {
|
|
263
|
+
const original = HTMLElement.prototype.attachInternals;
|
|
264
|
+
delete HTMLElement.prototype.attachInternals;
|
|
265
|
+
try {
|
|
266
|
+
class El extends UIElement {}
|
|
267
|
+
const tag = registerTestElement(El);
|
|
268
|
+
expect(() => mount(tag)).not.toThrow();
|
|
269
|
+
} finally {
|
|
270
|
+
HTMLElement.prototype.attachInternals = original;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('the no-op internals shim answers every call site the framework actually makes', () => {
|
|
275
|
+
const original = HTMLElement.prototype.attachInternals;
|
|
276
|
+
delete HTMLElement.prototype.attachInternals;
|
|
277
|
+
let el;
|
|
278
|
+
try {
|
|
279
|
+
class El extends UIElement {}
|
|
280
|
+
const tag = registerTestElement(El);
|
|
281
|
+
el = mount(tag);
|
|
282
|
+
} finally {
|
|
283
|
+
HTMLElement.prototype.attachInternals = original;
|
|
284
|
+
}
|
|
285
|
+
expect(() => el.internals.setFormValue('x')).not.toThrow();
|
|
286
|
+
expect(() => el.internals.setValidity({})).not.toThrow();
|
|
287
|
+
expect(el.internals.checkValidity()).toBe(true);
|
|
288
|
+
expect(el.internals.reportValidity()).toBe(true);
|
|
289
|
+
expect(el.internals.form).toBeNull();
|
|
290
|
+
expect(el.internals.labels).toEqual([]);
|
|
291
|
+
expect(el.internals.validity).toEqual({});
|
|
292
|
+
expect(el.internals.validationMessage).toBe('');
|
|
293
|
+
expect(el.internals.willValidate).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('a real attachInternals is still used when the browser provides one (no regression)', () => {
|
|
297
|
+
class El extends UIElement {}
|
|
298
|
+
const tag = registerTestElement(El);
|
|
299
|
+
const el = mount(tag);
|
|
300
|
+
// happy-dom's real attachInternals — not the shim.
|
|
301
|
+
expect(el.internals).not.toBe(null);
|
|
302
|
+
expect(typeof el.internals.setFormValue).toBe('function');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('adoptStyles no-ops instead of throwing when adoptedStyleSheets does not exist', () => {
|
|
306
|
+
const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'adoptedStyleSheets');
|
|
307
|
+
delete Document.prototype.adoptedStyleSheets;
|
|
308
|
+
try {
|
|
309
|
+
class El extends UIElement {
|
|
310
|
+
static styles = ['/* fake sheet */'];
|
|
311
|
+
}
|
|
312
|
+
const tag = registerTestElement(El);
|
|
313
|
+
expect(() => mount(tag)).not.toThrow();
|
|
314
|
+
} finally {
|
|
315
|
+
if (desc) Object.defineProperty(Document.prototype, 'adoptedStyleSheets', desc);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|