@xsynaptic/sonic-ui 0.10.4 → 0.12.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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -16
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +2 -0
  6. package/dist/default/elements/position-group.js +163 -0
  7. package/dist/default/elements/radio-group.js +34 -12
  8. package/dist/default/elements/switch.js +33 -167
  9. package/dist/default/elements/toggle.js +189 -0
  10. package/dist/default/elements/waveform.js +30 -10
  11. package/dist/default/elements/wavestrip.js +4 -0
  12. package/dist/default/index.js +2 -1
  13. package/dist/default/lib/key-press.js +27 -0
  14. package/dist/default/lib/toggle-travel.js +19 -0
  15. package/dist/dev/define/toggle.d.ts +2 -0
  16. package/dist/dev/define/toggle.js +8 -0
  17. package/dist/dev/define/toggle.js.map +1 -0
  18. package/dist/dev/define.d.ts +3 -1
  19. package/dist/dev/define.js +1 -0
  20. package/dist/dev/elements/button.js +2 -0
  21. package/dist/dev/elements/button.js.map +1 -1
  22. package/dist/dev/elements/position-group.d.ts +38 -0
  23. package/dist/dev/elements/position-group.js +164 -0
  24. package/dist/dev/elements/position-group.js.map +1 -0
  25. package/dist/dev/elements/radio-group.d.ts +4 -0
  26. package/dist/dev/elements/radio-group.js +34 -12
  27. package/dist/dev/elements/radio-group.js.map +1 -1
  28. package/dist/dev/elements/switch.d.ts +4 -19
  29. package/dist/dev/elements/switch.js +33 -167
  30. package/dist/dev/elements/switch.js.map +1 -1
  31. package/dist/dev/elements/toggle.d.ts +21 -0
  32. package/dist/dev/elements/toggle.js +197 -0
  33. package/dist/dev/elements/toggle.js.map +1 -0
  34. package/dist/dev/elements/waveform.d.ts +1 -1
  35. package/dist/dev/elements/waveform.js +30 -10
  36. package/dist/dev/elements/waveform.js.map +1 -1
  37. package/dist/dev/elements/wavestrip.d.ts +1 -0
  38. package/dist/dev/elements/wavestrip.js +4 -0
  39. package/dist/dev/elements/wavestrip.js.map +1 -1
  40. package/dist/dev/index.d.ts +3 -2
  41. package/dist/dev/index.js +2 -1
  42. package/dist/dev/lib/key-press.js +28 -0
  43. package/dist/dev/lib/key-press.js.map +1 -0
  44. package/dist/dev/lib/toggle-travel.js +20 -0
  45. package/dist/dev/lib/toggle-travel.js.map +1 -0
  46. package/dist/styles/button.css +2 -2
  47. package/dist/styles/controls.css +1 -0
  48. package/dist/styles/led.css +16 -13
  49. package/dist/styles/material.css +36 -9
  50. package/dist/styles/meter.css +7 -6
  51. package/dist/styles/segmented.css +8 -4
  52. package/dist/styles/spectrum.css +2 -2
  53. package/dist/styles/switch.css +6 -2
  54. package/dist/styles/toggle.css +481 -0
  55. package/package.json +2 -1
