@xsynaptic/sonic-ui 0.10.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +12 -0
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +2 -0
  6. package/dist/default/elements/position-group.js +163 -0
  7. package/dist/default/elements/radio-group.js +34 -12
  8. package/dist/default/elements/sonic-element.js +11 -2
  9. package/dist/default/elements/switch.js +34 -168
  10. package/dist/default/elements/toggle.js +189 -0
  11. package/dist/default/elements/value-element.js +9 -0
  12. package/dist/default/elements/waveform.js +38 -19
  13. package/dist/default/elements/wavestrip.js +6 -1
  14. package/dist/default/index.js +2 -1
  15. package/dist/default/lib/key-press.js +27 -0
  16. package/dist/default/lib/strip-scene.js +4 -3
  17. package/dist/default/lib/toggle-travel.js +19 -0
  18. package/dist/dev/define/toggle.d.ts +2 -0
  19. package/dist/dev/define/toggle.js +8 -0
  20. package/dist/dev/define/toggle.js.map +1 -0
  21. package/dist/dev/define.d.ts +3 -1
  22. package/dist/dev/define.js +1 -0
  23. package/dist/dev/elements/button.js +2 -0
  24. package/dist/dev/elements/button.js.map +1 -1
  25. package/dist/dev/elements/position-group.d.ts +38 -0
  26. package/dist/dev/elements/position-group.js +164 -0
  27. package/dist/dev/elements/position-group.js.map +1 -0
  28. package/dist/dev/elements/radio-group.d.ts +4 -0
  29. package/dist/dev/elements/radio-group.js +34 -12
  30. package/dist/dev/elements/radio-group.js.map +1 -1
  31. package/dist/dev/elements/sonic-element.d.ts +1 -0
  32. package/dist/dev/elements/sonic-element.js +11 -2
  33. package/dist/dev/elements/sonic-element.js.map +1 -1
  34. package/dist/dev/elements/switch.d.ts +4 -19
  35. package/dist/dev/elements/switch.js +34 -168
  36. package/dist/dev/elements/switch.js.map +1 -1
  37. package/dist/dev/elements/toggle.d.ts +21 -0
  38. package/dist/dev/elements/toggle.js +197 -0
  39. package/dist/dev/elements/toggle.js.map +1 -0
  40. package/dist/dev/elements/value-element.d.ts +3 -0
  41. package/dist/dev/elements/value-element.js +9 -0
  42. package/dist/dev/elements/value-element.js.map +1 -1
  43. package/dist/dev/elements/waveform.d.ts +1 -1
  44. package/dist/dev/elements/waveform.js +38 -19
  45. package/dist/dev/elements/waveform.js.map +1 -1
  46. package/dist/dev/elements/wavestrip.d.ts +3 -0
  47. package/dist/dev/elements/wavestrip.js +6 -1
  48. package/dist/dev/elements/wavestrip.js.map +1 -1
  49. package/dist/dev/index.d.ts +3 -2
  50. package/dist/dev/index.js +2 -1
  51. package/dist/dev/lib/key-press.js +28 -0
  52. package/dist/dev/lib/key-press.js.map +1 -0
  53. package/dist/dev/lib/strip-scene.js +4 -3
  54. package/dist/dev/lib/strip-scene.js.map +1 -1
  55. package/dist/dev/lib/toggle-travel.js +20 -0
  56. package/dist/dev/lib/toggle-travel.js.map +1 -0
  57. package/dist/styles/button.css +2 -2
  58. package/dist/styles/controls.css +1 -0
  59. package/dist/styles/led.css +16 -13
  60. package/dist/styles/material.css +36 -9
  61. package/dist/styles/meter.css +7 -6
  62. package/dist/styles/segmented.css +8 -4
  63. package/dist/styles/spectrum.css +2 -2
  64. package/dist/styles/switch.css +6 -2
  65. package/dist/styles/toggle.css +481 -0
  66. package/dist/styles/waveform.css +5 -2
  67. package/dist/styles/wavestrip.css +7 -0
  68. package/package.json +2 -1
