@xsynaptic/sonic-ui 0.17.0 → 0.19.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 (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"number.js","names":[],"sources":["../../../src/elements/number.ts"],"sourcesContent":["import { SonicValueElement } from '#elements/value-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-number': SonicNumber;\n\t}\n}\n\nconst renderNumber = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-number\" role=\"spinbutton\" tabindex=\"0\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-number-value\"></span>\n\t\t\t<input\n\t\t\t\tautocomplete=\"off\"\n\t\t\t\tclass=\"sonic-number-entry\"\n\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\thidden\n\t\t\t\tspellcheck=\"false\"\n\t\t\t\ttype=\"text\"\n\t\t\t/>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicNumber extends SonicValueElement {\n\tget press(): 'none' | 'step' {\n\t\treturn this.getAttribute('press') === 'step' ? 'step' : 'none';\n\t}\n\n\tset press(gesture: 'none' | 'step' | undefined) {\n\t\tthis.reflect('press', gesture);\n\t}\n\n\treadonly #number = renderNumber();\n\n\treadonly #digits = requireChild(this.#number, '.sonic-number-value', HTMLSpanElement);\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('press');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst number = this.#number;\n\n\t\tthis.keepControl(number, signal);\n\t\tthis.render();\n\t\tthis.checkStyles(number, 'number.css', ['cap', 'glass', 'pane', 'well', 'value']);\n\t\tthis.bindGestures(number, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: this.travelPx(number, '--_sonic-number-travel'),\n\t\t}));\n\t}\n\n\tprotected override controlRole(): 'spinbutton' {\n\t\treturn 'spinbutton';\n\t}\n\n\tprotected draw(): void {\n\t\tthis.#digits.textContent = this.valueText;\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#number;\n\t}\n\n\tprotected override tapTarget(): number | undefined {\n\t\tif (this.press !== 'step') return undefined;\n\n\t\tconst mapping = this.mapping();\n\t\tconst next = mapping.keyTarget('ArrowUp', this.value);\n\n\t\treturn next === undefined || next === this.value ? mapping.bounds[0] : next;\n\t}\n}\n"],"mappings":";;;;AASA,MAAM,eAAe,SACT;;;;;;;;;;;;IAaX,cACD;AAEA,IAAa,cAAb,cAAiC,kBAAkB;CAClD,IAAI,QAAyB;EAC5B,OAAO,KAAK,aAAa,OAAO,MAAM,SAAS,SAAS;CACzD;CAEA,IAAI,MAAM,SAAsC;EAC/C,KAAK,QAAQ,SAAS,OAAO;CAC9B;CAEA,AAAS,UAAU,aAAa;CAEhC,AAAS,UAAU,aAAa,KAAK,SAAS,uBAAuB,eAAe;CAEpF,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,OAAO;EAC9B,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EAEpB,KAAK,YAAY,QAAQ,MAAM;EAC/B,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,cAAc;GAAC;GAAO;GAAS;GAAQ;GAAQ;EAAO,CAAC;EAChF,KAAK,aAAa,QAAQ,eAAe;GACxC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,KAAK,SAAS,QAAQ,wBAAwB;EACzD,EAAE;CACH;CAEA,AAAmB,cAA4B;EAC9C,OAAO;CACR;CAEA,AAAU,OAAa;EACtB,KAAK,QAAQ,cAAc,KAAK;CACjC;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAmB,YAAgC;EAClD,IAAI,KAAK,UAAU,QAAQ,OAAO;EAElC,MAAM,UAAU,KAAK,QAAQ;EAC7B,MAAM,OAAO,QAAQ,UAAU,WAAW,KAAK,KAAK;EAEpD,OAAO,SAAS,UAAa,SAAS,KAAK,QAAQ,QAAQ,OAAO,KAAK;CACxE;AACD"}
1
+ {"version":3,"file":"number.js","names":[],"sources":["../../../src/elements/number.ts"],"sourcesContent":["import { SonicValueElement } from '#elements/value-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-number': SonicNumber;\n\t}\n}\n\nconst renderNumber = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-number\" role=\"spinbutton\" tabindex=\"0\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-number-value\"></span>\n\t\t\t<input\n\t\t\t\tautocomplete=\"off\"\n\t\t\t\tclass=\"sonic-number-entry\"\n\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\thidden\n\t\t\t\tspellcheck=\"false\"\n\t\t\t\ttype=\"text\"\n\t\t\t/>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicNumber extends SonicValueElement {\n\tget press(): 'none' | 'step' {\n\t\treturn this.getAttribute('press') === 'step' ? 'step' : 'none';\n\t}\n\n\tset press(gesture: 'none' | 'step' | undefined) {\n\t\tthis.reflect('press', gesture);\n\t}\n\n\treadonly #number = renderNumber();\n\n\treadonly #digits = requireChild(this.#number, '.sonic-number-value', HTMLSpanElement);\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('press');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst number = this.#number;\n\n\t\tthis.keepControl(number, signal);\n\t\tthis.render();\n\t\tif (__DEV__)\n\t\t\tthis.checkStyles(number, 'number.css', {\n\t\t\t\tbox: {\n\t\t\t\t\tproperty: 'padding-inline-start',\n\t\t\t\t\tratio: '--_sonic-number-inset-ratio',\n\t\t\t\t},\n\t\t\t\tmaterial: ['cap', 'glass', 'pane', 'well', 'value'],\n\t\t\t});\n\t\tthis.bindGestures(number, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: this.travelPx(number, '--_sonic-number-travel'),\n\t\t}));\n\t}\n\n\tprotected override controlRole(): 'spinbutton' {\n\t\treturn 'spinbutton';\n\t}\n\n\tprotected draw(): void {\n\t\tthis.#digits.textContent = this.valueText;\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#number;\n\t}\n\n\tprotected override tapTarget(): number | undefined {\n\t\tif (this.press !== 'step') return undefined;\n\n\t\tconst mapping = this.mapping();\n\t\tconst next = mapping.keyTarget('ArrowUp', this.value);\n\n\t\treturn next === undefined || next === this.value ? mapping.bounds[0] : next;\n\t}\n}\n"],"mappings":";;;;AAWA,MAAM,eAAe,SACT;;;;;;;;;;;;IAaX,cACD;AAEA,IAAa,cAAb,cAAiC,kBAAkB;CAClD,IAAI,QAAyB;EAC5B,OAAO,KAAK,aAAa,OAAO,MAAM,SAAS,SAAS;CACzD;CAEA,IAAI,MAAM,SAAsC;EAC/C,KAAK,QAAQ,SAAS,OAAO;CAC9B;CAEA,AAAS,UAAU,aAAa;CAEhC,AAAS,UAAU,aAAa,KAAK,SAAS,uBAAuB,eAAe;CAEpF,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,OAAO;EAC9B,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EAEpB,KAAK,YAAY,QAAQ,MAAM;EAC/B,KAAK,OAAO;EAEX,KAAK,YAAY,QAAQ,cAAc;GACtC,KAAK;IACJ,UAAU;IACV,OAAO;GACR;GACA,UAAU;IAAC;IAAO;IAAS;IAAQ;IAAQ;GAAO;EACnD,CAAC;EACF,KAAK,aAAa,QAAQ,eAAe;GACxC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,KAAK,SAAS,QAAQ,wBAAwB;EACzD,EAAE;CACH;CAEA,AAAmB,cAA4B;EAC9C,OAAO;CACR;CAEA,AAAU,OAAa;EACtB,KAAK,QAAQ,cAAc,KAAK;CACjC;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAmB,YAAgC;EAClD,IAAI,KAAK,UAAU,QAAQ,OAAO;EAElC,MAAM,UAAU,KAAK,QAAQ;EAC7B,MAAM,OAAO,QAAQ,UAAU,WAAW,KAAK,KAAK;EAEpD,OAAO,SAAS,UAAa,SAAS,KAAK,QAAQ,QAAQ,OAAO,KAAK;CACxE;AACD"}
@@ -1,3 +1,4 @@
1
+ import { Position } from "../lib/positions.js";
1
2
  import { SonicRadioGroupElement } from "./radio-group.js";
2
3
  //#region src/elements/position-group.d.ts
3
4
  type Orientation = 'horizontal' | 'vertical';
@@ -18,19 +19,24 @@ export declare abstract class SonicPositionGroupElement extends SonicRadioGroupE
18
19
  formResetCallback(): void;
