@adia-ai/web-components 0.5.14 → 0.5.16
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 +234 -0
- package/components/badge/class.js +5 -1
- package/components/button/class.js +5 -1
- package/components/code/class.js +42 -5
- package/components/code/code.a2ui.json +1 -1
- package/components/code/code.d.ts +27 -0
- package/components/code/code.yaml +7 -1
- package/components/heatmap/heatmap.css +1 -1
- package/components/list/list.css +6 -6
- package/components/nav-group/nav-group.css +2 -2
- package/components/page/page.css +1 -1
- package/components/segmented/class.js +22 -0
- package/components/select/select.css +2 -2
- package/components/slider/class.js +60 -6
- package/components/slider/slider.a2ui.json +10 -4
- package/components/slider/slider.css +142 -52
- package/components/slider/slider.test.js +18 -15
- package/components/slider/slider.yaml +8 -4
- package/components/stepper/stepper.css +24 -24
- package/components/swatch/class.js +92 -1
- package/components/swatch/swatch.css +2 -2
- package/components/swatch/swatch.test.js +269 -0
- package/components/swiper/class.js +247 -54
- package/components/swiper/swiper.a2ui.json +19 -0
- package/components/swiper/swiper.css +87 -21
- package/components/swiper/swiper.d.ts +6 -0
- package/components/swiper/swiper.yaml +18 -0
- package/components/tag/class.js +6 -1
- package/components/text/text.a2ui.json +16 -2
- package/components/text/text.d.ts +28 -45
- package/components/text/text.yaml +25 -1
- package/components/toolbar/toolbar.css +1 -1
- package/package.json +1 -1
- package/styles/colors/semantics.css +7 -84
|
@@ -218,14 +218,44 @@ export class UISwatch extends UIElement {
|
|
|
218
218
|
// survive stamping as direct host siblings, NOT funnel into #labelEl where
|
|
219
219
|
// absolute-positioning anchors against the label text bbox. Pull them out
|
|
220
220
|
// FIRST so the innerHTML='' wipe below doesn't lose them.
|
|
221
|
+
//
|
|
222
|
+
// §331 (v0.5.16, FB-39): also walk `display: contents` wrapper spans
|
|
223
|
+
// inserted by AdiaUI's `html\`\`` template engine (`core/template.js`
|
|
224
|
+
// `wrap()` at line 212). When the parent template interpolates child
|
|
225
|
+
// fragments via `${badgeFrag}` / `${dotFrag}`, each fragment lands
|
|
226
|
+
// INSIDE a wrapper span that's a direct child of <swatch-ui>; the
|
|
227
|
+
// chrome elements end up as GRANDCHILDREN, not direct children. The
|
|
228
|
+
// pre-§331 direct-children-only filter found `chromeSlot = []` →
|
|
229
|
+
// innerHTML='' wiped the wrapper spans + their chrome content →
|
|
230
|
+
// `#absorbChromeSlot()` in `#syncCore()` had nothing left to absorb.
|
|
231
|
+
// Deep-walk via querySelectorAll catches both shapes:
|
|
232
|
+
// - static-HTML / pre-template: direct-child <span slot="chrome">
|
|
233
|
+
// - template-render: <span slot="chrome"> nested in wrapper span
|
|
221
234
|
const chromeSlot = Array.from(this.children).filter(
|
|
222
235
|
n => n.getAttribute && n.getAttribute('slot') === 'chrome'
|
|
223
236
|
);
|
|
237
|
+
// Also collect chrome elements nested inside wrapper-spans (template-render path).
|
|
238
|
+
for (const wrapper of Array.from(this.children)) {
|
|
239
|
+
if (wrapper.getAttribute?.('slot') === 'chrome') continue;
|
|
240
|
+
const nested = wrapper.querySelectorAll?.('[slot="chrome"]');
|
|
241
|
+
if (nested) for (const n of nested) chromeSlot.push(n);
|
|
242
|
+
}
|
|
224
243
|
for (const n of chromeSlot) n.remove();
|
|
225
244
|
|
|
226
245
|
// Capture pre-existing default-slot content so consumer-authored
|
|
227
246
|
// children (e.g. <swatch-ui>Forecast</swatch-ui>) survive stamping.
|
|
228
247
|
// `[slot="chrome"]` children are already removed above, so won't appear here.
|
|
248
|
+
//
|
|
249
|
+
// §325 (v0.5.16, FB-36): filter pure-whitespace text nodes from the
|
|
250
|
+
// capture. Pre-§325, indented multi-line HTML (`<swatch-ui
|
|
251
|
+
// label="500">\n <span>tip</span>\n</swatch-ui>`) captured the
|
|
252
|
+
// leading whitespace text node, which then made #labelEl.firstChild
|
|
253
|
+
// a nodeType=3 text node. #syncCore()'s "stale-text-node" branch
|
|
254
|
+
// matched the whitespace + did textContent = this.label, wiping
|
|
255
|
+
// BOTH the whitespace AND the consumer's <span>. Filter here so
|
|
256
|
+
// #labelEl.firstChild is the actual element/non-whitespace text,
|
|
257
|
+
// and #syncCore's stale-text branch only fires for genuinely
|
|
258
|
+
// attr-driven cases.
|
|
229
259
|
const slotted = Array.from(this.childNodes).filter(n =>
|
|
230
260
|
!(n.nodeType === 1 && n.dataset && (
|
|
231
261
|
n.dataset.tile !== undefined ||
|
|
@@ -233,7 +263,7 @@ export class UISwatch extends UIElement {
|
|
|
233
263
|
n.dataset.detail !== undefined ||
|
|
234
264
|
n.dataset.badge !== undefined ||
|
|
235
265
|
n.dataset.copy !== undefined
|
|
236
|
-
))
|
|
266
|
+
)) && !(n.nodeType === 3 && !n.textContent.trim())
|
|
237
267
|
);
|
|
238
268
|
this.innerHTML = '';
|
|
239
269
|
|
|
@@ -282,9 +312,70 @@ export class UISwatch extends UIElement {
|
|
|
282
312
|
|
|
283
313
|
// ── Sync ──────────────────────────────────────────────────────────
|
|
284
314
|
|
|
315
|
+
/**
|
|
316
|
+
* §326 (v0.5.16, FB-37): Late-arriving `[slot="chrome"]` children may
|
|
317
|
+
* land on the host AFTER `#stamp()` runs — happens in any template
|
|
318
|
+
* engine (lit-html, AdiaUI `html\`\``, etc.) that batches child appends
|
|
319
|
+
* separately from element creation. `#stamp()` is gated by
|
|
320
|
+
* `this.#stamped` and won't re-run; this method picks up the late
|
|
321
|
+
* arrivals on every `render()` pass + moves them to the canonical
|
|
322
|
+
* sibling-of-tile position (between `#tileEl` and `#badgeEl`).
|
|
323
|
+
*
|
|
324
|
+
* Idempotent: chrome children already in the right slot are left
|
|
325
|
+
* alone (the `insertBefore` only moves them if they're not the
|
|
326
|
+
* #badgeEl's immediate previousSibling chain).
|
|
327
|
+
*/
|
|
328
|
+
#absorbChromeSlot() {
|
|
329
|
+
if (!this.#tileEl || !this.#badgeEl) return;
|
|
330
|
+
const chromeChildren = [];
|
|
331
|
+
for (const child of this.children) {
|
|
332
|
+
if (child === this.#tileEl) continue;
|
|
333
|
+
if (child === this.#badgeEl) break; // hit the badge — done scanning the head region
|
|
334
|
+
if (child.getAttribute?.('slot') === 'chrome') chromeChildren.push(child);
|
|
335
|
+
}
|
|
336
|
+
// Walk the rest of the children looking for chrome children that landed
|
|
337
|
+
// AFTER the internal structure (the common template-late-append case).
|
|
338
|
+
for (const child of Array.from(this.children)) {
|
|
339
|
+
if (
|
|
340
|
+
child !== this.#tileEl &&
|
|
341
|
+
child !== this.#badgeEl &&
|
|
342
|
+
child !== this.#labelEl &&
|
|
343
|
+
child !== this.#detailEl &&
|
|
344
|
+
child !== this.#copyEl &&
|
|
345
|
+
child.getAttribute?.('slot') === 'chrome'
|
|
346
|
+
) {
|
|
347
|
+
// Move to the canonical position: immediately after #tileEl.
|
|
348
|
+
// insertBefore is idempotent — moving an already-positioned child
|
|
349
|
+
// to the same spot is a no-op.
|
|
350
|
+
this.insertBefore(child, this.#badgeEl);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// §331 (v0.5.16, FB-39): also handle wrapper-spanned chrome children
|
|
354
|
+
// that arrive AFTER #stamp() (parent template's late update() pass).
|
|
355
|
+
// Hoist them OUT of any non-internal wrapper + into the canonical slot.
|
|
356
|
+
for (const child of Array.from(this.children)) {
|
|
357
|
+
if (
|
|
358
|
+
child === this.#tileEl ||
|
|
359
|
+
child === this.#badgeEl ||
|
|
360
|
+
child === this.#labelEl ||
|
|
361
|
+
child === this.#detailEl ||
|
|
362
|
+
child === this.#copyEl
|
|
363
|
+
) continue;
|
|
364
|
+
const nested = child.querySelectorAll?.('[slot="chrome"]');
|
|
365
|
+
if (!nested || !nested.length) continue;
|
|
366
|
+
for (const n of nested) {
|
|
367
|
+
this.insertBefore(n, this.#badgeEl);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
285
372
|
#syncCore() {
|
|
286
373
|
if (!this.#tileEl || !this.#labelEl) return;
|
|
287
374
|
|
|
375
|
+
// §326 (v0.5.16, FB-37): pick up `[slot="chrome"]` children that
|
|
376
|
+
// landed after #stamp() ran (template-render path).
|
|
377
|
+
this.#absorbChromeSlot();
|
|
378
|
+
|
|
288
379
|
// Normalize enum values; fall back silently when a consumer sets a typo.
|
|
289
380
|
const shape = SHAPES.has(this.shape) ? this.shape : 'square';
|
|
290
381
|
const size = SIZES.has(this.size) ? this.size : 'md';
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
line-height: 1;
|
|
292
292
|
cursor: pointer;
|
|
293
293
|
border-radius: var(--a-radius-xs);
|
|
294
|
-
transition: color
|
|
294
|
+
transition: color var(--a-duration-fast) var(--a-easing-out), background var(--a-duration-fast) var(--a-easing-out);
|
|
295
295
|
}
|
|
296
296
|
:scope > [data-copy]:hover { color: var(--a-fg); background: var(--a-bg-muted); }
|
|
297
297
|
:scope > [data-copy]:focus-visible {
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
:scope[selectable] {
|
|
309
309
|
cursor: pointer;
|
|
310
310
|
border-radius: var(--a-radius-sm);
|
|
311
|
-
transition: box-shadow
|
|
311
|
+
transition: box-shadow var(--a-duration-fast) var(--a-easing-out);
|
|
312
312
|
}
|
|
313
313
|
:scope[selectable]:focus { outline: none; }
|
|
314
314
|
:scope[selectable]:focus-visible {
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <swatch-ui> behavioral tests.
|
|
3
|
+
*
|
|
4
|
+
* Currently scoped to FB-36 / §325 regression coverage. Future tests for
|
|
5
|
+
* #stamp() lifecycle, chrome-slot composition, auto-contrast probe,
|
|
6
|
+
* etc. land here.
|
|
7
|
+
*
|
|
8
|
+
* Test setup note: happy-dom connects custom elements synchronously
|
|
9
|
+
* during innerHTML parsing — BEFORE sibling children inside the same
|
|
10
|
+
* element are appended. Real browsers connect after the full innerHTML
|
|
11
|
+
* is parsed. To exercise the FB-36 path (children present at connect
|
|
12
|
+
* time), tests use `document.createElement` + `appendChild` to build
|
|
13
|
+
* the full subtree FIRST, then attach to the document — guaranteeing
|
|
14
|
+
* the children are in `this.childNodes` when `connectedCallback()` fires.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
await import('./swatch.js');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function buildSwatch({ label, defaultSlot, shape = 'block', labelPosition = 'overlay', color = '#3b82f6' }) {
|
|
24
|
+
const swatch = document.createElement('swatch-ui');
|
|
25
|
+
swatch.setAttribute('shape', shape);
|
|
26
|
+
swatch.setAttribute('label-position', labelPosition);
|
|
27
|
+
swatch.setAttribute('color', color);
|
|
28
|
+
if (label !== undefined) swatch.setAttribute('label', label);
|
|
29
|
+
if (defaultSlot) {
|
|
30
|
+
// Mimic indented HTML's whitespace text nodes — the FB-36 trigger.
|
|
31
|
+
swatch.appendChild(document.createTextNode('\n '));
|
|
32
|
+
const child = document.createElement('span');
|
|
33
|
+
child.id = defaultSlot.id;
|
|
34
|
+
child.textContent = defaultSlot.text;
|
|
35
|
+
swatch.appendChild(child);
|
|
36
|
+
swatch.appendChild(document.createTextNode('\n '));
|
|
37
|
+
}
|
|
38
|
+
return swatch;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('<swatch-ui> default-slot + label-attr precedence (FB-36 / §325)', () => {
|
|
42
|
+
let host;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
host = document.createElement('div');
|
|
46
|
+
document.body.appendChild(host);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves default-slot element when label="X" is also present (FB-36 §325 fix)', async () => {
|
|
50
|
+
const swatch = buildSwatch({
|
|
51
|
+
label: '500',
|
|
52
|
+
defaultSlot: { id: 'probe-default', text: 'richlabel' },
|
|
53
|
+
});
|
|
54
|
+
host.appendChild(swatch);
|
|
55
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
56
|
+
|
|
57
|
+
const label = swatch.querySelector('[data-label]');
|
|
58
|
+
const span = swatch.querySelector('#probe-default');
|
|
59
|
+
|
|
60
|
+
// Pre-§325 the span was wiped + label="500" took precedence.
|
|
61
|
+
// Post-§325 the default-slot wins (yaml spec: "richer content").
|
|
62
|
+
expect(span).not.toBeNull();
|
|
63
|
+
expect(label.textContent.trim()).toBe('richlabel');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders label-attr text when no default-slot is present', async () => {
|
|
67
|
+
const swatch = buildSwatch({ label: '500' });
|
|
68
|
+
host.appendChild(swatch);
|
|
69
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
70
|
+
const label = swatch.querySelector('[data-label]');
|
|
71
|
+
expect(label.textContent.trim()).toBe('500');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders default-slot text when no label-attr is present', async () => {
|
|
75
|
+
const swatch = buildSwatch({
|
|
76
|
+
defaultSlot: { id: 'probe-only', text: 'defaultonly' },
|
|
77
|
+
});
|
|
78
|
+
host.appendChild(swatch);
|
|
79
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
80
|
+
const label = swatch.querySelector('[data-label]');
|
|
81
|
+
const span = swatch.querySelector('#probe-only');
|
|
82
|
+
expect(span).not.toBeNull();
|
|
83
|
+
expect(label.textContent.trim()).toBe('defaultonly');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('syncs label-attr changes when there is no default-slot (no whitespace regression)', async () => {
|
|
87
|
+
const swatch = buildSwatch({ label: '500' });
|
|
88
|
+
host.appendChild(swatch);
|
|
89
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
90
|
+
swatch.label = '600';
|
|
91
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
92
|
+
const label = swatch.querySelector('[data-label]');
|
|
93
|
+
expect(label.textContent.trim()).toBe('600');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('<swatch-ui> late-arriving [slot="chrome"] children (FB-37 / §326)', () => {
|
|
98
|
+
let host;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
host = document.createElement('div');
|
|
102
|
+
document.body.appendChild(host);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('absorbs chrome child appended AFTER connectedCallback (template-render path)', async () => {
|
|
106
|
+
// Mimic the template-engine timing: create the swatch + connect it FIRST
|
|
107
|
+
// (children empty), then append the chrome child + trigger a render.
|
|
108
|
+
const swatch = document.createElement('swatch-ui');
|
|
109
|
+
swatch.setAttribute('shape', 'block');
|
|
110
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
111
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
112
|
+
swatch.setAttribute('label', '500');
|
|
113
|
+
host.appendChild(swatch);
|
|
114
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
115
|
+
|
|
116
|
+
// At this point #stamp() has run + chromeSlot was empty.
|
|
117
|
+
// Append a chrome child LATER — this is what template engines do.
|
|
118
|
+
const chrome = document.createElement('span');
|
|
119
|
+
chrome.setAttribute('slot', 'chrome');
|
|
120
|
+
chrome.id = 'late-chrome';
|
|
121
|
+
chrome.textContent = '★';
|
|
122
|
+
swatch.appendChild(chrome);
|
|
123
|
+
|
|
124
|
+
// Trigger a re-render via property mutation (template engines typically
|
|
125
|
+
// set props after appending — this nudge fires render → #syncCore →
|
|
126
|
+
// #absorbChromeSlot picks up the late chrome child). Must use a value
|
|
127
|
+
// DIFFERENT from current to actually fire the signal write.
|
|
128
|
+
swatch.label = '999';
|
|
129
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
130
|
+
|
|
131
|
+
const found = swatch.querySelector('#late-chrome');
|
|
132
|
+
expect(found).not.toBeNull();
|
|
133
|
+
// Chrome child should be a direct host sibling positioned between tile + badge.
|
|
134
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
135
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
136
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
137
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('still handles chrome child appended BEFORE connect (static-HTML path)', async () => {
|
|
141
|
+
// Build swatch with chrome child present at connect time — the canonical
|
|
142
|
+
// FB-34 / §314 path should still work post-§326.
|
|
143
|
+
const swatch = document.createElement('swatch-ui');
|
|
144
|
+
swatch.setAttribute('shape', 'block');
|
|
145
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
146
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
147
|
+
swatch.setAttribute('label', '500');
|
|
148
|
+
const chrome = document.createElement('span');
|
|
149
|
+
chrome.setAttribute('slot', 'chrome');
|
|
150
|
+
chrome.id = 'early-chrome';
|
|
151
|
+
chrome.textContent = '⚑';
|
|
152
|
+
swatch.appendChild(chrome);
|
|
153
|
+
host.appendChild(swatch);
|
|
154
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
155
|
+
|
|
156
|
+
const found = swatch.querySelector('#early-chrome');
|
|
157
|
+
expect(found).not.toBeNull();
|
|
158
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
159
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
160
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
161
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('<swatch-ui> chrome child nested in display:contents wrapper (FB-39 / §331)', () => {
|
|
166
|
+
let host;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
host = document.createElement('div');
|
|
170
|
+
document.body.appendChild(host);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('hoists chrome child from inside wrapper span at #stamp() time (template-interpolation path)', async () => {
|
|
174
|
+
// Simulates AdiaUI's html`` template wrapping interpolated content in
|
|
175
|
+
// <span style="display: contents"> (per core/template.js:212 `wrap()`).
|
|
176
|
+
// When consumer writes `html`<swatch-ui>${chromeFrag}</swatch-ui>``,
|
|
177
|
+
// the chromeFrag's content lands NESTED inside a wrapper span — not
|
|
178
|
+
// as direct children of <swatch-ui>. Pre-§331 the chrome was wiped.
|
|
179
|
+
const swatch = document.createElement('swatch-ui');
|
|
180
|
+
swatch.setAttribute('shape', 'block');
|
|
181
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
182
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
183
|
+
swatch.setAttribute('label', '500');
|
|
184
|
+
|
|
185
|
+
const wrapper = document.createElement('span');
|
|
186
|
+
wrapper.style.display = 'contents';
|
|
187
|
+
const chrome = document.createElement('span');
|
|
188
|
+
chrome.setAttribute('slot', 'chrome');
|
|
189
|
+
chrome.id = 'wrapped-chrome';
|
|
190
|
+
chrome.textContent = 'P3';
|
|
191
|
+
wrapper.appendChild(chrome);
|
|
192
|
+
swatch.appendChild(wrapper);
|
|
193
|
+
|
|
194
|
+
host.appendChild(swatch);
|
|
195
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
196
|
+
|
|
197
|
+
const found = swatch.querySelector('#wrapped-chrome');
|
|
198
|
+
expect(found).not.toBeNull();
|
|
199
|
+
// Chrome child should be a direct host sibling positioned between tile + badge.
|
|
200
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
201
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
202
|
+
expect(found.parentElement).toBe(swatch);
|
|
203
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
204
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('handles MULTIPLE chrome children nested in separate wrapper spans', async () => {
|
|
208
|
+
// Each interpolated fragment in html`` gets its own wrapper span,
|
|
209
|
+
// so multi-chrome usage (`${badge}${dot1}${dot2}`) produces multiple
|
|
210
|
+
// wrapper spans, each containing one chrome child.
|
|
211
|
+
const swatch = document.createElement('swatch-ui');
|
|
212
|
+
swatch.setAttribute('shape', 'block');
|
|
213
|
+
swatch.setAttribute('label-position', 'overlay');
|
|
214
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
215
|
+
|
|
216
|
+
const ids = ['c1', 'c2', 'c3'];
|
|
217
|
+
for (const id of ids) {
|
|
218
|
+
const w = document.createElement('span');
|
|
219
|
+
w.style.display = 'contents';
|
|
220
|
+
const c = document.createElement('span');
|
|
221
|
+
c.setAttribute('slot', 'chrome');
|
|
222
|
+
c.id = id;
|
|
223
|
+
c.textContent = id;
|
|
224
|
+
w.appendChild(c);
|
|
225
|
+
swatch.appendChild(w);
|
|
226
|
+
}
|
|
227
|
+
host.appendChild(swatch);
|
|
228
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
229
|
+
|
|
230
|
+
for (const id of ids) {
|
|
231
|
+
const found = swatch.querySelector(`#${id}`);
|
|
232
|
+
expect(found).not.toBeNull();
|
|
233
|
+
expect(found.parentElement).toBe(swatch);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('hoists chrome from late-arriving wrapper span (post-stamp template re-render)', async () => {
|
|
238
|
+
// Parent template re-render after the swatch is already stamped:
|
|
239
|
+
// a new wrapper span carrying chrome content arrives after #stamp().
|
|
240
|
+
// #absorbChromeSlot() should pick it up on next render pass.
|
|
241
|
+
const swatch = document.createElement('swatch-ui');
|
|
242
|
+
swatch.setAttribute('shape', 'block');
|
|
243
|
+
swatch.setAttribute('color', '#3b82f6');
|
|
244
|
+
swatch.setAttribute('label', '500');
|
|
245
|
+
host.appendChild(swatch);
|
|
246
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
247
|
+
|
|
248
|
+
// Late: append a wrapper-span carrying chrome
|
|
249
|
+
const wrapper = document.createElement('span');
|
|
250
|
+
wrapper.style.display = 'contents';
|
|
251
|
+
const chrome = document.createElement('span');
|
|
252
|
+
chrome.setAttribute('slot', 'chrome');
|
|
253
|
+
chrome.id = 'late-wrapped';
|
|
254
|
+
wrapper.appendChild(chrome);
|
|
255
|
+
swatch.appendChild(wrapper);
|
|
256
|
+
|
|
257
|
+
// Nudge a render via a property change
|
|
258
|
+
swatch.label = '600';
|
|
259
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
260
|
+
|
|
261
|
+
const found = swatch.querySelector('#late-wrapped');
|
|
262
|
+
expect(found).not.toBeNull();
|
|
263
|
+
expect(found.parentElement).toBe(swatch);
|
|
264
|
+
const tile = swatch.querySelector('[data-tile]');
|
|
265
|
+
const badge = swatch.querySelector('[data-badge]');
|
|
266
|
+
expect(found.previousElementSibling).toBe(tile);
|
|
267
|
+
expect(found.nextElementSibling).toBe(badge);
|
|
268
|
+
});
|
|
269
|
+
});
|