@@ -0,0 +1,27 @@
1
+ //#region src/lib/key-press.ts
2
+ function bindKeyPress(root, signal) {
3
+ let pressed;
4
+ const release = () => {
5
+ if (!pressed) return;
6
+ delete pressed.dataset.sonicPressed;
7
+ pressed = void 0;
8
+ };
9
+ root.addEventListener("keydown", (event) => {
10
+ if (event.key !== " " || event.repeat || event.defaultPrevented) return;
11
+ const part = event.target;
12
+ if (!(part instanceof HTMLButtonElement) || part.disabled) return;
13
+ release();
14
+ pressed = part;
15
+ part.dataset.sonicPressed = "";
16
+ }, { signal });
17
+ root.addEventListener("keyup", (event) => {
18
+ if (event.key === " " || event.key === "Meta") release();
19
+ }, { signal });
20
+ root.addEventListener("focusout", (event) => {
21
+ if (event.target === pressed) release();
22
+ }, { signal });
23
+ signal.addEventListener("abort", release, { once: true });
24
+ }
25
+
26
+ //#endregion
27
+ export { bindKeyPress };
@@ -18,9 +18,10 @@ function stripBars(peaks, size, grid) {
18
18
  };
19
19
  });
20
20
  }
21
- function stripGroove(size, ratio) {
22
- if (ratio <= 0) return void 0;
23
- const height = Math.max(1, Math.round(size.height * ratio));
21
+ function stripGroove(size, { grooveRatio, grooveSize }) {
22
+ const thickness = grooveSize > 0 ? grooveSize * size.dpr : size.height * grooveRatio;
23
+ if (thickness <= 0) return void 0;
24
+ const height = Math.max(1, Math.round(Math.min(thickness, size.height)));
24
25
  return {
25
26
  height,
26
27
  radius: height / 2,
@@ -0,0 +1,19 @@
1
+ import { clamp } from "./math.js";
2
+
3
+ //#region src/lib/toggle-travel.ts
4
+ function travelFrom({ at, count, first, isOnCap, last, pointer, well }) {
5
+ const step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);
6
+ return {
7
+ first,
8
+ last,
9
+ origin: isOnCap ? pointer - at * step : well.start + well.thickness / 2,
10
+ step
11
+ };
12
+ }
13
+ function positionAt({ first, last, origin, step }, pointer) {
14
+ if (step <= 0) return first;
15
+ return clamp((pointer - origin) / step, first, last);
16
+ }
17
+
18
+ //#endregion
19
+ export { positionAt, travelFrom };
@@ -0,0 +1,2 @@
1
+ import { SonicToggle } from "../elements/toggle.js";
2
+ export type { SonicToggle };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicToggle } from "../elements/toggle.js";
3
+
4
+ //#region src/define/toggle.ts
5
+ defineOnce("sonic-toggle", SonicToggle);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=toggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle.js","names":[],"sources":["../../../src/define/toggle.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicToggle } from '#elements/toggle.ts';\n\ndefineOnce('sonic-toggle', SonicToggle);\n\nexport type { SonicToggle } from '#elements/toggle.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -18,10 +18,12 @@ import { SonicSplit } from "./elements/split.js";
18
18
  import "./define/split.js";
19
19
  import { SonicSwitch } from "./elements/switch.js";
20
20
  import "./define/switch.js";
21
+ import { SonicToggle } from "./elements/toggle.js";
22
+ import "./define/toggle.js";
21
23
  import { SonicWaveform } from "./elements/waveform.js";
22
24
  import "./define/waveform.js";
23
25
  import { SonicWavestrip } from "./elements/wavestrip.js";
24
26
  import "./define/wavestrip.js";
25
27
  import { SonicXy } from "./elements/xy.js";
26
28
  import "./define/xy.js";
27
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy };
29
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
@@ -8,6 +8,7 @@ import "./define/slider.js";
8
8
  import "./define/spectrum.js";
