@xsynaptic/sonic-ui 0.1.0 → 0.2.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 (117) 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/segmented.js +7 -0
  6. package/dist/default/define/slider.js +7 -0
  7. package/dist/default/elements/define-once.js +7 -0
  8. package/dist/{elements → default/elements}/dial.js +15 -3
  9. package/dist/default/elements/form-element.js +56 -0
  10. package/dist/default/elements/key.js +144 -0
  11. package/dist/{elements → default/elements}/meter.js +27 -16
  12. package/dist/default/elements/range-element.js +410 -0
  13. package/dist/{elements → default/elements}/range-entry.js +5 -4
  14. package/dist/default/elements/segmented.js +173 -0
  15. package/dist/{elements → default/elements}/slider.js +23 -8
  16. package/dist/default/elements/sonic-element.js +153 -0
  17. package/dist/{index.js → default/index.js} +2 -1
  18. package/dist/default/lib/copy-node.js +30 -0
  19. package/dist/default/lib/percent.js +12 -0
  20. package/dist/default/lib/read-px-property.js +8 -0
  21. package/dist/{lib → default/lib}/render.js +9 -2
  22. package/dist/default/lib/taper.js +32 -0
  23. package/dist/default/lib/write-attribute.js +11 -0
  24. package/dist/dev/define/dial.js.map +1 -0
  25. package/dist/dev/define/key.js.map +1 -0
  26. package/dist/dev/define/meter.js.map +1 -0
  27. package/dist/dev/define/segmented.js.map +1 -0
  28. package/dist/dev/define/slider.js.map +1 -0
  29. package/dist/dev/define.js +5 -0
  30. package/dist/dev/elements/define-once.js.map +1 -0
  31. package/dist/{elements → dev/elements}/dial.d.ts +4 -0
  32. package/dist/dev/elements/dial.js +70 -0
  33. package/dist/dev/elements/dial.js.map +1 -0
  34. package/dist/dev/elements/form-element.d.ts +22 -0
  35. package/dist/dev/elements/form-element.js +57 -0
  36. package/dist/dev/elements/form-element.js.map +1 -0
  37. package/dist/dev/elements/key.d.ts +30 -0
  38. package/dist/{elements → dev/elements}/key.js +71 -25
  39. package/dist/dev/elements/key.js.map +1 -0
  40. package/dist/{elements → dev/elements}/meter.d.ts +6 -0
  41. package/dist/dev/elements/meter.js +131 -0
  42. package/dist/dev/elements/meter.js.map +1 -0
  43. package/dist/{elements → dev/elements}/range-element.d.ts +25 -5
  44. package/dist/{elements → dev/elements}/range-element.js +116 -53
  45. package/dist/dev/elements/range-element.js.map +1 -0
  46. package/dist/dev/elements/range-entry.js +75 -0
  47. package/dist/dev/elements/range-entry.js.map +1 -0
  48. package/dist/{elements → dev/elements}/segmented.d.ts +5 -2
  49. package/dist/dev/elements/segmented.js +174 -0
  50. package/dist/dev/elements/segmented.js.map +1 -0
  51. package/dist/{elements → dev/elements}/slider.d.ts +6 -0
  52. package/dist/dev/elements/slider.js +86 -0
  53. package/dist/dev/elements/slider.js.map +1 -0
  54. package/dist/dev/elements/sonic-element.d.ts +33 -0
  55. package/dist/dev/elements/sonic-element.js +169 -0
  56. package/dist/dev/elements/sonic-element.js.map +1 -0
  57. package/dist/{index.d.ts → dev/index.d.ts} +2 -1
  58. package/dist/dev/index.js +8 -0
  59. package/dist/dev/lib/copy-node.js +31 -0
  60. package/dist/dev/lib/copy-node.js.map +1 -0
  61. package/dist/dev/lib/percent.d.ts +5 -0
  62. package/dist/dev/lib/percent.js +13 -0
  63. package/dist/dev/lib/percent.js.map +1 -0
  64. package/dist/dev/lib/read-px-property.js.map +1 -0
  65. package/dist/dev/lib/render.js +27 -0
  66. package/dist/dev/lib/render.js.map +1 -0
  67. package/dist/dev/lib/taper.js.map +1 -0
  68. package/dist/dev/lib/write-attribute.js.map +1 -0
  69. package/dist/skins/ivory.css +3 -3
  70. package/dist/skins/slate.css +4 -4
  71. package/dist/styles/controls.css +0 -1
  72. package/dist/styles/dial.css +33 -13
  73. package/dist/styles/key.css +30 -10
  74. package/dist/styles/material.css +58 -10
  75. package/dist/styles/meter.css +25 -5
  76. package/dist/styles/screen.css +7 -1
  77. package/dist/styles/segmented.css +24 -7
  78. package/dist/styles/slider.css +17 -4
  79. package/package.json +13 -12
  80. package/dist/define/dial.js.map +0 -1
  81. package/dist/define/key.js.map +0 -1
  82. package/dist/define/meter.js.map +0 -1
  83. package/dist/define/segmented.js.map +0 -1
  84. package/dist/define/slider.js.map +0 -1
  85. package/dist/elements/define-once.js.map +0 -1
  86. package/dist/elements/dial.js.map +0 -1
  87. package/dist/elements/key.d.ts +0 -17
  88. package/dist/elements/key.js.map +0 -1
  89. package/dist/elements/meter.js.map +0 -1
  90. package/dist/elements/range-element.js.map +0 -1
  91. package/dist/elements/range-entry.js.map +0 -1
  92. package/dist/elements/segmented.js +0 -104
  93. package/dist/elements/segmented.js.map +0 -1
  94. package/dist/elements/slider.js.map +0 -1
  95. package/dist/elements/sonic-element.d.ts +0 -20
  96. package/dist/elements/sonic-element.js +0 -94
  97. package/dist/elements/sonic-element.js.map +0 -1
  98. package/dist/lib/read-px-property.js.map +0 -1
  99. package/dist/lib/render.js.map +0 -1
  100. package/dist/lib/taper.js.map +0 -1
  101. package/dist/lib/write-attribute.js.map +0 -1
  102. /package/dist/{define.js → default/define.js} +0 -0
  103. /package/dist/{define → dev/define}/dial.d.ts +0 -0
  104. /package/dist/{define → dev/define}/dial.js +0 -0
  105. /package/dist/{define → dev/define}/key.d.ts +0 -0
  106. /package/dist/{define → dev/define}/key.js +0 -0
  107. /package/dist/{define → dev/define}/meter.d.ts +0 -0
  108. /package/dist/{define → dev/define}/meter.js +0 -0
  109. /package/dist/{define → dev/define}/segmented.d.ts +0 -0
  110. /package/dist/{define → dev/define}/segmented.js +0 -0
  111. /package/dist/{define → dev/define}/slider.d.ts +0 -0
  112. /package/dist/{define → dev/define}/slider.js +0 -0
  113. /package/dist/{define.d.ts → dev/define.d.ts} +0 -0
  114. /package/dist/{elements → dev/elements}/define-once.js +0 -0
  115. /package/dist/{lib → dev/lib}/read-px-property.js +0 -0
  116. /package/dist/{lib → dev/lib}/taper.js +0 -0
  117. /package/dist/{lib → dev/lib}/write-attribute.js +0 -0