@@ -0,0 +1,189 @@
1
+ import { placeChildren, requireChild, template } from "../lib/render.js";
2
+ import { bindDrag } from "../lib/pointer-drag.js";
3
+ import { dragThresholdPx } from "../lib/drag-step.js";
4
+ import { isUnder, optionValue } from "./radio-group.js";
5
+ import { SonicPositionGroupElement } from "./position-group.js";
6
+ import { positionAt, travelFrom } from "../lib/toggle-travel.js";
7
+
8
+ //#region src/elements/toggle.ts
9
+ const renderToggle = template(`
10
+ <div class="sonic-toggle">
11
+ <span aria-hidden="true" class="sonic-toggle-well"></span>
12
+ <span aria-hidden="true" class="sonic-toggle-cap"></span>
13
+ </div>
14
+ `, HTMLDivElement);
15
+ const renderPosition = template(`
16
+ <button class="sonic-toggle-position" role="radio" type="button">
17
+ <span class="sonic-toggle-legend"></span>
18
+ </button>
19
+ `, HTMLButtonElement);
20
+ const renderBare = template(`<button class="sonic-toggle-position" role="switch" type="button"></button>`, HTMLButtonElement);
21
+ var SonicToggle = class extends SonicPositionGroupElement {
22
+ bare = renderBare();
23
+ defaultOrientation = "horizontal";
24
+ group = renderToggle();
25
+ #cap = requireChild(this.group, ".sonic-toggle-cap", HTMLSpanElement);
26
+ #draggedTo;
27
+ #hasWarnedOfLegends = false;
28
+ #well = requireChild(this.group, ".sonic-toggle-well", HTMLSpanElement);
29
+ claimPress() {
30
+ return true;
31
+ }
32
+ connect(signal) {
33
+ super.connect(signal);
34
+ this.checkStyles(this.group, "toggle.css");
35
+ this.bare.addEventListener("click", (event) => {
36
+ if (event instanceof PointerEvent && event.pointerType !== "") return;
37
+ this.setChecked(!this.checked);
38
+ }, { signal });
39
+ this.#bindCap(signal);
40
+ }
41
+ isMirrored(child) {
42
+ return super.isMirrored(child) || child === this.#fixedLegend();
43
+ }
44
+ placeCopies(copies) {
45
+ const positions = copies.filter((copy) => super.isMirrored(copy));
46
+ placeChildren(this.#cap, copies.filter((copy) => !positions.includes(copy)));
47
+ super.placeCopies(positions);
48
+ }
49
+ render() {
50
+ if (!this.isBound()) return;
51
+ this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
52
+ this.#placeCap(this.#draggedTo ?? this.#restingAt());
53
+ super.render();
54
+ for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
55
+ }
56
+ renderOption() {
57
+ return renderPosition();
58
+ }
59
+ #bindCap(signal) {
60
+ bindDrag(this.group, {
61
+ grab: (event) => this.#grab(event),
62
+ lift: (drag, event) => {
63
+ if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
64
+ if (!isUnder(this.group, event.clientX, event.clientY)) return;
65
+ this.#press(drag.position);
66
+ },
67
+ move: (drag, event) => {
68
+ if (drag.isHolding || this.isDisabled()) return;
69
+ const pointer = this.along(event);
70
+ if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
71
+ drag.hasMoved = true;
72
+ this.toggleState("dragging", true);
73
+ this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
74
+ },
75
+ release: (drag) => {
76
+ delete (drag.position ?? this.bare).dataset.sonicPressed;
77
+ this.#draggedTo = void 0;
78
+ this.toggleState("dragging", false);
79
+ this.render();
80
+ }
81
+ }, signal);
82
+ }
83
+ #checkLegends() {}
84
+ #enabledEnds() {
85
+ if (this.isBare()) return [0, 1];
86
+ const options = this.options();
87
+ const isEnabled = (option) => !this.isOptionDisabled(option);
88
+ return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
89
+ }
90
+ #fixedLegend() {
91
+ return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
92
+ }
93
+ #grab(event) {
94
+ const position = this.optionOf(event.target);
95
+ if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
96
+ const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
97
+ const travel = this.#travel(event, isOnCap);
98
+ if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
99
+ (position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
100
+ return {
101
+ hasMoved: false,
102
+ isHolding: this.#holdFrom(position, event.pointerId),
103
+ position,
104
+ start: this.along(event),
105
+ thresholdPx: dragThresholdPx(event.pointerType),
106
+ travel
107
+ };
108
+ }
109
+ #holdFrom(position, pointerId) {
110
+ const target = position && this.pressTarget(position);
111
+ if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
112
+ this.hold(target, pointerId);
113
+ return true;
114
+ }
115
+ #isBlocked(position, isOnCap) {
116
+ return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
117
+ }
118
+ #onAt() {
119
+ return this.orientation === "vertical" ? 0 : 1;
120
+ }
121
+ #placeCap(at) {
122
+ this.group.style.setProperty("--_sonic-toggle-at", String(at));
123
+ }
124
+ #press(position) {
125
+ if (!position) {
126
+ this.setChecked(!this.checked);
127
+ return;
128
+ }
129
+ const target = this.pressTarget(position);
130
+ if (this.isOptionDisabled(target)) return;
131
+ this.select(target);
132
+ this.#refocus(target);
133
+ }
134
+ #refocus(position) {
135
+ if (this.group.matches(":focus-within")) position.focus();
136
+ }
137
+ #restingAt() {
138
+ if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
139
+ return Math.max(0, this.checkedIndex(this.options()));
140
+ }
141
+ #slideTo(at, pointerId) {
142
+ const crossed = this.options()[Math.round(at)];
143
+ this.#draggedTo = at;
144
+ this.#placeCap(at);
145
+ if (this.isBare()) {
146
+ this.setChecked(Math.round(at) === this.#onAt());
147
+ return;
148
+ }
149
+ if (!crossed || optionValue(crossed) === this.value) return;
150
+ this.releaseHold();
151
+ if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
152
+ if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
153
+ else this.select(crossed);
154
+ this.#refocus(crossed);
155
+ }
156
+ #stopCount() {
157
+ return this.options().length || 2;
158
+ }
159
+ #travel(event, isOnCap) {
160
+ const [first, last] = this.#enabledEnds();
161
+ if (first < 0) return void 0;
162
+ const box = this.#well.getBoundingClientRect();
163
+ const [start, length, thickness] = this.orientation === "vertical" ? [
164
+ box.top,
165
+ box.height,
166
+ box.width
167
+ ] : [
168
+ box.left,
169
+ box.width,
170
+ box.height
171
+ ];
172
+ return travelFrom({
173
+ at: this.#restingAt(),
174
+ count: this.#stopCount(),
175
+ first,
176
+ isOnCap,
177
+ last,
178
+ pointer: this.along(event),
179
+ well: {
180
+ length,
181
+ start,
182
+ thickness
183
+ }
184
+ });
185
+ }
186
+ };
187
+
188
+ //#endregion
189
+ export { SonicToggle };
@@ -125,20 +125,22 @@ var SonicWaveform = class extends SonicWaveElement {
125
125
  set peaks(peaks) {
126
126
  this.#peaks = peaks;
127
127
  this.renderEmpty();
128
- this.surface()?.invalidate();
128
+ this.#repaint();
129
129
  }
130
130
  get pending() {
131
131
  return this.#pending.map(([start, end]) => [start, end]);
132
132
  }
133
133
  set pending(regions) {
134
+ const pending = regions ? readRegions(regions) : [];
135
+ if (String(pending) === String(this.#pending)) return;
134
136
  const listedMs = this.#pendingListedMs;
135
137
  const nowMs = performance.now();
136
- this.#pending = regions ? readRegions(regions) : [];
138
+ this.#pending = pending;
137
139
  this.#pendingListedMs = new Map(this.#pending.map((region) => {
138
140
  const key = region.join(":");
139
141
  return [key, listedMs.get(key) ?? nowMs];
140
142
  }));
141
- this.surface()?.invalidate();
143
+ this.#repaint();
142
144
  }
