@xsynaptic/sonic-ui 0.24.0 → 0.25.1

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 (111) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +5 -3
  3. package/dist/default/define/region.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/envelope.js +11 -11
  6. package/dist/default/elements/form-element.js +10 -0
  7. package/dist/default/elements/readout.js +3 -2
  8. package/dist/default/elements/region-layer.js +251 -0
  9. package/dist/default/elements/region.js +157 -0
  10. package/dist/default/elements/sonic-element.js +26 -4
  11. package/dist/default/elements/split.js +2 -2
  12. package/dist/default/elements/toggle.js +2 -2
  13. package/dist/default/elements/value-element.js +9 -5
  14. package/dist/default/elements/value-gestures.js +7 -5
  15. package/dist/default/elements/wave-element.js +12 -3
  16. package/dist/default/elements/waveform.js +38 -2
  17. package/dist/default/elements/wavestrip.js +90 -17
  18. package/dist/default/elements/xy.js +3 -11
  19. package/dist/default/index.js +2 -1
  20. package/dist/default/lib/clock.js +1 -0
  21. package/dist/default/lib/drag-step.js +1 -1
  22. package/dist/default/lib/marker-band.js +40 -29
  23. package/dist/default/lib/region-drag.js +48 -0
  24. package/dist/default/lib/spoken-duration.js +3 -0
  25. package/dist/dev/define/region.d.ts +2 -0
  26. package/dist/dev/define/region.js +8 -0
  27. package/dist/dev/define/region.js.map +1 -0
  28. package/dist/dev/define.d.ts +3 -1
  29. package/dist/dev/define.js +1 -0
  30. package/dist/dev/elements/envelope.js +11 -11
  31. package/dist/dev/elements/envelope.js.map +1 -1
  32. package/dist/dev/elements/form-element.d.ts +1 -0
  33. package/dist/dev/elements/form-element.js +10 -0
  34. package/dist/dev/elements/form-element.js.map +1 -1
  35. package/dist/dev/elements/readout.js +3 -2
  36. package/dist/dev/elements/readout.js.map +1 -1
  37. package/dist/dev/elements/region-layer.js +252 -0
  38. package/dist/dev/elements/region-layer.js.map +1 -0
  39. package/dist/dev/elements/region.d.ts +29 -0
  40. package/dist/dev/elements/region.js +165 -0
  41. package/dist/dev/elements/region.js.map +1 -0
  42. package/dist/dev/elements/sonic-element.d.ts +1 -0
  43. package/dist/dev/elements/sonic-element.js +27 -5
  44. package/dist/dev/elements/sonic-element.js.map +1 -1
  45. package/dist/dev/elements/split.js +2 -2
  46. package/dist/dev/elements/split.js.map +1 -1
  47. package/dist/dev/elements/toggle.js +2 -2
  48. package/dist/dev/elements/toggle.js.map +1 -1
  49. package/dist/dev/elements/value-element.d.ts +3 -1
  50. package/dist/dev/elements/value-element.js +9 -5
  51. package/dist/dev/elements/value-element.js.map +1 -1
  52. package/dist/dev/elements/value-gestures.js +7 -5
  53. package/dist/dev/elements/value-gestures.js.map +1 -1
  54. package/dist/dev/elements/wave-element.d.ts +7 -1
  55. package/dist/dev/elements/wave-element.js +12 -3
  56. package/dist/dev/elements/wave-element.js.map +1 -1
  57. package/dist/dev/elements/waveform.d.ts +7 -0
  58. package/dist/dev/elements/waveform.js +38 -2
  59. package/dist/dev/elements/waveform.js.map +1 -1
  60. package/dist/dev/elements/wavestrip.d.ts +17 -1
  61. package/dist/dev/elements/wavestrip.js +90 -17
  62. package/dist/dev/elements/wavestrip.js.map +1 -1
  63. package/dist/dev/elements/xy.js +3 -11
  64. package/dist/dev/elements/xy.js.map +1 -1
  65. package/dist/dev/index.d.ts +3 -2
  66. package/dist/dev/index.js +2 -1
  67. package/dist/dev/lib/clock.js +1 -0
  68. package/dist/dev/lib/clock.js.map +1 -1
  69. package/dist/dev/lib/drag-step.js +1 -1
  70. package/dist/dev/lib/drag-step.js.map +1 -1
  71. package/dist/dev/lib/marker-band.d.ts +7 -0
  72. package/dist/dev/lib/marker-band.js +40 -29
  73. package/dist/dev/lib/marker-band.js.map +1 -1
  74. package/dist/dev/lib/region-drag.js +49 -0
  75. package/dist/dev/lib/region-drag.js.map +1 -0
  76. package/dist/dev/lib/spoken-duration.js +3 -0
  77. package/dist/dev/lib/spoken-duration.js.map +1 -1
  78. package/dist/styles/button.css +42 -15
  79. package/dist/styles/controls.css +1 -0
  80. package/dist/styles/dial.css +48 -33
  81. package/dist/styles/envelope.css +7 -6
  82. package/dist/styles/led.css +28 -13
  83. package/dist/styles/material/arc.css +2 -2
  84. package/dist/styles/material/cap.css +25 -15
  85. package/dist/styles/material/core.css +53 -1
  86. package/dist/styles/material/glass.css +12 -2
  87. package/dist/styles/material/groove.css +3 -3
  88. package/dist/styles/material/keycap.css +5 -5
  89. package/dist/styles/material/lens.css +1 -1
  90. package/dist/styles/material/pane.css +21 -11
  91. package/dist/styles/material/readout.css +23 -10
  92. package/dist/styles/material/{bracket.css → reticle.css} +15 -8
  93. package/dist/styles/material/scale.css +4 -4
  94. package/dist/styles/material/value.css +12 -3
  95. package/dist/styles/material/well.css +2 -2
  96. package/dist/styles/material.css +1 -1
  97. package/dist/styles/meter.css +24 -11
  98. package/dist/styles/number.css +2 -1
  99. package/dist/styles/panel.css +5 -6
  100. package/dist/styles/region.css +88 -0
  101. package/dist/styles/ring.css +5 -4
  102. package/dist/styles/screen.css +7 -5
  103. package/dist/styles/segmented.css +8 -7
  104. package/dist/styles/slider.css +40 -32
  105. package/dist/styles/spectrum.css +9 -1
  106. package/dist/styles/switch.css +52 -53
  107. package/dist/styles/toggle.css +33 -38
  108. package/dist/styles/waveform.css +5 -4
  109. package/dist/styles/wavestrip.css +15 -12
  110. package/dist/styles/xy.css +19 -19
  111. package/package.json +3 -2