@@ -0,0 +1,410 @@
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 } 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 sliderAttributes = [
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
+ drag.rawFraction = clampUnit(drag.rawFraction + (position - drag.lastPosition) / drag.travelPx * scale);
196
+ drag.lastPosition = position;
197
+ if (Math.abs(position - drag.startPosition) >= revealPx) this.#reveal(drag);
198
+ this.input(this.valueAt(drag.rawFraction));
199
+ }, { signal });
200
+ const endDrag = (event) => {
201
+ if (event.pointerId === this.#drag?.pointerId) this.#endDrag();
202
+ };
203
+ control.addEventListener("pointercancel", (event) => {
204
+ endDrag(event);
205
+ entry.forgetPress();
206
+ }, { signal });
207
+ control.addEventListener("lostpointercapture", endDrag, { signal });
208
+ control.addEventListener("pointerup", (event) => {
209
+ if (event.pointerId !== this.#drag?.pointerId) return;
210
+ this.#endDrag();
211
+ if (!entry.press(event)) return;
212
+ if (this.doublePress === "reset") this.#reset();
213
+ else entry.open();
214
+ }, { signal });
215
+ this.#bindKeys(control, entry, signal);
216
+ signal.addEventListener("abort", () => {
217
+ this.#endDrag();
218
+ this.#hideForKeys();
219
+ }, { once: true });
220
+ }
221
+ clampRange(next) {
222
+ return Math.min(this.#max(), Math.max(this.min, next));
223
+ }
224
+ fraction(value) {
225
+ return (this.#taper() ?? linearTaper(this.min, this.#max())).position(value);
226
+ }
227
+ input(next) {
228
+ const previous = this.#value;
229
+ this.#write(next);
230
+ if (this.#value === previous) return false;
231
+ this.dispatchEvent(new Event("input", {
232
+ bubbles: true,
233
+ composed: true
234
+ }));
235
+ return true;
236
+ }
237
+ originFraction() {
238
+ return this.fraction(this.origin ?? this.min);
239
+ }
240
+ positions() {
241
+ const positions = Math.round(this.range() / this.step) + 1;
242
+ if (!this.notched || !Number.isFinite(positions) || positions < 2) return;
243
+ return positions;
244
+ }
245
+ range() {
246
+ return this.#max() - this.min;
247
+ }
248
+ renderAria(control, orientation) {
249
+ this.writeFormValue(String(this.#value), String(this.#value));
250
+ if (this.#entry?.isOpen) {
251
+ for (const name of sliderAttributes) control.removeAttribute(name);
252
+ this.forwardNaming(control, false);
253
+ this.#renderReadout();
254
+ return;
255
+ }
256
+ control.setAttribute("role", "slider");
257
+ control.setAttribute("aria-valuemin", String(this.min));
258
+ control.setAttribute("aria-valuemax", String(this.#max()));
259
+ control.setAttribute("aria-valuenow", String(this.#value));
260
+ if (orientation) control.setAttribute("aria-orientation", orientation);
261
+ writeAttribute(control, "aria-valuetext", this.#formatValue?.(this.#value));
262
+ this.forwardNaming(control, true);
263
+ this.#renderDisabled(control);
264
+ this.#renderReadout();
265
+ }
266
+ restoreState(state) {
267
+ this.value = Number(state);
268
+ }
269
+ valueAt(fraction) {
270
+ return (this.#taper() ?? linearTaper(this.min, this.#max())).value(fraction);
271
+ }
272
+ #bindEntry(control, signal) {
273
+ const input = requireChild(control, "input", HTMLInputElement);
274
+ const bubble = requireChild(control, "[popover]", HTMLElement);
275
+ const anchor = `--sonic-readout-${this.#instance}`;
276
+ input.id = `sonic-entry-${this.#instance}`;
277
+ this.#readout = {
278
+ bubble,
279
+ text: requireChild(bubble, "span", HTMLElement)
280
+ };
281
+ control.style.setProperty("anchor-name", anchor);
282
+ bubble.style.setProperty("position-anchor", anchor);
283
+ const entry = new RangeEntry(input, {
284
+ commit: (text) => {
285
+ this.#commit((this.#parseValue ?? Number.parseFloat)(text));
286
+ },
287
+ text: () => this.#parseValue ? this.#valueText() : String(this.#value),
288
+ toggle: (isOpen) => {
289
+ if (isOpen) this.forwardNaming(input, true);
290
+ this.toggleState("editing", isOpen);
291
+ this.render();
292
+ }
293
+ });
294
+ this.#entry = entry;
295
+ entry.bind(control, signal);
296
+ return entry;
297
+ }
298
+ #bindKeys(control, entry, signal) {
299
+ control.addEventListener("keydown", (event) => {
300
+ if (this.isDisabled() || event.defaultPrevented || event.target !== control) return;
301
+ if (event.key === "Enter") {
302
+ event.preventDefault();
303
+ entry.open();
304
+ return;
305
+ }
306
+ const next = this.#keyTarget(event.key);
307
+ if (next === void 0) return;
308
+ event.preventDefault();
309
+ this.#commit(next);
310
+ this.#revealForKeys();
311
+ }, { signal });
312
+ control.addEventListener("blur", () => {
313
+ this.#hideForKeys();
314
+ }, { signal });
315
+ }
316
+ #clamp(next) {
317
+ const min = this.min;
318
+ const step = this.step;
319
+ const stepped = step > 0 ? min + Math.round((next - min) / step) * step : next;
320
+ return this.clampRange(Number(stepped.toPrecision(12)));
321
+ }
322
+ #commit(next) {
323
+ if (this.input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
324
+ }
325
+ #endDrag() {
326
+ const drag = this.#drag;
327
+ if (!drag) return;
328
+ clearTimeout(drag.revealTimer);
329
+ this.#drag = void 0;
330
+ this.toggleState("dragging", false);
331
+ this.#renderReadout();
332
+ if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
333
+ }
334
+ #hideForKeys() {
335
+ clearTimeout(this.#keyRevealTimer);
336
+ if (this.#keyRevealTimer === void 0) return;
337
+ this.#keyRevealTimer = void 0;
338
+ this.#renderReadout();
339
+ }
340
+ #isValueShown() {
341
+ return (this.#drag?.isRevealed === true || this.#keyRevealTimer !== void 0) && this.readout;
342
+ }
343
+ #keyTarget(key) {
344
+ if (key === "Home") return this.min;
345
+ if (key === "End") return this.#max();
346
+ if (resetKeys.has(key)) return this.default;
347
+ const steps = keySteps.get(key);
348
+ if (steps === void 0) return void 0;
349
+ const step = this.step > 0 ? this.step : this.range() / 100;
350
+ const taper = this.#taper();
351
+ if (!taper) return this.#value + steps * step;
352
+ const next = this.#clamp(taper.value(taper.position(this.#value) + steps / 100));
353
+ return next === this.#value ? this.#value + Math.sign(steps) * step : next;
354
+ }
355
+ #max() {
356
+ return Math.max(this.min, this.max);
357
+ }
358
+ #renderDisabled(control) {
359
+ if (this.isDisabled()) {
360
+ control.setAttribute("aria-disabled", "true");
361
+ control.removeAttribute("tabindex");
362
+ return;
363
+ }
364
+ control.removeAttribute("aria-disabled");
365
+ control.setAttribute("tabindex", "0");
366
+ }
367
+ #renderReadout() {
368
+ const readout = this.#readout;
369
+ if (!readout || !canPopover || !this.isConnected) return;
370
+ const isEditing = this.#entry?.isOpen === true;
371
+ readout.text.hidden = isEditing;
372
+ if (!isEditing) readout.text.textContent = this.#valueText();
373
+ readout.bubble.togglePopover(isEditing || this.#isValueShown());
374
+ }
375
+ #reset() {
376
+ this.#commit(this.default ?? NaN);
377
+ }
378
+ #reveal(drag) {
379
+ if (drag.isRevealed || this.#drag !== drag) return;
380
+ drag.isRevealed = true;
381
+ this.#renderReadout();
382
+ }
383
+ #revealForKeys() {
384
+ clearTimeout(this.#keyRevealTimer);
385
+ this.#keyRevealTimer = setTimeout(() => {
386
+ this.#hideForKeys();
387
+ }, keyRevealMs);
388
+ this.#renderReadout();
389
+ }
390
+ #taper() {
391
+ if (this.notched) return void 0;
392
+ const min = this.min;
393
+ const max = this.#max();
394
+ return (this.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, this.midpoint ?? NaN);
395
+ }
396
+ #valueText() {
397
+ return this.#formatValue?.(this.#value) ?? String(this.#value);
398
+ }
399
+ #write(next) {
400
+ if (!Number.isFinite(next)) return;
401
+ this.#isDirty = true;
402
+ const clamped = this.#clamp(next);
403
+ if (clamped === this.#value) return;
404
+ this.#value = clamped;
405
+ this.render();
406
+ }
407
+ };
408
+
409
+ //#endregion
410
+ 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 };
@@ -22,7 +22,23 @@ const renderSlider = template(`
22
22
  `, HTMLDivElement);
