@adia-ai/web-components 0.5.15 → 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 +47 -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/segmented/class.js +22 -0
- 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/swatch/class.js +36 -0
- package/components/swatch/swatch.test.js +106 -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/package.json +1 -1
|
@@ -1,24 +1,98 @@
|
|
|
1
1
|
@scope (slider-ui) {
|
|
2
2
|
:where(:scope) {
|
|
3
|
-
/*
|
|
3
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4
|
+
SLIDER GEOMETRY — Mathematical foundation
|
|
5
|
+
═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
Variables
|
|
8
|
+
W = track width (100% of the track element)
|
|
9
|
+
t = thumb width (full outer width including internal padding)
|
|
10
|
+
h = track height (var(--slider-track-height))
|
|
11
|
+
pad = internal padding on the thumb (var(--slider-thumb-padding))
|
|
12
|
+
p = progress (0.0 → 1.0, written by JS as --slider-pct)
|
|
13
|
+
|
|
14
|
+
The thumb consists of two layers:
|
|
15
|
+
• Container: the geometry element that the fill aligns to.
|
|
16
|
+
Sized to t × h. Provides the full-height grab area.
|
|
17
|
+
• Visual: the white pill rendered inside the container via
|
|
18
|
+
::before. Sized to (t−2·pad) × (h−2·pad), centered
|
|
19
|
+
by inset = pad on all sides.
|
|
20
|
+
|
|
21
|
+
── Progressive fill ──
|
|
22
|
+
The blue fill starts at the thumb's full outer width and
|
|
23
|
+
grows incrementally:
|
|
24
|
+
|
|
25
|
+
fill_width(p) = t + p · (W − t) (1)
|
|
26
|
+
|
|
27
|
+
At p = 0 → fill = t (entire thumb container is blue)
|
|
28
|
+
At p = 1 → fill = W (blue spans the full track)
|
|
29
|
+
|
|
30
|
+
The fill is anchored at the left track edge (left: 0), so:
|
|
31
|
+
|
|
32
|
+
fill_right(p) = t + p · (W − t) (2)
|
|
33
|
+
|
|
34
|
+
── Thumb travel ──
|
|
35
|
+
The thumb container's center moves across the travel zone:
|
|
36
|
+
|
|
37
|
+
travel = W − t (3)
|
|
38
|
+
|
|
39
|
+
thumb_center(p) = t/2 + p · (W − t) (4)
|
|
40
|
+
|
|
41
|
+
At p = 0 → center at t/2 (container left flush, visual inset by pad)
|
|
42
|
+
At p = 1 → center at W−t/2 (container right flush, visual inset by pad)
|
|
43
|
+
|
|
44
|
+
In CSS (percentages relative to W):
|
|
45
|
+
|
|
46
|
+
left = calc(t/2 + p · (W − t))
|
|
47
|
+
= calc(var(--slider-thumb-width) / 2
|
|
48
|
+
+ var(--slider-pct) * (100% - var(--slider-thumb-width)))
|
|
49
|
+
|
|
50
|
+
The element is shifted back by 50% via transform so its
|
|
51
|
+
geometric center lands on the computed point.
|
|
52
|
+
|
|
53
|
+
── Thumb visual inset ──
|
|
54
|
+
The white pill is rendered by ::before with:
|
|
55
|
+
inset: pad (applied on all four sides)
|
|
56
|
+
|
|
57
|
+
This means the visual pill is inset from the container edge
|
|
58
|
+
by pad pixels, giving breathing room between the white surface
|
|
59
|
+
and the track boundary at both extremes.
|
|
60
|
+
═══════════════════════════════════════════════════════════════ */
|
|
61
|
+
|
|
62
|
+
/* ── Layout ── */
|
|
4
63
|
--slider-gap: var(--a-space-1);
|
|
5
64
|
--slider-readout-gap: var(--a-space-0-5);
|
|
6
65
|
--slider-radius: var(--a-radius-full);
|
|
7
|
-
--slider-track-height: calc(var(--a-size) * 0.125);
|
|
8
|
-
--slider-track-area-height: calc(var(--a-size) * 0.75);
|
|
9
|
-
--slider-thumb-size: calc(var(--a-size) * 0.375);
|
|
10
66
|
|
|
11
|
-
/*
|
|
67
|
+
/* Track height scales with the universal [size] attribute
|
|
68
|
+
via --a-toggle-size: 16 sm / 20 md / 24 lg at density=1.
|
|
69
|
+
Override --slider-track-height directly for custom sizes. */
|
|
70
|
+
--slider-track-height: var(--a-toggle-size);
|
|
71
|
+
|
|
72
|
+
/* Thumb padding: internal breathing room on all sides.
|
|
73
|
+
The visual pill sits pad px away from the container edge. */
|
|
74
|
+
--slider-thumb-padding: 2px;
|
|
75
|
+
|
|
76
|
+
/* Visual thumb height: track height minus top and bottom padding. */
|
|
77
|
+
--slider-thumb-visual-h: calc(var(--slider-track-height) - 2 * var(--slider-thumb-padding));
|
|
78
|
+
|
|
79
|
+
/* Visual thumb width: 2×1 pill ratio. */
|
|
80
|
+
--slider-thumb-visual-w: calc(var(--slider-thumb-visual-h) * 2);
|
|
81
|
+
|
|
82
|
+
/* Container / geometry thumb width: visual width plus left and
|
|
83
|
+
right padding. This is the width used in the travel equation. */
|
|
84
|
+
--slider-thumb-width: calc(var(--slider-thumb-visual-w) + 2 * var(--slider-thumb-padding));
|
|
85
|
+
|
|
86
|
+
/* Progress fraction (0.0 → 1.0) written by JS. */
|
|
87
|
+
--slider-pct: 0;
|
|
88
|
+
|
|
89
|
+
/* ── Colors ──
|
|
90
|
+
Track: dim recessed surface | Fill: primary | Thumb: white chrome */
|
|
12
91
|
--slider-track-bg: var(--a-bg-muted);
|
|
13
|
-
--slider-fill-bg: var(--a-
|
|
14
|
-
--slider-thumb-bg: var(--a-
|
|
15
|
-
--slider-thumb-border: var(--a-bg); /* intentional: cutout ring must match page bg */
|
|
16
|
-
--slider-thumb-hover-ring: 0 0 0 4px var(--a-bg-muted);
|
|
92
|
+
--slider-fill-bg: var(--a-primary-bg);
|
|
93
|
+
--slider-thumb-bg: var(--a-chrome-light);
|
|
17
94
|
--slider-fill-bg-disabled: var(--a-border-subtle);
|
|
18
95
|
--slider-thumb-bg-disabled: var(--a-fg-muted);
|
|
19
|
-
--slider-label-fg: var(--a-fg-muted);
|
|
20
|
-
--slider-value-fg: var(--a-fg);
|
|
21
|
-
--slider-suffix-fg: var(--a-fg-muted);
|
|
22
96
|
|
|
23
97
|
/* ── Typography ── */
|
|
24
98
|
--slider-font-size: var(--a-ui-size);
|
|
@@ -28,7 +102,7 @@
|
|
|
28
102
|
--slider-duration: var(--a-duration-fast);
|
|
29
103
|
--slider-easing: var(--a-easing);
|
|
30
104
|
|
|
31
|
-
/* ──
|
|
105
|
+
/* ── Focus ── */
|
|
32
106
|
--slider-focus-ring: var(--a-focus-ring);
|
|
33
107
|
}
|
|
34
108
|
|
|
@@ -55,7 +129,7 @@
|
|
|
55
129
|
}
|
|
56
130
|
|
|
57
131
|
[slot="label"] {
|
|
58
|
-
color: var(--
|
|
132
|
+
color: var(--a-fg-subtle);
|
|
59
133
|
}
|
|
60
134
|
|
|
61
135
|
[slot="readout"] {
|
|
@@ -65,86 +139,102 @@
|
|
|
65
139
|
}
|
|
66
140
|
|
|
67
141
|
[slot="value"] {
|
|
68
|
-
color: var(--
|
|
142
|
+
color: var(--a-fg);
|
|
69
143
|
font-weight: var(--slider-value-weight);
|
|
70
144
|
font-variant-numeric: tabular-nums;
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
[slot="suffix"] {
|
|
74
|
-
color: var(--
|
|
148
|
+
color: var(--a-fg-muted);
|
|
75
149
|
}
|
|
76
150
|
|
|
77
|
-
/* Track */
|
|
151
|
+
/* Track: the reference frame W for all geometry calculations. */
|
|
78
152
|
[slot="track"] {
|
|
79
153
|
position: relative;
|
|
80
|
-
height: var(--slider-track-
|
|
81
|
-
|
|
82
|
-
|
|
154
|
+
height: var(--slider-track-height);
|
|
155
|
+
border-radius: var(--slider-radius);
|
|
156
|
+
background: var(--slider-track-bg);
|
|
83
157
|
cursor: pointer;
|
|
84
158
|
touch-action: none;
|
|
85
159
|
}
|
|
86
160
|
|
|
161
|
+
/* Fill — equation (1): width = t + p·(W−t) */
|
|
87
162
|
[slot="fill"] {
|
|
88
163
|
position: absolute;
|
|
164
|
+
top: 0;
|
|
89
165
|
left: 0;
|
|
90
|
-
height:
|
|
91
|
-
border-radius:
|
|
166
|
+
height: 100%;
|
|
167
|
+
border-radius: inherit;
|
|
92
168
|
background: var(--slider-fill-bg);
|
|
93
169
|
pointer-events: none;
|
|
170
|
+
width: calc(var(--slider-thumb-width)
|
|
171
|
+
+ var(--slider-pct) * (100% - var(--slider-thumb-width)));
|
|
94
172
|
}
|
|
95
173
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
left: 0;
|
|
100
|
-
right: 0;
|
|
101
|
-
height: var(--slider-track-height);
|
|
102
|
-
border-radius: var(--slider-radius);
|
|
103
|
-
background: var(--slider-track-bg);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* Thumb */
|
|
174
|
+
/* Thumb CONTAINER: full track height, geometry width.
|
|
175
|
+
Transparent background; the white pill is rendered by ::before.
|
|
176
|
+
This element provides a generous vertical grab area. */
|
|
107
177
|
[slot="thumb"] {
|
|
108
178
|
position: absolute;
|
|
109
|
-
|
|
110
|
-
|
|
179
|
+
top: 50%;
|
|
180
|
+
left: calc(var(--slider-thumb-width) / 2
|
|
181
|
+
+ var(--slider-pct) * (100% - var(--slider-thumb-width)));
|
|
182
|
+
width: var(--slider-thumb-width);
|
|
183
|
+
height: var(--slider-track-height);
|
|
111
184
|
border-radius: var(--slider-radius);
|
|
112
|
-
background:
|
|
113
|
-
|
|
114
|
-
transform: translateX(-50%);
|
|
185
|
+
background: transparent;
|
|
186
|
+
transform: translate(-50%, -50%);
|
|
115
187
|
cursor: grab;
|
|
116
188
|
touch-action: none;
|
|
117
189
|
transition:
|
|
118
|
-
|
|
119
|
-
|
|
190
|
+
left var(--slider-duration) var(--slider-easing),
|
|
191
|
+
transform var(--slider-duration) var(--slider-easing);
|
|
120
192
|
z-index: 1;
|
|
121
193
|
}
|
|
194
|
+
|
|
195
|
+
/* Thumb VISUAL: white pill rendered inside the transparent
|
|
196
|
+
container via ::before. Inset = padding on all four sides,
|
|
197
|
+
so the pill is vertically and horizontally inset from the
|
|
198
|
+
container edge by pad pixels. */
|
|
199
|
+
[slot="thumb"]::before {
|
|
200
|
+
content: '';
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset: var(--slider-thumb-padding);
|
|
203
|
+
border-radius: inherit;
|
|
204
|
+
background: var(--slider-thumb-bg);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* During drag: kill the left transition so the thumb follows
|
|
208
|
+
the pointer frame-by-frame without CSS interpolation lag. */
|
|
209
|
+
:scope[data-dragging] [slot="thumb"] {
|
|
210
|
+
transition: transform var(--slider-duration) var(--slider-easing);
|
|
211
|
+
}
|
|
212
|
+
|
|
122
213
|
[slot="thumb"]:hover {
|
|
123
|
-
|
|
214
|
+
transform: translate(-50%, -50%) scale(1.05);
|
|
124
215
|
}
|
|
216
|
+
|
|
125
217
|
[slot="thumb"]:active {
|
|
126
|
-
transform:
|
|
218
|
+
transform: translate(-50%, -50%) scale(1.1);
|
|
127
219
|
cursor: grabbing;
|
|
128
220
|
}
|
|
221
|
+
|
|
129
222
|
:scope:focus-visible { outline: none; }
|
|
130
223
|
:scope:focus-visible [slot="thumb"] { box-shadow: var(--slider-focus-ring); }
|
|
131
224
|
|
|
132
225
|
/* Disabled */
|
|
133
226
|
:scope[disabled] [slot="track"] { cursor: not-allowed; }
|
|
134
|
-
:scope[disabled] [slot="fill"]
|
|
135
|
-
:scope[disabled] [slot="thumb"] {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
wired on the host in class.js so screen readers announce this
|
|
141
|
-
as a description (distinct from aria-label, which comes from
|
|
142
|
-
[label]). Uses the same muted typography as field-ui's hint. */
|
|
227
|
+
:scope[disabled] [slot="fill"] { background: var(--slider-fill-bg-disabled); }
|
|
228
|
+
:scope[disabled] [slot="thumb"]::before {
|
|
229
|
+
background: var(--slider-thumb-bg-disabled);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── Hint (§184, v0.5.5, FEEDBACK-08 §7) ── */
|
|
143
233
|
[slot="hint"] {
|
|
144
234
|
display: block;
|
|
145
235
|
margin-top: var(--slider-hint-mt, var(--a-space-1));
|
|
146
236
|
font-size: var(--slider-hint-size, var(--a-ui-xs));
|
|
147
|
-
color:
|
|
237
|
+
color: var(--slider-hint-fg, var(--a-fg-muted));
|
|
148
238
|
line-height: var(--slider-hint-lh, 1.4);
|
|
149
239
|
}
|
|
150
240
|
}
|
|
@@ -14,43 +14,46 @@ function mount(html) {
|
|
|
14
14
|
describe('slider-ui', () => {
|
|
15
15
|
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// §330 (v0.5.16) — slider iOS-style pill redesign. Thumb position no
|
|
18
|
+
// longer set via thumb.style.left; instead the host gets a CSS custom
|
|
19
|
+
// property `--slider-pct` (fraction 0.0–1.0) and CSS calc() composes
|
|
20
|
+
// the full geometry: `fill_width(p) = t + p·(W−t)`, where t = thumb
|
|
21
|
+
// width and W = track width. Test asserts the CSS variable carries
|
|
22
|
+
// the expected fraction (50% → 0.5).
|
|
23
|
+
const pct = (s) => s.style.getPropertyValue('--slider-pct').trim();
|
|
24
|
+
|
|
25
|
+
it('renders host --slider-pct for initial value', async () => {
|
|
18
26
|
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
19
27
|
await tick();
|
|
20
|
-
|
|
21
|
-
expect(thumb.style.left).toBe('50%');
|
|
28
|
+
expect(pct(s)).toBe('0.5');
|
|
22
29
|
});
|
|
23
30
|
|
|
24
31
|
// Property reactivity contract — locks in the behavior that consumer
|
|
25
32
|
// feedback (FEEDBACK-adia-packages.md §5, 2026-05-12) incorrectly
|
|
26
33
|
// claimed was broken. UIElement's signal-backed property setters
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// would break undo/redo or any other programmatic-value flow.
|
|
31
|
-
it('moves thumb when .value is set programmatically', async () => {
|
|
34
|
+
// trigger the host's render effect on every property change; slider-ui's
|
|
35
|
+
// render() reads this.value and writes --slider-pct on the host.
|
|
36
|
+
it('updates --slider-pct when .value is set programmatically', async () => {
|
|
32
37
|
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
33
38
|
await tick();
|
|
34
|
-
|
|
35
|
-
expect(thumb.style.left).toBe('50%');
|
|
39
|
+
expect(pct(s)).toBe('0.5');
|
|
36
40
|
|
|
37
41
|
s.value = 75;
|
|
38
42
|
await tick();
|
|
39
|
-
expect(
|
|
43
|
+
expect(pct(s)).toBe('0.75');
|
|
40
44
|
|
|
41
45
|
s.value = 10;
|
|
42
46
|
await tick();
|
|
43
|
-
expect(
|
|
47
|
+
expect(pct(s)).toBe('0.1');
|
|
44
48
|
});
|
|
45
49
|
|
|
46
|
-
it('
|
|
50
|
+
it('updates --slider-pct when attribute is set imperatively', async () => {
|
|
47
51
|
const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
|
|
48
52
|
await tick();
|
|
49
|
-
const thumb = s.querySelector('[slot="thumb"]');
|
|
50
53
|
|
|
51
54
|
s.setAttribute('value', '25');
|
|
52
55
|
await tick();
|
|
53
|
-
expect(
|
|
56
|
+
expect(pct(s)).toBe('0.25');
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
it('updates [slot="value"] readout text when .value changes', async () => {
|
|
@@ -82,11 +82,15 @@ states:
|
|
|
82
82
|
traits: []
|
|
83
83
|
tokens:
|
|
84
84
|
--slider-fill:
|
|
85
|
-
description: Filled
|
|
86
|
-
--slider-thumb-
|
|
87
|
-
description: Thumb
|
|
85
|
+
description: Filled portion / progress color (was mixed accent, now primary)
|
|
86
|
+
--slider-thumb-width:
|
|
87
|
+
description: Thumb pill width (2× thumb-height, driven by track-height)
|
|
88
|
+
--slider-thumb-height:
|
|
89
|
+
description: Thumb pill height (track-height − 2× inset)
|
|
88
90
|
--slider-track:
|
|
89
|
-
description:
|
|
91
|
+
description: Unfilled track background color
|
|
92
|
+
--slider-track-height:
|
|
93
|
+
description: Full track height (scales via universal [size] attribute)
|
|
90
94
|
a2ui:
|
|
91
95
|
rules: []
|
|
92
96
|
anti_patterns: []
|
|
@@ -218,9 +218,28 @@ 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
|
|
@@ -331,6 +350,23 @@ export class UISwatch extends UIElement {
|
|
|
331
350
|
this.insertBefore(child, this.#badgeEl);
|
|
332
351
|
}
|
|
333
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
|
+
}
|
|
334
370
|
}
|
|
335
371
|
|
|
336
372
|
#syncCore() {
|
|
@@ -161,3 +161,109 @@ describe('<swatch-ui> late-arriving [slot="chrome"] children (FB-37 / §326)', (
|
|
|
161
161
|
expect(found.nextElementSibling).toBe(badge);
|
|
162
162
|
});
|
|
163
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
|
+
});
|