@@ -0,0 +1,252 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+ import { placeChildren, template } from "../lib/render.js";
3
+ import { capturePointer } from "../lib/pointer-drag.js";
4
+ import { dragThresholdPx } from "../lib/drag-step.js";
5
+ import { focusByPointer } from "../lib/focus-by-pointer.js";
6
+ import { SonicRegion, linkRegion, ownRegions } from "./region.js";
7
+ import { writeKind } from "../lib/marker-band.js";
8
+ import { moveRegionTo, regionAt, regionTravel, startRegionDrag, stepRegionDrag } from "../lib/region-drag.js";
9
+
10
+ //#region src/elements/region-layer.ts
11
+ const renderPart = template(`
12
+ <div class="sonic-region" role="slider" aria-orientation="horizontal">
13
+ <div class="sonic-region-bracket"></div>
14
+ </div>
15
+ `, HTMLDivElement);
16
+ function hasMoved(from, to) {
17
+ return from.start !== to.start || from.end !== to.end;
18
+ }
19
+ function clippedEnds(region, [low, high]) {
20
+ const ends = [...Math.min(region.start, region.end) < low ? ["start"] : [], ...Math.max(region.start, region.end) > high ? ["end"] : []];
21
+ return ends.length > 0 ? ends.join(" ") : void 0;
22
+ }
23
+ function writeProperty(part, name, value) {
24
+ if (part.style.getPropertyValue(name) !== value) part.style.setProperty(name, value);
25
+ }
26
+ var RegionLayer = class {
27
+ #drawn = [];
28
+ #held;
29
+ #host;
30
+ #key;
31
+ #keyScrub;
32
+ constructor(host, signal) {
33
+ const render = () => {
34
+ this.render();
35
+ };
36
+ const observer = new MutationObserver(render);
37
+ this.#host = host;
38
+ this.#bindPointer(signal);
39
+ this.#bindKeys(signal);
40
+ observer.observe(host.element, { childList: true });
41
+ ownRegions(host.element, (region) => {
42
+ this.#redraw(region);
43
+ }, signal);
44
+ signal.addEventListener("abort", () => {
45
+ observer.disconnect();
46
+ this.#release();
47
+ this.#endKeyScrub();
48
+ }, { once: true });
49
+ }
50
+ abandon() {
51
+ this.#release(true);
52
+ }
53
+ follow() {
54
+ if (this.#stripKey() !== this.#key) this.render();
55
+ }
56
+ isHeld() {
57
+ return this.#held !== void 0;
58
+ }
59
+ render() {
60
+ const host = this.#host;
61
+ const drawn = [...host.element.children].filter((child) => child instanceof SonicRegion).map((region) => this.#draw(region));
62
+ this.#drawn = drawn;
63
+ this.#key = this.#stripKey();
64
+ placeChildren(host.layer, drawn.map(({ part }) => part));
65
+ this.#dropGone();
66
+ }
67
+ take(event) {
68
+ const host = this.#host;
69
+ const { startPx, travelPx } = host.axis();
70
+ const mapping = host.mapping();
71
+ const index = regionAt(this.#drawn.map((entry) => {
72
+ if (!this.#isEnabled(entry)) return;
73
+ const { end, start } = entry.link.span();
74
+ return [mapping.proportionOf(start) * travelPx, mapping.proportionOf(end) * travelPx];
75
+ }), event.clientX - startPx);
76
+ const pressed = index === void 0 ? void 0 : this.#drawn[index];
77
+ if (!pressed) return false;
78
+ this.#endKeyScrub();
79
+ const from = pressed.link.span();
80
+ this.#held = {
81
+ drawn: pressed,
82
+ from,
83
+ outside: host.outside(),
84
+ pointerId: event.pointerId,
85
+ press: event,
86
+ state: startRegionDrag({
87
+ from,
88
+ position: event.clientX,
89
+ thresholdPx: dragThresholdPx(event.pointerType),
90
+ widthPx: travelPx
91
+ })
92
+ };
93
+ pressed.link.hold("pointer");
94
+ capturePointer(host.control, event.pointerId);
95
+ return true;
96
+ }
97
+ #bindKeys(signal) {
98
+ const { layer } = this.#host;
99
+ layer.addEventListener("keydown", (event) => {
100
+ const entry = this.#drawn.find(({ part }) => part === event.target);
101
+ if (!entry || event.defaultPrevented || this.#held) return;
102
+ if (this.#isEnabled(entry)) this.#keyTo(entry, event);
103
+ }, { signal });
104
+ layer.addEventListener("keyup", (event) => {
105
+ if (event.key === "Meta" || event.key === this.#keyScrub?.key) this.#endKeyScrub();
106
+ }, { signal });
107
+ layer.addEventListener("focusin", () => {
108
+ this.render();
109
+ }, { signal });
110
+ layer.addEventListener("focusout", () => {
111
+ this.#endKeyScrub();
112
+ }, { signal });
113
+ }
114
+ #bindPointer(signal) {
115
+ const whileHeld = (type, handle) => {
116
+ this.#host.control.addEventListener(type, (event) => {
117
+ const held = this.#held;
118
+ if (held && event instanceof PointerEvent && held.pointerId === event.pointerId) handle(held, event);
119
+ }, { signal });
120
+ };
121
+ whileHeld("pointermove", (hold, event) => {
122
+ this.#dragTo(hold, event);
123
+ });
124
+ whileHeld("pointerup", (hold) => {
125
+ const isPress = !hold.state.isEngaged;
126
+ this.#release();
127
+ if (isPress) this.#host.seek(hold.press);
128
+ });
129
+ whileHeld("pointercancel", () => {
130
+ this.#release(true);
131
+ });
132
+ whileHeld("lostpointercapture", () => {
133
+ this.#release();
134
+ });
135
+ }
136
+ #dragTo(hold, event) {
137
+ const wasEngaged = hold.state.isEngaged;
138
+ const { span, state } = stepRegionDrag(this.#host.mapping(), hold.state, {
139
+ isFine: event.shiftKey,
140
+ position: event.clientX
141
+ });
142
+ hold.state = state;
143
+ if (!span) return;
144
+ if (!wasEngaged) focusByPointer(hold.drawn.part);
145
+ hold.drawn.link.input(hold.outside?.(event) === true ? hold.from : span);
146
+ }
147
+ #draw(region) {
148
+ const host = this.#host;
149
+ const known = this.#drawn.find((entry) => entry.region === region);
150
+ const entry = known ?? {
151
+ kind: void 0,
152
+ link: linkRegion(region),
153
+ part: renderPart(),
154
+ region
155
+ };
156
+ const { link, part } = entry;
157
+ const mapping = host.mapping();
158
+ const span = link.span();
159
+ const [low, high] = regionTravel(span, mapping.bounds);
160
+ const canMove = this.#isEnabled(entry);
161
+ writeProperty(part, "--_sonic-region-from", String(mapping.proportionOf(span.start)));
162
+ writeProperty(part, "--_sonic-region-to", String(mapping.proportionOf(span.end)));
163
+ if (!known || entry.kind !== region.kind) {
164
+ entry.kind = region.kind;
165
+ writeKind(part, region.kind, "--_sonic-ink");
166
+ }
167
+ writeAttribute(part, "data-sonic-clipped", clippedEnds(region, mapping.bounds));
168
+ writeAttribute(part, "aria-valuemin", String(low));
169
+ writeAttribute(part, "aria-valuemax", String(high));
170
+ writeAttribute(part, "aria-valuenow", String(span.start));
171
+ writeAttribute(part, "aria-valuetext", host.speak(span));
172
+ writeAttribute(part, "aria-disabled", canMove ? void 0 : "true");
173
+ writeAttribute(part, "tabindex", canMove ? "0" : void 0);
174
+ link.draw(part);
175
+ return entry;
176
+ }
177
+ #dropGone() {
178
+ const isGone = (entry) => !this.#drawn.includes(entry) || !this.#isEnabled(entry);
179
+ if (this.#held && isGone(this.#held.drawn)) this.#release();
180
+ if (this.#keyScrub && isGone(this.#keyScrub.drawn)) this.#endKeyScrub();
181
+ }
182
+ #endKeyScrub() {
183
+ const scrub = this.#keyScrub;
184
+ if (!scrub) return;
185
+ const { link } = scrub.drawn;
186
+ this.#keyScrub = void 0;
187
+ link.hold(void 0);
188
+ if (hasMoved(scrub.from, link.span())) link.change();
189
+ }
190
+ #isEnabled({ link }) {
191
+ return !this.#host.isDisabled() && !link.isDisabled();
192
+ }
193
+ #keyTo(entry, event) {
194
+ const mapping = this.#host.mapping();
195
+ const from = entry.link.span();
196
+ const start = mapping.keyTarget(event.key, from.start);
197
+ if (start === void 0) return;
198
+ event.preventDefault();
199
+ const next = moveRegionTo(mapping, from, start);
200
+ if (!event.repeat) {
201
+ this.#endKeyScrub();
202
+ if (entry.link.input(next)) entry.link.change();
203
+ return;
204
+ }
205
+ if (this.#keyScrub?.drawn !== entry || this.#keyScrub.key !== event.key) {
206
+ this.#endKeyScrub();
207
+ this.#keyScrub = {
208
+ drawn: entry,
209
+ from,
210
+ key: event.key
211
+ };
212
+ entry.link.hold("keys");
213
+ }
214
+ entry.link.input(next);
215
+ }
216
+ #redraw(region) {
217
+ if (this.#drawn.every((entry) => entry.region !== region)) {
218
+ this.render();
219
+ return;
220
+ }
221
+ this.#draw(region);
222
+ this.#dropGone();
223
+ }
224
+ #release(isRestored = false) {
225
+ const hold = this.#held;
226
+ if (!hold) return;
227
+ const { link } = hold.drawn;
228
+ this.#held = void 0;
229
+ if (isRestored) link.input(hold.from);
230
+ link.hold(void 0);
231
+ if (hasMoved(hold.from, link.span())) link.change();
232
+ }
233
+ #stripKey() {
234
+ const host = this.#host;
235
+ const mapping = host.mapping();
236
+ const [low, high] = mapping.bounds;
237
+ return [
238
+ low,
239
+ high,
240
+ mapping.proportionOf((low + high) / 2),
241
+ host.isDisabled(),
242
+ host.speak({
243
+ end: high,
244
+ start: high
245
+ })
246
+ ].join("|");
247
+ }
248
+ };
249
+
250
+ //#endregion
251
+ export { RegionLayer };
252
+ //# sourceMappingURL=region-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-layer.js","names":[],"sources":["../../../src/elements/region-layer.ts"],"sourcesContent":["import type { RegionLink } from '#elements/region.ts';\nimport type { RegionDragState, RegionSpan } from '#lib/region-drag.ts';\nimport type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { linkRegion, ownRegions, SonicRegion } from '#elements/region.ts';\nimport { dragThresholdPx } from '#lib/drag-step.ts';\nimport { focusByPointer } from '#lib/focus-by-pointer.ts';\nimport { writeKind } from '#lib/marker-band.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport {\n\tmoveRegionTo,\n\tregionAt,\n\tregionTravel,\n\tstartRegionDrag,\n\tstepRegionDrag,\n} from '#lib/region-drag.ts';\nimport { placeChildren, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ninterface LayerHost {\n\taxis: () => { startPx: number; travelPx: number };\n\tcontrol: HTMLElement;\n\telement: HTMLElement;\n\tisDisabled: () => boolean;\n\tlayer: HTMLElement;\n\tmapping: () => ValueMapping;\n\toutside: () => ((event: PointerEvent) => boolean) | undefined;\n\tseek: (event: PointerEvent) => void;\n\tspeak: (span: RegionSpan) => string;\n}\n\ninterface Drawn {\n\tkind: string | undefined;\n\tlink: RegionLink;\n\tpart: HTMLElement;\n\tregion: SonicRegion;\n}\n\ninterface KeyScrub {\n\tdrawn: Drawn;\n\tfrom: RegionSpan;\n\tkey: string;\n}\n\ninterface PointerHold {\n\tdrawn: Drawn;\n\tfrom: RegionSpan;\n\toutside: ((event: PointerEvent) => boolean) | undefined;\n\tpointerId: number;\n\tpress: PointerEvent;\n\tstate: RegionDragState;\n}\n\nconst renderPart = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-region\" role=\"slider\" aria-orientation=\"horizontal\">\n\t\t\t<div class=\"sonic-region-bracket\"></div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction hasMoved(from: RegionSpan, to: RegionSpan): boolean {\n\treturn from.start !== to.start || from.end !== to.end;\n}\n\nfunction clippedEnds(region: SonicRegion, [low, high]: [number, number]): string | undefined {\n\tconst ends = [\n\t\t...(Math.min(region.start, region.end) < low ? ['start'] : []),\n\t\t...(Math.max(region.start, region.end) > high ? ['end'] : []),\n\t];\n\n\treturn ends.length > 0 ? ends.join(' ') : undefined;\n}\n\nfunction writeProperty(part: HTMLElement, name: string, value: string): void {\n\tif (part.style.getPropertyValue(name) !== value) part.style.setProperty(name, value);\n}\n\nexport class RegionLayer {\n\t#drawn: Array<Drawn> = [];\n\n\t#held: PointerHold | undefined;\n\n\treadonly #host: LayerHost;\n\n\t#key: string | undefined;\n\n\t#keyScrub: KeyScrub | undefined;\n\n\tconstructor(host: LayerHost, signal: AbortSignal) {\n\t\tconst render = (): void => {\n\t\t\tthis.render();\n\t\t};\n\t\tconst observer = new MutationObserver(render);\n\n\t\tthis.#host = host;\n\t\tthis.#bindPointer(signal);\n\t\tthis.#bindKeys(signal);\n\t\tobserver.observe(host.element, { childList: true });\n\t\townRegions(\n\t\t\thost.element,\n\t\t\t(region) => {\n\t\t\t\tthis.#redraw(region);\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t\tthis.#release();\n\t\t\t\tthis.#endKeyScrub();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tabandon(): void {\n\t\tthis.#release(true);\n\t}\n\n\tfollow(): void {\n\t\tif (this.#stripKey() !== this.#key) this.render();\n\t}\n\n\tisHeld(): boolean {\n\t\treturn this.#held !== undefined;\n\t}\n\n\trender(): void {\n\t\tconst host = this.#host;\n\t\tconst regions = [...host.element.children].filter((child) => child instanceof SonicRegion);\n\t\tconst drawn = regions.map((region) => this.#draw(region));\n\n\t\tthis.#drawn = drawn;\n\t\tthis.#key = this.#stripKey();\n\t\tplaceChildren(\n\t\t\thost.layer,\n\t\t\tdrawn.map(({ part }) => part),\n\t\t);\n\t\tthis.#dropGone();\n\t}\n\n\ttake(event: PointerEvent): boolean {\n\t\tconst host = this.#host;\n\t\tconst { startPx, travelPx } = host.axis();\n\t\tconst mapping = host.mapping();\n\t\tconst index = regionAt(\n\t\t\tthis.#drawn.map((entry) => {\n\t\t\t\tif (!this.#isEnabled(entry)) return;\n\n\t\t\t\tconst { end, start } = entry.link.span();\n\n\t\t\t\treturn [mapping.proportionOf(start) * travelPx, mapping.proportionOf(end) * travelPx];\n\t\t\t}),\n\t\t\tevent.clientX - startPx,\n\t\t);\n\t\tconst pressed = index === undefined ? undefined : this.#drawn[index];\n\t\tif (!pressed) return false;\n\n\t\tthis.#endKeyScrub();\n\n\t\tconst from = pressed.link.span();\n\n\t\tthis.#held = {\n\t\t\tdrawn: pressed,\n\t\t\tfrom,\n\t\t\toutside: host.outside(),\n\t\t\tpointerId: event.pointerId,\n\t\t\tpress: event,\n\t\t\tstate: startRegionDrag({\n\t\t\t\tfrom,\n\t\t\t\tposition: event.clientX,\n\t\t\t\tthresholdPx: dragThresholdPx(event.pointerType),\n\t\t\t\twidthPx: travelPx,\n\t\t\t}),\n\t\t};\n\t\tpressed.link.hold('pointer');\n\t\tcapturePointer(host.control, event.pointerId);\n\n\t\treturn true;\n\t}\n\n\t#bindKeys(signal: AbortSignal): void {\n\t\tconst { layer } = this.#host;\n\n\t\tlayer.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tconst entry = this.#drawn.find(({ part }) => part === event.target);\n\t\t\t\tif (!entry || event.defaultPrevented || this.#held) return;\n\n\t\t\t\tif (this.#isEnabled(entry)) this.#keyTo(entry, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tlayer.addEventListener(\n\t\t\t'keyup',\n\t\t\t(event) => {\n\t\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\t\tif (event.key === 'Meta' || event.key === this.#keyScrub?.key) this.#endKeyScrub();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// No event fires when a label arrives, so focus picks it up\n\t\tlayer.addEventListener(\n\t\t\t'focusin',\n\t\t\t() => {\n\t\t\t\tthis.render();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tlayer.addEventListener(\n\t\t\t'focusout',\n\t\t\t() => {\n\t\t\t\tthis.#endKeyScrub();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#bindPointer(signal: AbortSignal): void {\n\t\tconst whileHeld = (\n\t\t\ttype: string,\n\t\t\thandle: (hold: PointerHold, event: PointerEvent) => void,\n\t\t): void => {\n\t\t\tthis.#host.control.addEventListener(\n\t\t\t\ttype,\n\t\t\t\t(event) => {\n\t\t\t\t\tconst held = this.#held;\n\n\t\t\t\t\tif (held && event instanceof PointerEvent && held.pointerId === event.pointerId) {\n\t\t\t\t\t\thandle(held, event);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t};\n\n\t\twhileHeld('pointermove', (hold, event) => {\n\t\t\tthis.#dragTo(hold, event);\n\t\t});\n\t\twhileHeld('pointerup', (hold) => {\n\t\t\tconst isPress = !hold.state.isEngaged;\n\n\t\t\tthis.#release();\n\t\t\tif (isPress) this.#host.seek(hold.press);\n\t\t});\n\t\twhileHeld('pointercancel', () => {\n\t\t\tthis.#release(true);\n\t\t});\n\t\twhileHeld('lostpointercapture', () => {\n\t\t\tthis.#release();\n\t\t});\n\t}\n\n\t#dragTo(hold: PointerHold, event: PointerEvent): void {\n\t\tconst wasEngaged = hold.state.isEngaged;\n\t\tconst { span, state } = stepRegionDrag(this.#host.mapping(), hold.state, {\n\t\t\tisFine: event.shiftKey,\n\t\t\tposition: event.clientX,\n\t\t});\n\n\t\thold.state = state;\n\t\tif (!span) return;\n\n\t\tif (!wasEngaged) focusByPointer(hold.drawn.part);\n\t\thold.drawn.link.input(hold.outside?.(event) === true ? hold.from : span);\n\t}\n\n\t#draw(region: SonicRegion): Drawn {\n\t\tconst host = this.#host;\n\t\tconst known = this.#drawn.find((entry) => entry.region === region);\n\t\tconst entry = known ?? {\n\t\t\tkind: undefined,\n\t\t\tlink: linkRegion(region),\n\t\t\tpart: renderPart(),\n\t\t\tregion,\n\t\t};\n\t\tconst { link, part } = entry;\n\t\tconst mapping = host.mapping();\n\t\tconst span = link.span();\n\t\tconst [low, high] = regionTravel(span, mapping.bounds);\n\t\tconst canMove = this.#isEnabled(entry);\n\n\t\twriteProperty(part, '--_sonic-region-from', String(mapping.proportionOf(span.start)));\n\t\twriteProperty(part, '--_sonic-region-to', String(mapping.proportionOf(span.end)));\n\t\tif (!known || entry.kind !== region.kind) {\n\t\t\tentry.kind = region.kind;\n\t\t\twriteKind(part, region.kind, '--_sonic-ink');\n\t\t}\n\t\twriteAttribute(part, 'data-sonic-clipped', clippedEnds(region, mapping.bounds));\n\t\twriteAttribute(part, 'aria-valuemin', String(low));\n\t\twriteAttribute(part, 'aria-valuemax', String(high));\n\t\twriteAttribute(part, 'aria-valuenow', String(span.start));\n\t\twriteAttribute(part, 'aria-valuetext', host.speak(span));\n\t\twriteAttribute(part, 'aria-disabled', canMove ? undefined : 'true');\n\t\twriteAttribute(part, 'tabindex', canMove ? '0' : undefined);\n\t\tlink.draw(part);\n\n\t\treturn entry;\n\t}\n\n\t#dropGone(): void {\n\t\tconst isGone = (entry: Drawn): boolean =>\n\t\t\t!this.#drawn.includes(entry) || !this.#isEnabled(entry);\n\n\t\tif (this.#held && isGone(this.#held.drawn)) this.#release();\n\t\tif (this.#keyScrub && isGone(this.#keyScrub.drawn)) this.#endKeyScrub();\n\t}\n\n\t#endKeyScrub(): void {\n\t\tconst scrub = this.#keyScrub;\n\t\tif (!scrub) return;\n\n\t\tconst { link } = scrub.drawn;\n\n\t\tthis.#keyScrub = undefined;\n\t\tlink.hold(undefined);\n\t\tif (hasMoved(scrub.from, link.span())) link.change();\n\t}\n\n\t#isEnabled({ link }: Drawn): boolean {\n\t\treturn !this.#host.isDisabled() && !link.isDisabled();\n\t}\n\n\t#keyTo(entry: Drawn, event: KeyboardEvent): void {\n\t\tconst mapping = this.#host.mapping();\n\t\tconst from = entry.link.span();\n\t\tconst start = mapping.keyTarget(event.key, from.start);\n\t\tif (start === undefined) return;\n\n\t\tevent.preventDefault();\n\n\t\tconst next = moveRegionTo(mapping, from, start);\n\n\t\tif (!event.repeat) {\n\t\t\tthis.#endKeyScrub();\n\t\t\tif (entry.link.input(next)) entry.link.change();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.#keyScrub?.drawn !== entry || this.#keyScrub.key !== event.key) {\n\t\t\tthis.#endKeyScrub();\n\t\t\tthis.#keyScrub = { drawn: entry, from, key: event.key };\n\t\t\tentry.link.hold('keys');\n\t\t}\n\t\tentry.link.input(next);\n\t}\n\n\t#redraw(region: SonicRegion): void {\n\t\tif (this.#drawn.every((entry) => entry.region !== region)) {\n\t\t\tthis.render();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#draw(region);\n\t\tthis.#dropGone();\n\t}\n\n\t#release(isRestored = false): void {\n\t\tconst hold = this.#held;\n\t\tif (!hold) return;\n\n\t\tconst { link } = hold.drawn;\n\n\t\tthis.#held = undefined;\n\t\tif (isRestored) link.input(hold.from);\n\t\tlink.hold(undefined);\n\t\tif (hasMoved(hold.from, link.span())) link.change();\n\t}\n\n\t// A changed value formatter is told by what it says of one time\n\t#stripKey(): string {\n\t\tconst host = this.#host;\n\t\tconst mapping = host.mapping();\n\t\tconst [low, high] = mapping.bounds;\n\n\t\treturn [\n\t\t\tlow,\n\t\t\thigh,\n\t\t\tmapping.proportionOf((low + high) / 2),\n\t\t\thost.isDisabled(),\n\t\t\thost.speak({ end: high, start: high }),\n\t\t].join('|');\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { SonicFormElement } from "./form-element.js";
2
+ import { Seconds } from "../lib/units.js";
3
+ //#region src/elements/region.d.ts
4
+ declare global {
5
+ interface HTMLElementTagNameMap {
6
+ 'sonic-region': SonicRegion;
7
+ }
8
+ }
9
+ export declare class SonicRegion extends SonicFormElement {
10
+ #private;
11
+ static readonly observedAttributes: string[];
12
+ get dragging(): boolean;
13
+ get end(): Seconds;
14
+ set end(next: null | Seconds | undefined);
15
+ /** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */
16
+ get kind(): string | undefined;
17
+ set kind(kind: string | undefined);
18
+ get start(): Seconds;
19
+ set start(next: null | Seconds | undefined);
20
+ attributeChangedCallback(name: string): void;
21
+ connectedCallback(): void;
22
+ formResetCallback(): void;
23
+ protected connect(): void;
24
+ protected focusTarget(): HTMLElement | undefined;
25
+ protected render(): void;
26
+ protected restoreState(state: string): void;
27
+ }
28
+ //#endregion
29
+ //# sourceMappingURL=region.d.ts.map
@@ -0,0 +1,165 @@
1
+ import { SonicFormElement } from "./form-element.js";
2
+
3
+ //#region src/elements/region.ts
4
+ const owners = /* @__PURE__ */ new WeakMap();
5
+ const warned = /* @__PURE__ */ new Set();
6
+ let link;
7
+ function linkRegion(element) {
8
+ return link(element);
9
+ }
10
+ function ownRegions(host, changed, signal) {
11
+ owners.set(host, changed);
12
+ signal.addEventListener("abort", () => {
13
+ if (owners.get(host) === changed) owners.delete(host);
14
+ }, { once: true });
15
+ }
16
+ function warnOnce(key, message) {
17
+ if (warned.has(key)) return;
18
+ warned.add(key);
19
+ console.warn(message);
20
+ }
21
+ var SonicRegion = class extends SonicFormElement {
22
+ static observedAttributes = [
23
+ ...SonicFormElement.observedAttributes,
24
+ "end",
25
+ "kind",
26
+ "name",
27
+ "start"
28
+ ];
29
+ static {
30
+ link = (element) => ({
31
+ change: () => {
32
+ element.#report("change");
33
+ },
34
+ draw: (part) => {
35
+ element.#draw(part);
36
+ },
37
+ hold: (by) => {
38
+ element.#held = by;
39
+ element.toggleState("dragging", by === "pointer");
40
+ },
41
+ input: (span) => element.#input(span),
42
+ isDisabled: () => element.isDisabled(),
43
+ span: () => {
44
+ const { end, start } = element.#span;
45
+ return {
46
+ end: Math.max(start, end),
47
+ start: Math.min(start, end)
48
+ };
49
+ }
50
+ });
51
+ }
52
+ get dragging() {
53
+ return this.hasState("dragging");
54
+ }
55
+ get end() {
56
+ return this.#span.end;
57
+ }
58
+ set end(next) {
59
+ if (!this.#held) this.reflect("end", next);
60
+ }
61
+ /** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */
62
+ get kind() {
63
+ return this.getAttribute("kind") ?? void 0;
64
+ }
65
+ set kind(kind) {
66
+ this.reflect("kind", kind);
67
+ }
68
+ get start() {
69
+ return this.#span.start;
70
+ }
71
+ set start(next) {
72
+ if (!this.#held) this.reflect("start", next);
73
+ }
74
+ #formState = "";
75
+ #held;
76
+ #part;
77
+ #span = {
78
+ end: 0,
79
+ start: 0
80
+ };
81
+ attributeChangedCallback(name) {
82
+ const isSpan = name === "end" || name === "start";
83
+ if (isSpan && this.#held) return;
84
+ if (isSpan) this.#readSpan(name);
85
+ this.render();
86
+ }
87
+ connectedCallback() {
88
+ this.upgradeProperties("end", "kind", "start");
89
+ super.connectedCallback();
90
+ }
91
+ formResetCallback() {
92
+ if (this.#held) return;
93
+ this.#readSpan("start");
94
+ this.#readSpan("end");
95
+ this.render();
96
+ }
97
+ connect() {
98
+ this.render();
99
+ if (this.parentElement?.localName !== "sonic-wavestrip") warnOnce("parent", "<sonic-region> draws nothing of its own, so it shows only as a child of <sonic-wavestrip>");
100
+ }
101
+ focusTarget() {
102
+ return this.#part;
103
+ }
104
+ render() {
105
+ if (!this.isBound() || !this.parentElement) return;
106
+ owners.get(this.parentElement)?.(this);
107
+ }
108
+ restoreState(state) {
109
+ const [start = NaN, end = NaN] = state.split(",").map(Number);
110
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
111
+ this.#span = {
112
+ end,
113
+ start
114
+ };
115
+ this.render();
116
+ }
117
+ #draw(part) {
118
+ this.#part = part;
119
+ this.forwardNaming(part, true);
120
+ this.#writeForm();
121
+ if (!this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby") && this.namingLabels().length === 0) warnOnce("name", "<sonic-region> is a slider with no name; give it an aria-label or aria-labelledby");
122
+ }
123
+ #input(span) {
124
+ const hasMoved = span.start !== this.#span.start || span.end !== this.#span.end;
125
+ if (hasMoved) {
126
+ this.#span = span;
127
+ this.render();
128
+ this.#report("input");
129
+ }
130
+ return hasMoved;
131
+ }
132
+ #readSpan(name) {
133
+ const { end, start } = this.#span;
134
+ if (name === "end") {
135
+ this.#span = {
136
+ end: this.numberAttribute("end", start),
137
+ start
138
+ };
139
+ return;
140
+ }
141
+ const next = this.numberAttribute("start", 0);
142
+ this.#span = {
143
+ end: this.hasAttribute("end") ? end : next,
144
+ start: next
145
+ };
146
+ }
147
+ #report(type) {
148
+ this.dispatchEvent(new Event(type, {
149
+ bubbles: true,
150
+ composed: type === "input"
151
+ }));
152
+ }
153
+ #writeForm() {
154
+ const { end, start } = this.#span;
155
+ const state = `${String(start)},${String(end)}`;
156
+ const formState = `${this.name}=${state}`;
157
+ if (formState === this.#formState) return;
158
+ this.#formState = formState;
159
+ this.writeFormFields([["start", start], ["end", end]], state);
160
+ }
161
+ };
162
+
163
+ //#endregion
164
+ export { SonicRegion, linkRegion, ownRegions };
165
+ //# sourceMappingURL=region.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.js","names":[],"sources":["../../../src/elements/region.ts"],"sourcesContent":["import type { RegionSpan } from '#lib/region-drag.ts';\nimport type { Seconds } from '#lib/units.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\n\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-region': SonicRegion;\n\t}\n}\n\ntype RegionHold = 'keys' | 'pointer';\n\nexport interface RegionLink {\n\tchange: () => void;\n\tdraw: (part: HTMLElement) => void;\n\thold: (by: RegionHold | undefined) => void;\n\tinput: (span: RegionSpan) => boolean;\n\tisDisabled: () => boolean;\n\tspan: () => RegionSpan;\n}\n\nconst owners = new WeakMap<Element, (region: SonicRegion) => void>();\n\nconst warned = new Set<string>();\n\n// Filled from inside the class, so it reaches protected members and nothing lands on the element's public type\nlet link: (element: SonicRegion) => RegionLink;\n\nexport function linkRegion(element: SonicRegion): RegionLink {\n\treturn link(element);\n}\n\nexport function ownRegions(\n\thost: Element,\n\tchanged: (region: SonicRegion) => void,\n\tsignal: AbortSignal,\n): void {\n\towners.set(host, changed);\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\tif (owners.get(host) === changed) owners.delete(host);\n\t\t},\n\t\t{ once: true },\n\t);\n}\n\nfunction warnOnce(key: string, message: string): void {\n\tif (!__DEV__ || warned.has(key)) return;\n\n\twarned.add(key);\n\tconsole.warn(message);\n}\n\nexport class SonicRegion extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'end',\n\t\t'kind',\n\t\t'name',\n\t\t'start',\n\t];\n\n\tstatic {\n\t\tlink = (element) => ({\n\t\t\tchange: () => {\n\t\t\t\telement.#report('change');\n\t\t\t},\n\t\t\tdraw: (part) => {\n\t\t\t\telement.#draw(part);\n\t\t\t},\n\t\t\thold: (by) => {\n\t\t\t\telement.#held = by;\n\t\t\t\telement.toggleState('dragging', by === 'pointer');\n\t\t\t},\n\t\t\tinput: (span) => element.#input(span),\n\t\t\tisDisabled: () => element.isDisabled(),\n\t\t\tspan: () => {\n\t\t\t\tconst { end, start } = element.#span;\n\n\t\t\t\treturn { end: Math.max(start, end), start: Math.min(start, end) };\n\t\t\t},\n\t\t});\n\t}\n\n\tget dragging(): boolean {\n\t\treturn this.hasState('dragging');\n\t}\n\n\tget end(): Seconds {\n\t\treturn this.#span.end;\n\t}\n\n\tset end(next: null | Seconds | undefined) {\n\t\tif (!this.#held) this.reflect('end', next);\n\t}\n\n\t/** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */\n\tget kind(): string | undefined {\n\t\treturn this.getAttribute('kind') ?? undefined;\n\t}\n\n\tset kind(kind: string | undefined) {\n\t\tthis.reflect('kind', kind);\n\t}\n\n\tget start(): Seconds {\n\t\treturn this.#span.start;\n\t}\n\n\tset start(next: null | Seconds | undefined) {\n\t\tif (!this.#held) this.reflect('start', next);\n\t}\n\n\t#formState = '';\n\n\t// A held region owns its values, so a host writing them on every `timeupdate` does not fight the gesture\n\t#held: RegionHold | undefined;\n\n\t#part: HTMLElement | undefined;\n\n\t#span: RegionSpan = { end: 0, start: 0 };\n\n\tattributeChangedCallback(name: string): void {\n\t\tconst isSpan = name === 'end' || name === 'start';\n\n\t\tif (isSpan && this.#held) return;\n\t\tif (isSpan) this.#readSpan(name);\n\t\tthis.render();\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('end', 'kind', 'start');\n\t\tsuper.connectedCallback();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tif (this.#held) return;\n\n\t\tthis.#readSpan('start');\n\t\tthis.#readSpan('end');\n\t\tthis.render();\n\t}\n\n\tprotected connect(): void {\n\t\tthis.render();\n\t\tif (this.parentElement?.localName !== 'sonic-wavestrip') {\n\t\t\twarnOnce(\n\t\t\t\t'parent',\n\t\t\t\t'<sonic-region> draws nothing of its own, so it shows only as a child of <sonic-wavestrip>',\n\t\t\t);\n\t\t}\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.#part;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound() || !this.parentElement) return;\n\n\t\towners.get(this.parentElement)?.(this);\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tconst [start = NaN, end = NaN] = state.split(',').map(Number);\n\t\tif (!Number.isFinite(start) || !Number.isFinite(end)) return;\n\n\t\tthis.#span = { end, start };\n\t\tthis.render();\n\t}\n\n\t#draw(part: HTMLElement): void {\n\t\tthis.#part = part;\n\t\tthis.forwardNaming(part, true);\n\t\tthis.#writeForm();\n\t\tif (\n\t\t\t__DEV__ &&\n\t\t\t!this.hasAttribute('aria-label') &&\n\t\t\t!this.hasAttribute('aria-labelledby') &&\n\t\t\tthis.namingLabels().length === 0\n\t\t) {\n\t\t\twarnOnce(\n\t\t\t\t'name',\n\t\t\t\t'<sonic-region> is a slider with no name; give it an aria-label or aria-labelledby',\n\t\t\t);\n\t\t}\n\t}\n\n\t#input(span: RegionSpan): boolean {\n\t\tconst hasMoved = span.start !== this.#span.start || span.end !== this.#span.end;\n\n\t\tif (hasMoved) {\n\t\t\tthis.#span = span;\n\t\t\tthis.render();\n\t\t\tthis.#report('input');\n\t\t}\n\n\t\treturn hasMoved;\n\t}\n\n\t// One end at a time, so a write to one leaves the other where a gesture put it\n\t#readSpan(name: 'end' | 'start'): void {\n\t\tconst { end, start } = this.#span;\n\n\t\tif (name === 'end') {\n\t\t\tthis.#span = { end: this.numberAttribute('end', start), start };\n\t\t\treturn;\n\t\t}\n\n\t\tconst next = this.numberAttribute('start', 0);\n\n\t\tthis.#span = { end: this.hasAttribute('end') ? end : next, start: next };\n\t}\n\n\t#report(type: 'change' | 'input'): void {\n\t\tthis.dispatchEvent(new Event(type, { bubbles: true, composed: type === 'input' }));\n\t}\n\n\t#writeForm(): void {\n\t\tconst { end, start } = this.#span;\n\t\tconst state = `${String(start)},${String(end)}`;\n\t\tconst formState = `${this.name}=${state}`;\n\t\tif (formState === this.#formState) return;\n\n\t\tthis.#formState = formState;\n\t\tthis.writeFormFields(\n\t\t\t[\n\t\t\t\t['start', start],\n\t\t\t\t['end', end],\n\t\t\t],\n\t\t\tstate,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
@@ -29,6 +29,7 @@ export declare abstract class SonicElement extends HTMLElement {
29
29
  protected reflect(name: string, value: boolean | null | number | string | undefined): void;
30
30
  protected toggleState(state: string, isOn: boolean): void;
31
31
  protected upgradeProperties(...names: Array<keyof this>): void;
32
+ protected usesChild(_child: Node): boolean;
32
33
  }
