@xsynaptic/sonic-ui 0.1.0 → 0.3.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 (128) hide show
  1. package/README.md +8 -1
  2. package/dist/default/define/dial.js +7 -0
  3. package/dist/default/define/key.js +7 -0
  4. package/dist/default/define/meter.js +7 -0
  5. package/dist/default/define/number.js +7 -0
  6. package/dist/default/define/segmented.js +7 -0
  7. package/dist/default/define/slider.js +7 -0
  8. package/dist/{define.js → default/define.js} +1 -0
  9. package/dist/default/elements/define-once.js +7 -0
  10. package/dist/default/elements/dial.js +81 -0
  11. package/dist/default/elements/form-element.js +56 -0
  12. package/dist/default/elements/key.js +144 -0
  13. package/dist/{elements → default/elements}/meter.js +27 -16
  14. package/dist/default/elements/number.js +46 -0
  15. package/dist/default/elements/range-element.js +433 -0
  16. package/dist/{elements → default/elements}/range-entry.js +5 -4
  17. package/dist/default/elements/segmented.js +173 -0
  18. package/dist/default/elements/slider.js +99 -0
  19. package/dist/default/elements/sonic-element.js +153 -0
  20. package/dist/{index.js → default/index.js} +3 -1
  21. package/dist/default/lib/copy-node.js +30 -0
  22. package/dist/default/lib/percent.js +12 -0
  23. package/dist/default/lib/read-px-property.js +8 -0
  24. package/dist/{lib → default/lib}/render.js +9 -2
  25. package/dist/{lib → default/lib}/taper.js +4 -2
  26. package/dist/default/lib/write-attribute.js +11 -0
  27. package/dist/dev/define/dial.js.map +1 -0
  28. package/dist/dev/define/key.js.map +1 -0
  29. package/dist/dev/define/meter.js.map +1 -0
  30. package/dist/dev/define/number.d.ts +2 -0
  31. package/dist/dev/define/number.js +8 -0
  32. package/dist/dev/define/number.js.map +1 -0
  33. package/dist/dev/define/segmented.js.map +1 -0
  34. package/dist/dev/define/slider.js.map +1 -0
  35. package/dist/{define.d.ts → dev/define.d.ts} +3 -1
  36. package/dist/dev/define.js +6 -0
  37. package/dist/dev/elements/define-once.js.map +1 -0
  38. package/dist/{elements → dev/elements}/dial.d.ts +7 -0
  39. package/dist/{elements → dev/elements}/dial.js +31 -6
  40. package/dist/dev/elements/dial.js.map +1 -0
  41. package/dist/dev/elements/form-element.d.ts +22 -0
  42. package/dist/dev/elements/form-element.js +57 -0
  43. package/dist/dev/elements/form-element.js.map +1 -0
  44. package/dist/dev/elements/key.d.ts +30 -0
  45. package/dist/{elements → dev/elements}/key.js +71 -25
  46. package/dist/dev/elements/key.js.map +1 -0
  47. package/dist/{elements → dev/elements}/meter.d.ts +6 -0
  48. package/dist/dev/elements/meter.js +131 -0
  49. package/dist/dev/elements/meter.js.map +1 -0
  50. package/dist/dev/elements/number.d.ts +16 -0
  51. package/dist/dev/elements/number.js +47 -0
  52. package/dist/dev/elements/number.js.map +1 -0
  53. package/dist/dev/elements/range-element.d.ts +56 -0
  54. package/dist/{elements → dev/elements}/range-element.js +153 -67
  55. package/dist/dev/elements/range-element.js.map +1 -0
  56. package/dist/dev/elements/range-entry.js +75 -0
  57. package/dist/dev/elements/range-entry.js.map +1 -0
  58. package/dist/{elements → dev/elements}/segmented.d.ts +5 -2
  59. package/dist/dev/elements/segmented.js +174 -0
  60. package/dist/dev/elements/segmented.js.map +1 -0
  61. package/dist/{elements → dev/elements}/slider.d.ts +8 -0
  62. package/dist/{elements → dev/elements}/slider.js +37 -7
  63. package/dist/dev/elements/slider.js.map +1 -0
  64. package/dist/dev/elements/sonic-element.d.ts +33 -0
  65. package/dist/dev/elements/sonic-element.js +169 -0
  66. package/dist/dev/elements/sonic-element.js.map +1 -0
  67. package/dist/{index.d.ts → dev/index.d.ts} +3 -1
  68. package/dist/dev/index.js +9 -0
  69. package/dist/dev/lib/copy-node.js +31 -0
  70. package/dist/dev/lib/copy-node.js.map +1 -0
  71. package/dist/dev/lib/percent.d.ts +5 -0
  72. package/dist/dev/lib/percent.js +13 -0
  73. package/dist/dev/lib/percent.js.map +1 -0
  74. package/dist/dev/lib/read-px-property.js.map +1 -0
  75. package/dist/dev/lib/render.js +27 -0
  76. package/dist/dev/lib/render.js.map +1 -0
  77. package/dist/dev/lib/taper.js +36 -0
  78. package/dist/dev/lib/taper.js.map +1 -0
  79. package/dist/dev/lib/write-attribute.js.map +1 -0
  80. package/dist/skins/ivory.css +3 -3
  81. package/dist/skins/slate.css +4 -4
  82. package/dist/styles/controls.css +2 -1
  83. package/dist/styles/dial.css +70 -16
  84. package/dist/styles/key.css +71 -11
  85. package/dist/styles/led.css +75 -0
  86. package/dist/styles/material.css +154 -25
  87. package/dist/styles/meter.css +44 -8
  88. package/dist/styles/number.css +81 -0
  89. package/dist/styles/screen.css +6 -33
  90. package/dist/styles/segmented.css +45 -7
  91. package/dist/styles/slider.css +68 -4
  92. package/package.json +16 -13
  93. package/dist/define/dial.js.map +0 -1
  94. package/dist/define/key.js.map +0 -1
  95. package/dist/define/meter.js.map +0 -1
  96. package/dist/define/segmented.js.map +0 -1
  97. package/dist/define/slider.js.map +0 -1
  98. package/dist/elements/define-once.js.map +0 -1
  99. package/dist/elements/dial.js.map +0 -1
  100. package/dist/elements/key.d.ts +0 -17
  101. package/dist/elements/key.js.map +0 -1
  102. package/dist/elements/meter.js.map +0 -1
  103. package/dist/elements/range-element.d.ts +0 -32
  104. package/dist/elements/range-element.js.map +0 -1
  105. package/dist/elements/range-entry.js.map +0 -1
  106. package/dist/elements/segmented.js +0 -104
  107. package/dist/elements/segmented.js.map +0 -1
  108. package/dist/elements/slider.js.map +0 -1
  109. package/dist/elements/sonic-element.d.ts +0 -20
  110. package/dist/elements/sonic-element.js +0 -94
  111. package/dist/elements/sonic-element.js.map +0 -1
  112. package/dist/lib/read-px-property.js.map +0 -1
  113. package/dist/lib/render.js.map +0 -1
  114. package/dist/lib/taper.js.map +0 -1
  115. package/dist/lib/write-attribute.js.map +0 -1
  116. /package/dist/{define → dev/define}/dial.d.ts +0 -0
  117. /package/dist/{define → dev/define}/dial.js +0 -0
  118. /package/dist/{define → dev/define}/key.d.ts +0 -0
  119. /package/dist/{define → dev/define}/key.js +0 -0
  120. /package/dist/{define → dev/define}/meter.d.ts +0 -0
  121. /package/dist/{define → dev/define}/meter.js +0 -0
  122. /package/dist/{define → dev/define}/segmented.d.ts +0 -0
  123. /package/dist/{define → dev/define}/segmented.js +0 -0
  124. /package/dist/{define → dev/define}/slider.d.ts +0 -0
  125. /package/dist/{define → dev/define}/slider.js +0 -0
  126. /package/dist/{elements → dev/elements}/define-once.js +0 -0
  127. /package/dist/{lib → dev/lib}/read-px-property.js +0 -0
  128. /package/dist/{lib → dev/lib}/write-attribute.js +0 -0