143
145
  get pendingDelay() {
144
146
  return Math.max(0, this.numberAttribute("pending-delay", 0));
@@ -177,6 +179,7 @@ var SonicWaveform = class extends SonicWaveElement {
177
179
  }
178
180
  set requestPeaks(request) {
179
181
  this.#requestPeaks = request;
182
+ this.#askedKey = void 0;
180
183
  this.surface()?.invalidate();
181
184
  }
182
185
  get zoom() {
@@ -192,11 +195,13 @@ var SonicWaveform = class extends SonicWaveElement {
192
195
  lengths = {};
193
196
  sheet = "waveform.css";
194
197
  sizeProperty = "--_sonic-waveform-size";
195
- #asked = /* @__PURE__ */ new Set();
198
+ #asked = /* @__PURE__ */ new WeakSet();
199
+ #askedKey;
196
200
  #clock = createTrackingClock();
197
201
  #drawn;
198
202
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
199
203
  #grabbed;
204
+ #isAsking = false;
200
205
  #kindColours = /* @__PURE__ */ new Map();
201
206
  #paintedKey = "";
202
207
  #peaks;
@@ -287,10 +292,22 @@ var SonicWaveform = class extends SonicWaveElement {
287
292
  this.surface()?.rebuild();
288
293
  }
289
294
  #ask(wanted) {
290
- const asked = this.#requestPeaks?.(...wanted);
291
- if (!(asked instanceof Promise) || this.#asked.has(asked)) return;
292
- this.#asked.add(asked);
293
- this.#repaintAfter(asked);
295
+ const key = wanted.join(":");
296
+ if (key === this.#askedKey) return;
297
+ let asked;
298
+ this.#askedKey = key;
299
+ this.#isAsking = true;
300
+ try {
301
+ asked = this.#requestPeaks?.(...wanted);
302
+ } finally {
303
+ this.#isAsking = false;
304
+ }
305
+ const promises = asked instanceof Promise ? [asked] : asked ?? [];
306
+ for (const promise of promises) {
307
+ if (this.#asked.has(promise)) continue;
308
+ this.#asked.add(promise);
309
+ this.#repaintAfter(promise);
310
+ }
294
311
  }
295
312
  #duePending() {
296
313
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
@@ -363,10 +380,13 @@ var SonicWaveform = class extends SonicWaveElement {
363
380
  for (const probe of probes) probe.remove();
364
381
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
365
382
  }
383
+ #repaint() {
384
+ if (!this.#isAsking) this.#askedKey = void 0;
385
+ this.surface()?.invalidate();
386
+ }
366
387
  async #repaintAfter(asked) {
367
388
  await Promise.allSettled([asked]);
368
- this.#asked.delete(asked);
369
- this.surface()?.invalidate();
389
+ this.#repaint();
370
390
  }
371
391
  #sourceSeconds() {
372
392
  return this.#readTime?.() ?? this.playback();
@@ -80,6 +80,10 @@ var SonicWavestrip = class extends SonicWaveElement {
80
80
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
81
81
  #paintedKey = "";
82
82
  #peaks;
83
+ clientXOf(value) {
84
+ const { startPx, travelPx } = this.#axis();
85
+ return startPx + this.mapping().proportionOf(value) * travelPx;
86
+ }
83
87
  connectedCallback() {
84
88
  this.upgradeProperties("buffered", "cancellable", "peaks");
85
89
  super.connectedCallback();
@@ -9,10 +9,11 @@ import { SonicSlider } from "./elements/slider.js";
9
9
  import { SonicSpectrum } from "./elements/spectrum.js";
10
10
  import { SonicSplit } from "./elements/split.js";
11
11
  import { SonicSwitch } from "./elements/switch.js";
12
+ import { SonicToggle } from "./elements/toggle.js";
12
13
  import { SonicWaveform } from "./elements/waveform.js";
13
14
  import { SonicWavestrip } from "./elements/wavestrip.js";
14
15
  import { SonicXy } from "./elements/xy.js";
15
16
  import { crossfadeGains } from "./lib/crossfade.js";
16
17
  import { formatPercent, parsePercent } from "./lib/percent.js";
17
18
 
18
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
19
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
@@ -0,0 +1,27 @@
1
+ //#region src/lib/key-press.ts
2
+ function bindKeyPress(root, signal) {
3
+ let pressed;
4
+ const release = () => {
5
+ if (!pressed) return;
6
+ delete pressed.dataset.sonicPressed;
7
+ pressed = void 0;
8
+ };
9
+ root.addEventListener("keydown", (event) => {
10
+ if (event.key !== " " || event.repeat || event.defaultPrevented) return;
11
+ const part = event.target;
12
+ if (!(part instanceof HTMLButtonElement) || part.disabled) return;
13
+ release();
14
+ pressed = part;
15
+ part.dataset.sonicPressed = "";
16
+ }, { signal });
17
+ root.addEventListener("keyup", (event) => {
18
+ if (event.key === " " || event.key === "Meta") release();
19
+ }, { signal });
20
+ root.addEventListener("focusout", (event) => {
21
+ if (event.target === pressed) release();
22
+ }, { signal });
23
+ signal.addEventListener("abort", release, { once: true });
24
+ }
25
+
26
+ //#endregion
27
+ export { bindKeyPress };
@@ -0,0 +1,19 @@
1
+ import { clamp } from "./math.js";
2
+
3
+ //#region src/lib/toggle-travel.ts
4
+ function travelFrom({ at, count, first, isOnCap, last, pointer, well }) {
5
+ const step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);
6
+ return {
7
+ first,
8
+ last,
9
+ origin: isOnCap ? pointer - at * step : well.start + well.thickness / 2,
10
+ step
11
+ };
12
+ }
13
+ function positionAt({ first, last, origin, step }, pointer) {
14
+ if (step <= 0) return first;
15
+ return clamp((pointer - origin) / step, first, last);
16
+ }
17
+
18
+ //#endregion
19
+ export { positionAt, travelFrom };
@@ -0,0 +1,2 @@
1
+ import { SonicToggle } from "../elements/toggle.js";
2
+ export type { SonicToggle };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicToggle } from "../elements/toggle.js";
3
+
4
+ //#region src/define/toggle.ts
5
+ defineOnce("sonic-toggle", SonicToggle);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=toggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle.js","names":[],"sources":["../../../src/define/toggle.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicToggle } from '#elements/toggle.ts';\n\ndefineOnce('sonic-toggle', SonicToggle);\n\nexport type { SonicToggle } from '#elements/toggle.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -18,10 +18,12 @@ import { SonicSplit } from "./elements/split.js";
18
18
  import "./define/split.js";
19
19
  import { SonicSwitch } from "./elements/switch.js";
20
20
  import "./define/switch.js";
21
+ import { SonicToggle } from "./elements/toggle.js";
22
+ import "./define/toggle.js";
21
23
  import { SonicWaveform } from "./elements/waveform.js";
22
24
  import "./define/waveform.js";
23
25
  import { SonicWavestrip } from "./elements/wavestrip.js";
24
26
  import "./define/wavestrip.js";
25
27
  import { SonicXy } from "./elements/xy.js";
26
28
  import "./define/xy.js";
27
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy };
29
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
@@ -8,6 +8,7 @@ import "./define/slider.js";
8
8
  import "./define/spectrum.js";
9
9
  import "./define/split.js";
10
10
  import "./define/switch.js";
11
+ import "./define/toggle.js";
11
12
  import "./define/waveform.js";
12
13
  import "./define/wavestrip.js";
13
14
  import "./define/xy.js";
@@ -2,6 +2,7 @@ import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicFormElement } from "./form-element.js";
3
3
  import { copyNode } from "../lib/copy-node.js";
4
4
  import { bindHold } from "../lib/hold.js";
5
+ import { bindKeyPress } from "../lib/key-press.js";
5
6
  import { placeChildren, requireChild, template } from "../lib/render.js";
6
7
  import { mirrorChildren } from "../lib/mirror-children.js";
7
8
  import { capturePointer } from "../lib/pointer-drag.js";
@@ -125,6 +126,7 @@ var SonicButton = class extends SonicFormElement {
125
126
  this.dispatchEvent(new Event("change", { bubbles: true }));
126
127
  }, { signal });