33
34
  //#endregion
34
35
  //# sourceMappingURL=sonic-element.d.ts.map
@@ -9,6 +9,8 @@ const namingAttributes = [
9
9
  "aria-labelledby"
10
10
  ];
11
11
  const checkedSheets = /* @__PURE__ */ new WeakMap();
12
+ const canMatchStates = CSS.supports("selector(:state(a))");
13
+ const canReferElements = "ariaLabelledByElements" in Element.prototype;
12
14
  function isLost(drawn, { token }) {
13
15
  return getComputedStyle(drawn).getPropertyValue(token) === "";
14
16
  }
@@ -44,8 +46,9 @@ function writeLabelledBy(target, labels) {
44
46
  if (current?.length === labels.length && labels.every((label, index) => label === current[index])) return;
45
47
  target.ariaLabelledByElements = labels;
46
48
  }
47
- var SonicElement = class extends HTMLElement {
49
+ var SonicElement = class SonicElement extends HTMLElement {
48
50
  static observedAttributes = [...namingAttributes, "disabled"];
51
+ static #labelCount = 0;
49
52
  get disabled() {
50
53
  return this.hasAttribute("disabled");
51
54
  }
@@ -54,7 +57,15 @@ var SonicElement = class extends HTMLElement {
54
57
  }
55
58
  internals = "attachInternals" in HTMLElement.prototype ? this.attachInternals() : void 0;
56
59
  #connection;
60
+ #customStates = canMatchStates ? this.internals?.states : void 0;
57
61
  #states = /* @__PURE__ */ new Set();
62
+ static #labelId(label) {
63
+ if (label.id === "") {
64
+ SonicElement.#labelCount += 1;
65
+ label.id = `sonic-label-${String(SonicElement.#labelCount)}`;
66
+ }
67
+ return label.id;
68
+ }
58
69
  blur() {
59
70
  this.#focused()?.blur();
60
71
  }
@@ -134,7 +145,7 @@ var SonicElement = class extends HTMLElement {
134
145
  const value = this.getAttribute(name);
135
146
  writeAttribute(target, name, isNamed && value !== null ? value : void 0);
136
147
  }
137
- if (labels.length > 0) writeLabelledBy(target, labels);
148
+ if (labels.length > 0) this.#writeLabels(target, labels);
138
149
  }