23
23
  var SonicSlider = class extends SonicRangeElement {
24
24
  static observedAttributes = [...SonicRangeElement.observedAttributes, "orientation"];
25
+ get groovePress() {
26
+ return this.getAttribute("groove-press") === "none" ? "none" : "jump";
27
+ }
28
+ set groovePress(gesture) {
29
+ this.reflect("groove-press", gesture);
30
+ }
31
+ get orientation() {
32
+ return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
33
+ }
34
+ set orientation(direction) {
35
+ this.reflect("orientation", direction);
36
+ }
25
37
  #slider = renderSlider();
38
+ connectedCallback() {
39
+ this.upgradeProperties("groovePress", "orientation");
40
+ super.connectedCallback();
41
+ }
26
42
  connect(signal) {
27
43
  const slider = this.#slider;
28
44
  const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
@@ -32,11 +48,14 @@ var SonicSlider = class extends SonicRangeElement {
32
48
  this.bindGestures(slider, signal, (event) => {
33
49
  const axis = this.#axis(slider, cap);
34
50
  if (event.target instanceof Node && cap.contains(event.target)) return axis;
35
- if (this.getAttribute("groove-press") === "none") return;
51
+ if (this.groovePress === "none") return;
36
52
  this.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));
37
53
  return axis;
38
54
  });
39
55
  }