@@ -0,0 +1,433 @@
1
+ import { requireChild } from "../lib/render.js";
2
+ import { writeAttribute } from "../lib/write-attribute.js";
3
+ import { SonicFormElement } from "./form-element.js";
4
+ import { RangeEntry } from "./range-entry.js";
5
+ import { clampUnit, linearTaper, logTaper, skewTaper, wrapUnit } from "../lib/taper.js";
6
+
7
+ //#region src/elements/range-element.ts
8
+ const fineScale = .1;
9
+ const keyRevealMs = 1e3;
10
+ const pageSteps = 10;
11
+ const revealMs = 250;
12
+ const revealPx = 4;
13
+ const canPopover = "togglePopover" in HTMLElement.prototype;
14
+ let instanceCount = 0;
15
+ const resetKeys = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
16
+ const keySteps = /* @__PURE__ */ new Map([
17
+ ["ArrowDown", -1],
18
+ ["ArrowLeft", -1],
19
+ ["ArrowRight", 1],
20
+ ["ArrowUp", 1],
21
+ ["PageDown", -10],
22
+ ["PageUp", pageSteps]
23
+ ]);
24
+ const roleAttributes = [
25
+ "aria-disabled",
26
+ "aria-orientation",
27
+ "aria-valuemax",
28
+ "aria-valuemin",
29
+ "aria-valuenow",
30
+ "aria-valuetext",
31
+ "role",
32
+ "tabindex"
33
+ ];
34
+ var SonicRangeElement = class extends SonicFormElement {
35
+ static observedAttributes = [
36
+ ...SonicFormElement.observedAttributes,
37
+ "max",
38
+ "midpoint",
39
+ "min",
40
+ "notched",
41
+ "origin",
42
+ "readout",
43
+ "step",
44
+ "taper",
45
+ "value"
46
+ ];
47
+ get default() {
48
+ return this.optionalNumberAttribute("default");
49
+ }
50
+ set default(value) {
51
+ this.reflect("default", value);
52
+ }
53
+ get dimmed() {
54
+ return this.hasAttribute("dimmed");
55
+ }
56
+ set dimmed(isDimmed) {
57
+ this.reflect("dimmed", isDimmed);
58
+ }
59
+ get doublePress() {
60
+ return this.getAttribute("double-press") === "reset" ? "reset" : "type";
61
+ }
62
+ set doublePress(gesture) {
63
+ this.reflect("double-press", gesture);
64
+ }
65
+ get formatValue() {
66
+ return this.#formatValue;
67
+ }
68
+ set formatValue(format) {
69
+ this.#formatValue = format;
70
+ this.render();
71
+ }
72
+ get max() {
73
+ return this.numberAttribute("max", 100);
74
+ }
75
+ set max(value) {
76
+ this.reflect("max", value);
77
+ }
78
+ get midpoint() {
79
+ return this.optionalNumberAttribute("midpoint");
80
+ }
81
+ set midpoint(value) {
82
+ this.reflect("midpoint", value);
83
+ }
84
+ get min() {
85
+ return this.numberAttribute("min", 0);
86
+ }
87
+ set min(value) {
88
+ this.reflect("min", value);
89
+ }
90
+ get notched() {
91
+ return this.hasAttribute("notched");
92
+ }
93
+ set notched(isNotched) {
94
+ this.reflect("notched", isNotched);
95
+ }
96
+ get origin() {
97
+ return this.optionalNumberAttribute("origin");
98
+ }
99
+ set origin(value) {
100
+ this.reflect("origin", value);
101
+ }
102
+ get parseValue() {
103
+ return this.#parseValue;
104
+ }
105
+ set parseValue(parse) {
106
+ this.#parseValue = parse;
107
+ }
108
+ get readout() {
109
+ return this.hasAttribute("readout");
110
+ }
111
+ set readout(isShown) {
112
+ this.reflect("readout", isShown);
113
+ }
114
+ get step() {
115
+ return this.numberAttribute("step", 1);
116
+ }
117
+ set step(value) {
118
+ this.reflect("step", value);
119
+ }
120
+ get taper() {
121
+ return this.getAttribute("taper") === "log" ? "log" : "linear";
122
+ }
123
+ set taper(curve) {
124
+ this.reflect("taper", curve);
125
+ }
126
+ get value() {
127
+ return this.#value;
128
+ }
129
+ set value(next) {
130
+ if (this.#drag) return;
131
+ this.#write(next);
132
+ }
133
+ #drag;
134
+ #entry;
135
+ #formatValue;
136
+ #instance = String(instanceCount += 1);
137
+ #isDirty = false;
138
+ #keyRevealTimer;
139
+ #parseValue;
140
+ #readout;
141
+ #value = 0;
142
+ attributeChangedCallback(name) {
143
+ if (name === "value") {
144
+ if (this.#drag) return;
145
+ this.#isDirty = false;
146
+ }
147
+ if (name === "disabled" && this.isDisabled()) {
148
+ this.#entry?.close(false);
149
+ this.#endDrag();
150
+ }
151
+ this.#value = this.#clamp(this.#isDirty ? this.#value : this.numberAttribute("value", this.min));
152
+ this.render();
153
+ }
154
+ connectedCallback() {
155
+ this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "formatValue", "parseValue", "value");
156
+ super.connectedCallback();
157
+ }
158
+ bindGestures(control, signal, grab) {
159
+ const entry = this.#bindEntry(control, signal);
160
+ control.addEventListener("pointerdown", (event) => {
161
+ if (event.button !== 0 || this.isDisabled() || entry.isOpen) return;
162
+ if (event.pointerType === "touch") event.preventDefault();
163
+ control.focus();
164
+ if (event.metaKey || event.ctrlKey) {
165
+ this.#reset();
166
+ return;
167
+ }
168
+ const fromValue = this.#value;
169
+ const axis = grab(event);
170
+ if (!axis) return;
171
+ const startPosition = axis.position(event);
172
+ const drag = {
173
+ fromValue,
174
+ isRevealed: false,
175
+ lastPosition: startPosition,
176
+ pointerId: event.pointerId,
177
+ position: axis.position,
178
+ rawFraction: this.fraction(this.#value),
179
+ revealTimer: void 0,
180
+ startPosition,
181
+ travelPx: axis.travelPx
182
+ };
183
+ drag.revealTimer = setTimeout(() => {
184
+ this.#reveal(drag);
185
+ }, revealMs);
186
+ this.#drag = drag;
187
+ control.setPointerCapture(event.pointerId);
188
+ this.toggleState("dragging", true);
189
+ }, { signal });
190
+ control.addEventListener("pointermove", (event) => {
191
+ const drag = this.#drag;
192
+ if (event.pointerId !== drag?.pointerId) return;
193
+ const position = drag.position(event);
194
+ const scale = event.shiftKey ? fineScale : 1;
195
+ const fraction = drag.rawFraction + (position - drag.lastPosition) / drag.travelPx * scale;
196
+ drag.rawFraction = this.wraps() ? wrapUnit(fraction) : clampUnit(fraction);
197
+ drag.lastPosition = position;
198
+ if (Math.abs(position - drag.startPosition) >= revealPx) this.#reveal(drag);
199
+ this.input(this.valueAt(drag.rawFraction));
200
+ }, { signal });
201
+ const endDrag = (event) => {
202
+ if (event.pointerId === this.#drag?.pointerId) this.#endDrag();
203
+ };
204
+ control.addEventListener("pointercancel", (event) => {
205
+ endDrag(event);
206
+ entry.forgetPress();
207
+ }, { signal });
208
+ control.addEventListener("lostpointercapture", endDrag, { signal });
209
+ control.addEventListener("pointerup", (event) => {
210
+ if (event.pointerId !== this.#drag?.pointerId) return;
211
+ this.#endDrag();
212
+ if (!entry.press(event)) return;
213
+ if (this.doublePress === "reset") this.#reset();
214
+ else entry.open();
215
+ }, { signal });
216
+ this.#bindKeys(control, entry, signal);
217
+ signal.addEventListener("abort", () => {
218
+ this.#endDrag();
219
+ this.#hideForKeys();
220
+ }, { once: true });
221
+ }
222
+ clampRange(next) {
223
+ const min = this.min;
224
+ if (!this.wraps()) return Math.min(this.#max(), Math.max(min, next));
225
+ const range = this.range();
226
+ if (range <= 0) return min;
227
+ const offset = Number((((next - min) % range + range) % range).toPrecision(12));
228
+ return offset >= range ? min : Number((min + offset).toPrecision(12));
229
+ }
230
+ controlRole() {
231
+ return "slider";
232
+ }
233
+ fraction(value) {
234
+ return (this.#taper() ?? linearTaper(this.min, this.#max())).position(value);
235
+ }
236
+ input(next) {
237
+ const previous = this.#value;
238
+ this.#write(next);
239
+ if (this.#value === previous) return false;
240
+ this.dispatchEvent(new Event("input", {
241
+ bubbles: true,
242
+ composed: true
243
+ }));
244
+ return true;
245
+ }
246
+ modulationFractions(amount) {
247
+ const value = this.fraction(this.#value);
248
+ const modulated = this.fraction(this.#value + amount);
249
+ return [Math.min(value, modulated), Math.max(value, modulated)];
250
+ }
251
+ originFraction() {
252
+ return this.fraction(this.origin ?? this.min);
253
+ }
254
+ positions() {
255
+ const positions = Math.round(this.range() / this.step) + (this.wraps() ? 0 : 1);
256
+ if (!this.notched || !Number.isFinite(positions) || positions < 2) return;
257
+ return positions;
258
+ }
259
+ range() {
260
+ return this.#max() - this.min;
261
+ }
262
+ renderAria(control, orientation) {
263
+ this.writeFormValue(String(this.#value), String(this.#value));
264
+ if (this.#entry?.isOpen) {
265
+ for (const name of roleAttributes) control.removeAttribute(name);
266
+ this.forwardNaming(control, false);
267
+ this.#renderReadout();
268
+ return;
269
+ }
270
+ control.setAttribute("role", this.controlRole());
271
+ control.setAttribute("aria-valuemin", String(this.min));
272
+ control.setAttribute("aria-valuemax", String(this.#max()));
273
+ control.setAttribute("aria-valuenow", String(this.#value));
274
+ if (orientation) control.setAttribute("aria-orientation", orientation);
275
+ writeAttribute(control, "aria-valuetext", this.#formatValue?.(this.#value));
276
+ this.forwardNaming(control, true);
277
+ this.#renderDisabled(control);
278
+ this.#renderReadout();
279
+ }
280
+ restoreState(state) {
281
+ this.value = Number(state);
282
+ }
283
+ valueAt(fraction) {
284
+ return (this.#taper() ?? linearTaper(this.min, this.#max())).value(fraction);
285
+ }
286
+ valueText() {
287
+ return this.#formatValue?.(this.#value) ?? String(this.#value);
288
+ }
289
+ wraps() {
290
+ return false;
291
+ }
292
+ #bindEntry(control, signal) {
293
+ const input = requireChild(control, "input", HTMLInputElement);
294
+ const bubble = control.querySelector("[popover]");
295
+ input.id = `sonic-entry-${this.#instance}`;
296
+ if (bubble) {
297
+ const anchor = `--sonic-readout-${this.#instance}`;
298
+ this.#readout = {
299
+ bubble,
300
+ text: requireChild(bubble, "span", HTMLElement)
301
+ };
302
+ control.style.setProperty("anchor-name", anchor);
303
+ bubble.style.setProperty("position-anchor", anchor);
304
+ }
305
+ const entry = new RangeEntry(input, {
306
+ commit: (text) => {
307
+ this.#commit((this.#parseValue ?? Number.parseFloat)(text));
308
+ },
309
+ text: () => this.#parseValue ? this.valueText() : String(this.#value),
310
+ toggle: (isOpen) => {
311
+ if (isOpen) this.forwardNaming(input, true);
312
+ this.toggleState("editing", isOpen);
313
+ this.render();
314
+ }
315
+ });
316
+ this.#entry = entry;
317
+ entry.bind(control, signal);
318
+ return entry;
319
+ }
320
+ #bindKeys(control, entry, signal) {
321
+ control.addEventListener("keydown", (event) => {
322
+ if (this.isDisabled() || event.defaultPrevented || event.target !== control) return;
323
+ if (event.key === "Enter") {
324
+ event.preventDefault();
325
+ entry.open();
326
+ return;
327
+ }
328
+ const next = this.#keyTarget(event.key);
329
+ if (next === void 0) return;
330
+ event.preventDefault();
331
+ this.#commit(next);
332
+ this.#revealForKeys();
333
+ }, { signal });
334
+ control.addEventListener("blur", () => {
335
+ this.#hideForKeys();
336
+ }, { signal });
337
+ }
338
+ #clamp(next) {
339
+ const min = this.min;
340
+ const step = this.step;
341
+ const stepped = step > 0 ? min + Math.round((next - min) / step) * step : next;
342
+ return this.clampRange(Number(stepped.toPrecision(12)));
343
+ }
344
+ #commit(next) {
345
+ if (this.input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
346
+ }
347
+ #end(step) {
348
+ if (!this.wraps()) return this.#max();
349
+ return this.min + (Math.ceil(this.range() / step) - 1) * step;
350
+ }
351
+ #endDrag() {
352
+ const drag = this.#drag;
353
+ if (!drag) return;
354
+ clearTimeout(drag.revealTimer);
355
+ this.#drag = void 0;
356
+ this.toggleState("dragging", false);
357
+ this.#renderReadout();
358
+ if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
359
+ }
360
+ #hideForKeys() {
361
+ clearTimeout(this.#keyRevealTimer);
362
+ if (this.#keyRevealTimer === void 0) return;
363
+ this.#keyRevealTimer = void 0;
364
+ this.#renderReadout();
365
+ }
366
+ #isValueShown() {
367
+ return (this.#drag?.isRevealed === true || this.#keyRevealTimer !== void 0) && this.readout;
368
+ }
369
+ #keyTarget(key) {
370
+ if (key === "Home") return this.min;
371
+ const step = this.step > 0 ? this.step : this.range() / 100;
372
+ if (key === "End") return this.#end(step);
373
+ if (resetKeys.has(key)) return this.default;
374
+ const steps = keySteps.get(key);
375
+ if (steps === void 0) return void 0;
376
+ const taper = this.#taper();
377
+ if (!taper) return this.#value + steps * step;
378
+ const next = this.#clamp(taper.value(taper.position(this.#value) + steps / 100));
379
+ return next === this.#value ? this.#value + Math.sign(steps) * step : next;
380
+ }
381
+ #max() {
382
+ return Math.max(this.min, this.max);
383
+ }
384
+ #renderDisabled(control) {
385
+ if (this.isDisabled()) {
386
+ control.setAttribute("aria-disabled", "true");
387
+ control.removeAttribute("tabindex");
388
+ return;
389
+ }
390
+ control.removeAttribute("aria-disabled");
391
+ control.setAttribute("tabindex", "0");
392
+ }
393
+ #renderReadout() {
394
+ const readout = this.#readout;
395
+ if (!readout || !canPopover || !this.isConnected) return;
396
+ const isEditing = this.#entry?.isOpen === true;
397
+ readout.text.hidden = isEditing;
398
+ if (!isEditing) readout.text.textContent = this.valueText();
399
+ readout.bubble.togglePopover(isEditing || this.#isValueShown());
400
+ }
401
+ #reset() {
402
+ this.#commit(this.default ?? NaN);
403
+ }
404
+ #reveal(drag) {
405
+ if (drag.isRevealed || this.#drag !== drag) return;
406
+ drag.isRevealed = true;
407
+ this.#renderReadout();
408
+ }
409
+ #revealForKeys() {
410
+ clearTimeout(this.#keyRevealTimer);
411
+ this.#keyRevealTimer = setTimeout(() => {
412
+ this.#hideForKeys();
413
+ }, keyRevealMs);
414
+ this.#renderReadout();
415
+ }
416
+ #taper() {
417
+ if (this.notched || this.wraps()) return void 0;
418
+ const min = this.min;
419
+ const max = this.#max();
420
+ return (this.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, this.midpoint ?? NaN);
421
+ }
422
+ #write(next) {
423
+ if (!Number.isFinite(next)) return;
424
+ this.#isDirty = true;
425
+ const clamped = this.#clamp(next);
426
+ if (clamped === this.#value) return;
427
+ this.#value = clamped;
428
+ this.render();
429
+ }
430
+ };
431
+
432
+ //#endregion
433
+ export { SonicRangeElement };
@@ -12,6 +12,7 @@ var RangeEntry = class {
12
12
  #input;
13
13
  #isOpen = false;
14
14
  #lastPress;
15
+ #openedText = "";
15
16
  #options;
16
17
  constructor(input, options) {
17
18
  this.#input = input;
@@ -40,7 +41,7 @@ var RangeEntry = class {
40
41
  this.#isOpen = false;
41
42
  this.#input.hidden = true;
42
43
  this.#options.toggle(false);
43
- if (isCommitting) this.#options.commit(this.#input.value);
44
+ if (isCommitting && this.#input.value !== this.#openedText) this.#options.commit(this.#input.value);
44
45
  }
45
46
  forgetPress() {
46
47
  this.#lastPress = void 0;
@@ -48,7 +49,8 @@ var RangeEntry = class {
48
49
  open() {
49
50
  const input = this.#input;
50
51
  this.#isOpen = true;
51
- input.value = this.#options.text();
52
+ this.#openedText = this.#options.text();
53
+ input.value = this.#openedText;
52
54
  input.hidden = false;
53
55
  this.#options.toggle(true);
54
56
  input.focus();
@@ -69,5 +71,4 @@ var RangeEntry = class {
69
71
  };
70
72
 
71
73
  //#endregion
72
- export { RangeEntry };
73
- //# sourceMappingURL=range-entry.js.map
74
+ export { RangeEntry };
@@ -0,0 +1,173 @@
1
+ import { template } from "../lib/render.js";
2
+ import { SonicFormElement } from "./form-element.js";
3
+ import { copyNode } from "../lib/copy-node.js";
4
+
5
+ //#region src/elements/segmented.ts
6
+ const renderSegmented = template(`<div class="sonic-segmented" role="radiogroup"></div>`, HTMLDivElement);
7
+ const renderSegment = template(`
8
+ <button class="sonic-segmented-segment" role="radio" type="button">
9
+ <span class="sonic-segmented-cap"></span>
10
+ </button>
11
+ `, HTMLButtonElement);
12
+ const keySteps = /* @__PURE__ */ new Map([
13
+ ["ArrowDown", 1],
14
+ ["ArrowLeft", -1],
15
+ ["ArrowRight", 1],
16
+ ["ArrowUp", -1]
17
+ ]);
18
+ function isUnder(segment, x, y) {
19
+ const box = segment.getBoundingClientRect();
20
+ return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
21
+ }
22
+ function segmentValue(segment) {
23
+ return segment.querySelector("[data-sonic-value]")?.dataset.sonicValue;
24
+ }
25
+ var SonicSegmented = class extends SonicFormElement {
26
+ static observedAttributes = [...SonicFormElement.observedAttributes, "value"];
27
+ get value() {
28
+ return this.#value;
29
+ }
30
+ set value(next) {
31
+ if (next === this.#value) return;
32
+ this.#value = next;
33
+ this.render();
34
+ }
35
+ #press;
36
+ #segmented = renderSegmented();
37
+ #value = "";
38
+ attributeChangedCallback(name) {
39
+ if (name === "value") this.#value = this.getAttribute("value") ?? "";
40
+ this.render();
41
+ }
42
+ connect(signal) {
43
+ const segmented = this.#segmented;
44
+ this.upgradeProperties("value");
45
+ this.mirrorChildren({
46
+ control: segmented,
47
+ copy: (options) => {
48
+ this.#copyOptions(options);
49
+ },
50
+ isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]")
51
+ }, signal);
52
+ this.checkStyles(segmented, "segmented.css");
53
+ segmented.addEventListener("click", (event) => {
54
+ if (event instanceof PointerEvent && event.pointerType !== "") return;
55
+ const segment = this.#segmentOf(event.target);
56
+ if (segment) this.#select(segment);
57
+ }, { signal });
58
+ this.#bindPress(segmented, signal);
59
+ segmented.addEventListener("keydown", (event) => {
60
+ if (event.defaultPrevented) return;
61
+ const segment = this.#segmentOf(event.target);
62
+ const next = segment && this.#keyTarget(segment, event.key);
63
+ if (!next) return;
64
+ event.preventDefault();
65
+ this.#select(next);
66
+ next.focus();
67
+ }, { signal });
68
+ }
69
+ focusTarget() {
70
+ return this.#segments().find((segment) => segment.tabIndex === 0);
71
+ }
72
+ render() {
73
+ const segmented = this.#segmented;
74
+ const segments = this.#segments();
75
+ const checked = segments.find((segment) => segmentValue(segment) === this.#value);
76
+ const focusable = checked ?? segments[0];
77
+ for (const segment of segments) {
78
+ segment.disabled = this.isDisabled();
79
+ segment.setAttribute("aria-checked", String(segment === checked));
80
+ segment.tabIndex = segment === focusable ? 0 : -1;
81
+ }
82
+ this.forwardNaming(segmented, true);
83
+ this.writeFormValue(checked ? this.#value : null, this.#value);
84
+ }
85
+ restoreState(state) {
86
+ this.value = state;
87
+ }
88
+ #bindPress(segmented, signal) {
89
+ const isOwn = (event) => event.pointerId === this.#press?.pointerId;
90
+ const release = (event) => {
91
+ if (isOwn(event)) this.#hold(void 0);
92
+ };
93
+ segmented.addEventListener("pointerdown", (event) => {
94
+ const segment = this.#segmentOf(event.target);
95
+ if (!segment || event.button !== 0 || this.isDisabled()) return;
96
+ segmented.setPointerCapture(event.pointerId);
97
+ this.#hold({
98
+ pointerId: event.pointerId,
99
+ segment
100
+ });
101
+ }, { signal });
102
+ segmented.addEventListener("pointermove", (event) => {
103
+ if (!isOwn(event)) return;
104
+ this.#hold({
105
+ pointerId: event.pointerId,
106
+ segment: this.#segments().find((segment) => isUnder(segment, event.clientX, event.clientY))
107
+ });
108
+ }, { signal });
109
+ segmented.addEventListener("pointerup", (event) => {
110
+ const segment = isOwn(event) ? this.#press?.segment : void 0;
111
+ release(event);
112
+ if (!segment || this.isDisabled()) return;
113
+ this.#select(segment);
114
+ if (segmented.matches(":focus-within")) segment.focus();
115
+ }, { signal });
116
+ segmented.addEventListener("pointercancel", release, { signal });
117
+ segmented.addEventListener("lostpointercapture", release, { signal });
118
+ signal.addEventListener("abort", () => {
119
+ this.#hold(void 0);
120
+ }, { once: true });
121
+ }
122
+ #copyOptions(options) {
123
+ const segmented = this.#segmented;
124
+ const segments = this.#segments();
125
+ const focused = this.#segmentOf(segmented.querySelector(":scope > :focus"));
126
+ const focusedValue = focused && segmentValue(focused);
127
+ for (const [index, option] of options.entries()) {
128
+ let segment = segments[index];
129
+ if (!segment) {
130
+ segment = renderSegment();
131
+ segmented.append(segment);
132
+ }
133
+ segment.firstElementChild?.replaceChildren(copyNode(option));
134
+ }
135
+ for (const segment of segments.slice(options.length)) segment.remove();
136
+ this.render();
137
+ if (focused) this.#refocus(focused, focusedValue);
138
+ }
139
+ #hold(press) {
140
+ this.#press = press;
141
+ for (const segment of this.#segments()) segment.toggleAttribute("data-sonic-pressed", segment === press?.segment);
142
+ }
143
+ #keyTarget(segment, key) {
144
+ const segments = this.#segments();
145
+ if (key === "Home") return segments[0];
146
+ if (key === "End") return segments.at(-1);
147
+ const step = keySteps.get(key);
148
+ if (step === void 0) return void 0;
149
+ return segments[(segments.indexOf(segment) + step + segments.length) % segments.length];
150
+ }
151
+ #refocus(focused, value) {
152
+ const segments = this.#segments();
153
+ const next = segments.find((segment) => segmentValue(segment) === value) ?? segments.find((segment) => segment.tabIndex === 0);
154
+ if (next !== focused) next?.focus();
155
+ }
156
+ #segmentOf(target) {
157
+ if (!(target instanceof Element)) return void 0;
158
+ const segment = target.closest(".sonic-segmented-segment");
159
+ return segment instanceof HTMLButtonElement && segment.parentElement === this.#segmented ? segment : void 0;
160
+ }
161
+ #segments() {
162
+ return [...this.#segmented.children].filter((child) => child instanceof HTMLButtonElement);
163
+ }
164
+ #select(segment) {
165
+ const next = segmentValue(segment);
166
+ if (next === void 0 || next === this.#value) return;
167
+ this.value = next;
168
+ this.dispatchEvent(new Event("change", { bubbles: true }));
169
+ }
170
+ };
171
+
172
+ //#endregion
173
+ export { SonicSegmented };