19
20
  protected activate(): void;
20
21
  protected along(event: PointerEvent): number;
21
- protected checkedIndex(options: Array<HTMLButtonElement>): number;
22
- protected chooseByClick(option: HTMLButtonElement): void;
23
- protected chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void;
22
+ protected checkedIndex(): number;
23
+ protected chooseByClick(index: number): void;
24
+ protected chooseByKey(index: number, event: KeyboardEvent): void;
24
25
  protected connect(signal: AbortSignal): void;
25
26
  protected focusTarget(): HTMLElement | undefined;
26
- protected hold(option: HTMLButtonElement, by: number | string): void;
27
+ protected hold(index: number, by: number | string): void;
27
28
  protected isBare(): boolean;
28
- protected isMomentary(option: HTMLButtonElement): boolean;
29
+ protected isChecked(index: number): boolean;
30
+ protected isMomentary(index: number): boolean;
29
31
  protected isWrapping(): boolean;
30
- protected pressTarget(option: HTMLButtonElement): HTMLButtonElement;
32
+ protected onIndex(): number;
33
+ protected positions(): Array<Position>;
34
+ protected pressedIndex(target: EventTarget | null): number;
35
+ protected pressTarget(index: number): number;
31
36
  protected releaseHold(): void;
32
37
  protected render(): void;
33
38
  protected restoreState(state: string): void;
39
+ protected select(index: number): void;
34
40
  protected setChecked(isChecked: boolean): void;
35
41
  protected springBack(): void;
36
42
  }
@@ -1,8 +1,13 @@
1
1
  import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { bindHold } from "../lib/hold.js";
3
+ import { isMomentaryAt, pressTarget, restOf } from "../lib/positions.js";
3
4
  import { SonicRadioGroupElement, optionValue } from "./radio-group.js";
4
5
 
5
6
  //#region src/elements/position-group.ts