56
+ focusTarget() {
57
+ return this.#slider;
58
+ }
40
59
  render() {
41
60
  const slider = this.#slider;
42
61
  const positions = this.positions();
@@ -44,12 +63,12 @@ var SonicSlider = class extends SonicRangeElement {
44
63
  slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
45
64
  if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
46
65
  else slider.style.setProperty("--_sonic-slider-positions", String(positions));
47
- this.renderAria(slider, this.#isVertical() ? "vertical" : "horizontal");
66
+ this.renderAria(slider, this.orientation);
48
67
  }
49
68
  #axis(slider, cap) {
50
69
  const track = slider.getBoundingClientRect();
51
70
  const capBox = cap.getBoundingClientRect();
52
- if (this.#isVertical()) return {
71
+ if (this.orientation === "vertical") return {
53
72
  position: (event) => -event.clientY,
54
73
  startPx: capBox.height / 2 - track.bottom,
55
74
  travelPx: Math.max(1, track.height - capBox.height)
@@ -60,11 +79,7 @@ var SonicSlider = class extends SonicRangeElement {
60
79
  travelPx: Math.max(1, track.width - capBox.width)
61
80
  };
62
81
  }
63
- #isVertical() {
64
- return this.getAttribute("orientation") === "vertical";
65
- }
66
82
  };
67
83
 
68
84
  //#endregion
69
- export { SonicSlider };
70
- //# sourceMappingURL=slider.js.map
85
+ export { SonicSlider };