127
128
  this.#bindMomentary(button, signal);
129
+ bindKeyPress(button, signal);
128
130
  }
129
131
  focusTarget() {
130
132
  return this.#button;
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nconst popupAttributes = {\n\tcontrols: 'aria-controls',\n\texpanded: 'aria-expanded',\n\tpopup: 'aria-haspopup',\n} as const;\n\nconst pressWhens = new Set(['pressed', 'released']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t...Object.keys(popupAttributes),\n\t\t'busy',\n\t\t'legend',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget legend(): string | undefined {\n\t\treturn this.getAttribute('legend') ?? undefined;\n\t}\n\n\tset legend(name: string | undefined) {\n\t\tthis.reflect('legend', name);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'legend',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t\tthis.#showLegend();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\t\tthis.#forwardPopup(button);\n\t\tthis.#showLegend();\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tcapturePointer(button, event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#forwardPopup(button: HTMLButtonElement): void {\n\t\tfor (const [name, aria] of Object.entries(popupAttributes)) {\n\t\t\twriteAttribute(button, aria, this.getAttribute(name) ?? undefined);\n\t\t}\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#showLegend(): void {\n\t\tconst legend = this.legend;\n\n\t\tfor (const part of this.#button.querySelectorAll<HTMLElement | SVGElement>(\n\t\t\t':scope > .sonic-button-cap > [data-sonic-when]',\n\t\t)) {\n\t\t\tconst when = part.dataset.sonicWhen ?? '';\n\n\t\t\tpart.toggleAttribute('data-sonic-hidden', !pressWhens.has(when) && when !== legend);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { bindKeyPress } from '#lib/key-press.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nconst popupAttributes = {\n\tcontrols: 'aria-controls',\n\texpanded: 'aria-expanded',\n\tpopup: 'aria-haspopup',\n} as const;\n\nconst pressWhens = new Set(['pressed', 'released']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t...Object.keys(popupAttributes),\n\t\t'busy',\n\t\t'legend',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget legend(): string | undefined {\n\t\treturn this.getAttribute('legend') ?? undefined;\n\t}\n\n\tset legend(name: string | undefined) {\n\t\tthis.reflect('legend', name);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'legend',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t\tthis.#showLegend();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t\tbindKeyPress(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\t\tthis.#forwardPopup(button);\n\t\tthis.#showLegend();\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tcapturePointer(button, event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#forwardPopup(button: HTMLButtonElement): void {\n\t\tfor (const [name, aria] of Object.entries(popupAttributes)) {\n\t\t\twriteAttribute(button, aria, this.getAttribute(name) ?? undefined);\n\t\t}\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#showLegend(): void {\n\t\tconst legend = this.legend;\n\n\t\tfor (const part of this.#button.querySelectorAll<HTMLElement | SVGElement>(\n\t\t\t':scope > .sonic-button-cap > [data-sonic-when]',\n\t\t)) {\n\t\t\tconst when = part.dataset.sonicWhen ?? '';\n\n\t\t\tpart.toggleAttribute('data-sonic-hidden', !pressWhens.has(when) && when !== legend);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { SonicRadioGroupElement } from "./radio-group.js";
2
+ //#region src/elements/position-group.d.ts
3
+ type Orientation = 'horizontal' | 'vertical';
4
+ export declare abstract class SonicPositionGroupElement extends SonicRadioGroupElement {
5
+ #private;
6
+ static readonly observedAttributes: string[];
7
+ get checked(): boolean;
8
+ set checked(isChecked: boolean | undefined);
9
+ get defaultChecked(): boolean;
10
+ set defaultChecked(isChecked: boolean);
11
+ get orientation(): Orientation;
12
+ set orientation(direction: Orientation | undefined);
13
+ get value(): string;
14
+ set value(next: string);
15
+ protected abstract readonly bare: HTMLButtonElement;
16
+ protected abstract readonly defaultOrientation: Orientation;
17
+ attributeChangedCallback(name: string): void;
18
+ formResetCallback(): void;
19
+ protected activate(): void;
20
+ protected along(event: PointerEvent): number;
21
+ protected checkedIndex(options: Array<HTMLButtonElement>): number;
22
+ protected chooseByClick(option: HTMLButtonElement): void;
23
+ protected chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void;
24
+ protected connect(signal: AbortSignal): void;
25
+ protected focusTarget(): HTMLElement | undefined;
26
+ protected hold(option: HTMLButtonElement, by: number | string): void;
27
+ protected isBare(): boolean;
28
+ protected isMomentary(option: HTMLButtonElement): boolean;
29
+ protected isWrapping(): boolean;
30
+ protected pressTarget(option: HTMLButtonElement): HTMLButtonElement;
31
+ protected releaseHold(): void;
32
+ protected render(): void;
33
+ protected restoreState(state: string): void;
34
+ protected setChecked(isChecked: boolean): void;
35
+ protected springBack(): void;
36
+ }
37
+ //#endregion
38
+ //# sourceMappingURL=position-group.d.ts.map
@@ -0,0 +1,164 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+ import { bindHold } from "../lib/hold.js";
3
+ import { SonicRadioGroupElement, optionValue } from "./radio-group.js";
4
+
5
+ //#region src/elements/position-group.ts
6
+ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
7
+ static observedAttributes = [
8
+ ...SonicRadioGroupElement.observedAttributes,
9
+ "checked",
10
+ "orientation"
11
+ ];
12
+ get checked() {
13
+ return this.#isDirty ? this.#checked : this.defaultChecked;
14
+ }
15
+ set checked(isChecked) {
16
+ this.#isDirty = true;
17
+ this.#checked = isChecked === true;
18
+ this.render();
19
+ }
20
+ get defaultChecked() {
21
+ return this.hasAttribute("checked");
22
+ }
23
+ set defaultChecked(isChecked) {
24
+ this.reflect("checked", isChecked);
25
+ }
26
+ get orientation() {
27
+ const orientation = this.getAttribute("orientation");
28
+ return orientation === "horizontal" || orientation === "vertical" ? orientation : this.defaultOrientation;
29
+ }
30
+ set orientation(direction) {
31
+ this.reflect("orientation", direction);
32
+ }
33
+ get value() {
34
+ const value = super.value;
35
+ return value === "" && this.isBare() ? "on" : value;
36
+ }
37
+ set value(next) {
38
+ super.value = next;
39
+ }
40
+ #checked = false;
41
+ #holding;
42
+ #isDirty = false;
43
+ attributeChangedCallback(name) {
44
+ if (name === "checked") this.#isDirty = false;
45
+ if (this.isDisabled()) this.#holding?.release();
46
+ super.attributeChangedCallback(name);
47
+ }
48
+ formResetCallback() {
49
+ this.#isDirty = false;
50
+ super.formResetCallback();
51
+ }
52
+ activate() {
53
+ if (this.isBare()) this.bare.click();
54
+ else super.activate();
55
+ }
56
+ along(event) {
57
+ return this.orientation === "vertical" ? event.clientY : event.clientX;
58
+ }
59
+ checkedIndex(options) {
60
+ return options.findIndex((option) => optionValue(option) === this.value);
61
+ }
62
+ chooseByClick(option) {
63
+ super.chooseByClick(option);
64
+ if (this.isMomentary(option)) this.springBack();
65
+ }
66
+ chooseByKey(option, event) {
67
+ if (!this.isMomentary(option)) {
68
+ super.chooseByKey(option, event);
69
+ return;
70
+ }
71
+ if (event.repeat) return;
72
+ this.hold(option, event.key);
73
+ option.focus();
74
+ }
75
+ connect(signal) {
76
+ this.upgradeProperties("defaultChecked", "checked", "orientation");
77
+ super.connect(signal);
78
+ this.#holding = bindHold(this.group, {
79
+ canHold: () => !this.isDisabled(),
80
+ leave: "focusout",
81
+ onHold: () => {
82
+ this.toggleState("held", true);
83
+ },
84
+ onRelease: () => {
85
+ this.toggleState("held", false);
86
+ this.springBack();
87
+ }
88
+ }, signal);
89
+ }
90
+ focusTarget() {
91
+ return this.isBare() ? this.bare : super.focusTarget();
92
+ }
93
+ hold(option, by) {
94
+ if (this.#holding?.hold(by) === true) this.select(option);
95
+ }
96
+ isBare() {
97
+ return this.options().length === 0;
98
+ }
99
+ isMomentary(option) {
100
+ const options = this.options();
101
+ return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
102
+ }
103
+ isWrapping() {
104
+ return false;
105
+ }
106
+ pressTarget(option) {
107
+ const options = this.options();
108
+ const index = this.checkedIndex(options);
109
+ return (options.length === 2 && index >= 0 ? options[1 - index] : void 0) ?? option;
110
+ }
111
+ releaseHold() {
112
+ this.#holding?.release();
113
+ }
114
+ render() {
115
+ if (!this.isBound()) return;
116
+ const group = this.group;
117
+ const count = this.options().length;
118
+ if (count === 0) {
119
+ this.#renderBare();
120
+ return;
121
+ }
122
+ this.bare.remove();
123
+ writeAttribute(group, "role", "radiogroup");
124
+ writeAttribute(group, "data-sonic-position-count", String(count));
125
+ super.render();
126
+ }
127
+ restoreState(state) {
128
+ if (this.isBare()) this.checked = state === "true";
129
+ else super.restoreState(state);
130
+ }
131
+ setChecked(isChecked) {
132
+ if (isChecked === this.checked) return;
133
+ this.checked = isChecked;
134
+ this.dispatchEvent(new Event("change", { bubbles: true }));
135
+ }
136
+ springBack() {
137
+ const options = this.options();
138
+ const index = this.checkedIndex(options);
139
+ const held = options[index];
140
+ if (!held || !this.isMomentary(held)) return;
141
+ const rest = options[index === 0 ? 1 : index - 1];
142
+ if (!rest) return;
143
+ const isFocused = this.group.contains(this.ownerDocument.activeElement);
144
+ this.select(rest);
145
+ if (isFocused) rest.focus();
146
+ }
147
+ #renderBare() {
148
+ const group = this.group;
149
+ const bare = this.bare;
150
+ const isChecked = this.checked;
151
+ writeAttribute(group, "role", void 0);
152
+ writeAttribute(group, "data-sonic-position-count", void 0);
153
+ if (bare.parentElement !== group) group.append(bare);
154
+ writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
155
+ writeAttribute(bare, "aria-checked", String(isChecked));
156
+ this.forwardNaming(group, false);
157
+ this.forwardNaming(bare, true);
158
+ this.writeFormValue(isChecked ? this.value : null, String(isChecked));
159
+ }
160
+ };
161
+
162
+ //#endregion
163
+ export { SonicPositionGroupElement };
164
+ //# sourceMappingURL=position-group.js.map