7
+ const latching = {
8
+ isDisabled: false,
9
+ isMomentary: false
10
+ };
6
11
  var SonicPositionGroupElement = class extends SonicRadioGroupElement {
7
12
  static observedAttributes = [
8
13
  ...SonicRadioGroupElement.observedAttributes,
@@ -56,21 +61,22 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
56
61
  along(event) {
57
62
  return this.orientation === "vertical" ? event.clientY : event.clientX;
58
63
  }
59
- checkedIndex(options) {
60
- return options.findIndex((option) => optionValue(option) === this.value);
64
+ checkedIndex() {
65
+ if (this.isBare()) return this.checked ? this.onIndex() : 1 - this.onIndex();
66
+ return this.options().findIndex((option) => optionValue(option) === this.value);
61
67
  }
62
- chooseByClick(option) {
63
- super.chooseByClick(option);
64
- if (this.isMomentary(option)) this.springBack();
68
+ chooseByClick(index) {
69
+ super.chooseByClick(index);
70
+ if (this.isMomentary(index)) this.springBack();
65
71
  }
66
- chooseByKey(option, event) {
67
- if (!this.isMomentary(option)) {
68
- super.chooseByKey(option, event);
72
+ chooseByKey(index, event) {
73
+ if (!this.isMomentary(index)) {
74
+ super.chooseByKey(index, event);
69
75
  return;
70
76
  }
71
77
  if (event.repeat) return;
72
- this.hold(option, event.key);
73
- option.focus();
78
+ this.hold(index, event.key);
79
+ this.options()[index]?.focus();
74
80
  }
75
81
  connect(signal) {
76
82
  this.upgradeProperties("defaultChecked", "checked", "orientation");
@@ -90,23 +96,33 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
90
96
  focusTarget() {
91
97
  return this.isBare() ? this.bare : super.focusTarget();
92
98
  }
93
- hold(option, by) {
94
- if (this.#holding?.hold(by) === true) this.select(option);
99
+ hold(index, by) {
100
+ if (this.#holding?.hold(by) === true) this.select(index);
95
101
  }
96
102
  isBare() {
97
103
  return this.options().length === 0;
98
104
  }
99
- isMomentary(option) {
100
- const options = this.options();
101
- return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
105
+ isChecked(index) {
106
+ const option = this.options()[index];
107
+ return option ? optionValue(option) === this.value : index === this.checkedIndex();
108
+ }
109
+ isMomentary(index) {
110
+ return isMomentaryAt(this.positions(), index);
102
111
  }
103
112
  isWrapping() {
104
113
  return false;
105
114
  }
106
- pressTarget(option) {
107
- const options = this.options();
108
- const index = this.checkedIndex(options);
109
- return (options.length === 2 && index >= 0 ? options[1 - index] : void 0) ?? option;
115
+ onIndex() {
116
+ return this.orientation === "vertical" ? 0 : 1;
117
+ }
118
+ positions() {
119
+ return this.isBare() ? [latching, latching] : super.positions();
120
+ }
121
+ pressedIndex(target) {
122
+ return target === this.bare ? 0 : this.indexOf(target);
123
+ }
124
+ pressTarget(index) {
125
+ return pressTarget(this.positions(), this.checkedIndex(), index);
110
126
  }
111
127
  releaseHold() {
112
128
  this.#holding?.release();
@@ -128,21 +144,21 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
128
144
  if (this.isBare()) this.checked = state === "true";
129
145
  else super.restoreState(state);
130
146
  }
147
+ select(index) {
148
+ if (this.isBare()) this.setChecked(index === this.onIndex());
149
+ else super.select(index);
150
+ }
131
151
  setChecked(isChecked) {
132
152
  if (isChecked === this.checked) return;
133
153
  this.checked = isChecked;
134
154
  this.dispatchEvent(new Event("change", { bubbles: true }));
135
155
  }
136
156
  springBack() {
137
- const options = this.options();
138
- const index = this.checkedIndex(options);
139
- const held = options[index];
140
- if (!held || !this.isMomentary(held)) return;
141
- const rest = options[index === 0 ? 1 : index - 1];
142
- if (!rest) return;
157
+ const rest = restOf(this.positions(), this.checkedIndex());
158
+ if (rest === void 0) return;
143
159
  const isFocused = this.group.contains(this.ownerDocument.activeElement);
144
160
  this.select(rest);
145
- if (isFocused) rest.focus();
161
+ if (isFocused) this.options()[rest]?.focus();
146
162
  }
147
163
  #renderBare() {
148
164
  const group = this.group;
@@ -1 +1 @@
1
- {"version":3,"file":"position-group.js","names":[],"sources":["../../../src/elements/position-group.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { optionValue, SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ntype Orientation = 'horizontal' | 'vertical';\n\nexport abstract class SonicPositionGroupElement 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\tget orientation(): Orientation {\n\t\tconst orientation = this.getAttribute('orientation');\n\n\t\treturn orientation === 'horizontal' || orientation === 'vertical'\n\t\t\t? orientation\n\t\t\t: this.defaultOrientation;\n\t}\n\n\tset orientation(direction: Orientation | 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 abstract readonly bare: HTMLButtonElement;\n\n\tprotected abstract readonly defaultOrientation: Orientation;\n\n\t#checked = false;\n\n\t#holding: Hold | undefined;\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 along(event: PointerEvent): number {\n\t\treturn this.orientation === 'vertical' ? event.clientY : event.clientX;\n\t}\n\n\tprotected checkedIndex(options: Array<HTMLButtonElement>): number {\n\t\treturn options.findIndex((option) => optionValue(option) === this.value);\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 connect(signal: AbortSignal): void {\n\t\tthis.upgradeProperties('defaultChecked', 'checked', 'orientation');\n\t\tsuper.connect(signal);\n\t\tthis.#holding = bindHold(\n\t\t\tthis.group,\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 hold(option: HTMLButtonElement, by: number | string): void {\n\t\tif (this.#holding?.hold(by) === true) this.select(option);\n\t}\n\n\tprotected isBare(): boolean {\n\t\treturn this.options().length === 0;\n\t}\n\n\tprotected 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\tprotected override isWrapping(): boolean {\n\t\treturn false;\n\t}\n\n\tprotected pressTarget(option: HTMLButtonElement): HTMLButtonElement {\n\t\tconst options = this.options();\n\t\tconst index = this.checkedIndex(options);\n\t\tconst other = options.length === 2 && index >= 0 ? options[1 - index] : undefined;\n\n\t\treturn other ?? option;\n\t}\n\n\tprotected releaseHold(): void {\n\t\tthis.#holding?.release();\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst group = this.group;\n\t\tconst count = this.options().length;\n\n\t\tif (count === 0) {\n\t\t\tthis.#renderBare();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.bare.remove();\n\t\twriteAttribute(group, 'role', 'radiogroup');\n\t\twriteAttribute(group, 'data-sonic-position-count', String(count));\n\t\tsuper.render();\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\tprotected 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\tprotected 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#renderBare(): void {\n\t\tconst group = this.group;\n\t\tconst bare = this.bare;\n\t\tconst isChecked = this.checked;\n\n\t\twriteAttribute(group, 'role', undefined);\n\t\twriteAttribute(group, 'data-sonic-position-count', undefined);\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\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"],"mappings":"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"}
1
+ {"version":3,"file":"position-group.js","names":[],"sources":["../../../src/elements/position-group.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\nimport type { Position } from '#lib/positions.ts';\n\nimport { optionValue, SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { isMomentaryAt, pressTarget, restOf } from '#lib/positions.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ntype Orientation = 'horizontal' | 'vertical';\n\nconst latching: Position = { isDisabled: false, isMomentary: false };\n\nexport abstract class SonicPositionGroupElement 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\tget orientation(): Orientation {\n\t\tconst orientation = this.getAttribute('orientation');\n\n\t\treturn orientation === 'horizontal' || orientation === 'vertical'\n\t\t\t? orientation\n\t\t\t: this.defaultOrientation;\n\t}\n\n\tset orientation(direction: Orientation | 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 abstract readonly bare: HTMLButtonElement;\n\n\tprotected abstract readonly defaultOrientation: Orientation;\n\n\t#checked = false;\n\n\t#holding: Hold | undefined;\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 along(event: PointerEvent): number {\n\t\treturn this.orientation === 'vertical' ? event.clientY : event.clientX;\n\t}\n\n\tprotected checkedIndex(): number {\n\t\tif (this.isBare()) return this.checked ? this.onIndex() : 1 - this.onIndex();\n\n\t\treturn this.options().findIndex((option) => optionValue(option) === this.value);\n\t}\n\n\tprotected override chooseByClick(index: number): void {\n\t\tsuper.chooseByClick(index);\n\t\tif (this.isMomentary(index)) this.springBack();\n\t}\n\n\tprotected override chooseByKey(index: number, event: KeyboardEvent): void {\n\t\tif (!this.isMomentary(index)) {\n\t\t\tsuper.chooseByKey(index, event);\n\t\t\treturn;\n\t\t}\n\t\tif (event.repeat) return;\n\n\t\tthis.hold(index, event.key);\n\t\tthis.options()[index]?.focus();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tthis.upgradeProperties('defaultChecked', 'checked', 'orientation');\n\t\tsuper.connect(signal);\n\t\tthis.#holding = bindHold(\n\t\t\tthis.group,\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 hold(index: number, by: number | string): void {\n\t\tif (this.#holding?.hold(by) === true) this.select(index);\n\t}\n\n\tprotected isBare(): boolean {\n\t\treturn this.options().length === 0;\n\t}\n\n\t// A row may repeat a value, and every position that holds it is checked\n\tprotected isChecked(index: number): boolean {\n\t\tconst option = this.options()[index];\n\n\t\treturn option ? optionValue(option) === this.value : index === this.checkedIndex();\n\t}\n\n\tprotected isMomentary(index: number): boolean {\n\t\treturn isMomentaryAt(this.positions(), index);\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn false;\n\t}\n\n\t// On is up, as on hardware\n\tprotected onIndex(): number {\n\t\treturn this.orientation === 'vertical' ? 0 : 1;\n\t}\n\n\tprotected override positions(): Array<Position> {\n\t\treturn this.isBare() ? [latching, latching] : super.positions();\n\t}\n\n\tprotected pressedIndex(target: EventTarget | null): number {\n\t\treturn target === this.bare ? 0 : this.indexOf(target);\n\t}\n\n\tprotected pressTarget(index: number): number {\n\t\treturn pressTarget(this.positions(), this.checkedIndex(), index);\n\t}\n\n\tprotected releaseHold(): void {\n\t\tthis.#holding?.release();\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst group = this.group;\n\t\tconst count = this.options().length;\n\n\t\tif (count === 0) {\n\t\t\tthis.#renderBare();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.bare.remove();\n\t\twriteAttribute(group, 'role', 'radiogroup');\n\t\twriteAttribute(group, 'data-sonic-position-count', String(count));\n\t\tsuper.render();\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\tprotected override select(index: number): void {\n\t\tif (this.isBare()) this.setChecked(index === this.onIndex());\n\t\telse super.select(index);\n\t}\n\n\tprotected 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\tprotected springBack(): void {\n\t\tconst rest = restOf(this.positions(), this.checkedIndex());\n\t\tif (rest === undefined) return;\n\n\t\tconst isFocused = this.group.contains(this.ownerDocument.activeElement);\n\n\t\tthis.select(rest);\n\t\tif (isFocused) this.options()[rest]?.focus();\n\t}\n\n\t#renderBare(): void {\n\t\tconst group = this.group;\n\t\tconst bare = this.bare;\n\t\tconst isChecked = this.checked;\n\n\t\twriteAttribute(group, 'role', undefined);\n\t\twriteAttribute(group, 'data-sonic-position-count', undefined);\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\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"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { SonicFormElement } from "./form-element.js";
2
+ import { Position } from "../lib/positions.js";
2
3
  //#region src/elements/radio-group.d.ts
3
4
  export declare abstract class SonicRadioGroupElement extends SonicFormElement {
4
5
  #private;
@@ -7,23 +8,26 @@ export declare abstract class SonicRadioGroupElement extends SonicFormElement {
7
8
  set value(next: string);
8
9
  protected abstract readonly group: HTMLElement;
9
10
  attributeChangedCallback(name: string): void;
10
- protected chooseByClick(option: HTMLButtonElement): void;
11
- protected chooseByKey(option: HTMLButtonElement, _event: KeyboardEvent): void;
12
- protected claimPress(_option: HTMLButtonElement, _event: PointerEvent): boolean;
11
+ protected chooseByClick(index: number): void;
12
+ protected chooseByKey(index: number, _event: KeyboardEvent): void;
13
+ protected claimPress(_index: number, _event: PointerEvent): boolean;
13
14
  protected connect(signal: AbortSignal): void;
14
15
  protected focusTarget(): HTMLElement | undefined;
16
+ protected indexOf(target: EventTarget | null): number;
15
17
  protected isMirrored(child: Node): boolean;
16
- protected isOptionDisabled(option: HTMLButtonElement): boolean;
18
+ protected isOptionDisabled(index: number): boolean;
17
19
  protected isWrapping(): boolean;
18
20
  protected optionOf(target: EventTarget | null): HTMLButtonElement | undefined;
19
21
  protected options(): Array<HTMLButtonElement>;
20
22
  protected placeCopies(copies: Array<Node>): void;
21
- protected releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined;
23
+ protected positions(): Array<Position>;
24
+ protected refocusAt(index: number): void;
25
+ protected releaseTarget(index: number): number | undefined;
22
26
  protected render(): void;
23
27
  protected abstract renderOption(): HTMLButtonElement;
24
28
  protected restoreState(state: string): void;
25
- protected select(option: HTMLButtonElement): void;
26
- protected stepFrom(from: number, step: number): HTMLButtonElement | undefined;
29
+ protected select(index: number): void;
30
+ protected stepFrom(from: number, step: number): number | undefined;
27
31
  }
28
32
  //#endregion
29
33
  //# sourceMappingURL=radio-group.d.ts.map
@@ -1,4 +1,3 @@
1
- import { clamp } from "../lib/math.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicFormElement } from "./form-element.js";
4
3
  import { copyNode } from "../lib/copy-node.js";
@@ -6,14 +5,9 @@ import { bindKeyPress } from "../lib/key-press.js";
6
5
  import { placeChildren } from "../lib/render.js";
7
6
  import { mirrorChildren } from "../lib/mirror-children.js";
8
7
  import { bindDrag } from "../lib/pointer-drag.js";
8
+ import { keyTarget, stepFrom } from "../lib/positions.js";
9
9
 
10
10
  //#region src/elements/radio-group.ts
11
- const keySteps = /* @__PURE__ */ new Map([
12
- ["ArrowDown", 1],
13
- ["ArrowLeft", -1],
14
- ["ArrowRight", 1],
15
- ["ArrowUp", -1]
16
- ]);
17
11
  function isInside(box, x, y) {
18
12
  return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
19
13
  }
@@ -38,14 +32,14 @@ var SonicRadioGroupElement = class extends SonicFormElement {
38
32
  if (name === "value") this.#value = this.getAttribute("value") ?? "";
39
33
  this.render();
40
34
  }
41
- chooseByClick(option) {
42
- this.select(option);
35
+ chooseByClick(index) {
36
+ this.select(index);
43
37
  }
44
- chooseByKey(option, _event) {
45
- this.select(option);
46
- option.focus();
38
+ chooseByKey(index, _event) {
39
+ this.select(index);
40
+ this.options()[index]?.focus();
47
41
  }
48
- claimPress(_option, _event) {
42
+ claimPress(_index, _event) {
49
43
  return false;
50
44
  }
51
45
  connect(signal) {
@@ -61,16 +55,19 @@ var SonicRadioGroupElement = class extends SonicFormElement {
61
55
  }, signal);
62
56
  group.addEventListener("click", (event) => {
63
57
  if (event instanceof PointerEvent && event.pointerType !== "") return;
64
- const option = this.optionOf(event.target);
65
- if (option) this.chooseByClick(option);
58
+ const index = this.indexOf(event.target);
59
+ if (index !== -1) this.chooseByClick(index);
66
60
  }, { signal });
67
61
  this.#bindPress(group, signal);
68
62
  bindKeyPress(group, signal);
69
63
  group.addEventListener("keydown", (event) => {
70
64
  if (event.defaultPrevented) return;
71
- const option = this.optionOf(event.target);
72
- const next = option && this.#keyTarget(option, event.key);
73
- if (!next) return;
65
+ const index = this.indexOf(event.target);
66
+ const next = index === -1 ? void 0 : keyTarget(this.positions(), index, {
67
+ isWrapping: this.isWrapping(),
68
+ key: event.key
69
+ });
70
+ if (next === void 0) return;
74
71
  event.preventDefault();
75
72
  this.chooseByKey(next, event);
76
73
  }, { signal });
@@ -78,11 +75,15 @@ var SonicRadioGroupElement = class extends SonicFormElement {
78
75
  focusTarget() {
79
76
  return this.options().find((option) => option.tabIndex === 0);
80
77
  }
78
+ indexOf(target) {
79
+ const option = this.optionOf(target);
80
+ return option ? this.options().indexOf(option) : -1;
81
+ }
81
82
  isMirrored(child) {
82
83
  return child instanceof Element && child.matches("[data-sonic-value]");
83
84
  }
84
- isOptionDisabled(option) {
85
- return option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-disabled") === true;
85
+ isOptionDisabled(index) {
86
+ return this.positions()[index]?.isDisabled !== false;
86
87
  }
87
88
  isWrapping() {
88
89
  return true;
@@ -98,14 +99,27 @@ var SonicRadioGroupElement = class extends SonicFormElement {
98
99
  placeCopies(copies) {
99
100
  this.#copyOptions(copies);
100
101
  }
101
- releaseTarget(option) {
102
- return option;
102
+ positions() {
103
+ return this.options().map((option) => {
104
+ const child = option.querySelector("[data-sonic-value]");
105
+ return {
106
+ isDisabled: child?.hasAttribute("data-sonic-disabled") === true,
107
+ isMomentary: child?.hasAttribute("data-sonic-momentary") === true
108
+ };
109
+ });
110
+ }
111
+ refocusAt(index) {
112
+ if (this.group.matches(":focus-within")) this.options()[index]?.focus();
113
+ }
114
+ releaseTarget(index) {
115
+ return index;
103
116
  }
104
117
  render() {
105
118
  if (!this.isBound()) return;
106
119
  const options = this.options();
120
+ const positions = this.positions();
107
121
  const checked = options.find((option) => optionValue(option) === this.#value);
108
- const enabled = this.isDisabled() ? [] : options.filter((option) => !this.isOptionDisabled(option));
122
+ const enabled = this.isDisabled() ? [] : options.filter((_option, index) => positions[index]?.isDisabled === false);
109
123
  const focusable = enabled.find((option) => option === checked) ?? enabled[0];
110
124
  for (const option of options) {
111
125
  writeAttribute(option, "disabled", enabled.includes(option) ? void 0 : "");
@@ -118,45 +132,42 @@ var SonicRadioGroupElement = class extends SonicFormElement {
118
132
  restoreState(state) {
119
133
  this.value = state;
120
134
  }
121
- select(option) {
122
- const next = optionValue(option);
135
+ select(index) {
136
+ const option = this.options()[index];
137
+ const next = option && optionValue(option);
123
138
  if (next === void 0 || next === this.#value) return;
124
139
  this.value = next;
125
140
  this.dispatchEvent(new Event("change", { bubbles: true }));
126
141
  }
127
142
  stepFrom(from, step) {
128
- const options = this.options();
129
- let index = from;
130
- for (const _ of options) {
131
- const next = index + step;
132
- index = this.isWrapping() ? (next + options.length) % options.length : clamp(next, 0, options.length - 1);
133
- const option = options[index];
134
- if (option && !this.isOptionDisabled(option)) return option;
135
- }
143
+ return stepFrom(this.positions(), from, {
144
+ isWrapping: this.isWrapping(),
145
+ step
146
+ });
136
147
  }
137
148
  #bindPress(group, signal) {
138
149
  bindDrag(group, {
139
150
  grab: (event) => {
140
- const option = this.optionOf(event.target);
141
- if (!option || this.isDisabled() || this.isOptionDisabled(option)) return;
142
- if (this.claimPress(option, event)) return;
151
+ const index = this.indexOf(event.target);
152
+ if (index === -1 || this.isDisabled() || this.isOptionDisabled(index)) return;
153
+ if (this.claimPress(index, event)) return;
143
154
  const boxes = this.#optionBoxes();
144
- this.#markPressed(option);
155
+ this.#markPressed(index);
145
156
  return {
146
157
  boxes,
147
- option
158
+ index
148
159
  };
149
160
  },
150
161
  lift: (press) => {
151
- if (!press.option || this.isDisabled()) return;
152
- const option = this.releaseTarget(press.option);
153
- if (!option) return;
154
- this.select(option);
155
- if (group.matches(":focus-within")) option.focus();
162
+ if (press.index === void 0 || this.isDisabled()) return;
163
+ const index = this.releaseTarget(press.index);
164
+ if (index === void 0) return;
165
+ this.select(index);
166
+ this.refocusAt(index);
156
167
  },
157
168
  move: (press, event) => {
158
- press.option = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
159
- this.#markPressed(press.option);
169
+ press.index = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
170
+ this.#markPressed(press.index);
160
171
  },
161
172
  release: () => {
162
173
  this.#markPressed(void 0);
@@ -180,20 +191,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
180
191
  this.render();
181
192
  if (focused) this.#refocus(focused, focusedValue);
182
193
  }
183
- #keyTarget(option, key) {
184
- const options = this.options();
185
- const isEnabled = (other) => !this.isOptionDisabled(other);
186
- if (key === "Home") return options.find((other) => isEnabled(other));
187
- if (key === "End") return options.findLast((other) => isEnabled(other));
188
- const step = keySteps.get(key);
189
- if (step === void 0) return void 0;
190
- return this.stepFrom(options.indexOf(option), step) ?? option;
191
- }
192
194
  #markPressed(pressed) {
193
- for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
195
+ for (const [index, option] of this.options().entries()) option.toggleAttribute("data-sonic-pressed", index === pressed);
194
196
  }
195
197
  #optionBoxes() {
196
- return this.options().filter((option) => !this.isOptionDisabled(option)).map((option) => [option, option.getBoundingClientRect()]);
198
+ const positions = this.positions();
199
+ return this.options().flatMap((option, index) => positions[index]?.isDisabled === false ? [[index, option.getBoundingClientRect()]] : []);
197
200
  }
198
201
  #refocus(focused, value) {
199
202
  const options = this.options();
@@ -1 +1 @@
1
- {"version":3,"file":"radio-group.js","names":[],"sources":["../../../src/elements/radio-group.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindKeyPress } from '#lib/key-press.ts';\nimport { clamp } from '#lib/math.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { placeChildren } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\n// RTL is not mirrored\nconst keySteps = new Map([\n\t['ArrowDown', 1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', -1],\n]);\n\ninterface Press {\n\tboxes: Array<[HTMLButtonElement, DOMRect]>;\n\toption: HTMLButtonElement | undefined;\n}\n\nfunction isInside(box: DOMRect, x: number, y: number): boolean {\n\treturn x >= box.left && x < box.right && y >= box.top && y < box.bottom;\n}\n\nexport function isUnder(option: Element, x: number, y: number): boolean {\n\treturn isInside(option.getBoundingClientRect(), x, y);\n}\n\nexport function optionValue(option: Element): string | undefined {\n\treturn option.querySelector<HTMLElement | SVGElement>('[data-sonic-value]')?.dataset.sonicValue;\n}\n\nexport abstract class SonicRadioGroupElement extends SonicFormElement {\n\tstatic override readonly observedAttributes = [...SonicFormElement.observedAttributes, 'value'];\n\n\tget value(): string {\n\t\treturn this.#value;\n\t}\n\n\tset value(next: string) {\n\t\tif (next === this.#value) return;\n\n\t\tthis.#value = next;\n\t\tthis.render();\n\t}\n\n\tprotected abstract readonly group: HTMLElement;\n\n\t#value = '';\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'value') this.#value = this.getAttribute('value') ?? '';\n\t\tthis.render();\n\t}\n\n\tprotected chooseByClick(option: HTMLButtonElement): void {\n\t\tthis.select(option);\n\t}\n\n\tprotected chooseByKey(option: HTMLButtonElement, _event: KeyboardEvent): void {\n\t\tthis.select(option);\n\t\toption.focus();\n\t}\n\n\tprotected claimPress(_option: HTMLButtonElement, _event: PointerEvent): boolean {\n\t\treturn false;\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tthis.upgradeProperties('value');\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: group,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tisCopied: (child) => this.isMirrored(child),\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tthis.placeCopies(copies);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tgroup.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (event instanceof PointerEvent && event.pointerType !== '') return;\n\n\t\t\t\tconst option = this.optionOf(event.target);\n\t\t\t\tif (option) this.chooseByClick(option);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindPress(group, signal);\n\t\tbindKeyPress(group, signal);\n\t\tgroup.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.defaultPrevented) return;\n\n\t\t\t\tconst option = this.optionOf(event.target);\n\t\t\t\tconst next = option && this.#keyTarget(option, event.key);\n\t\t\t\tif (!next) return;\n\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.chooseByKey(next, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.options().find((option) => option.tabIndex === 0);\n\t}\n\n\tprotected isMirrored(child: Node): boolean {\n\t\treturn child instanceof Element && child.matches('[data-sonic-value]');\n\t}\n\n\tprotected isOptionDisabled(option: HTMLButtonElement): boolean {\n\t\treturn option.querySelector('[data-sonic-value]')?.hasAttribute('data-sonic-disabled') === true;\n\t}\n\n\tprotected isWrapping(): boolean {\n\t\treturn true;\n\t}\n\n\tprotected optionOf(target: EventTarget | null): HTMLButtonElement | undefined {\n\t\tif (!(target instanceof Element)) return undefined;\n\n\t\tconst option = target.closest('[role=\"radio\"]');\n\n\t\treturn option instanceof HTMLButtonElement && option.parentElement === this.group\n\t\t\t? option\n\t\t\t: undefined;\n\t}\n\n\tprotected options(): Array<HTMLButtonElement> {\n\t\treturn [...this.group.children].filter(\n\t\t\t(child): child is HTMLButtonElement =>\n\t\t\t\tchild instanceof HTMLButtonElement && child.getAttribute('role') === 'radio',\n\t\t);\n\t}\n\n\tprotected placeCopies(copies: Array<Node>): void {\n\t\tthis.#copyOptions(copies);\n\t}\n\n\tprotected releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined {\n\t\treturn option;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst options = this.options();\n\t\tconst checked = options.find((option) => optionValue(option) === this.#value);\n\t\tconst enabled = this.isDisabled()\n\t\t\t? []\n\t\t\t: options.filter((option) => !this.isOptionDisabled(option));\n\t\tconst focusable = enabled.find((option) => option === checked) ?? enabled[0];\n\n\t\tfor (const option of options) {\n\t\t\twriteAttribute(option, 'disabled', enabled.includes(option) ? undefined : '');\n\t\t\twriteAttribute(option, 'aria-checked', String(option === checked));\n\t\t\twriteAttribute(option, 'tabindex', option === focusable ? '0' : '-1');\n\t\t}\n\t\tthis.forwardNaming(this.group, true);\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(checked ? this.#value : null, this.#value);\n\t}\n\n\tprotected abstract renderOption(): HTMLButtonElement;\n\n\tprotected restoreState(state: string): void {\n\t\tthis.value = state;\n\t}\n\n\tprotected select(option: HTMLButtonElement): void {\n\t\tconst next = optionValue(option);\n\t\tif (next === undefined || next === this.#value) return;\n\n\t\tthis.value = next;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\tprotected stepFrom(from: number, step: number): HTMLButtonElement | undefined {\n\t\tconst options = this.options();\n\t\tlet index = from;\n\n\t\tfor (const _ of options) {\n\t\t\tconst next = index + step;\n\n\t\t\tindex = this.isWrapping()\n\t\t\t\t? (next + options.length) % options.length\n\t\t\t\t: clamp(next, 0, options.length - 1);\n\n\t\t\tconst option = options[index];\n\t\t\tif (option && !this.isOptionDisabled(option)) return option;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t#bindPress(group: HTMLElement, signal: AbortSignal): void {\n\t\tbindDrag<Press>(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tconst option = this.optionOf(event.target);\n\t\t\t\t\tif (!option || this.isDisabled() || this.isOptionDisabled(option)) return;\n\t\t\t\t\tif (this.claimPress(option, event)) return;\n\n\t\t\t\t\tconst boxes = this.#optionBoxes();\n\n\t\t\t\t\tthis.#markPressed(option);\n\n\t\t\t\t\treturn { boxes, option };\n\t\t\t\t},\n\t\t\t\tlift: (press) => {\n\t\t\t\t\tif (!press.option || this.isDisabled()) return;\n\n\t\t\t\t\tconst option = this.releaseTarget(press.option);\n\t\t\t\t\tif (!option) return;\n\n\t\t\t\t\tthis.select(option);\n\t\t\t\t\tif (group.matches(':focus-within')) option.focus();\n\t\t\t\t},\n\t\t\t\tmove: (press, event) => {\n\t\t\t\t\tpress.option = press.boxes.find(([, box]) =>\n\t\t\t\t\t\tisInside(box, event.clientX, event.clientY),\n\t\t\t\t\t)?.[0];\n\t\t\t\t\tthis.#markPressed(press.option);\n\t\t\t\t},\n\t\t\t\trelease: () => {\n\t\t\t\t\tthis.#markPressed(undefined);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#copyOptions(copies: Array<Node>): void {\n\t\tconst group = this.group;\n\t\tconst options = this.options();\n\t\tconst focused = this.optionOf(group.querySelector(':scope > :focus'));\n\t\tconst focusedValue = focused && optionValue(focused);\n\n\t\tfor (const [index, copy] of copies.entries()) {\n\t\t\tlet option = options[index];\n\t\t\tif (!option) {\n\t\t\t\toption = this.renderOption();\n\t\t\t\tgroup.append(option);\n\t\t\t}\n\n\t\t\tif (option.firstElementChild) placeChildren(option.firstElementChild, [copy]);\n\t\t}\n\t\tfor (const option of options.slice(copies.length)) option.remove();\n\t\tthis.render();\n\t\tif (focused) this.#refocus(focused, focusedValue);\n\t}\n\n\t#keyTarget(option: HTMLButtonElement, key: string): HTMLButtonElement | undefined {\n\t\tconst options = this.options();\n\t\tconst isEnabled = (other: HTMLButtonElement): boolean => !this.isOptionDisabled(other);\n\n\t\tif (key === 'Home') return options.find((other) => isEnabled(other));\n\t\tif (key === 'End') return options.findLast((other) => isEnabled(other));\n\n\t\tconst step = keySteps.get(key);\n\t\tif (step === undefined) return undefined;\n\n\t\treturn this.stepFrom(options.indexOf(option), step) ?? option;\n\t}\n\n\t#markPressed(pressed: HTMLButtonElement | undefined): void {\n\t\tfor (const option of this.options()) {\n\t\t\toption.toggleAttribute('data-sonic-pressed', option === pressed);\n\t\t}\n\t}\n\n\t#optionBoxes(): Array<[HTMLButtonElement, DOMRect]> {\n\t\treturn this.options()\n\t\t\t.filter((option) => !this.isOptionDisabled(option))\n\t\t\t.map((option) => [option, option.getBoundingClientRect()]);\n\t}\n\n\t#refocus(focused: HTMLButtonElement, value: string | undefined): void {\n\t\tconst options = this.options();\n\t\tconst next =\n\t\t\toptions.find((option) => optionValue(option) === value) ??\n\t\t\toptions.find((option) => option.tabIndex === 0);\n\n\t\tif (next !== focused) next?.focus();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"radio-group.js","names":[],"sources":["../../../src/elements/radio-group.ts"],"sourcesContent":["import type { Position } from '#lib/positions.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindKeyPress } from '#lib/key-press.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { keyTarget, stepFrom } from '#lib/positions.ts';\nimport { placeChildren } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ninterface Press {\n\tboxes: Array<[number, DOMRect]>;\n\tindex: number | undefined;\n}\n\nfunction isInside(box: DOMRect, x: number, y: number): boolean {\n\treturn x >= box.left && x < box.right && y >= box.top && y < box.bottom;\n}\n\nexport function isUnder(option: Element, x: number, y: number): boolean {\n\treturn isInside(option.getBoundingClientRect(), x, y);\n}\n\nexport function optionValue(option: Element): string | undefined {\n\treturn option.querySelector<HTMLElement | SVGElement>('[data-sonic-value]')?.dataset.sonicValue;\n}\n\nexport abstract class SonicRadioGroupElement extends SonicFormElement {\n\tstatic override readonly observedAttributes = [...SonicFormElement.observedAttributes, 'value'];\n\n\tget value(): string {\n\t\treturn this.#value;\n\t}\n\n\tset value(next: string) {\n\t\tif (next === this.#value) return;\n\n\t\tthis.#value = next;\n\t\tthis.render();\n\t}\n\n\tprotected abstract readonly group: HTMLElement;\n\n\t#value = '';\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'value') this.#value = this.getAttribute('value') ?? '';\n\t\tthis.render();\n\t}\n\n\tprotected chooseByClick(index: number): void {\n\t\tthis.select(index);\n\t}\n\n\tprotected chooseByKey(index: number, _event: KeyboardEvent): void {\n\t\tthis.select(index);\n\t\tthis.options()[index]?.focus();\n\t}\n\n\tprotected claimPress(_index: number, _event: PointerEvent): boolean {\n\t\treturn false;\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tthis.upgradeProperties('value');\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: group,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tisCopied: (child) => this.isMirrored(child),\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tthis.placeCopies(copies);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tgroup.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (event instanceof PointerEvent && event.pointerType !== '') return;\n\n\t\t\t\tconst index = this.indexOf(event.target);\n\t\t\t\tif (index !== -1) this.chooseByClick(index);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindPress(group, signal);\n\t\tbindKeyPress(group, signal);\n\t\tgroup.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.defaultPrevented) return;\n\n\t\t\t\tconst index = this.indexOf(event.target);\n\t\t\t\tconst next =\n\t\t\t\t\tindex === -1\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: keyTarget(this.positions(), index, { isWrapping: this.isWrapping(), key: event.key });\n\t\t\t\tif (next === undefined) return;\n\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.chooseByKey(next, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.options().find((option) => option.tabIndex === 0);\n\t}\n\n\tprotected indexOf(target: EventTarget | null): number {\n\t\tconst option = this.optionOf(target);\n\n\t\treturn option ? this.options().indexOf(option) : -1;\n\t}\n\n\tprotected isMirrored(child: Node): boolean {\n\t\treturn child instanceof Element && child.matches('[data-sonic-value]');\n\t}\n\n\tprotected isOptionDisabled(index: number): boolean {\n\t\treturn this.positions()[index]?.isDisabled !== false;\n\t}\n\n\tprotected isWrapping(): boolean {\n\t\treturn true;\n\t}\n\n\tprotected optionOf(target: EventTarget | null): HTMLButtonElement | undefined {\n\t\tif (!(target instanceof Element)) return undefined;\n\n\t\tconst option = target.closest('[role=\"radio\"]');\n\n\t\treturn option instanceof HTMLButtonElement && option.parentElement === this.group\n\t\t\t? option\n\t\t\t: undefined;\n\t}\n\n\tprotected options(): Array<HTMLButtonElement> {\n\t\treturn [...this.group.children].filter(\n\t\t\t(child): child is HTMLButtonElement =>\n\t\t\t\tchild instanceof HTMLButtonElement && child.getAttribute('role') === 'radio',\n\t\t);\n\t}\n\n\tprotected placeCopies(copies: Array<Node>): void {\n\t\tthis.#copyOptions(copies);\n\t}\n\n\tprotected positions(): Array<Position> {\n\t\treturn this.options().map((option) => {\n\t\t\tconst child = option.querySelector('[data-sonic-value]');\n\n\t\t\treturn {\n\t\t\t\tisDisabled: child?.hasAttribute('data-sonic-disabled') === true,\n\t\t\t\tisMomentary: child?.hasAttribute('data-sonic-momentary') === true,\n\t\t\t};\n\t\t});\n\t}\n\n\tprotected refocusAt(index: number): void {\n\t\tif (this.group.matches(':focus-within')) this.options()[index]?.focus();\n\t}\n\n\tprotected releaseTarget(index: number): number | undefined {\n\t\treturn index;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst options = this.options();\n\t\tconst positions = this.positions();\n\t\tconst checked = options.find((option) => optionValue(option) === this.#value);\n\t\tconst enabled = this.isDisabled()\n\t\t\t? []\n\t\t\t: options.filter((_option, index) => positions[index]?.isDisabled === false);\n\t\tconst focusable = enabled.find((option) => option === checked) ?? enabled[0];\n\n\t\tfor (const option of options) {\n\t\t\twriteAttribute(option, 'disabled', enabled.includes(option) ? undefined : '');\n\t\t\twriteAttribute(option, 'aria-checked', String(option === checked));\n\t\t\twriteAttribute(option, 'tabindex', option === focusable ? '0' : '-1');\n\t\t}\n\t\tthis.forwardNaming(this.group, true);\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(checked ? this.#value : null, this.#value);\n\t}\n\n\tprotected abstract renderOption(): HTMLButtonElement;\n\n\tprotected restoreState(state: string): void {\n\t\tthis.value = state;\n\t}\n\n\tprotected select(index: number): void {\n\t\tconst option = this.options()[index];\n\t\tconst next = option && optionValue(option);\n\t\tif (next === undefined || next === this.#value) return;\n\n\t\tthis.value = next;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\tprotected stepFrom(from: number, step: number): number | undefined {\n\t\treturn stepFrom(this.positions(), from, { isWrapping: this.isWrapping(), step });\n\t}\n\n\t#bindPress(group: HTMLElement, signal: AbortSignal): void {\n\t\tbindDrag<Press>(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tconst index = this.indexOf(event.target);\n\t\t\t\t\tif (index === -1 || this.isDisabled() || this.isOptionDisabled(index)) return;\n\t\t\t\t\tif (this.claimPress(index, event)) return;\n\n\t\t\t\t\tconst boxes = this.#optionBoxes();\n\n\t\t\t\t\tthis.#markPressed(index);\n\n\t\t\t\t\treturn { boxes, index };\n\t\t\t\t},\n\t\t\t\tlift: (press) => {\n\t\t\t\t\tif (press.index === undefined || this.isDisabled()) return;\n\n\t\t\t\t\tconst index = this.releaseTarget(press.index);\n\t\t\t\t\tif (index === undefined) return;\n\n\t\t\t\t\tthis.select(index);\n\t\t\t\t\tthis.refocusAt(index);\n\t\t\t\t},\n\t\t\t\tmove: (press, event) => {\n\t\t\t\t\tpress.index = press.boxes.find(([, box]) =>\n\t\t\t\t\t\tisInside(box, event.clientX, event.clientY),\n\t\t\t\t\t)?.[0];\n\t\t\t\t\tthis.#markPressed(press.index);\n\t\t\t\t},\n\t\t\t\trelease: () => {\n\t\t\t\t\tthis.#markPressed(undefined);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#copyOptions(copies: Array<Node>): void {\n\t\tconst group = this.group;\n\t\tconst options = this.options();\n\t\tconst focused = this.optionOf(group.querySelector(':scope > :focus'));\n\t\tconst focusedValue = focused && optionValue(focused);\n\n\t\tfor (const [index, copy] of copies.entries()) {\n\t\t\tlet option = options[index];\n\t\t\tif (!option) {\n\t\t\t\toption = this.renderOption();\n\t\t\t\tgroup.append(option);\n\t\t\t}\n\n\t\t\tif (option.firstElementChild) placeChildren(option.firstElementChild, [copy]);\n\t\t}\n\t\tfor (const option of options.slice(copies.length)) option.remove();\n\t\tthis.render();\n\t\tif (focused) this.#refocus(focused, focusedValue);\n\t}\n\n\t#markPressed(pressed: number | undefined): void {\n\t\tfor (const [index, option] of this.options().entries()) {\n\t\t\toption.toggleAttribute('data-sonic-pressed', index === pressed);\n\t\t}\n\t}\n\n\t#optionBoxes(): Array<[number, DOMRect]> {\n\t\tconst positions = this.positions();\n\n\t\treturn this.options().flatMap((option, index) =>\n\t\t\tpositions[index]?.isDisabled === false\n\t\t\t\t? [[index, option.getBoundingClientRect()] satisfies [number, DOMRect]]\n\t\t\t\t: [],\n\t\t);\n\t}\n\n\t#refocus(focused: HTMLButtonElement, value: string | undefined): void {\n\t\tconst options = this.options();\n\t\tconst next =\n\t\t\toptions.find((option) => optionValue(option) === value) ??\n\t\t\toptions.find((option) => option.tabIndex === 0);\n\n\t\tif (next !== focused) next?.focus();\n\t}\n}\n"],"mappings":"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"}
@@ -17,9 +17,9 @@ function scaleMark(original) {
17
17
  return mark;
18
18
  }
19
19
  function bindScale(element, { control, marks }, signal) {
20
- const { mapping, watch } = linkValue(element);
20
+ const { model } = linkValue(element);
21
21
  const render = () => {
22
- const { proportionOf } = mapping();
22
+ const { proportionOf } = model.mapping();
23
23
  for (const mark of marks.children) {
24
24
  if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
25
25
  const at = String(proportionOf(scaleValue(mark)));
@@ -36,7 +36,7 @@ function bindScale(element, { control, marks }, signal) {
36
36
  render();
37
37
  }
38
38
  }, signal);
39
- signal.addEventListener("abort", watch(render), { once: true });
39
+ signal.addEventListener("abort", model.watch(render), { once: true });
40
40
  }
41
41
 
42
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"scale.js","names":[],"sources":["../../../src/elements/scale.ts"],"sourcesContent":["import type { SonicValueElement } from '#elements/value-element.ts';\n\nimport { linkValue } from '#elements/value-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { placeChildren } from '#lib/render.ts';\n\ntype ScaleMark = HTMLElement | SVGElement;\n\nfunction scaleValue(mark: ScaleMark): number {\n\tconst text = mark.dataset.sonicValue?.trim();\n\n\treturn text ? Number(text) : NaN;\n}\n\nfunction scaleMark(original: ChildNode): ScaleMark | undefined {\n\tconst mark = copyNode(original);\n\tif (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return undefined;\n\tif (!Number.isFinite(scaleValue(mark))) return undefined;\n\n\tconst isTick = mark.childElementCount === 0 && mark.textContent.trim() === '';\n\n\tmark.classList.add(isTick ? 'sonic-scale-tick' : 'sonic-scale-label');\n\n\treturn mark;\n}\n\nexport function bindScale(\n\telement: SonicValueElement,\n\t{ control, marks }: { control: HTMLElement; marks: HTMLElement },\n\tsignal: AbortSignal,\n): void {\n\tconst { mapping, watch } = linkValue(element);\n\tconst render = (): void => {\n\t\tconst { proportionOf } = mapping();\n\n\t\tfor (const mark of marks.children) {\n\t\t\tif (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;\n\n\t\t\tconst at = String(proportionOf(scaleValue(mark)));\n\n\t\t\tif (mark.style.getPropertyValue('--_sonic-scale-at') !== at) {\n\t\t\t\tmark.style.setProperty('--_sonic-scale-at', at);\n\t\t\t}\n\t\t}\n\t};\n\n\tmirrorChildren(\n\t\telement,\n\t\t{\n\t\t\tcontrol,\n\t\t\tcopy: scaleMark,\n\t\t\tisCopied: (child) => child instanceof Element && child.matches('[data-sonic-value]'),\n\t\t\tisPassed: (child) => child instanceof Element && child.matches('.sonic-led'),\n\t\t\tplace: (copies) => {\n\t\t\t\tplaceChildren(marks, copies);\n\t\t\t\trender();\n\t\t\t},\n\t\t},\n\t\tsignal,\n\t);\n\tsignal.addEventListener('abort', watch(render), { once: true });\n}\n"],"mappings":";;;;;;AASA,SAAS,WAAW,MAAyB;CAC5C,MAAM,OAAO,KAAK,QAAQ,YAAY,KAAK;CAE3C,OAAO,OAAO,OAAO,IAAI,IAAI;AAC9B;AAEA,SAAS,UAAU,UAA4C;CAC9D,MAAM,OAAO,SAAS,QAAQ;CAC9B,IAAI,EAAE,gBAAgB,eAAe,gBAAgB,aAAa,OAAO;CACzE,IAAI,CAAC,OAAO,SAAS,WAAW,IAAI,CAAC,GAAG,OAAO;CAE/C,MAAM,SAAS,KAAK,sBAAsB,KAAK,KAAK,YAAY,KAAK,MAAM;CAE3E,KAAK,UAAU,IAAI,SAAS,qBAAqB,mBAAmB;CAEpE,OAAO;AACR;AAEA,SAAgB,UACf,SACA,EAAE,SAAS,SACX,QACO;CACP,MAAM,EAAE,SAAS,UAAU,UAAU,OAAO;CAC5C,MAAM,eAAqB;EAC1B,MAAM,EAAE,iBAAiB,QAAQ;EAEjC,KAAK,MAAM,QAAQ,MAAM,UAAU;GAClC,IAAI,EAAE,gBAAgB,eAAe,gBAAgB,aAAa;GAElE,MAAM,KAAK,OAAO,aAAa,WAAW,IAAI,CAAC,CAAC;GAEhD,IAAI,KAAK,MAAM,iBAAiB,mBAAmB,MAAM,IACxD,KAAK,MAAM,YAAY,qBAAqB,EAAE;EAEhD;CACD;CAEA,eACC,SACA;EACC;EACA,MAAM;EACN,WAAW,UAAU,iBAAiB,WAAW,MAAM,QAAQ,oBAAoB;EACnF,WAAW,UAAU,iBAAiB,WAAW,MAAM,QAAQ,YAAY;EAC3E,QAAQ,WAAW;GAClB,cAAc,OAAO,MAAM;GAC3B,OAAO;EACR;CACD,GACA,MACD;CACA,OAAO,iBAAiB,SAAS,MAAM,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/D"}
1
+ {"version":3,"file":"scale.js","names":[],"sources":["../../../src/elements/scale.ts"],"sourcesContent":["import type { SonicValueElement } from '#elements/value-element.ts';\n\nimport { linkValue } from '#elements/value-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { placeChildren } from '#lib/render.ts';\n\ntype ScaleMark = HTMLElement | SVGElement;\n\nfunction scaleValue(mark: ScaleMark): number {\n\tconst text = mark.dataset.sonicValue?.trim();\n\n\treturn text ? Number(text) : NaN;\n}\n\nfunction scaleMark(original: ChildNode): ScaleMark | undefined {\n\tconst mark = copyNode(original);\n\tif (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return undefined;\n\tif (!Number.isFinite(scaleValue(mark))) return undefined;\n\n\tconst isTick = mark.childElementCount === 0 && mark.textContent.trim() === '';\n\n\tmark.classList.add(isTick ? 'sonic-scale-tick' : 'sonic-scale-label');\n\n\treturn mark;\n}\n\nexport function bindScale(\n\telement: SonicValueElement,\n\t{ control, marks }: { control: HTMLElement; marks: HTMLElement },\n\tsignal: AbortSignal,\n): void {\n\tconst { model } = linkValue(element);\n\tconst render = (): void => {\n\t\tconst { proportionOf } = model.mapping();\n\n\t\tfor (const mark of marks.children) {\n\t\t\tif (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;\n\n\t\t\tconst at = String(proportionOf(scaleValue(mark)));\n\n\t\t\tif (mark.style.getPropertyValue('--_sonic-scale-at') !== at) {\n\t\t\t\tmark.style.setProperty('--_sonic-scale-at', at);\n\t\t\t}\n\t\t}\n\t};\n\n\tmirrorChildren(\n\t\telement,\n\t\t{\n\t\t\tcontrol,\n\t\t\tcopy: scaleMark,\n\t\t\tisCopied: (child) => child instanceof Element && child.matches('[data-sonic-value]'),\n\t\t\tisPassed: (child) => child instanceof Element && child.matches('.sonic-led'),\n\t\t\tplace: (copies) => {\n\t\t\t\tplaceChildren(marks, copies);\n\t\t\t\trender();\n\t\t\t},\n\t\t},\n\t\tsignal,\n\t);\n\tsignal.addEventListener('abort', model.watch(render), { once: true });\n}\n"],"mappings":";;;;;;AASA,SAAS,WAAW,MAAyB;CAC5C,MAAM,OAAO,KAAK,QAAQ,YAAY,KAAK;CAE3C,OAAO,OAAO,OAAO,IAAI,IAAI;AAC9B;AAEA,SAAS,UAAU,UAA4C;CAC9D,MAAM,OAAO,SAAS,QAAQ;CAC9B,IAAI,EAAE,gBAAgB,eAAe,gBAAgB,aAAa,OAAO;CACzE,IAAI,CAAC,OAAO,SAAS,WAAW,IAAI,CAAC,GAAG,OAAO;CAE/C,MAAM,SAAS,KAAK,sBAAsB,KAAK,KAAK,YAAY,KAAK,MAAM;CAE3E,KAAK,UAAU,IAAI,SAAS,qBAAqB,mBAAmB;CAEpE,OAAO;AACR;AAEA,SAAgB,UACf,SACA,EAAE,SAAS,SACX,QACO;CACP,MAAM,EAAE,UAAU,UAAU,OAAO;CACnC,MAAM,eAAqB;EAC1B,MAAM,EAAE,iBAAiB,MAAM,QAAQ;EAEvC,KAAK,MAAM,QAAQ,MAAM,UAAU;GAClC,IAAI,EAAE,gBAAgB,eAAe,gBAAgB,aAAa;GAElE,MAAM,KAAK,OAAO,aAAa,WAAW,IAAI,CAAC,CAAC;GAEhD,IAAI,KAAK,MAAM,iBAAiB,mBAAmB,MAAM,IACxD,KAAK,MAAM,YAAY,qBAAqB,EAAE;EAEhD;CACD;CAEA,eACC,SACA;EACC;EACA,MAAM;EACN,WAAW,UAAU,iBAAiB,WAAW,MAAM,QAAQ,oBAAoB;EACnF,WAAW,UAAU,iBAAiB,WAAW,MAAM,QAAQ,YAAY;EAC3E,QAAQ,WAAW;GAClB,cAAc,OAAO,MAAM;GAC3B,OAAO;EACR;CACD,GACA,MACD;CACA,OAAO,iBAAiB,SAAS,MAAM,MAAM,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;AACrE"}
@@ -12,11 +12,18 @@ var SonicSegmented = class extends SonicRadioGroupElement {
12
12
  group = renderSegmented();
13
13
  connect(signal) {
14
14
  super.connect(signal);
15
- this.checkStyles(this.group, "segmented.css", [
16
- "well",
17
- "keycap",
18
- ["cap", ".sonic-segmented-option"]
19
- ]);
15
+ this.checkStyles(this.group, "segmented.css", {
16
+ box: {
17
+ property: "padding-inline-start",
18
+ ratio: "--_sonic-segmented-padding-ratio",
19
+ selector: ".sonic-segmented-cap"
20
+ },
21
+ material: [
22
+ "well",
23
+ "keycap",
24
+ ["cap", ".sonic-segmented-option"]
25
+ ]
26
+ });
20
27
  }
21
28
  renderOption() {
22
29
  return renderOption();
@@ -1 +1 @@
1
- {"version":3,"file":"segmented.js","names":[],"sources":["../../../src/elements/segmented.ts"],"sourcesContent":["import { SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-segmented': SonicSegmented;\n\t}\n}\n\nconst renderSegmented = template(\n\t/* HTML */ `<div class=\"sonic-segmented\" role=\"radiogroup\"></div>`,\n\tHTMLDivElement,\n);\n\nconst renderOption = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-segmented-option\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-segmented-cap\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nexport class SonicSegmented extends SonicRadioGroupElement {\n\tprotected readonly group = renderSegmented();\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(this.group, 'segmented.css', [\n\t\t\t'well',\n\t\t\t'keycap',\n\t\t\t['cap', '.sonic-segmented-option'],\n\t\t]);\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderOption();\n\t}\n}\n"],"mappings":";;;;AASA,MAAM,kBAAkB,SACZ,yDACX,cACD;AAEA,MAAM,eAAe,SACT;;;;IAKX,iBACD;AAEA,IAAa,iBAAb,cAAoC,uBAAuB;CAC1D,AAAmB,QAAQ,gBAAgB;CAE3C,AAAmB,QAAQ,QAA2B;EACrD,MAAM,QAAQ,MAAM;EACpB,KAAK,YAAY,KAAK,OAAO,iBAAiB;GAC7C;GACA;GACA,CAAC,OAAO,yBAAyB;EAClC,CAAC;CACF;CAEA,AAAU,eAAkC;EAC3C,OAAO,aAAa;CACrB;AACD"}
1
+ {"version":3,"file":"segmented.js","names":[],"sources":["../../../src/elements/segmented.ts"],"sourcesContent":["import { SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-segmented': SonicSegmented;\n\t}\n}\n\nconst renderSegmented = template(\n\t/* HTML */ `<div class=\"sonic-segmented\" role=\"radiogroup\"></div>`,\n\tHTMLDivElement,\n);\n\nconst renderOption = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-segmented-option\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-segmented-cap\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nexport class SonicSegmented extends SonicRadioGroupElement {\n\tprotected readonly group = renderSegmented();\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tif (__DEV__)\n\t\t\tthis.checkStyles(this.group, 'segmented.css', {\n\t\t\t\tbox: {\n\t\t\t\t\tproperty: 'padding-inline-start',\n\t\t\t\t\tratio: '--_sonic-segmented-padding-ratio',\n\t\t\t\t\tselector: '.sonic-segmented-cap',\n\t\t\t\t},\n\t\t\t\tmaterial: ['well', 'keycap', ['cap', '.sonic-segmented-option']],\n\t\t\t});\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderOption();\n\t}\n}\n"],"mappings":";;;;AAWA,MAAM,kBAAkB,SACZ,yDACX,cACD;AAEA,MAAM,eAAe,SACT;;;;IAKX,iBACD;AAEA,IAAa,iBAAb,cAAoC,uBAAuB;CAC1D,AAAmB,QAAQ,gBAAgB;CAE3C,AAAmB,QAAQ,QAA2B;EACrD,MAAM,QAAQ,MAAM;EAEnB,KAAK,YAAY,KAAK,OAAO,iBAAiB;GAC7C,KAAK;IACJ,UAAU;IACV,OAAO;IACP,UAAU;GACX;GACA,UAAU;IAAC;IAAQ;IAAU,CAAC,OAAO,yBAAyB;GAAC;EAChE,CAAC;CACH;CAEA,AAAU,eAAkC;EAC3C,OAAO,aAAa;CACrB;AACD"}