@xsynaptic/sonic-ui 0.27.0 → 0.28.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -0
  3. package/dist/default/define/ticker.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/sonic-element.js +1 -1
  6. package/dist/default/elements/ticker.js +313 -0
  7. package/dist/default/elements/wavestrip.js +2 -5
  8. package/dist/default/index.js +2 -1
  9. package/dist/default/lib/cancel-zone.js +12 -0
  10. package/dist/default/lib/distribute.js +2 -3
  11. package/dist/default/lib/mirror-children.js +1 -1
  12. package/dist/default/lib/pointer-drag.js +8 -2
  13. package/dist/default/lib/ticker-leg.js +54 -0
  14. package/dist/dev/define/ticker.d.ts +2 -0
  15. package/dist/dev/define/ticker.js +8 -0
  16. package/dist/dev/define/ticker.js.map +1 -0
  17. package/dist/dev/define.d.ts +3 -1
  18. package/dist/dev/define.js +1 -0
  19. package/dist/dev/elements/sonic-element.d.ts +1 -0
  20. package/dist/dev/elements/sonic-element.js +3 -2
  21. package/dist/dev/elements/sonic-element.js.map +1 -1
  22. package/dist/dev/elements/ticker.d.ts +25 -0
  23. package/dist/dev/elements/ticker.js +314 -0
  24. package/dist/dev/elements/ticker.js.map +1 -0
  25. package/dist/dev/elements/wavestrip.js +2 -5
  26. package/dist/dev/elements/wavestrip.js.map +1 -1
  27. package/dist/dev/index.d.ts +2 -1
  28. package/dist/dev/index.js +2 -1
  29. package/dist/dev/lib/cancel-zone.js +13 -0
  30. package/dist/dev/lib/cancel-zone.js.map +1 -0
  31. package/dist/dev/lib/check-styles.js +20 -12
  32. package/dist/dev/lib/check-styles.js.map +1 -1
  33. package/dist/dev/lib/distribute.js +2 -3
  34. package/dist/dev/lib/distribute.js.map +1 -1
  35. package/dist/dev/lib/mirror-children.js +9 -7
  36. package/dist/dev/lib/mirror-children.js.map +1 -1
  37. package/dist/dev/lib/pointer-drag.js +8 -2
  38. package/dist/dev/lib/pointer-drag.js.map +1 -1
  39. package/dist/dev/lib/ticker-leg.js +55 -0
  40. package/dist/dev/lib/ticker-leg.js.map +1 -0
  41. package/dist/styles/controls.css +1 -0
  42. package/dist/styles/dial.css +28 -5
  43. package/dist/styles/material/cap.css +16 -5
  44. package/dist/styles/material/core.css +1 -0
  45. package/dist/styles/slider.css +2 -1
  46. package/dist/styles/switch.css +3 -1
  47. package/dist/styles/ticker.css +115 -0
  48. package/dist/styles/toggle.css +7 -5
  49. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `--sonic-gloss` scales a cap's sheen apart from `--sonic-relief`: 0 is matte, 1 the default. A slider cap has no sheen until the token is set, so there `1` is not the same as unset.
8
+ - - `<sonic-ticker>`: one line of content that scrolls there and back when it does not fit its box, registered by `./define/ticker` with `ticker.css`, which needs no material sheet. It stands still with an ellipsis under reduced motion, and pauses under a mouse, while a link round it has keyboard focus, off screen, and while `paused` is set. A drag along the line moves the tape; with `cancellable` a drag that strays past `--sonic-cancel-zone`, shared with the wave strip, puts it back. `text` replaces the content. A link among the children is copied and still navigates.
9
+ - Ticker surface: tokens `--sonic-ticker-rate` (a length a second, `40px`), `--sonic-ticker-hold` (`2s`) and `--sonic-ticker-fade` (`0.75rem`); hook classes `.sonic-ticker` and `.sonic-ticker-tape`; states `data-sonic-overflowing`, `data-sonic-dragging`, `data-sonic-cancelling` and `data-sonic-paused`, and `data-sonic-clipped` naming each end with content past it.
10
+
11
+ ### Patch Changes
12
+
13
+ - Switch and toggle: the bat and the cap land with a small overshoot, and the switch's flip takes 120ms, up from 90ms.
14
+ - Split: a move the siblings cannot absorb on their own steps is turned back, so the total holds across mixed step grids.
15
+
3
16
  ## 0.27.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -88,6 +88,7 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
88
88
  | `slider.css` | core, cap, glass, readout, value, groove, scale |
89
89
  | `spectrum.css` | core |
90
90
  | `switch.css` | core, cap |
91
+ | `ticker.css` | |
91
92
  | `toggle.css` | core, cap, well, keycap |
92
93
  | `waveform.css` | core, cap, glass, readout, value |
93
94
  | `wavestrip.css` | core, cap, glass, readout, value |
@@ -0,0 +1,7 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicTicker } from "../elements/ticker.js";
3
+
4
+ //#region src/define/ticker.ts
5
+ defineOnce("sonic-ticker", SonicTicker);
6
+
7
+ //#endregion
@@ -9,6 +9,7 @@ import "./define/slider.js";
9
9
  import "./define/spectrum.js";
10
10
  import "./define/split.js";
11
11
  import "./define/switch.js";
12
+ import "./define/ticker.js";
12
13
  import "./define/toggle.js";
13
14
  import "./define/waveform.js";
14
15
  import "./define/wavestrip.js";
@@ -48,7 +48,7 @@ var SonicElement = class SonicElement extends HTMLElement {
48
48
  this.#connection = void 0;
49
49
  throw error;
50
50
  }
51
- const { boxProbe: box, control, sheet } = this;
51
+ const { boxProbe: box, control, sheet, sheetToken: token } = this;
52
52
  }
53
53
  connectedMoveCallback() {}
