@xsynaptic/sonic-ui 0.10.4 → 0.13.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 (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +39 -15
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +7 -1
  6. package/dist/default/elements/dial.js +7 -1
  7. package/dist/default/elements/display-element.js +5 -1
  8. package/dist/default/elements/envelope.js +6 -1
  9. package/dist/default/elements/meter.js +1 -1
  10. package/dist/default/elements/number.js +7 -1
  11. package/dist/default/elements/position-group.js +163 -0
  12. package/dist/default/elements/radio-group.js +34 -12
  13. package/dist/default/elements/segmented.js +5 -1
  14. package/dist/default/elements/slider.js +7 -1
  15. package/dist/default/elements/sonic-element.js +1 -1
  16. package/dist/default/elements/switch.js +34 -168
  17. package/dist/default/elements/toggle.js +193 -0
  18. package/dist/default/elements/value-element.js +3 -0
  19. package/dist/default/elements/value-gestures.js +7 -0
  20. package/dist/default/elements/wave-element.js +8 -1
  21. package/dist/default/elements/waveform.js +159 -13
  22. package/dist/default/elements/wavestrip.js +4 -0
  23. package/dist/default/elements/xy.js +7 -1
  24. package/dist/default/index.js +2 -1
  25. package/dist/default/lib/key-press.js +27 -0
  26. package/dist/default/lib/toggle-travel.js +19 -0
  27. package/dist/default/lib/zoom-gesture.js +19 -0
  28. package/dist/dev/define/toggle.d.ts +2 -0
  29. package/dist/dev/define/toggle.js +8 -0
  30. package/dist/dev/define/toggle.js.map +1 -0
  31. package/dist/dev/define.d.ts +3 -1
  32. package/dist/dev/define.js +1 -0
  33. package/dist/dev/elements/button.js +7 -1
  34. package/dist/dev/elements/button.js.map +1 -1
  35. package/dist/dev/elements/dial.js +7 -1
  36. package/dist/dev/elements/dial.js.map +1 -1
  37. package/dist/dev/elements/display-element.js +5 -1
  38. package/dist/dev/elements/display-element.js.map +1 -1
  39. package/dist/dev/elements/envelope.js +6 -1
  40. package/dist/dev/elements/envelope.js.map +1 -1
  41. package/dist/dev/elements/meter.js +1 -1
  42. package/dist/dev/elements/meter.js.map +1 -1
  43. package/dist/dev/elements/number.js +7 -1
  44. package/dist/dev/elements/number.js.map +1 -1
  45. package/dist/dev/elements/position-group.d.ts +38 -0
  46. package/dist/dev/elements/position-group.js +164 -0
  47. package/dist/dev/elements/position-group.js.map +1 -0
  48. package/dist/dev/elements/radio-group.d.ts +4 -0
  49. package/dist/dev/elements/radio-group.js +34 -12
  50. package/dist/dev/elements/radio-group.js.map +1 -1
  51. package/dist/dev/elements/segmented.js +5 -1
  52. package/dist/dev/elements/segmented.js.map +1 -1
  53. package/dist/dev/elements/slider.js +7 -1
  54. package/dist/dev/elements/slider.js.map +1 -1
  55. package/dist/dev/elements/sonic-element.d.ts +16 -1
  56. package/dist/dev/elements/sonic-element.js +27 -5
  57. package/dist/dev/elements/sonic-element.js.map +1 -1
  58. package/dist/dev/elements/switch.d.ts +4 -19
  59. package/dist/dev/elements/switch.js +34 -168
  60. package/dist/dev/elements/switch.js.map +1 -1
  61. package/dist/dev/elements/toggle.d.ts +21 -0
  62. package/dist/dev/elements/toggle.js +201 -0
  63. package/dist/dev/elements/toggle.js.map +1 -0
  64. package/dist/dev/elements/value-element.d.ts +1 -0
  65. package/dist/dev/elements/value-element.js +3 -0
  66. package/dist/dev/elements/value-element.js.map +1 -1
  67. package/dist/dev/elements/value-gestures.js +7 -0
  68. package/dist/dev/elements/value-gestures.js.map +1 -1
  69. package/dist/dev/elements/wave-element.js +8 -1
  70. package/dist/dev/elements/wave-element.js.map +1 -1
  71. package/dist/dev/elements/waveform.d.ts +11 -1
  72. package/dist/dev/elements/waveform.js +159 -13
  73. package/dist/dev/elements/waveform.js.map +1 -1
  74. package/dist/dev/elements/wavestrip.d.ts +1 -0
  75. package/dist/dev/elements/wavestrip.js +4 -0
  76. package/dist/dev/elements/wavestrip.js.map +1 -1
  77. package/dist/dev/elements/xy.js +7 -1
  78. package/dist/dev/elements/xy.js.map +1 -1
  79. package/dist/dev/index.d.ts +3 -2
  80. package/dist/dev/index.js +2 -1
  81. package/dist/dev/lib/key-press.js +28 -0
  82. package/dist/dev/lib/key-press.js.map +1 -0
  83. package/dist/dev/lib/toggle-travel.js +20 -0
  84. package/dist/dev/lib/toggle-travel.js.map +1 -0
  85. package/dist/dev/lib/zoom-gesture.js +20 -0
  86. package/dist/dev/lib/zoom-gesture.js.map +1 -0
  87. package/dist/styles/button.css +5 -3
  88. package/dist/styles/controls.css +1 -0
  89. package/dist/styles/dial.css +3 -1
  90. package/dist/styles/envelope.css +3 -1
  91. package/dist/styles/led.css +19 -14
  92. package/dist/styles/material/arc.css +21 -0
  93. package/dist/styles/material/bracket.css +31 -0
  94. package/dist/styles/material/cap.css +179 -0
  95. package/dist/styles/material/core.css +103 -0
  96. package/dist/styles/material/glass.css +62 -0
  97. package/dist/styles/material/groove.css +10 -0
  98. package/dist/styles/material/keycap.css +95 -0
  99. package/dist/styles/material/lens.css +11 -0
  100. package/dist/styles/material/pane.css +97 -0
  101. package/dist/styles/material/readout.css +87 -0
  102. package/dist/styles/material/scale.css +30 -0
  103. package/dist/styles/material/value.css +43 -0
  104. package/dist/styles/material/well.css +24 -0
  105. package/dist/styles/material.css +15 -745
  106. package/dist/styles/meter.css +10 -7
  107. package/dist/styles/number.css +3 -1
  108. package/dist/styles/panel.css +3 -1
  109. package/dist/styles/ring.css +3 -1
  110. package/dist/styles/screen.css +3 -1
  111. package/dist/styles/segmented.css +11 -5
  112. package/dist/styles/slider.css +3 -1
  113. package/dist/styles/spectrum.css +5 -3
  114. package/dist/styles/switch.css +9 -3
  115. package/dist/styles/toggle.css +483 -0
  116. package/dist/styles/waveform.css +3 -1
  117. package/dist/styles/wavestrip.css +3 -1
  118. package/dist/styles/xy.css +3 -1
  119. package/package.json +3 -1
@@ -1,9 +1,7 @@
1
- import { clamp } from "../lib/math.js";
2
- import { writeAttribute } from "../lib/write-attribute.js";
3
- import { bindHold } from "../lib/hold.js";
4
1
  import { requireChild, template } from "../lib/render.js";
5
2
  import { bindDrag } from "../lib/pointer-drag.js";
6
- import { SonicRadioGroupElement, isUnder, optionValue } from "./radio-group.js";
3
+ import { isUnder, optionValue } from "./radio-group.js";
4
+ import { SonicPositionGroupElement } from "./position-group.js";
7
5
 
8
6
  //#region src/elements/switch.ts
9
7
  const renderSwitch = template(`
@@ -19,165 +17,59 @@ const renderPosition = template(`
19
17
  `, HTMLButtonElement);
20
18
  const renderBare = template(`<button class="sonic-switch-position" role="switch" type="button"></button>`, HTMLButtonElement);
21
19
  const pressCenterZone = .08;
22
- function pressedIndex(index, count, offset) {
23
- if (count === 2) return index === 0 ? 1 : 0;
24
- const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
25
- const next = index + side;
26
- return next < 0 || next >= count ? index - side : next;
27
- }
28
- var SonicSwitch = class extends SonicRadioGroupElement {
29
- static observedAttributes = [
30
- ...SonicRadioGroupElement.observedAttributes,
31
- "checked",
32
- "orientation"
33
- ];
34
- get checked() {
35
- return this.#isDirty ? this.#checked : this.defaultChecked;
36
- }
37
- set checked(isChecked) {
38
- this.#isDirty = true;
39
- this.#checked = isChecked === true;
40
- this.render();
41
- }
42
- get defaultChecked() {
43
- return this.hasAttribute("checked");
44
- }
45
- set defaultChecked(isChecked) {
46
- this.reflect("checked", isChecked);
47
- }
48
- get orientation() {
49
- return this.getAttribute("orientation") === "horizontal" ? "horizontal" : "vertical";
50
- }
51
- set orientation(direction) {
52
- this.reflect("orientation", direction);
53
- }
54
- get value() {
55
- const value = super.value;
56
- return value === "" && this.#isBare() ? "on" : value;
57
- }
58
- set value(next) {
59
- super.value = next;
60
- }
20
+ var SonicSwitch = class extends SonicPositionGroupElement {
21
+ bare = renderBare();
22
+ defaultOrientation = "vertical";
61
23
  group = renderSwitch();
62
- #bare = renderBare();
63
24
  #bat = requireChild(this.group, ".sonic-switch-bat", HTMLSpanElement);
64
- #checked = false;
65
- #holding;
66
25
  #isClickSwallowed = false;
67
- #isDirty = false;
68
- attributeChangedCallback(name) {
69
- if (name === "checked") this.#isDirty = false;
70
- if (this.isDisabled()) this.#holding?.release();
71
- super.attributeChangedCallback(name);
72
- }
73
- formResetCallback() {
74
- this.#isDirty = false;
75
- super.formResetCallback();
76
- }
77
- activate() {
78
- if (this.#isBare()) this.#bare.click();
79
- else super.activate();
80
- }
81
- chooseByClick(option) {
82
- super.chooseByClick(option);
83
- if (this.#isMomentary(option)) this.#springBack();
84
- }
85
- chooseByKey(option, event) {
86
- if (!this.#isMomentary(option)) {
87
- super.chooseByKey(option, event);
88
- return;
89
- }
90
- if (event.repeat) return;
91
- this.#hold(option, event.key);
92
- option.focus();
93
- }
94
26
  claimPress(option, event) {
95
27
  if (this.#isOnBat(event)) return true;
96
- if (!this.#isMomentary(option)) return false;
28
+ const target = this.pressTarget(option);
29
+ if (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;
97
30
  this.group.setPointerCapture(event.pointerId);
98
- this.#hold(option, event.pointerId);
31
+ this.hold(target, event.pointerId);
99
32
  return true;
100
33
  }
101
34
  connect(signal) {
102
35
  const group = this.group;
103
- this.upgradeProperties("defaultChecked", "checked", "orientation");
104
36
  super.connect(signal);
105
- this.checkStyles(group, "switch.css");
106
- this.#bare.addEventListener("click", (event) => {
37
+ this.checkStyles(group, "switch.css", ["cap"]);
38
+ this.bare.addEventListener("click", (event) => {
107
39
  const isSwallowed = this.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== "";
108
40
  this.#isClickSwallowed = false;
109
- if (!isSwallowed) this.#setChecked(!this.checked);
41
+ if (!isSwallowed) this.setChecked(!this.checked);
110
42
  }, { signal });
111
- this.#bare.addEventListener("pointerdown", () => {
43
+ this.bare.addEventListener("pointerdown", () => {
112
44
  this.#isClickSwallowed = false;
113
45
  }, { signal });
114
46
  this.#bindBat(group, signal);
115
- this.#holding = bindHold(group, {
116
- canHold: () => !this.isDisabled(),
117
- leave: "focusout",
118
- onHold: () => {
119
- this.toggleState("held", true);
120
- },
121
- onRelease: () => {
122
- this.toggleState("held", false);
123
- this.#springBack();
124
- }
125
- }, signal);
126
- }
127
- focusTarget() {
128
- return this.#isBare() ? this.#bare : super.focusTarget();
129
- }
130
- isWrapping() {
131
- return false;
132
47
  }
133
48
  releaseTarget(option) {
134
- if (this.#isMomentary(option)) return void 0;
135
- const options = this.options();
136
- if (options.length === 2 && optionValue(option) === this.value) return options.find((other) => other !== option);
137
- return option;
49
+ const target = this.pressTarget(option);
50
+ return this.isMomentary(target) || this.isOptionDisabled(target) ? void 0 : target;
138
51
  }
139
52
  render() {
140
53
  if (!this.isBound()) return;
141
- const group = this.group;
54
+ super.render();
142
55
  const options = this.options();
143
56
  if (options.length > 0) {
144
- this.#bare.remove();
145
- writeAttribute(group, "role", "radiogroup");
146
- writeAttribute(group, "data-sonic-position-count", String(options.length));
147
- super.render();
148
- this.#renderThrow(this.#checkedIndex(options));
57
+ this.#renderThrow(this.checkedIndex(options));
149
58
  return;
150
59
  }
151
- const bare = this.#bare;
152
- const isChecked = this.checked;
153
- writeAttribute(group, "role", void 0);
154
- delete group.dataset.sonicPositionCount;
155
- if (bare.parentElement !== group) group.append(bare);
156
- writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
157
- writeAttribute(bare, "aria-checked", String(isChecked));
158
- this.forwardNaming(group, false);
159
- this.forwardNaming(bare, true);
160
60
  const onSide = this.orientation === "horizontal" ? 1 : -1;
161
- group.style.setProperty("--_sonic-switch-at", String(isChecked ? onSide : -onSide));
162
- this.writeFormValue(isChecked ? this.value : null, String(isChecked));
61
+ this.group.style.setProperty("--_sonic-switch-at", String(this.checked ? onSide : -onSide));
163
62
  }
164
63
  renderOption() {
165
64
  return renderPosition();
166
65
  }
167
- restoreState(state) {
168
- if (this.#isBare()) this.checked = state === "true";
169
- else super.restoreState(state);
170
- }
171
- #along(event) {
172
- return this.orientation === "horizontal" ? event.clientX : event.clientY;
173
- }
174
66
  #bindBat(group, signal) {
175
67
  bindDrag(group, {
176
68
  grab: (event) => {
177
- if (!(event.target === this.#bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
69
+ if (!(event.target === this.bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
178
70
  return {
179
71
  hasMoved: false,
180
- last: this.#along(event),
72
+ last: this.along(event),
181
73
  width: this.#bat.getBoundingClientRect().width
182
74
  };
183
75
  },
@@ -187,83 +79,57 @@ var SonicSwitch = class extends SonicRadioGroupElement {
187
79
  },
188
80
  move: (press, event) => {
189
81
  if (this.isDisabled()) return;
190
- const delta = this.#along(event) - press.last;
82
+ const delta = this.along(event) - press.last;
191
83
  if (Math.abs(delta) < press.width / 4) return;
192
84
  press.hasMoved = true;
193
- press.last = this.#along(event);
85
+ press.last = this.along(event);
194
86
  this.#throwBy(Math.sign(delta), event.pointerId);
195
87
  }
196
88
  }, signal);
197
89
  }
198
- #checkedIndex(options) {
199
- return options.findIndex((option) => optionValue(option) === this.value);
200
- }
201
90
  #hasSprungFrom(option) {
202
- if (!option || !this.#isMomentary(option)) return false;
203
- this.#holding?.release();
91
+ if (!option || !this.isMomentary(option)) return false;
92
+ this.releaseHold();
204
93
  return this.value !== optionValue(option);
205
94
  }
206
- #hold(option, by) {
207
- if (this.#holding?.hold(by) === true) this.select(option);
208
- }
209
- #isBare() {
210
- return this.options().length === 0;
211
- }
212
- #isMomentary(option) {
213
- const options = this.options();
214
- return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
215
- }
216
95
  #isOnBat(event) {
217
96
  return isUnder(this.#bat, event.clientX, event.clientY);
218
97
  }
219
98
  #pressBat(event) {
220
99
  const options = this.options();
221
100
  if (options.length === 0) {
222
- this.#setChecked(!this.checked);
101
+ this.setChecked(!this.checked);
223
102
  return;
224
103
  }
225
104
  const box = this.#bat.getBoundingClientRect();
226
105
  const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
227
- const offset = (this.#along(event) - start) / size - .5;
228
- const option = options[pressedIndex(this.#checkedIndex(options), options.length, offset)];
106
+ const offset = (this.along(event) - start) / size - .5;
107
+ const index = this.checkedIndex(options);
108
+ const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
109
+ const toward = this.stepFrom(index, side);
110
+ const option = toward && toward !== options[index] ? toward : this.stepFrom(index, -side);
229
111
  if (!option) return;
230
112
  this.chooseByClick(option);
231
113
  this.#refocus(options);
232
114
  }
233
115
  #refocus(options) {
234
- if (this.group.matches(":focus-within")) options[this.#checkedIndex(options)]?.focus();
116
+ if (this.group.matches(":focus-within")) options[this.checkedIndex(options)]?.focus();
235
117
  }
236
118
  #renderThrow(index) {
237
119
  const count = this.options().length;
238
120
  const at = index < 0 || count < 2 ? 0 : 2 * index / (count - 1) - 1;
239
121
  this.group.style.setProperty("--_sonic-switch-at", String(at));
240
122
  }
241
- #setChecked(isChecked) {
242
- if (isChecked === this.checked) return;
243
- this.checked = isChecked;
244
- this.dispatchEvent(new Event("change", { bubbles: true }));
245
- }
246
- #springBack() {
247
- const options = this.options();
248
- const index = this.#checkedIndex(options);
249
- const held = options[index];
250
- if (!held || !this.#isMomentary(held)) return;
251
- const rest = options[index === 0 ? 1 : index - 1];
252
- if (!rest) return;
253
- const isFocused = this.group.contains(this.ownerDocument.activeElement);
254
- this.select(rest);
255
- if (isFocused) rest.focus();
256
- }
257
123
  #throwBy(direction, pointerId) {
258
124
  const options = this.options();
259
125
  if (options.length === 0) {
260
- this.#setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
126
+ this.setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
261
127
  return;
262
128
  }
263
- const index = this.#checkedIndex(options);
264
- const option = options[clamp(index + direction, 0, options.length - 1)];
129
+ const index = this.checkedIndex(options);
130
+ const option = this.stepFrom(index, direction);
265
131
  if (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;
266
- if (this.#isMomentary(option)) this.#hold(option, pointerId);
132
+ if (this.isMomentary(option)) this.hold(option, pointerId);
267
133
  else this.select(option);
268
134
  this.#refocus(options);
269
135
  }
@@ -1 +1 @@
1
- {"version":3,"file":"switch.js","names":[],"sources":["../../../src/elements/switch.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { isUnder, optionValue, SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { clamp } from '#lib/math.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-switch': SonicSwitch;\n\t}\n}\n\nconst renderSwitch = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-switch\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bushing\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bat\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-switch-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-switch-label\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-switch-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface BatPress {\n\thasMoved: boolean;\n\tlast: number;\n\twidth: number;\n}\n\nconst pressCenterZone = 0.08;\n\nfunction pressedIndex(index: number, count: number, offset: number): number {\n\tif (count === 2) return index === 0 ? 1 : 0;\n\n\tconst side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);\n\tconst next = index + side;\n\n\treturn next < 0 || next >= count ? index - side : next;\n}\n\nexport class SonicSwitch extends SonicRadioGroupElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRadioGroupElement.observedAttributes,\n\t\t'checked',\n\t\t'orientation',\n\t];\n\n\tget checked(): boolean {\n\t\treturn this.#isDirty ? this.#checked : this.defaultChecked;\n\t}\n\n\tset checked(isChecked: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#checked = isChecked === true;\n\t\tthis.render();\n\t}\n\n\tget defaultChecked(): boolean {\n\t\treturn this.hasAttribute('checked');\n\t}\n\n\tset defaultChecked(isChecked: boolean) {\n\t\tthis.reflect('checked', isChecked);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'horizontal' ? 'horizontal' : 'vertical';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\toverride get value(): string {\n\t\tconst value = super.value;\n\n\t\treturn value === '' && this.#isBare() ? 'on' : value;\n\t}\n\n\toverride set value(next: string) {\n\t\tsuper.value = next;\n\t}\n\n\tprotected readonly group = renderSwitch();\n\n\treadonly #bare = renderBare();\n\n\treadonly #bat = requireChild(this.group, '.sonic-switch-bat', HTMLSpanElement);\n\n\t#checked = false;\n\n\t#holding: Hold | undefined;\n\n\t#isClickSwallowed = false;\n\n\t#isDirty = false;\n\n\toverride attributeChangedCallback(name: string): void {\n\t\tif (name === 'checked') this.#isDirty = false;\n\t\tif (this.isDisabled()) this.#holding?.release();\n\t\tsuper.attributeChangedCallback(name);\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.#isDirty = false;\n\t\tsuper.formResetCallback();\n\t}\n\n\tprotected override activate(): void {\n\t\tif (this.#isBare()) this.#bare.click();\n\t\telse super.activate();\n\t}\n\n\tprotected override chooseByClick(option: HTMLButtonElement): void {\n\t\tsuper.chooseByClick(option);\n\t\tif (this.#isMomentary(option)) this.#springBack();\n\t}\n\n\tprotected override chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void {\n\t\tif (!this.#isMomentary(option)) {\n\t\t\tsuper.chooseByKey(option, event);\n\t\t\treturn;\n\t\t}\n\t\tif (event.repeat) return;\n\n\t\tthis.#hold(option, event.key);\n\t\toption.focus();\n\t}\n\n\tprotected override claimPress(option: HTMLButtonElement, event: PointerEvent): boolean {\n\t\tif (this.#isOnBat(event)) return true;\n\t\tif (!this.#isMomentary(option)) return false;\n\n\t\tthis.group.setPointerCapture(event.pointerId);\n\t\tthis.#hold(option, event.pointerId);\n\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tthis.upgradeProperties('defaultChecked', 'checked', 'orientation');\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(group, 'switch.css');\n\n\t\tthis.#bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tconst isSwallowed =\n\t\t\t\t\tthis.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== '';\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tif (!isSwallowed) this.#setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bare.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t() => {\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindBat(group, signal);\n\n\t\tthis.#holding = bindHold(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled(),\n\t\t\t\tleave: 'focusout',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.toggleState('held', true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.toggleState('held', false);\n\t\t\t\t\tthis.#springBack();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.#isBare() ? this.#bare : super.focusTarget();\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn false;\n\t}\n\n\tprotected override releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined {\n\t\tif (this.#isMomentary(option)) return undefined;\n\n\t\tconst options = this.options();\n\t\tif (options.length === 2 && optionValue(option) === this.value) {\n\t\t\treturn options.find((other) => other !== option);\n\t\t}\n\n\t\treturn option;\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst group = this.group;\n\t\tconst options = this.options();\n\n\t\tif (options.length > 0) {\n\t\t\tthis.#bare.remove();\n\t\t\twriteAttribute(group, 'role', 'radiogroup');\n\t\t\twriteAttribute(group, 'data-sonic-position-count', String(options.length));\n\t\t\tsuper.render();\n\t\t\tthis.#renderThrow(this.#checkedIndex(options));\n\t\t\treturn;\n\t\t}\n\n\t\tconst bare = this.#bare;\n\t\tconst isChecked = this.checked;\n\n\t\twriteAttribute(group, 'role', undefined);\n\t\tdelete group.dataset.sonicPositionCount;\n\t\tif (bare.parentElement !== group) group.append(bare);\n\t\twriteAttribute(bare, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(bare, 'aria-checked', String(isChecked));\n\t\tthis.forwardNaming(group, false);\n\t\tthis.forwardNaming(bare, true);\n\t\tconst onSide = this.orientation === 'horizontal' ? 1 : -1;\n\n\t\tgroup.style.setProperty('--_sonic-switch-at', String(isChecked ? onSide : -onSide));\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(isChecked ? this.value : null, String(isChecked));\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\tprotected override restoreState(state: string): void {\n\t\tif (this.#isBare()) this.checked = state === 'true';\n\t\telse super.restoreState(state);\n\t}\n\n\t#along(event: PointerEvent): number {\n\t\treturn this.orientation === 'horizontal' ? event.clientX : event.clientY;\n\t}\n\n\t#bindBat(group: HTMLElement, signal: AbortSignal): void {\n\t\tbindDrag<BatPress>(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tconst isOnPosition =\n\t\t\t\t\t\tevent.target === this.#bare || this.optionOf(event.target) !== undefined;\n\t\t\t\t\tif (!isOnPosition || this.isDisabled() || !this.#isOnBat(event)) return;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\thasMoved: false,\n\t\t\t\t\t\tlast: this.#along(event),\n\t\t\t\t\t\twidth: this.#bat.getBoundingClientRect().width,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tlift: (press, event) => {\n\t\t\t\t\tif (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);\n\t\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t\t},\n\t\t\t\tmove: (press, event) => {\n\t\t\t\t\tif (this.isDisabled()) return;\n\n\t\t\t\t\tconst delta = this.#along(event) - press.last;\n\t\t\t\t\tif (Math.abs(delta) < press.width / 4) return;\n\n\t\t\t\t\tpress.hasMoved = true;\n\t\t\t\t\tpress.last = this.#along(event);\n\t\t\t\t\tthis.#throwBy(Math.sign(delta), event.pointerId);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#checkedIndex(options: Array<HTMLButtonElement>): number {\n\t\treturn options.findIndex((option) => optionValue(option) === this.value);\n\t}\n\n\t#hasSprungFrom(option: HTMLButtonElement | undefined): boolean {\n\t\tif (!option || !this.#isMomentary(option)) return false;\n\n\t\tthis.#holding?.release();\n\n\t\treturn this.value !== optionValue(option);\n\t}\n\n\t#hold(option: HTMLButtonElement, by: number | string): void {\n\t\tif (this.#holding?.hold(by) === true) this.select(option);\n\t}\n\n\t#isBare(): boolean {\n\t\treturn this.options().length === 0;\n\t}\n\n\t#isMomentary(option: HTMLButtonElement): boolean {\n\t\tconst options = this.options();\n\t\tconst isEnd = option === options[0] || option === options.at(-1);\n\n\t\treturn (\n\t\t\tisEnd &&\n\t\t\toptions.length > 1 &&\n\t\t\toption.querySelector('[data-sonic-value]')?.hasAttribute('data-sonic-momentary') === true\n\t\t);\n\t}\n\n\t#isOnBat(event: PointerEvent): boolean {\n\t\treturn isUnder(this.#bat, event.clientX, event.clientY);\n\t}\n\n\t#pressBat(event: PointerEvent): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst box = this.#bat.getBoundingClientRect();\n\t\tconst [start, size] =\n\t\t\tthis.orientation === 'horizontal' ? [box.left, box.width] : [box.top, box.height];\n\t\tconst offset = (this.#along(event) - start) / size - 0.5;\n\t\tconst option = options[pressedIndex(this.#checkedIndex(options), options.length, offset)];\n\t\tif (!option) return;\n\n\t\tthis.chooseByClick(option);\n\t\tthis.#refocus(options);\n\t}\n\n\t#refocus(options: Array<HTMLButtonElement>): void {\n\t\tif (this.group.matches(':focus-within')) options[this.#checkedIndex(options)]?.focus();\n\t}\n\n\t#renderThrow(index: number): void {\n\t\tconst count = this.options().length;\n\t\tconst at = index < 0 || count < 2 ? 0 : (2 * index) / (count - 1) - 1;\n\n\t\tthis.group.style.setProperty('--_sonic-switch-at', String(at));\n\t}\n\n\t#setChecked(isChecked: boolean): void {\n\t\tif (isChecked === this.checked) return;\n\n\t\tthis.checked = isChecked;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#springBack(): void {\n\t\tconst options = this.options();\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst held = options[index];\n\t\tif (!held || !this.#isMomentary(held)) return;\n\n\t\tconst rest = options[index === 0 ? 1 : index - 1];\n\t\tif (!rest) return;\n\n\t\tconst isFocused = this.group.contains(this.ownerDocument.activeElement);\n\n\t\tthis.select(rest);\n\t\tif (isFocused) rest.focus();\n\t}\n\n\t#throwBy(direction: number, pointerId: number): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(direction === (this.orientation === 'horizontal' ? 1 : -1));\n\t\t\treturn;\n\t\t}\n\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst option = options[clamp(index + direction, 0, options.length - 1)];\n\t\tif (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;\n\n\t\tif (this.#isMomentary(option)) this.#hold(option, pointerId);\n\t\telse this.select(option);\n\t\tthis.#refocus(options);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"switch.js","names":[],"sources":["../../../src/elements/switch.ts"],"sourcesContent":["import { SonicPositionGroupElement } from '#elements/position-group.ts';\nimport { isUnder, optionValue } from '#elements/radio-group.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-switch': SonicSwitch;\n\t}\n}\n\nconst renderSwitch = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-switch\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bushing\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bat\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-switch-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-switch-label\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-switch-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface BatPress {\n\thasMoved: boolean;\n\tlast: number;\n\twidth: number;\n}\n\nconst pressCenterZone = 0.08;\n\nexport class SonicSwitch extends SonicPositionGroupElement {\n\tprotected readonly bare = renderBare();\n\n\tprotected readonly defaultOrientation = 'vertical';\n\n\tprotected readonly group = renderSwitch();\n\n\treadonly #bat = requireChild(this.group, '.sonic-switch-bat', HTMLSpanElement);\n\n\t#isClickSwallowed = false;\n\n\tprotected override claimPress(option: HTMLButtonElement, event: PointerEvent): boolean {\n\t\tif (this.#isOnBat(event)) return true;\n\n\t\tconst target = this.pressTarget(option);\n\t\tif (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;\n\n\t\tthis.group.setPointerCapture(event.pointerId);\n\t\tthis.hold(target, event.pointerId);\n\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(group, 'switch.css', ['cap']);\n\n\t\tthis.bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tconst isSwallowed =\n\t\t\t\t\tthis.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== '';\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tif (!isSwallowed) this.setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.bare.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t() => {\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindBat(group, signal);\n\t}\n\n\tprotected override releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined {\n\t\tconst target = this.pressTarget(option);\n\n\t\treturn this.isMomentary(target) || this.isOptionDisabled(target) ? undefined : target;\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tsuper.render();\n\n\t\tconst options = this.options();\n\t\tif (options.length > 0) {\n\t\t\tthis.#renderThrow(this.checkedIndex(options));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onSide = this.orientation === 'horizontal' ? 1 : -1;\n\n\t\tthis.group.style.setProperty('--_sonic-switch-at', String(this.checked ? onSide : -onSide));\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\t#bindBat(group: HTMLElement, signal: AbortSignal): void {\n\t\tbindDrag<BatPress>(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tconst isOnPosition =\n\t\t\t\t\t\tevent.target === this.bare || this.optionOf(event.target) !== undefined;\n\t\t\t\t\tif (!isOnPosition || this.isDisabled() || !this.#isOnBat(event)) return;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\thasMoved: false,\n\t\t\t\t\t\tlast: this.along(event),\n\t\t\t\t\t\twidth: this.#bat.getBoundingClientRect().width,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tlift: (press, event) => {\n\t\t\t\t\tif (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);\n\t\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t\t},\n\t\t\t\tmove: (press, event) => {\n\t\t\t\t\tif (this.isDisabled()) return;\n\n\t\t\t\t\tconst delta = this.along(event) - press.last;\n\t\t\t\t\tif (Math.abs(delta) < press.width / 4) return;\n\n\t\t\t\t\tpress.hasMoved = true;\n\t\t\t\t\tpress.last = this.along(event);\n\t\t\t\t\tthis.#throwBy(Math.sign(delta), event.pointerId);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#hasSprungFrom(option: HTMLButtonElement | undefined): boolean {\n\t\tif (!option || !this.isMomentary(option)) return false;\n\n\t\tthis.releaseHold();\n\n\t\treturn this.value !== optionValue(option);\n\t}\n\n\t#isOnBat(event: PointerEvent): boolean {\n\t\treturn isUnder(this.#bat, event.clientX, event.clientY);\n\t}\n\n\t#pressBat(event: PointerEvent): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst box = this.#bat.getBoundingClientRect();\n\t\tconst [start, size] =\n\t\t\tthis.orientation === 'horizontal' ? [box.left, box.width] : [box.top, box.height];\n\t\tconst offset = (this.along(event) - start) / size - 0.5;\n\t\tconst index = this.checkedIndex(options);\n\t\tconst side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);\n\t\tconst toward = this.stepFrom(index, side);\n\t\tconst option = toward && toward !== options[index] ? toward : this.stepFrom(index, -side);\n\t\tif (!option) return;\n\n\t\tthis.chooseByClick(option);\n\t\tthis.#refocus(options);\n\t}\n\n\t#refocus(options: Array<HTMLButtonElement>): void {\n\t\tif (this.group.matches(':focus-within')) options[this.checkedIndex(options)]?.focus();\n\t}\n\n\t#renderThrow(index: number): void {\n\t\tconst count = this.options().length;\n\t\tconst at = index < 0 || count < 2 ? 0 : (2 * index) / (count - 1) - 1;\n\n\t\tthis.group.style.setProperty('--_sonic-switch-at', String(at));\n\t}\n\n\t#throwBy(direction: number, pointerId: number): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.setChecked(direction === (this.orientation === 'horizontal' ? 1 : -1));\n\t\t\treturn;\n\t\t}\n\n\t\tconst index = this.checkedIndex(options);\n\t\tconst option = this.stepFrom(index, direction);\n\t\tif (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;\n\n\t\tif (this.isMomentary(option)) this.hold(option, pointerId);\n\t\telse this.select(option);\n\t\tthis.#refocus(options);\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { SonicPositionGroupElement } from "./position-group.js";
2
+ //#region src/elements/toggle.d.ts
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ 'sonic-toggle': SonicToggle;
6
+ }
7
+ }
8
+ export declare class SonicToggle extends SonicPositionGroupElement {
9
+ #private;
10
+ protected readonly bare: HTMLButtonElement;
11
+ protected readonly defaultOrientation = "horizontal";
12
+ protected readonly group: HTMLDivElement;
13
+ protected claimPress(): boolean;
14
+ protected connect(signal: AbortSignal): void;
15
+ protected isMirrored(child: Node): boolean;
16
+ protected placeCopies(copies: Array<Node>): void;
17
+ protected render(): void;
18
+ protected renderOption(): HTMLButtonElement;
19
+ }
20
+ //#endregion
21
+ //# sourceMappingURL=toggle.d.ts.map
@@ -0,0 +1,201 @@
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
+ "cap",
36
+ "well",
37
+ "keycap"
38
+ ]);
39
+ this.bare.addEventListener("click", (event) => {
40
+ if (event instanceof PointerEvent && event.pointerType !== "") return;
41
+ this.setChecked(!this.checked);
42
+ }, { signal });
43
+ this.#bindCap(signal);
44
+ }
45
+ isMirrored(child) {
46
+ return super.isMirrored(child) || child === this.#fixedLegend();
47
+ }
48
+ placeCopies(copies) {
49
+ const positions = copies.filter((copy) => super.isMirrored(copy));
50
+ placeChildren(this.#cap, copies.filter((copy) => !positions.includes(copy)));
51
+ super.placeCopies(positions);
52
+ }
53
+ render() {
54
+ if (!this.isBound()) return;
55
+ this.#checkLegends();
56
+ this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
57
+ this.#placeCap(this.#draggedTo ?? this.#restingAt());
58
+ super.render();
59
+ for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
60
+ }
61
+ renderOption() {
62
+ return renderPosition();
63
+ }
64
+ #bindCap(signal) {
65
+ bindDrag(this.group, {
66
+ grab: (event) => this.#grab(event),
67
+ lift: (drag, event) => {
68
+ if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
69
+ if (!isUnder(this.group, event.clientX, event.clientY)) return;
70
+ this.#press(drag.position);
71
+ },
72
+ move: (drag, event) => {
73
+ if (drag.isHolding || this.isDisabled()) return;
74
+ const pointer = this.along(event);
75
+ if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
76
+ drag.hasMoved = true;
77
+ this.toggleState("dragging", true);
78
+ this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
79
+ },
80
+ release: (drag) => {
81
+ delete (drag.position ?? this.bare).dataset.sonicPressed;
82
+ this.#draggedTo = void 0;
83
+ this.toggleState("dragging", false);
84
+ this.render();
85
+ }
86
+ }, signal);
87
+ }
88
+ #checkLegends() {
89
+ if (this.#hasWarnedOfLegends) return;
90
+ const count = this.options().length;
91
+ if (count <= 3 || this.getAttribute("legends") !== "outside") return;
92
+ this.#hasWarnedOfLegends = true;
93
+ console.warn(`<sonic-toggle> lays out legends="outside" for two or three positions, and this one has ${String(count)}`);
94
+ }
95
+ #enabledEnds() {
96
+ if (this.isBare()) return [0, 1];
97
+ const options = this.options();
98
+ const isEnabled = (option) => !this.isOptionDisabled(option);
99
+ return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
100
+ }
101
+ #fixedLegend() {
102
+ return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
103
+ }
104
+ #grab(event) {
105
+ const position = this.optionOf(event.target);
106
+ if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
107
+ const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
108
+ const travel = this.#travel(event, isOnCap);
109
+ if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
110
+ (position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
111
+ return {
112
+ hasMoved: false,
113
+ isHolding: this.#holdFrom(position, event.pointerId),
114
+ position,
115
+ start: this.along(event),
116
+ thresholdPx: dragThresholdPx(event.pointerType),
117
+ travel
118
+ };
119
+ }
120
+ #holdFrom(position, pointerId) {
121
+ const target = position && this.pressTarget(position);
122
+ if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
123
+ this.hold(target, pointerId);
124
+ return true;
125
+ }
126
+ #isBlocked(position, isOnCap) {
127
+ return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
128
+ }
129
+ #onAt() {
130
+ return this.orientation === "vertical" ? 0 : 1;
131
+ }
132
+ #placeCap(at) {
133
+ this.group.style.setProperty("--_sonic-toggle-at", String(at));
134
+ }
135
+ #press(position) {
136
+ if (!position) {
137
+ this.setChecked(!this.checked);
138
+ return;
139
+ }
140
+ const target = this.pressTarget(position);
141
+ if (this.isOptionDisabled(target)) return;
142
+ this.select(target);
143
+ this.#refocus(target);
144
+ }
145
+ #refocus(position) {
146
+ if (this.group.matches(":focus-within")) position.focus();
147
+ }
148
+ #restingAt() {
149
+ if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
150
+ return Math.max(0, this.checkedIndex(this.options()));
151
+ }
152
+ #slideTo(at, pointerId) {
153
+ const crossed = this.options()[Math.round(at)];
154
+ this.#draggedTo = at;
155
+ this.#placeCap(at);
156
+ if (this.isBare()) {
157
+ this.setChecked(Math.round(at) === this.#onAt());
158
+ return;
159
+ }
160
+ if (!crossed || optionValue(crossed) === this.value) return;
161
+ this.releaseHold();
162
+ if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
163
+ if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
164
+ else this.select(crossed);
165
+ this.#refocus(crossed);
166
+ }
167
+ #stopCount() {
168
+ return this.options().length || 2;
169
+ }
170
+ #travel(event, isOnCap) {
171
+ const [first, last] = this.#enabledEnds();
172
+ if (first < 0) return void 0;
173
+ const box = this.#well.getBoundingClientRect();
174
+ const [start, length, thickness] = this.orientation === "vertical" ? [
175
+ box.top,
176
+ box.height,
177
+ box.width
178
+ ] : [
179
+ box.left,
180
+ box.width,
181
+ box.height
182
+ ];
183
+ return travelFrom({
184
+ at: this.#restingAt(),
185
+ count: this.#stopCount(),
186
+ first,
187
+ isOnCap,
188
+ last,
189
+ pointer: this.along(event),
190
+ well: {
191
+ length,
192
+ start,
193
+ thickness
194
+ }
195
+ });
196
+ }
197
+ };
198
+
199
+ //#endregion
200
+ export { SonicToggle };
201
+ //# sourceMappingURL=toggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle.js","names":[],"sources":["../../../src/elements/toggle.ts"],"sourcesContent":["import type { ToggleTravel } from '#lib/toggle-travel.ts';\n\nimport { SonicPositionGroupElement } from '#elements/position-group.ts';\nimport { isUnder, optionValue } from '#elements/radio-group.ts';\nimport { dragThresholdPx } from '#lib/drag-step.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { positionAt, travelFrom } from '#lib/toggle-travel.ts';\n\n// eslint-disable-next-line unicorn/consistent-boolean-name -- the name bundlers and other kits use\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-toggle': SonicToggle;\n\t}\n}\n\n// fallow-ignore-next-line code-duplication -- each control draws its own parts\nconst renderToggle = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-toggle\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-toggle-well\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-toggle-cap\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-toggle-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-toggle-legend\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-toggle-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface CapDrag {\n\thasMoved: boolean;\n\tisHolding: boolean;\n\tposition: HTMLButtonElement | undefined;\n\tstart: number;\n\tthresholdPx: number;\n\ttravel: ToggleTravel;\n}\n\nexport class SonicToggle extends SonicPositionGroupElement {\n\tprotected readonly bare = renderBare();\n\n\tprotected readonly defaultOrientation = 'horizontal';\n\n\tprotected readonly group = renderToggle();\n\n\treadonly #cap = requireChild(this.group, '.sonic-toggle-cap', HTMLSpanElement);\n\n\t#draggedTo: number | undefined;\n\n\t#hasWarnedOfLegends = false;\n\n\treadonly #well = requireChild(this.group, '.sonic-toggle-well', HTMLSpanElement);\n\n\t// Every press is the toggle's own, so the base's slide-off never starts\n\tprotected override claimPress(): boolean {\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(this.group, 'toggle.css', ['cap', 'well', 'keycap']);\n\t\tthis.bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A pointer's press is taken on its release, before the click it sends\n\t\t\t\tif (event instanceof PointerEvent && event.pointerType !== '') return;\n\n\t\t\t\tthis.setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindCap(signal);\n\t}\n\n\tprotected override isMirrored(child: Node): boolean {\n\t\treturn super.isMirrored(child) || child === this.#fixedLegend();\n\t}\n\n\tprotected override placeCopies(copies: Array<Node>): void {\n\t\tconst positions = copies.filter((copy) => super.isMirrored(copy));\n\n\t\tplaceChildren(\n\t\t\tthis.#cap,\n\t\t\tcopies.filter((copy) => !positions.includes(copy)),\n\t\t);\n\t\tsuper.placeCopies(positions);\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tif (__DEV__) this.#checkLegends();\n\t\tthis.group.style.setProperty('--_sonic-toggle-count', String(this.#stopCount()));\n\t\tthis.#placeCap(this.#draggedTo ?? this.#restingAt());\n\t\tsuper.render();\n\t\tfor (const [index, option] of this.options().entries()) {\n\t\t\toption.style.setProperty('--_sonic-toggle-index', String(index));\n\t\t}\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\t#bindCap(signal: AbortSignal): void {\n\t\tbindDrag<CapDrag>(\n\t\t\tthis.group,\n\t\t\t{\n\t\t\t\tgrab: (event) => this.#grab(event),\n\t\t\t\tlift: (drag, event) => {\n\t\t\t\t\tif (drag.hasMoved || drag.isHolding || this.isDisabled()) return;\n\t\t\t\t\tif (!isUnder(this.group, event.clientX, event.clientY)) return;\n\n\t\t\t\t\tthis.#press(drag.position);\n\t\t\t\t},\n\t\t\t\tmove: (drag, event) => {\n\t\t\t\t\t// A held finger wobbles, and a drag back across halfway would let go\n\t\t\t\t\tif (drag.isHolding || this.isDisabled()) return;\n\t\t\t\t\tconst pointer = this.along(event);\n\t\t\t\t\tif (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;\n\n\t\t\t\t\tdrag.hasMoved = true;\n\t\t\t\t\tthis.toggleState('dragging', true);\n\t\t\t\t\tthis.#slideTo(positionAt(drag.travel, pointer), event.pointerId);\n\t\t\t\t},\n\t\t\t\trelease: (drag) => {\n\t\t\t\t\tdelete (drag.position ?? this.bare).dataset.sonicPressed;\n\t\t\t\t\tthis.#draggedTo = undefined;\n\t\t\t\t\tthis.toggleState('dragging', false);\n\t\t\t\t\tthis.render();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#checkLegends(): void {\n\t\tif (!__DEV__ || this.#hasWarnedOfLegends) return;\n\n\t\tconst count = this.options().length;\n\t\tif (count <= 3 || this.getAttribute('legends') !== 'outside') return;\n\n\t\tthis.#hasWarnedOfLegends = true;\n\t\tconsole.warn(\n\t\t\t`<sonic-toggle> lays out legends=\"outside\" for two or three positions, and this one has ${String(count)}`,\n\t\t);\n\t}\n\n\t#enabledEnds(): [number, number] {\n\t\tif (this.isBare()) return [0, 1];\n\n\t\tconst options = this.options();\n\t\tconst isEnabled = (option: HTMLButtonElement): boolean => !this.isOptionDisabled(option);\n\n\t\treturn [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];\n\t}\n\n\t#fixedLegend(): Element | undefined {\n\t\treturn [...this.children].find((child) => child !== this.group && !super.isMirrored(child));\n\t}\n\n\t#grab(event: PointerEvent): CapDrag | undefined {\n\t\tconst position = this.optionOf(event.target);\n\t\tif (this.isDisabled() || (!position && event.target !== this.bare)) return undefined;\n\n\t\tconst isOnCap = isUnder(this.#cap, event.clientX, event.clientY);\n\t\tconst travel = this.#travel(event, isOnCap);\n\t\tif (!travel || this.#isBlocked(position, isOnCap)) return undefined;\n\n\t\t(position ?? this.bare).toggleAttribute('data-sonic-pressed', true);\n\n\t\treturn {\n\t\t\thasMoved: false,\n\t\t\tisHolding: this.#holdFrom(position, event.pointerId),\n\t\t\tposition,\n\t\t\tstart: this.along(event),\n\t\t\tthresholdPx: dragThresholdPx(event.pointerType),\n\t\t\ttravel,\n\t\t};\n\t}\n\n\t#holdFrom(position: HTMLButtonElement | undefined, pointerId: number): boolean {\n\t\tconst target = position && this.pressTarget(position);\n\t\tif (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;\n\n\t\tthis.hold(target, pointerId);\n\n\t\treturn true;\n\t}\n\n\t#isBlocked(position: HTMLButtonElement | undefined, isOnCap: boolean): boolean {\n\t\treturn !isOnCap && position !== undefined && this.isOptionDisabled(position);\n\t}\n\n\t// On is up, as on hardware\n\t#onAt(): number {\n\t\treturn this.orientation === 'vertical' ? 0 : 1;\n\t}\n\n\t#placeCap(at: number): void {\n\t\tthis.group.style.setProperty('--_sonic-toggle-at', String(at));\n\t}\n\n\t#press(position: HTMLButtonElement | undefined): void {\n\t\tif (!position) {\n\t\t\tthis.setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst target = this.pressTarget(position);\n\t\tif (this.isOptionDisabled(target)) return;\n\n\t\tthis.select(target);\n\t\tthis.#refocus(target);\n\t}\n\n\t#refocus(position: HTMLButtonElement): void {\n\t\tif (this.group.matches(':focus-within')) position.focus();\n\t}\n\n\t#restingAt(): number {\n\t\tif (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();\n\n\t\treturn Math.max(0, this.checkedIndex(this.options()));\n\t}\n\n\t#slideTo(at: number, pointerId: number): void {\n\t\tconst crossed = this.options()[Math.round(at)];\n\n\t\tthis.#draggedTo = at;\n\t\tthis.#placeCap(at);\n\t\tif (this.isBare()) {\n\t\t\tthis.setChecked(Math.round(at) === this.#onAt());\n\t\t\treturn;\n\t\t}\n\t\tif (!crossed || optionValue(crossed) === this.value) return;\n\n\t\tthis.releaseHold();\n\t\tif (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;\n\n\t\tif (this.isMomentary(crossed)) this.hold(crossed, pointerId);\n\t\telse this.select(crossed);\n\t\tthis.#refocus(crossed);\n\t}\n\n\t#stopCount(): number {\n\t\treturn this.options().length || 2;\n\t}\n\n\t#travel(event: PointerEvent, isOnCap: boolean): ToggleTravel | undefined {\n\t\tconst [first, last] = this.#enabledEnds();\n\t\tif (first < 0) return undefined;\n\n\t\tconst box = this.#well.getBoundingClientRect();\n\t\tconst [start, length, thickness] =\n\t\t\tthis.orientation === 'vertical'\n\t\t\t\t? [box.top, box.height, box.width]\n\t\t\t\t: [box.left, box.width, box.height];\n\n\t\treturn travelFrom({\n\t\t\tat: this.#restingAt(),\n\t\t\tcount: this.#stopCount(),\n\t\t\tfirst,\n\t\t\tisOnCap,\n\t\t\tlast,\n\t\t\tpointer: this.along(event),\n\t\t\twell: { length, start, thickness },\n\t\t});\n\t}\n}\n"],"mappings":"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"}
@@ -64,6 +64,7 @@ export declare abstract class SonicValueElement extends SonicFormElement {
64
64
  get valueText(): string;
65
65
  attributeChangedCallback(name: string): void;
66
66
  connectedCallback(): void;
67
+ protected abandonHold(): void;
67
68
  protected bindGestures(control: HTMLElement, signal: AbortSignal, grab: (event: PointerEvent) => undefined | ValueAxis): void;
68
69
  protected bindHover(control: HTMLElement, signal: AbortSignal, { canShow, place, valueAt }: HoverBinding): void;
69
70
  protected controlOrientation(): 'horizontal' | 'vertical' | undefined;
@@ -259,6 +259,9 @@ var SonicValueElement = class extends SonicFormElement {
259
259
  this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "spokenStep", "step", "taper", "positions", "detent", "formatEntry", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
260
260
  super.connectedCallback();
261
261
  }
262
+ abandonHold() {
263
+ this.#gestures?.abandon();
264
+ }
262
265
  bindGestures(control, signal, grab) {
263
266
  const bubble = control.querySelector("[popover]");
264
267
  if (bubble) this.#readout = {