139
150
  hasState(state) {
140
151
  return this.#states.has(state);
@@ -148,7 +159,7 @@ var SonicElement = class extends HTMLElement {
148
159
  keepControl(control, signal) {
149
160
  const keep = () => {
150
161
  appendOnce(this, control);
151
- checkChildren(this, [...this.childNodes].filter((child) => child !== control));
162
+ checkChildren(this, [...this.childNodes].filter((child) => child !== control && !this.usesChild(child)));
152
163
  };
153
164
  const observer = new MutationObserver(keep);
154
165
  keep();
@@ -178,11 +189,11 @@ var SonicElement = class extends HTMLElement {
178
189
  toggleState(state, isOn) {
179
190
  if (isOn) {
180
191
  this.#states.add(state);
181
- this.internals?.states.add(state);
192
+ this.#customStates?.add(state);
182
193
  return;
183
194
  }
184
195
  this.#states.delete(state);
185
- this.internals?.states.delete(state);
196
+ this.#customStates?.delete(state);
186
197
  }
187
198
  upgradeProperties(...names) {
188
199
  for (const name of names) {
@@ -192,6 +203,9 @@ var SonicElement = class extends HTMLElement {
192
203
  this[name] = value;
193
204
  }
194
205
  }
206
+ usesChild(_child) {
207
+ return false;
208
+ }
195
209
  #checkBox(control, checked, box) {
196
210
  if (!control.isConnected) return;
197
211
  if (!checked.has("host")) this.#checkHost(checked);
@@ -210,6 +224,14 @@ var SonicElement = class extends HTMLElement {
210
224
  const active = root instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;
211
225
  return active instanceof HTMLElement && this.contains(active) ? active : void 0;
212
226
  }
227
+ #writeLabels(target, labels) {
228
+ if (canReferElements) {
229
+ writeLabelledBy(target, labels);
230
+ return;
231
+ }
232
+ const ids = labels.map((label) => SonicElement.#labelId(label));
233
+ writeAttribute(target, "aria-labelledby", ids.join(" "));
234
+ }
213
235
  };
214
236
 
215
237
  //#endregion