54
54
  disconnectedCallback() {
@@ -0,0 +1,313 @@
1
+ import { clamp } from "../lib/math.js";
2
+ import { writeAttribute } from "../lib/write-attribute.js";
3
+ import { SonicElement } from "./sonic-element.js";
4
+ import { copyNode } from "../lib/copy-node.js";
5
+ import { placeChildren, requireChild, template } from "../lib/render.js";
6
+ import { mirrorChildren } from "../lib/mirror-children.js";
7
+ import { bindDrag, capturePointer } from "../lib/pointer-drag.js";
8
+ import { readMsProperty } from "../lib/read-ms-property.js";
9
+ import { dragThresholdPx } from "../lib/drag-step.js";
10
+ import { readPxProperty } from "../lib/read-px-property.js";
11
+ import { outsideCancelZone } from "../lib/cancel-zone.js";
12
+ import { clippedEnds, fadeWidth, legDeparture, legFades, legPosition, tickerLeg } from "../lib/ticker-leg.js";
13
+
14
+ //#region src/elements/ticker.ts
15
+ const renderTicker = template(`<span class="sonic-ticker"><span class="sonic-ticker-tape"></span></span>`, HTMLSpanElement);
16
+ const fadePx = 12;
17
+ const holdMs = 2e3;
18
+ const ratePxPerSecond = 40;
19
+ var SonicTicker = class extends SonicElement {
20
+ static observedAttributes = ["paused"];
21
+ get cancellable() {
22
+ return this.hasAttribute("cancellable");
23
+ }
24
+ set cancellable(isCancellable) {
25
+ this.reflect("cancellable", isCancellable);
26
+ }
27
+ get paused() {
28
+ return this.hasAttribute("paused");
29
+ }
30
+ set paused(isPaused) {
31
+ this.reflect("paused", isPaused);
32
+ }
33
+ get text() {
34
+ return this.#originals().map((original) => original.textContent).join("");
35
+ }
36
+ set text(text) {
37
+ for (const original of this.#originals()) original.remove();
38
+ if (text !== void 0 && text !== "") this.prepend(text);
39
+ }
40
+ control = renderTicker();
41
+ sheet = "ticker.css";
42
+ sheetToken = "--_sonic-ticker-fade";
43
+ #fade = fadePx;
44
+ #heading = 1;
45
+ #isClickSwallowed = false;
46
+ #isFocusInside = false;
47
+ #isHovered = false;
48
+ #isOffScreen = false;
49
+ #isPressed = false;
50
+ #mark;
51
+ #measured = "";
52
+ #position = 0;
53
+ #reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
54
+ #running;
55
+ #travel = {
56
+ distance: 0,
57
+ hold: holdMs,
58
+ rate: ratePxPerSecond
59
+ };
60
+ attributeChangedCallback() {
61
+ this.render();
62
+ }
63
+ connect(signal) {
64
+ const control = this.control;
65
+ const tape = requireChild(control, ".sonic-ticker-tape", HTMLSpanElement);
66
+ const measure = () => {
67
+ this.#measure(tape);
68
+ };
69
+ const observer = new ResizeObserver(measure);
70
+ const drag = this.#bindDrag(tape, signal);
71
+ this.upgradeProperties("cancellable", "paused", "text");
72
+ mirrorChildren(this, {
73
+ canCopyLinks: true,
74
+ control,
75
+ copy: copyNode,
76
+ place: (copies) => {
77
+ placeChildren(tape, copies);
78
+ drag.end();
79
+ this.#measure(tape, {
80
+ from: 0,
81
+ heading: 1
82
+ });
83
+ }
84
+ }, signal);
85
+ this.render();
86
+ observer.observe(control);
87
+ observer.observe(tape);
88
+ if ("fonts" in document) document.fonts.addEventListener("loadingdone", measure, { signal });
89
+ this.#bindFocus(tape, signal);
90
+ this.#bindPauses(signal);
91
+ this.#reducedMotion.addEventListener("change", () => {
92
+ this.#run(tape);
93
+ }, { signal });
94
+ signal.addEventListener("abort", () => {
95
+ observer.disconnect();
96
+ this.#stop();
97
+ }, { once: true });
98
+ }
99
+ render() {
100
+ if (!this.isBound()) return;
101
+ this.toggleState("paused", this.paused);
102
+ this.#syncPause();
103
+ }
104
+ #bindDrag(tape, signal) {
105
+ const control = this.control;
106
+ control.addEventListener("click", (event) => {
107
+ if (!this.#isClickSwallowed) return;
108
+ this.#isClickSwallowed = false;
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ }, {
112
+ capture: true,
113
+ signal
114
+ });
115
+ this.getRootNode().addEventListener("dragstart", (event) => {
116
+ if (this.#isPressed) event.preventDefault();
117
+ }, { signal });
118
+ return bindDrag(control, {
119
+ grab: (event) => {
120
+ if (this.#travel.distance <= 0) return;
121
+ return {
122
+ from: 0,
123
+ isDragging: false,
124
+ outside: void 0,
125
+ thresholdPx: dragThresholdPx(event.pointerType),
126
+ x: event.clientX
127
+ };
128
+ },
129
+ isCaptureLate: true,
130
+ move: (held, event) => {
131
+ if (!held.isDragging) {
132
+ if (Math.abs(event.clientX - held.x) < held.thresholdPx) return;
133
+ const { from, heading } = this.#where();
134
+ held.isDragging = true;
135
+ held.from = from;
136
+ held.outside = this.cancellable ? outsideCancelZone(control, "--_sonic-ticker-cancel-zone") : void 0;
137
+ held.x = event.clientX;
138
+ capturePointer(control, event.pointerId);
139
+ this.toggleState("dragging", true);
140
+ this.#stop();
141
+ this.#heading = heading;
142
+ }
143
+ const isOutside = held.outside?.(event) === true;
144
+ this.toggleState("cancelling", isOutside);
145
+ this.#place(tape, isOutside ? held.from : held.from - (event.clientX - held.x));
146
+ },
147
+ release: (held) => {
148
+ if (!held.isDragging) return;
149
+ this.#isClickSwallowed = true;
150
+ setTimeout(() => {
151
+ this.#isClickSwallowed = false;
152
+ }, 0);
153
+ this.toggleState("dragging", false);
154
+ this.toggleState("cancelling", false);
155
+ this.#run(tape);
156
+ },
157
+ toggle: (isPressed) => {
158
+ this.#isPressed = isPressed;
159
+ this.#syncPause();
160
+ }
161
+ }, signal);
162
+ }
163
+ #bindFocus(tape, signal) {
164
+ const control = this.control;
165
+ control.addEventListener("focusout", (event) => {
166
+ if (event.relatedTarget instanceof Node && control.contains(event.relatedTarget)) return;
167
+ control.scrollLeft = 0;
168
+ this.#isFocusInside = false;
169
+ this.#run(tape);
170
+ }, { signal });
171
+ control.addEventListener("focusin", (event) => {
172
+ this.#isFocusInside = true;
173
+ this.#run(tape);
174
+ if (event.target instanceof Element) event.target.scrollIntoView({ block: "nearest" });
175
+ }, { signal });
176
+ }
177
+ #bindPauses(signal) {
178
+ const control = this.control;
179
+ for (const type of ["pointerenter", "pointerleave"]) control.addEventListener(type, (event) => {
180
+ if (event.pointerType !== "mouse") return;
181
+ this.#isHovered = type === "pointerenter";
182
+ this.#syncPause();
183
+ }, { signal });
184
+ for (const type of ["focusin", "focusout"]) this.getRootNode().addEventListener(type, () => {
185
+ this.#syncPause();
186
+ }, { signal });
187
+ const view = new IntersectionObserver((entries) => {
188
+ this.#isOffScreen = entries.at(-1)?.isIntersecting === false;
189
+ this.#syncPause();
190
+ });
191
+ view.observe(control);
192
+ signal.addEventListener("abort", () => {
193
+ view.disconnect();
194
+ }, { once: true });
195
+ }
196
+ #isPaused() {
197
+ return this.paused || this.#isHovered || this.#isPressed || this.#isOffScreen || this.control.closest(":focus-visible") !== null;
198
+ }
199
+ #measure(tape, departure) {
200
+ const control = this.control;
201
+ const styles = getComputedStyle(control);
202
+ const box = control.clientWidth - readPxProperty(styles, "padding-left", 0) - readPxProperty(styles, "padding-right", 0);
203
+ const travel = {
204
+ distance: Math.max(0, Math.round(tape.offsetWidth - box)),
205
+ hold: readMsProperty(styles, "--_sonic-ticker-hold", holdMs),
206
+ rate: readPxProperty(styles, "--_sonic-ticker-rate", ratePxPerSecond)
207
+ };
208
+ const fade = readPxProperty(styles, "--_sonic-ticker-fade", fadePx);
209
+ const measured = [...Object.values(travel), fade].join(" ");
210
+ if (!departure && measured === this.#measured) return;
211
+ const from = departure ?? this.#where();
212
+ this.#fade = fade;
213
+ this.#measured = measured;
214
+ this.#travel = travel;
215
+ this.toggleState("overflowing", travel.distance > 0);
216
+ this.#run(tape, from);
217
+ }
218
+ #originals() {
219
+ return [...this.childNodes].filter((child) => child !== this.control);
220
+ }
221
+ #place(tape, position) {
222
+ const { distance } = this.#travel;
223
+ this.#position = clamp(position, 0, distance);
224
+ tape.style.translate = this.#position > 0 ? `${String(-this.#position)}px` : "";
225
+ writeAttribute(this.control, "data-sonic-clipped", clippedEnds(this.#position, distance));
226
+ this.#writeFades(this.#position);
227
+ }
228
+ #run(tape, departure = this.#where()) {
229
+ const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") ? tickerLeg(this.#travel, departure) : void 0;
230
+ this.#stop();
231
+ this.#heading = departure.heading;
232
+ if (leg) this.#start(tape, leg);
233
+ else this.#place(tape, this.#isFocusInside ? 0 : departure.from);
234
+ }
235
+ #start(tape, leg) {
236
+ const animation = tape.animate({ translate: [`${String(-leg.from)}px`, `${String(-leg.to)}px`] }, {
237
+ delay: leg.hold,
238
+ duration: leg.duration,
239
+ fill: "both"
240
+ });
241
+ animation.addEventListener("finish", () => {
242
+ this.#run(tape, {
243
+ from: leg.to,
244
+ heading: leg.heading
245
+ });
246
+ });
247
+ this.#running = {
248
+ animation,
249
+ fades: [],
250
+ leg
251
+ };
252
+ tape.style.translate = "";
253
+ writeAttribute(this.control, "data-sonic-clipped", clippedEnds(leg.from, this.#travel.distance));
254
+ this.#syncPause();
255
+ }
256
+ #startFades(leg, elapsed) {
257
+ const fades = legFades(leg, this.#travel.distance, this.#fade);
258
+ return ["start", "end"].flatMap((end) => {
259
+ const { delay, duration, widths } = fades[end];
260
+ if (duration <= 0 || delay + duration <= elapsed) return [];
261
+ return this.control.animate({ [`--_sonic-ticker-fade-${end}`]: widths.map((width) => `${String(width)}px`) }, {
262
+ delay: delay - elapsed,
263
+ duration,
264
+ fill: "both"
265
+ });
266
+ });
267
+ }
268
+ #stop() {
269
+ const running = this.#running;
270
+ clearTimeout(this.#mark);
271
+ if (!running) return;
272
+ for (const fade of running.fades) fade.cancel();
273
+ running.animation.cancel();
274
+ this.#running = void 0;
275
+ }
276
+ #syncPause() {
277
+ const running = this.#running;
278
+ if (!running) return;
279
+ const { animation, leg } = running;
280
+ const elapsed = Number(animation.currentTime);
281
+ clearTimeout(this.#mark);
282
+ for (const fade of running.fades) fade.cancel();
283
+ running.fades = [];
284
+ this.#writeFades(legPosition(leg, elapsed));
285
+ if (elapsed >= leg.hold) writeAttribute(this.control, "data-sonic-clipped", "start end");
286
+ if (this.#isPaused()) {
287
+ animation.pause();
288
+ return;
289
+ }
290
+ if (animation.playState === "paused") animation.play();
291
+ running.fades = this.#startFades(leg, elapsed);
292
+ if (elapsed >= leg.hold) return;
293
+ this.#mark = setTimeout(() => {
294
+ writeAttribute(this.control, "data-sonic-clipped", "start end");
295
+ }, leg.hold - elapsed);
296
+ }
297
+ #where() {
298
+ const running = this.#running;
299
+ if (!running) return {
300
+ from: this.#position,
301
+ heading: this.#heading
302
+ };
303
+ return legDeparture(running.leg, Number(running.animation.currentTime));
304
+ }
305
+ #writeFades(position) {
306
+ const { style } = this.control;
307
+ style.setProperty("--_sonic-ticker-fade-start", `${String(fadeWidth(position, this.#fade))}px`);
308
+ style.setProperty("--_sonic-ticker-fade-end", `${String(fadeWidth(this.#travel.distance - position, this.#fade))}px`);
309
+ }
310
+ };
311
+
312
+ //#endregion
313
+ export { SonicTicker };
@@ -1,8 +1,8 @@
1
1
  import { clamp, clampProportion } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