9
9
  import "./define/split.js";
10
10
  import "./define/switch.js";
11
+ import "./define/toggle.js";
11
12
  import "./define/waveform.js";
12
13
  import "./define/wavestrip.js";
13
14
  import "./define/xy.js";
@@ -2,6 +2,7 @@ import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicFormElement } from "./form-element.js";
3
3
  import { copyNode } from "../lib/copy-node.js";
4
4
  import { bindHold } from "../lib/hold.js";
5
+ import { bindKeyPress } from "../lib/key-press.js";
5
6
  import { placeChildren, requireChild, template } from "../lib/render.js";
6
7
  import { mirrorChildren } from "../lib/mirror-children.js";
7
8
  import { capturePointer } from "../lib/pointer-drag.js";
@@ -125,6 +126,7 @@ var SonicButton = class extends SonicFormElement {
125
126
  this.dispatchEvent(new Event("change", { bubbles: true }));
126
127
  }, { signal });
127
128
  this.#bindMomentary(button, signal);
129
+ bindKeyPress(button, signal);
128
130
  }
129
131
  focusTarget() {
130
132
  return this.#button;
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nconst popupAttributes = {\n\tcontrols: 'aria-controls',\n\texpanded: 'aria-expanded',\n\tpopup: 'aria-haspopup',\n} as const;\n\nconst pressWhens = new Set(['pressed', 'released']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t...Object.keys(popupAttributes),\n\t\t'busy',\n\t\t'legend',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget legend(): string | undefined {\n\t\treturn this.getAttribute('legend') ?? undefined;\n\t}\n\n\tset legend(name: string | undefined) {\n\t\tthis.reflect('legend', name);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'legend',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t\tthis.#showLegend();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\t\tthis.#forwardPopup(button);\n\t\tthis.#showLegend();\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tcapturePointer(button, event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#forwardPopup(button: HTMLButtonElement): void {\n\t\tfor (const [name, aria] of Object.entries(popupAttributes)) {\n\t\t\twriteAttribute(button, aria, this.getAttribute(name) ?? undefined);\n\t\t}\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#showLegend(): void {\n\t\tconst legend = this.legend;\n\n\t\tfor (const part of this.#button.querySelectorAll<HTMLElement | SVGElement>(\n\t\t\t':scope > .sonic-button-cap > [data-sonic-when]',\n\t\t)) {\n\t\t\tconst when = part.dataset.sonicWhen ?? '';\n\n\t\t\tpart.toggleAttribute('data-sonic-hidden', !pressWhens.has(when) && when !== legend);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { bindKeyPress } from '#lib/key-press.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nconst popupAttributes = {\n\tcontrols: 'aria-controls',\n\texpanded: 'aria-expanded',\n\tpopup: 'aria-haspopup',\n} as const;\n\nconst pressWhens = new Set(['pressed', 'released']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t...Object.keys(popupAttributes),\n\t\t'busy',\n\t\t'legend',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget legend(): string | undefined {\n\t\treturn this.getAttribute('legend') ?? undefined;\n\t}\n\n\tset legend(name: string | undefined) {\n\t\tthis.reflect('legend', name);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'legend',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t\tthis.#showLegend();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t\tbindKeyPress(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\t\tthis.#forwardPopup(button);\n\t\tthis.#showLegend();\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tcapturePointer(button, event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#forwardPopup(button: HTMLButtonElement): void {\n\t\tfor (const [name, aria] of Object.entries(popupAttributes)) {\n\t\t\twriteAttribute(button, aria, this.getAttribute(name) ?? undefined);\n\t\t}\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#showLegend(): void {\n\t\tconst legend = this.legend;\n\n\t\tfor (const part of this.#button.querySelectorAll<HTMLElement | SVGElement>(\n\t\t\t':scope > .sonic-button-cap > [data-sonic-when]',\n\t\t)) {\n\t\t\tconst when = part.dataset.sonicWhen ?? '';\n\n\t\t\tpart.toggleAttribute('data-sonic-hidden', !pressWhens.has(when) && when !== legend);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { SonicRadioGroupElement } from "./radio-group.js";
2
+ //#region src/elements/position-group.d.ts
3
+ type Orientation = 'horizontal' | 'vertical';
4
+ export declare abstract class SonicPositionGroupElement extends SonicRadioGroupElement {
5
+ #private;
6
+ static readonly observedAttributes: string[];
7
+ get checked(): boolean;
8
+ set checked(isChecked: boolean | undefined);
9
+ get defaultChecked(): boolean;
10
+ set defaultChecked(isChecked: boolean);
11
+ get orientation(): Orientation;
12
+ set orientation(direction: Orientation | undefined);
13
+ get value(): string;
14
+ set value(next: string);
15
+ protected abstract readonly bare: HTMLButtonElement;
16
+ protected abstract readonly defaultOrientation: Orientation;
17
+ attributeChangedCallback(name: string): void;
18
+ formResetCallback(): void;
19
+ protected activate(): void;
20
+ protected along(event: PointerEvent): number;
21
+ protected checkedIndex(options: Array<HTMLButtonElement>): number;
22
+ protected chooseByClick(option: HTMLButtonElement): void;
23
+ protected chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void;
24
+ protected connect(signal: AbortSignal): void;
25
+ protected focusTarget(): HTMLElement | undefined;
26
+ protected hold(option: HTMLButtonElement, by: number | string): void;
27
+ protected isBare(): boolean;
28
+ protected isMomentary(option: HTMLButtonElement): boolean;
29
+ protected isWrapping(): boolean;
30
+ protected pressTarget(option: HTMLButtonElement): HTMLButtonElement;
31
+ protected releaseHold(): void;
32
+ protected render(): void;
33
+ protected restoreState(state: string): void;
34
+ protected setChecked(isChecked: boolean): void;
35
+ protected springBack(): void;
36
+ }
37
+ //#endregion
38
+ //# sourceMappingURL=position-group.d.ts.map
@@ -0,0 +1,164 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+ import { bindHold } from "../lib/hold.js";
3
+ import { SonicRadioGroupElement, optionValue } from "./radio-group.js";
4
+
5
+ //#region src/elements/position-group.ts
6
+ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
7
+ static observedAttributes = [
8
+ ...SonicRadioGroupElement.observedAttributes,
9
+ "checked",
10
+ "orientation"
11
+ ];
12
+ get checked() {
13
+ return this.#isDirty ? this.#checked : this.defaultChecked;
14
+ }
15
+ set checked(isChecked) {
16
+ this.#isDirty = true;
17
+ this.#checked = isChecked === true;
18
+ this.render();
19
+ }
20
+ get defaultChecked() {
21
+ return this.hasAttribute("checked");
22
+ }
23
+ set defaultChecked(isChecked) {
24
+ this.reflect("checked", isChecked);
25
+ }
26
+ get orientation() {
27
+ const orientation = this.getAttribute("orientation");
28
+ return orientation === "horizontal" || orientation === "vertical" ? orientation : this.defaultOrientation;
29
+ }
30
+ set orientation(direction) {
31
+ this.reflect("orientation", direction);
32
+ }
33
+ get value() {
34
+ const value = super.value;
35
+ return value === "" && this.isBare() ? "on" : value;
36
+ }
37
+ set value(next) {
38
+ super.value = next;
39
+ }
40
+ #checked = false;
41
+ #holding;
42
+ #isDirty = false;
43
+ attributeChangedCallback(name) {
44
+ if (name === "checked") this.#isDirty = false;
45
+ if (this.isDisabled()) this.#holding?.release();
46
+ super.attributeChangedCallback(name);
47
+ }
48
+ formResetCallback() {
49
+ this.#isDirty = false;
50
+ super.formResetCallback();
51
+ }
52
+ activate() {
53
+ if (this.isBare()) this.bare.click();
54
+ else super.activate();
55
+ }
56
+ along(event) {
57
+ return this.orientation === "vertical" ? event.clientY : event.clientX;
58
+ }
59
+ checkedIndex(options) {
60
+ return options.findIndex((option) => optionValue(option) === this.value);
61
+ }
62
+ chooseByClick(option) {
63
+ super.chooseByClick(option);
64
+ if (this.isMomentary(option)) this.springBack();
65
+ }
66
+ chooseByKey(option, event) {
67
+ if (!this.isMomentary(option)) {
68
+ super.chooseByKey(option, event);
69
+ return;
70
+ }
71
+ if (event.repeat) return;
72
+ this.hold(option, event.key);
73
+ option.focus();
74
+ }
75
+ connect(signal) {
76
+ this.upgradeProperties("defaultChecked", "checked", "orientation");
77
+ super.connect(signal);
78
+ this.#holding = bindHold(this.group, {
79
+ canHold: () => !this.isDisabled(),
80
+ leave: "focusout",
81
+ onHold: () => {
82
+ this.toggleState("held", true);
83
+ },
84
+ onRelease: () => {
85
+ this.toggleState("held", false);
86
+ this.springBack();
87
+ }
88
+ }, signal);
89
+ }
90
+ focusTarget() {
91
+ return this.isBare() ? this.bare : super.focusTarget();
92
+ }
93
+ hold(option, by) {
94
+ if (this.#holding?.hold(by) === true) this.select(option);
95
+ }
96
+ isBare() {
97
+ return this.options().length === 0;
98
+ }
99
+ isMomentary(option) {
100
+ const options = this.options();
101
+ return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
102
+ }
103
+ isWrapping() {
104
+ return false;
105
+ }
106
+ pressTarget(option) {
107
+ const options = this.options();
108
+ const index = this.checkedIndex(options);
109
+ return (options.length === 2 && index >= 0 ? options[1 - index] : void 0) ?? option;
110
+ }
111
+ releaseHold() {
112
+ this.#holding?.release();
113
+ }
114
+ render() {
115
+ if (!this.isBound()) return;
116
+ const group = this.group;
117
+ const count = this.options().length;
118
+ if (count === 0) {
119
+ this.#renderBare();
120
+ return;
121
+ }
122
+ this.bare.remove();
123
+ writeAttribute(group, "role", "radiogroup");
124
+ writeAttribute(group, "data-sonic-position-count", String(count));
125
+ super.render();
126
+ }
127
+ restoreState(state) {
128
+ if (this.isBare()) this.checked = state === "true";
129
+ else super.restoreState(state);
130
+ }
131
+ setChecked(isChecked) {
132
+ if (isChecked === this.checked) return;
133
+ this.checked = isChecked;
134
+ this.dispatchEvent(new Event("change", { bubbles: true }));
135
+ }
136
+ springBack() {
137
+ const options = this.options();
138
+ const index = this.checkedIndex(options);
139
+ const held = options[index];
140
+ if (!held || !this.isMomentary(held)) return;
141
+ const rest = options[index === 0 ? 1 : index - 1];
142
+ if (!rest) return;
143
+ const isFocused = this.group.contains(this.ownerDocument.activeElement);
144
+ this.select(rest);
145
+ if (isFocused) rest.focus();
146
+ }
147
+ #renderBare() {
148
+ const group = this.group;
149
+ const bare = this.bare;
150
+ const isChecked = this.checked;
151
+ writeAttribute(group, "role", void 0);
152
+ writeAttribute(group, "data-sonic-position-count", void 0);
153
+ if (bare.parentElement !== group) group.append(bare);
154
+ writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
155
+ writeAttribute(bare, "aria-checked", String(isChecked));
156
+ this.forwardNaming(group, false);
157
+ this.forwardNaming(bare, true);
158
+ this.writeFormValue(isChecked ? this.value : null, String(isChecked));
159
+ }
160
+ };
161
+
162
+ //#endregion
163
+ export { SonicPositionGroupElement };
164
+ //# sourceMappingURL=position-group.js.map
@@ -0,0 +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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"}
@@ -12,14 +12,18 @@ export declare abstract class SonicRadioGroupElement extends SonicFormElement {
12
12
  protected claimPress(_option: HTMLButtonElement, _event: PointerEvent): boolean;
13
13
  protected connect(signal: AbortSignal): void;
14
14
  protected focusTarget(): HTMLElement | undefined;
15
+ protected isMirrored(child: Node): boolean;
16
+ protected isOptionDisabled(option: HTMLButtonElement): boolean;
15
17
  protected isWrapping(): boolean;
16
18
  protected optionOf(target: EventTarget | null): HTMLButtonElement | undefined;
17
19
  protected options(): Array<HTMLButtonElement>;
20
+ protected placeCopies(copies: Array<Node>): void;
18
21
  protected releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined;
19
22
  protected render(): void;
20
23
  protected abstract renderOption(): HTMLButtonElement;
21
24
  protected restoreState(state: string): void;
22
25
  protected select(option: HTMLButtonElement): void;
26
+ protected stepFrom(from: number, step: number): HTMLButtonElement | undefined;
23
27
  }