- import { readPxProperty } from "../lib/read-px-property.js";
4
3
  import { scrubRegions } from "../lib/scrub.js";
5
4
  import { sortedRegions } from "../lib/time-regions.js";
5
+ import { outsideCancelZone } from "../lib/cancel-zone.js";
6
6
  import { SonicWaveElement } from "./wave-element.js";
7
7
  import { createMarkerBand } from "../lib/marker-band.js";
8
8
  import { RegionLayer } from "./region-layer.js";
@@ -22,7 +22,6 @@ const lengths = {
22
22
  pitch: "--_sonic-wavestrip-bar-pitch",
23
23
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
24
24
  };
25
- const cancelZonePx = 48;
26
25
  const renderWavestrip = template(`
27
26
  <div class="sonic-wavestrip">
28
27
  <canvas class="sonic-wavestrip-canvas" role="slider" tabindex="0"></canvas>
@@ -256,9 +255,7 @@ var SonicWavestrip = class extends SonicWaveElement {
256
255
  }
257
256
  #outside() {
258
257
  if (!this.cancellable) return void 0;
259
- const box = this.control.getBoundingClientRect();
260
- const zone = readPxProperty(getComputedStyle(this.control), "--_sonic-cancel-zone", cancelZonePx);
261
- return (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;
258
+ return outsideCancelZone(this.control, "--_sonic-cancel-zone");
262
259
  }
263
260
  #placeReadout() {
264
261
  const at = this.mapping().proportionOf(this.readoutValue());
@@ -10,6 +10,7 @@ import { SonicSlider } from "./elements/slider.js";
10
10
  import { SonicSpectrum } from "./elements/spectrum.js";
11
11
  import { SonicSplit } from "./elements/split.js";
12
12
  import { SonicSwitch } from "./elements/switch.js";
13
+ import { SonicTicker } from "./elements/ticker.js";
13
14
  import { SonicToggle } from "./elements/toggle.js";
14
15
  import { formatClock, parseClock } from "./lib/clock.js";
15
16
  import { SonicWaveform } from "./elements/waveform.js";
@@ -18,4 +19,4 @@ import { SonicXy } from "./elements/xy.js";
18
19
  import { crossfadeGains } from "./lib/crossfade.js";
19
20
  import { formatPercent, parsePercent } from "./lib/percent.js";
20
21
 
21
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
22
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicTicker, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
@@ -0,0 +1,12 @@
1
+ import { readPxProperty } from "./read-px-property.js";
2
+
3
+ //#region src/lib/cancel-zone.ts
4
+ const cancelZonePx = 48;
5
+ function outsideCancelZone(control, property) {
6
+ const box = control.getBoundingClientRect();
7
+ const zone = readPxProperty(getComputedStyle(control), property, cancelZonePx);
8
+ return (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;
9
+ }
10
+
11
+ //#endregion
12
+ export { outsideCancelZone };
@@ -187,8 +187,7 @@ function close(members, rule, held) {
187
187
  const { landing, move, mover } = held;
188
188
  const { index } = move;
189
189
  const landed = landing.values[index] ?? mover.value;
190
- const targets = moverTargets(members, landing, index);
191
- return nearest(targets.flatMap((target) => {
190
+ return nearest(moverTargets(members, landing, index).flatMap((target) => {
192
191
  const attempt = spread(members, rule, {
193
192
  index,
194
193
  target
@@ -197,7 +196,7 @@ function close(members, rule, held) {
197
196
  }), move, {
198
197
  landed,
199
198
  start: mover.value
200
- }) ?? landing.values.with(index, targets[0] ?? landed);
199
+ }) ?? members.map(({ value }) => value);
201
200
  }
202
201
  function distribute(members, rule, move) {
203
202
  const landing = spread(members, rule, move);
@@ -14,7 +14,7 @@ function isCopiedChange(host, record, { control, isCopied }) {
14
14
  if (child === void 0) return false;
15
15
  return isCopied(child) || record.type === "attributes" && record.target === child;
16
16
  }
17
- function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
17
+ function mirrorChildren(host, { canCopyLinks = false, control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
18
18
  const slots = hostSlots.get(host) ?? attachSlots(host);
19
19
  let copies = /* @__PURE__ */ new Map();
20
20
  hostSlots.set(host, slots);
@@ -5,6 +5,7 @@ function capturePointer(target, pointerId) {
5
5
  } catch {}
6
6
  }
7
7
  function bindDrag(target, handlers, signal) {
8
+ const isCaptureLate = handlers.isCaptureLate === true;
8
9
  let held;
9
10
  const end = () => {
10
11
  if (!held) return;
@@ -28,14 +29,19 @@ function bindDrag(target, handlers, signal) {
28
29
  pointerId: event.pointerId
29
30
  };
30
31
  handlers.toggle?.(true);
31
- capturePointer(target, event.pointerId);
32
+ if (!isCaptureLate) capturePointer(target, event.pointerId);
32
33
  }, { signal });
33
34
  whileHeld("pointermove", handlers.move);
34
35
  whileHeld("pointercancel", (drag) => {
35
36
  handlers.cancel?.(drag);
36
37
  end();
37
38
  });
38
- whileHeld("lostpointercapture", end);
39
+ whileHeld("lostpointercapture", (_drag, event) => {
40
+ if (!isCaptureLate || event.target === target) end();
41
+ });
42
+ if (isCaptureLate) whileHeld("pointerleave", (_drag, event) => {
43
+ if (!target.hasPointerCapture(event.pointerId)) end();
44
+ });
39
45
  whileHeld("pointerup", (drag, event) => {
40
46
  end();
41
47
  handlers.lift?.(drag, event);
@@ -0,0 +1,54 @@
1
+ import { clamp, clampProportion } from "./math.js";
2
+
3
+ //#region src/lib/ticker-leg.ts
4
+ const fadeApproach = 3;
5
+ function tickerLeg({ distance, hold: fullHold, rate }, { from, heading, hold = fullHold }) {
6
+ if (!(distance > 0) || !(rate > 0)) return void 0;
7
+ const start = clamp(from, 0, distance);
8
+ const isOutbound = start === (heading > 0 ? distance : 0) ? heading < 0 : heading > 0;
9
+ const to = isOutbound ? distance : 0;
10
+ return {
11
+ duration: Math.abs(to - start) / rate * 1e3,
12
+ from: start,
13
+ heading: isOutbound ? 1 : -1,
14
+ hold: Math.max(0, hold),
15
+ to
16
+ };
17
+ }
18
+ function legPosition(leg, elapsed) {
19
+ return leg.from + (leg.to - leg.from) * clampProportion((elapsed - leg.hold) / leg.duration);
20
+ }
21
+ function legDeparture(leg, elapsed) {
22
+ return {
23
+ from: legPosition(leg, elapsed),
24
+ heading: leg.heading,
25
+ hold: Math.max(0, leg.hold - elapsed)
26
+ };
27
+ }
28
+ function clippedEnds(position, distance) {
29
+ const ends = [...position > 0 ? ["start"] : [], ...position < distance ? ["end"] : []];
30
+ return ends.length > 0 ? ends.join(" ") : void 0;
31
+ }
32
+ function fadeWidth(past, fade) {
33
+ return clamp(past / fadeApproach, 0, Math.max(0, fade));
34
+ }
35
+ function legFade(leg, [pastFrom, pastTo], fade) {
36
+ const reach = Math.max(0, fade) * fadeApproach;
37
+ const msPerPx = leg.duration / Math.abs(leg.to - leg.from);
38
+ const from = Math.min(pastFrom, reach);
39
+ const to = Math.min(pastTo, reach);
40
+ return {
41
+ delay: leg.hold + (pastFrom - from) * msPerPx,
42
+ duration: Math.abs(to - from) * msPerPx,
43
+ widths: [from / fadeApproach, to / fadeApproach]
44
+ };
45
+ }
46
+ function legFades(leg, distance, fade) {
47
+ return {
48
+ end: legFade(leg, [distance - leg.from, distance - leg.to], fade),
49
+ start: legFade(leg, [leg.from, leg.to], fade)
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { clippedEnds, fadeWidth, legDeparture, legFades, legPosition, tickerLeg };
@@ -0,0 +1,2 @@
1
+ import { SonicTicker } from "../elements/ticker.js";
2
+ export type { SonicTicker };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicTicker } from "../elements/ticker.js";
3
+
4
+ //#region src/define/ticker.ts
5
+ defineOnce("sonic-ticker", SonicTicker);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=ticker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticker.js","names":[],"sources":["../../../src/define/ticker.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicTicker } from '#elements/ticker.ts';\n\ndefineOnce('sonic-ticker', SonicTicker);\n\nexport type { SonicTicker } from '#elements/ticker.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -20,6 +20,8 @@ import { SonicSplit } from "./elements/split.js";
20
20
  import "./define/split.js";
21
21
  import { SonicSwitch } from "./elements/switch.js";
22
22
  import "./define/switch.js";
23
+ import { SonicTicker } from "./elements/ticker.js";
24
+ import "./define/ticker.js";
23
25
  import { SonicToggle } from "./elements/toggle.js";
24
26
  import "./define/toggle.js";
25
27
  import { SonicWaveform } from "./elements/waveform.js";
@@ -28,4 +30,4 @@ import { SonicWavestrip } from "./elements/wavestrip.js";
28
30
  import "./define/wavestrip.js";
29
31
  import { SonicXy } from "./elements/xy.js";
30
32
  import "./define/xy.js";
31
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
33
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicTicker, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
@@ -9,6 +9,7 @@ import "./define/slider.js";
9
9
  import "./define/spectrum.js";
10
10
  import "./define/split.js";
11
11
  import "./define/switch.js";
12
+ import "./define/ticker.js";
12
13
  import "./define/toggle.js";
13
14
  import "./define/waveform.js";
14
15
  import "./define/wavestrip.js";
@@ -9,6 +9,7 @@ export declare abstract class SonicElement extends HTMLElement {
9
9
  protected readonly control: HTMLElement | undefined;
10
10
  protected readonly internals: ElementInternals | undefined;
11
11
  protected readonly sheet: string | undefined;
12
+ protected readonly sheetToken: `--_sonic-${string}` | undefined;
12
13
  blur(): void;
13
14
  connectedCallback(): void;
14
15
  connectedMoveCallback(): void;
@@ -49,11 +49,12 @@ var SonicElement = class SonicElement extends HTMLElement {
49
49
  this.#connection = void 0;
50
50
  throw error;
51
51
  }
52
- const { boxProbe: box, control, sheet } = this;
52
+ const { boxProbe: box, control, sheet, sheetToken: token } = this;
53
53
  if (control && sheet !== void 0) checkStyles(this, {
54
54
  box,
55
55
  control,
56
- sheet
56
+ sheet,
57
+ token
57
58
  });
58
59
  }
59
60
  connectedMoveCallback() {}
@@ -1 +1 @@
1
- {"version":3,"file":"sonic-element.js","names":[],"sources":["../../../src/elements/sonic-element.ts"],"sourcesContent":["import type { BoxProbe } from '#lib/check-styles.ts';\n\nimport { checkStyles } from '#lib/check-styles.ts';\nimport { toNumber } from '#lib/math.ts';\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare const __DEV__: boolean;\n\nconst namingAttributes = ['aria-describedby', 'aria-label', 'aria-labelledby'];\n\nconst canReferElements = 'ariaLabelledByElements' in Element.prototype;\n\nfunction writeLabelledBy(target: Element, labels: Array<Element>): void {\n\tconst current = target.ariaLabelledByElements;\n\tif (\n\t\tcurrent?.length === labels.length &&\n\t\tlabels.every((label, index) => label === current[index])\n\t) {\n\t\treturn;\n\t}\n\n\ttarget.ariaLabelledByElements = labels;\n}\n\n// eslint-disable-next-line wc/define-tag-after-class-definition -- abstract; only subclasses are defined\nexport abstract class SonicElement extends HTMLElement {\n\tstatic readonly observedAttributes = [...namingAttributes, 'disabled'];\n\n\tstatic #labelCount = 0;\n\n\tget disabled(): boolean {\n\t\treturn this.hasAttribute('disabled');\n\t}\n\n\tset disabled(isDisabled: boolean) {\n\t\tthis.reflect('disabled', isDisabled);\n\t}\n\n\tdeclare protected readonly boxProbe: BoxProbe | undefined;\n\n\tdeclare protected readonly control: HTMLElement | undefined;\n\n\t// happy-dom has no `ElementInternals`\n\tprotected readonly internals =\n\t\t'attachInternals' in HTMLElement.prototype ? this.attachInternals() : undefined;\n\n\tdeclare protected readonly sheet: string | undefined;\n\n\t#connection: AbortController | undefined;\n\n\treadonly #states = new Set<string>();\n\n\tstatic #labelId(label: Element): string {\n\t\tif (label.id === '') {\n\t\t\tSonicElement.#labelCount += 1;\n\t\t\tlabel.id = `sonic-label-${String(SonicElement.#labelCount)}`;\n\t\t}\n\n\t\treturn label.id;\n\t}\n\n\toverride blur(): void {\n\t\tthis.#focused()?.blur();\n\t}\n\n\tconnectedCallback(): void {\n\t\tthis.upgradeProperties('disabled');\n\t\tif (this.#connection) return;\n\n\t\tconst connection = new AbortController();\n\n\t\tthis.#connection = connection;\n\t\ttry {\n\t\t\tthis.connect(connection.signal);\n\t\t} catch (error) {\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t\tthrow error;\n\t\t}\n\n\t\tconst { boxProbe: box, control, sheet } = this;\n\n\t\tif (__DEV__ && control && sheet !== undefined) checkStyles(this, { box, control, sheet });\n\t}\n\n\tconnectedMoveCallback(): void {\n\t\t// Defined so `moveBefore` from a persisting router keeps every binding\n\t}\n\n\t// Without `moveBefore` (Safari), a persisting router disconnects and reconnects in one task\n\tdisconnectedCallback(): void {\n\t\tconst connection = this.#connection;\n\t\tif (!connection) return;\n\n\t\tqueueMicrotask(() => {\n\t\t\tif (this.isConnected || this.#connection !== connection) return;\n\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t});\n\t}\n\n\toverride focus(options?: FocusOptions): void {\n\t\tconst target = this.focusTarget();\n\t\tif (!target) {\n\t\t\tsuper.focus(options);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.isDisabled() && !this.#focused()) target.focus(options);\n\t}\n\n\tprotected abstract connect(signal: AbortSignal): void;\n\n\tprotected focusTarget(): HTMLElement | undefined {\n\t\treturn undefined;\n\t}\n\n\tprotected forwardNaming(target: Element, isNamed: boolean): void {\n\t\tconst labels = isNamed ? this.namingLabels() : [];\n\n\t\tfor (const name of namingAttributes) {\n\t\t\t// `ariaLabelledByElements` holds the attribute, empty\n\t\t\tif (name === 'aria-labelledby' && labels.length > 0) continue;\n\n\t\t\tconst value = this.getAttribute(name);\n\n\t\t\twriteAttribute(target, name, isNamed && value !== null ? value : undefined);\n\t\t}\n\t\tif (labels.length > 0) this.#writeLabels(target, labels);\n\t}\n\n\tprotected hasState(state: string): boolean {\n\t\treturn this.#states.has(state);\n\t}\n\n\t// happy-dom matches `:disabled` on no custom element, and an element not form-associated never matches\n\tprotected isBound(): boolean {\n\t\treturn this.#connection !== undefined;\n\t}\n\n\tprotected isDisabled(): boolean {\n\t\treturn this.disabled || this.matches(':disabled');\n\t}\n\n\t// A morph against server HTML, or a stray `innerHTML`, deletes the control\n\tprotected keepControl(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\t\tif (!control) return;\n\n\t\tconst keep = (): void => {\n\t\t\tappendOnce(this, control);\n\t\t\tif (__DEV__)\n\t\t\t\tcheckChildren(\n\t\t\t\t\tthis,\n\t\t\t\t\t[...this.childNodes].filter((child) => child !== control && !this.usesChild(child)),\n\t\t\t\t);\n\t\t};\n\t\tconst observer = new MutationObserver(keep);\n\n\t\tkeep();\n\t\tobserver.observe(this, { childList: true });\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected namingLabels(): Array<Element> {\n\t\treturn [];\n\t}\n\n\tprotected numberAttribute(name: string, fallback: number): number {\n\t\tconst parsed = toNumber(this.getAttribute(name));\n\n\t\treturn Number.isFinite(parsed) ? parsed : fallback;\n\t}\n\n\tprotected optionalNumberAttribute(name: string): number | undefined {\n\t\tconst value = this.numberAttribute(name, NaN);\n\n\t\treturn Number.isNaN(value) ? undefined : value;\n\t}\n\n\t// Never compared; a host attribute set again resets the value a gesture left dirty\n\tprotected reflect(name: string, value: boolean | null | number | string | undefined): void {\n\t\tif (typeof value !== 'number' && typeof value !== 'string' && value !== true) {\n\t\t\tthis.removeAttribute(name);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setAttribute(name, value === true ? '' : value.toString());\n\t}\n\n\tprotected stateTarget(): Element | undefined {\n\t\treturn this.control;\n\t}\n\n\tprotected toggleState(state: string, isOn: boolean): void {\n\t\tif (isOn) this.#states.add(state);\n\t\telse this.#states.delete(state);\n\t\tthis.stateTarget()?.toggleAttribute(`data-sonic-${state}`, isOn);\n\t}\n\n\t// A property set before upgrade shadows its accessor\n\tprotected upgradeProperties(...names: Array<keyof this>): void {\n\t\tfor (const name of names) {\n\t\t\tif (!Object.hasOwn(this, name)) continue;\n\n\t\t\tconst value = this[name];\n\n\t\t\tReflect.deleteProperty(this, name);\n\t\t\tthis[name] = value;\n\t\t}\n\t}\n\n\tprotected usesChild(_child: Node): boolean {\n\t\treturn false;\n\t}\n\n\t#focused(): HTMLElement | undefined {\n\t\tconst root = this.getRootNode();\n\t\tconst active =\n\t\t\troot instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;\n\n\t\treturn active instanceof HTMLElement && this.contains(active) ? active : undefined;\n\t}\n\n\t#writeLabels(target: Element, labels: Array<Element>): void {\n\t\tif (canReferElements) {\n\t\t\twriteLabelledBy(target, labels);\n\t\t\treturn;\n\t\t}\n\n\t\tconst ids = labels.map((label) => SonicElement.#labelId(label));\n\n\t\twriteAttribute(target, 'aria-labelledby', ids.join(' '));\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sonic-element.js","names":[],"sources":["../../../src/elements/sonic-element.ts"],"sourcesContent":["import type { BoxProbe } from '#lib/check-styles.ts';\n\nimport { checkStyles } from '#lib/check-styles.ts';\nimport { toNumber } from '#lib/math.ts';\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare const __DEV__: boolean;\n\nconst namingAttributes = ['aria-describedby', 'aria-label', 'aria-labelledby'];\n\nconst canReferElements = 'ariaLabelledByElements' in Element.prototype;\n\nfunction writeLabelledBy(target: Element, labels: Array<Element>): void {\n\tconst current = target.ariaLabelledByElements;\n\tif (\n\t\tcurrent?.length === labels.length &&\n\t\tlabels.every((label, index) => label === current[index])\n\t) {\n\t\treturn;\n\t}\n\n\ttarget.ariaLabelledByElements = labels;\n}\n\n// eslint-disable-next-line wc/define-tag-after-class-definition -- abstract; only subclasses are defined\nexport abstract class SonicElement extends HTMLElement {\n\tstatic readonly observedAttributes = [...namingAttributes, 'disabled'];\n\n\tstatic #labelCount = 0;\n\n\tget disabled(): boolean {\n\t\treturn this.hasAttribute('disabled');\n\t}\n\n\tset disabled(isDisabled: boolean) {\n\t\tthis.reflect('disabled', isDisabled);\n\t}\n\n\tdeclare protected readonly boxProbe: BoxProbe | undefined;\n\n\tdeclare protected readonly control: HTMLElement | undefined;\n\n\t// happy-dom has no `ElementInternals`\n\tprotected readonly internals =\n\t\t'attachInternals' in HTMLElement.prototype ? this.attachInternals() : undefined;\n\n\tdeclare protected readonly sheet: string | undefined;\n\n\tdeclare protected readonly sheetToken: `--_sonic-${string}` | undefined;\n\n\t#connection: AbortController | undefined;\n\n\treadonly #states = new Set<string>();\n\n\tstatic #labelId(label: Element): string {\n\t\tif (label.id === '') {\n\t\t\tSonicElement.#labelCount += 1;\n\t\t\tlabel.id = `sonic-label-${String(SonicElement.#labelCount)}`;\n\t\t}\n\n\t\treturn label.id;\n\t}\n\n\toverride blur(): void {\n\t\tthis.#focused()?.blur();\n\t}\n\n\tconnectedCallback(): void {\n\t\tthis.upgradeProperties('disabled');\n\t\tif (this.#connection) return;\n\n\t\tconst connection = new AbortController();\n\n\t\tthis.#connection = connection;\n\t\ttry {\n\t\t\tthis.connect(connection.signal);\n\t\t} catch (error) {\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t\tthrow error;\n\t\t}\n\n\t\tconst { boxProbe: box, control, sheet, sheetToken: token } = this;\n\n\t\tif (__DEV__ && control && sheet !== undefined)\n\t\t\tcheckStyles(this, { box, control, sheet, token });\n\t}\n\n\tconnectedMoveCallback(): void {\n\t\t// Defined so `moveBefore` from a persisting router keeps every binding\n\t}\n\n\t// Without `moveBefore` (Safari), a persisting router disconnects and reconnects in one task\n\tdisconnectedCallback(): void {\n\t\tconst connection = this.#connection;\n\t\tif (!connection) return;\n\n\t\tqueueMicrotask(() => {\n\t\t\tif (this.isConnected || this.#connection !== connection) return;\n\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t});\n\t}\n\n\toverride focus(options?: FocusOptions): void {\n\t\tconst target = this.focusTarget();\n\t\tif (!target) {\n\t\t\tsuper.focus(options);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.isDisabled() && !this.#focused()) target.focus(options);\n\t}\n\n\tprotected abstract connect(signal: AbortSignal): void;\n\n\tprotected focusTarget(): HTMLElement | undefined {\n\t\treturn undefined;\n\t}\n\n\tprotected forwardNaming(target: Element, isNamed: boolean): void {\n\t\tconst labels = isNamed ? this.namingLabels() : [];\n\n\t\tfor (const name of namingAttributes) {\n\t\t\t// `ariaLabelledByElements` holds the attribute, empty\n\t\t\tif (name === 'aria-labelledby' && labels.length > 0) continue;\n\n\t\t\tconst value = this.getAttribute(name);\n\n\t\t\twriteAttribute(target, name, isNamed && value !== null ? value : undefined);\n\t\t}\n\t\tif (labels.length > 0) this.#writeLabels(target, labels);\n\t}\n\n\tprotected hasState(state: string): boolean {\n\t\treturn this.#states.has(state);\n\t}\n\n\t// happy-dom matches `:disabled` on no custom element, and an element not form-associated never matches\n\tprotected isBound(): boolean {\n\t\treturn this.#connection !== undefined;\n\t}\n\n\tprotected isDisabled(): boolean {\n\t\treturn this.disabled || this.matches(':disabled');\n\t}\n\n\t// A morph against server HTML, or a stray `innerHTML`, deletes the control\n\tprotected keepControl(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\t\tif (!control) return;\n\n\t\tconst keep = (): void => {\n\t\t\tappendOnce(this, control);\n\t\t\tif (__DEV__)\n\t\t\t\tcheckChildren(\n\t\t\t\t\tthis,\n\t\t\t\t\t[...this.childNodes].filter((child) => child !== control && !this.usesChild(child)),\n\t\t\t\t);\n\t\t};\n\t\tconst observer = new MutationObserver(keep);\n\n\t\tkeep();\n\t\tobserver.observe(this, { childList: true });\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected namingLabels(): Array<Element> {\n\t\treturn [];\n\t}\n\n\tprotected numberAttribute(name: string, fallback: number): number {\n\t\tconst parsed = toNumber(this.getAttribute(name));\n\n\t\treturn Number.isFinite(parsed) ? parsed : fallback;\n\t}\n\n\tprotected optionalNumberAttribute(name: string): number | undefined {\n\t\tconst value = this.numberAttribute(name, NaN);\n\n\t\treturn Number.isNaN(value) ? undefined : value;\n\t}\n\n\t// Never compared; a host attribute set again resets the value a gesture left dirty\n\tprotected reflect(name: string, value: boolean | null | number | string | undefined): void {\n\t\tif (typeof value !== 'number' && typeof value !== 'string' && value !== true) {\n\t\t\tthis.removeAttribute(name);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setAttribute(name, value === true ? '' : value.toString());\n\t}\n\n\tprotected stateTarget(): Element | undefined {\n\t\treturn this.control;\n\t}\n\n\tprotected toggleState(state: string, isOn: boolean): void {\n\t\tif (isOn) this.#states.add(state);\n\t\telse this.#states.delete(state);\n\t\tthis.stateTarget()?.toggleAttribute(`data-sonic-${state}`, isOn);\n\t}\n\n\t// A property set before upgrade shadows its accessor\n\tprotected upgradeProperties(...names: Array<keyof this>): void {\n\t\tfor (const name of names) {\n\t\t\tif (!Object.hasOwn(this, name)) continue;\n\n\t\t\tconst value = this[name];\n\n\t\t\tReflect.deleteProperty(this, name);\n\t\t\tthis[name] = value;\n\t\t}\n\t}\n\n\tprotected usesChild(_child: Node): boolean {\n\t\treturn false;\n\t}\n\n\t#focused(): HTMLElement | undefined {\n\t\tconst root = this.getRootNode();\n\t\tconst active =\n\t\t\troot instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;\n\n\t\treturn active instanceof HTMLElement && this.contains(active) ? active : undefined;\n\t}\n\n\t#writeLabels(target: Element, labels: Array<Element>): void {\n\t\tif (canReferElements) {\n\t\t\twriteLabelledBy(target, labels);\n\t\t\treturn;\n\t\t}\n\n\t\tconst ids = labels.map((label) => SonicElement.#labelId(label));\n\n\t\twriteAttribute(target, 'aria-labelledby', ids.join(' '));\n\t}\n}\n"],"mappings":"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"}