24
28
  //#endregion
25
29
  //# sourceMappingURL=radio-group.d.ts.map
@@ -1,6 +1,8 @@
1
+ import { clamp } from "../lib/math.js";
1
2
  import { writeAttribute } from "../lib/write-attribute.js";
2
3
  import { SonicFormElement } from "./form-element.js";
3
4
  import { copyNode } from "../lib/copy-node.js";
5
+ import { bindKeyPress } from "../lib/key-press.js";
4
6
  import { placeChildren } from "../lib/render.js";
5
7
  import { mirrorChildren } from "../lib/mirror-children.js";
6
8
  import { bindDrag } from "../lib/pointer-drag.js";
@@ -52,9 +54,9 @@ var SonicRadioGroupElement = class extends SonicFormElement {
52
54
  mirrorChildren(this, {
53
55
  control: group,
54
56
  copy: copyNode,
55
- isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
57
+ isCopied: (child) => this.isMirrored(child),
56
58
  place: (copies) => {
57
- this.#copyOptions(copies);
59
+ this.placeCopies(copies);
58
60
  }
59
61
  }, signal);
60
62
  group.addEventListener("click", (event) => {
@@ -63,6 +65,7 @@ var SonicRadioGroupElement = class extends SonicFormElement {
63
65
  if (option) this.chooseByClick(option);
64
66
  }, { signal });
65
67
  this.#bindPress(group, signal);
68
+ bindKeyPress(group, signal);
66
69
  group.addEventListener("keydown", (event) => {
67
70
  if (event.defaultPrevented) return;
68
71
  const option = this.optionOf(event.target);
@@ -75,6 +78,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
75
78
  focusTarget() {
76
79
  return this.options().find((option) => option.tabIndex === 0);
77
80
  }
81
+ isMirrored(child) {
82
+ return child instanceof Element && child.matches("[data-sonic-value]");
83
+ }
84
+ isOptionDisabled(option) {
85
+ return option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-disabled") === true;
86
+ }
78
87
  isWrapping() {
79
88
  return true;
80
89
  }
@@ -86,6 +95,9 @@ var SonicRadioGroupElement = class extends SonicFormElement {
86
95
  options() {
87
96
  return [...this.group.children].filter((child) => child instanceof HTMLButtonElement && child.getAttribute("role") === "radio");
88
97
  }
98
+ placeCopies(copies) {
99
+ this.#copyOptions(copies);
100
+ }
89
101
  releaseTarget(option) {
90
102
  return option;
91
103
  }
@@ -93,10 +105,10 @@ var SonicRadioGroupElement = class extends SonicFormElement {
93
105
  if (!this.isBound()) return;
94
106
  const options = this.options();
95
107
  const checked = options.find((option) => optionValue(option) === this.#value);
96
- const focusable = checked ?? options[0];
97
- const isDisabled = this.isDisabled();
108
+ const enabled = this.isDisabled() ? [] : options.filter((option) => !this.isOptionDisabled(option));
109
+ const focusable = enabled.find((option) => option === checked) ?? enabled[0];
98
110
  for (const option of options) {
99
- writeAttribute(option, "disabled", isDisabled ? "" : void 0);
111
+ writeAttribute(option, "disabled", enabled.includes(option) ? void 0 : "");
100
112
  writeAttribute(option, "aria-checked", String(option === checked));
101
113
  writeAttribute(option, "tabindex", option === focusable ? "0" : "-1");
102
114
  }
@@ -112,11 +124,22 @@ var SonicRadioGroupElement = class extends SonicFormElement {
112
124
  this.value = next;
113
125
  this.dispatchEvent(new Event("change", { bubbles: true }));
114
126
  }
127
+ 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
+ }
136
+ }
115
137
  #bindPress(group, signal) {
116
138
  bindDrag(group, {
117
139
  grab: (event) => {
118
140
  const option = this.optionOf(event.target);
119
- if (!option || this.isDisabled() || this.claimPress(option, event)) return;
141
+ if (!option || this.isDisabled() || this.isOptionDisabled(option)) return;
142
+ if (this.claimPress(option, event)) return;
120
143
  const boxes = this.#optionBoxes();
121
144
  this.#markPressed(option);
122
145
  return {
@@ -159,19 +182,18 @@ var SonicRadioGroupElement = class extends SonicFormElement {
159
182
  }
160
183
  #keyTarget(option, key) {
161
184
  const options = this.options();
162
- if (key === "Home") return options[0];
163
- if (key === "End") return options.at(-1);
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));
164
188
  const step = keySteps.get(key);
165
189
  if (step === void 0) return void 0;
166
- const index = options.indexOf(option) + step;
167
- if (this.isWrapping()) return options[(index + options.length) % options.length];
168
- return options[Math.min(Math.max(index, 0), options.length - 1)];
190
+ return this.stepFrom(options.indexOf(option), step) ?? option;
169
191
  }
170
192
  #markPressed(pressed) {
171
193
  for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
172
194
  }
173
195
  #optionBoxes() {
174
- return this.options().map((option) => [option, option.getBoundingClientRect()]);
196
+ return this.options().filter((option) => !this.isOptionDisabled(option)).map((option) => [option, option.getBoundingClientRect()]);
175
197
  }
176
198
  #refocus(focused, value) {
177
199
  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 { 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) => child instanceof Element && child.matches('[data-sonic-value]'),\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tthis.#copyOptions(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\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 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 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 focusable = checked ?? options[0];\n\t\tconst isDisabled = this.isDisabled();\n\n\t\tfor (const option of options) {\n\t\t\twriteAttribute(option, 'disabled', isDisabled ? '' : 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\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.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\n\t\tif (key === 'Home') return options[0];\n\t\tif (key === 'End') return options.at(-1);\n\n\t\tconst step = keySteps.get(key);\n\t\tif (step === undefined) return undefined;\n\n\t\tconst index = options.indexOf(option) + step;\n\t\tif (this.isWrapping()) return options[(index + options.length) % options.length];\n\n\t\treturn options[Math.min(Math.max(index, 0), options.length - 1)];\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().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 { 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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"}
@@ -14,6 +14,7 @@ export declare abstract class SonicElement extends HTMLElement {
14
14
  protected abstract connect(signal: AbortSignal): void;
15
15
  protected focusTarget(): HTMLElement | undefined;
16
16
  protected forwardNaming(target: Element, isNamed: boolean): void;
17
+ protected hasState(state: string): boolean;
17
18
  protected isBound(): boolean;
18
19
  protected isDisabled(): boolean;
19
20
  protected keepControl(control: Element, signal: AbortSignal): void;