@xsynaptic/sonic-ui 0.10.4 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -16
- package/dist/default/define/toggle.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/button.js +2 -0
- package/dist/default/elements/position-group.js +163 -0
- package/dist/default/elements/radio-group.js +34 -12
- package/dist/default/elements/switch.js +33 -167
- package/dist/default/elements/toggle.js +189 -0
- package/dist/default/elements/waveform.js +30 -10
- package/dist/default/elements/wavestrip.js +4 -0
- package/dist/default/index.js +2 -1
- package/dist/default/lib/key-press.js +27 -0
- package/dist/default/lib/toggle-travel.js +19 -0
- package/dist/dev/define/toggle.d.ts +2 -0
- package/dist/dev/define/toggle.js +8 -0
- package/dist/dev/define/toggle.js.map +1 -0
- package/dist/dev/define.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/button.js +2 -0
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/position-group.d.ts +38 -0
- package/dist/dev/elements/position-group.js +164 -0
- package/dist/dev/elements/position-group.js.map +1 -0
- package/dist/dev/elements/radio-group.d.ts +4 -0
- package/dist/dev/elements/radio-group.js +34 -12
- package/dist/dev/elements/radio-group.js.map +1 -1
- package/dist/dev/elements/switch.d.ts +4 -19
- package/dist/dev/elements/switch.js +33 -167
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/toggle.d.ts +21 -0
- package/dist/dev/elements/toggle.js +197 -0
- package/dist/dev/elements/toggle.js.map +1 -0
- package/dist/dev/elements/waveform.d.ts +1 -1
- package/dist/dev/elements/waveform.js +30 -10
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +1 -0
- package/dist/dev/elements/wavestrip.js +4 -0
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/index.d.ts +3 -2
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/key-press.js +28 -0
- package/dist/dev/lib/key-press.js.map +1 -0
- package/dist/dev/lib/toggle-travel.js +20 -0
- package/dist/dev/lib/toggle-travel.js.map +1 -0
- package/dist/styles/button.css +2 -2
- package/dist/styles/controls.css +1 -0
- package/dist/styles/led.css +16 -13
- package/dist/styles/material.css +36 -9
- package/dist/styles/meter.css +7 -6
- package/dist/styles/segmented.css +8 -4
- package/dist/styles/spectrum.css +2 -2
- package/dist/styles/switch.css +6 -2
- package/dist/styles/toggle.css +481 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @xsynaptic/sonic-ui
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Wave controls: `requestPeaks` may return one promise or an iterable of them, each repainting once however often it is returned, and its `PeaksRequest` type is exported. A write to `pending` or `peaks` inside the call no longer asks again for the same window, and `pending` ignores an unchanged list. The waveform now asks when its window moves, its data is written or a promise settles, not on every repaint. `<sonic-wavestrip>` gains `clientXOf(value)`, the viewport x where a value sits.
|
|
8
|
+
|
|
9
|
+
## 0.11.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- - Breaking: lit colours take status names. `data-sonic-lit` values `alt`, `hot`, `clip` and `dim` are now `ok`, `warning`, `danger` and `idle`; `--sonic-lit-alt`, `--sonic-hot` and `--sonic-clip` are now `--sonic-lit-ok`, `--sonic-lit-warning` and `--sonic-lit-danger`. A meter's and a spectrum's hot and clip zones keep their names and draw in the warning and danger colours.
|
|
14
|
+
- Toggle: new `<sonic-toggle>`, a round cap sliding in a well, two or three positions or on/off, with lit, momentary and disabled positions and an optional LED or icon on the cap.
|
|
15
|
+
- Segmented control and switch: `data-sonic-disabled` on a position child disables that one position; arrows skip it, a press and the bat pass it by. A disabled switch position's label is drawn faint, which also dims the labels of a wholly disabled switch.
|
|
16
|
+
- Switch: a press on the rest label of a two-position switch holds its momentary position instead of latching it.
|
|
17
|
+
- Button and segmented control: a held Space shows the press in Firefox. `data-sonic-pressed` marks a part held by a key as well as by a pointer.
|
|
18
|
+
- Segmented control: on a skin whose ink is dark the chosen legend is drawn in the ink, so it reads on a light cap; the lit well still marks the choice.
|
|
19
|
+
|
|
3
20
|
## 0.10.4
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -29,25 +29,14 @@ Under a hydrating framework, register the definitions once hydration is done (in
|
|
|
29
29
|
|
|
30
30
|
## Wave controls
|
|
31
31
|
|
|
32
|
-
`<sonic-wavestrip>` is the overview of a track, scrubbed to seek; `<sonic-waveform>` is the close-up that scrolls under a fixed playhead. Both are value controls in seconds
|
|
32
|
+
`<sonic-wavestrip>` is the overview of a track, scrubbed to seek; `<sonic-waveform>` is the close-up that scrolls under a fixed playhead. Both are value controls in seconds that take their data as properties (`peaks`, `markers`); the types list the rest.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
| --- | --- | --- | --- |
|
|
36
|
-
| Attributes | `fill`, `readout`, `disabled`, `dimmed` | `cancellable` | `playing`, `playback-rate`, `zoom`, `pending-delay`, `reduced-motion` |
|
|
37
|
-
| Properties | `markers`, `currentMarker`, `formatValue`, `formatSpokenValue`, `repaint()` | `peaks`, `buffered` | `peaks`, `pending`, `readTime`, `requestPeaks` |
|
|
38
|
-
| Gesture, read only | `dragging`, `revealed`, `cancelling`, `pointerType` | `hoverValue` | |
|
|
39
|
-
| Events, all bubbling | `input`, `change`, `sonic-reveal`, `sonic-marker` | `sonic-hover` | |
|
|
40
|
-
| States | `dragging`, `revealed`, `cancelling`, `editing`, `empty` | | `pending` |
|
|
41
|
-
| Hook classes | | `sonic-wavestrip`, `-canvas`, `-markers`, `-marker`, `-region`, `-readout`, `-entry` | `sonic-waveform`, `-canvas`, `-scrim`, `-playhead`, `-ghost`, `-label`, `-readout`, `-entry` |
|
|
34
|
+
Their events bubble and carry no detail, so read the property when one fires: `revealed` on `sonic-reveal`, `currentMarker` on `sonic-marker`, and the strip's `hoverValue` on `sonic-hover`, which `clientXOf(value)` turns into a viewport x.
|
|
42
35
|
|
|
43
|
-
The
|
|
36
|
+
The strip draws one part per marker inside `.sonic-wavestrip-markers`, in `start` order: a `.sonic-wavestrip-marker` dot, or a `.sonic-wavestrip-region` when the marker has an `end`. Marker labels are hidden from assistive technology; fold `currentMarker` into `formatSpokenValue` to have one spoken.
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
The waveform calls `requestPeaks(fromSeconds, toSeconds)` as its window moves, often every frame, so return at once for a span already loaded or on its way. Return a promise, or several, and the waveform repaints as each settles; an `async` function is the wrong shape, since it hands back a new settled promise each call. `pending` is yours to keep: list the spans being fetched, and clear each as it lands.
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`requestPeaks(fromSeconds, toSeconds)` is called whenever the window the waveform wants moves, often every frame, so it should return at once for a span already loaded or on its way. Return a promise and the waveform repaints when it settles, resolved or rejected. `peaks` holds no record of what has loaded, so `pending` stays yours: list the spans you are fetching, and clear them as each lands.
|
|
50
|
-
|
|
51
|
-
Sizes are ratios of the control's own size (`--sonic-wavestrip-size`, `--sonic-waveform-size`). Where a `fill` control should keep an absolute length, these override their ratios: `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size`, `--sonic-waveform-label-font-size`, `--sonic-waveform-label-inset-inline` and `--sonic-waveform-label-inset-block`. `--sonic-waveform-label-line-height` defaults to 1.
|
|
40
|
+
With `fill` a control takes its container's height and its ratios follow. `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size` and `--sonic-waveform-label-font-size` hold an absolute length instead.
|
|
52
41
|
|
|
53
42
|
Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
|
package/dist/default/define.js
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,163 @@
|
|
|
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 };
|
|
@@ -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) =>
|
|
57
|
+
isCopied: (child) => this.isMirrored(child),
|
|
56
58
|
place: (copies) => {
|
|
57
|
-
this
|
|
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
|
|
97
|
-
const
|
|
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",
|
|
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.
|
|
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
|
-
|
|
163
|
-
if (key === "
|
|
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
|
-
|
|
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,9 +1,7 @@
|
|
|
1
|
-
import { clamp } from "../lib/math.js";
|
|
2
|
-
import { writeAttribute } from "../lib/write-attribute.js";
|
|
3
|
-
import { bindHold } from "../lib/hold.js";
|
|
4
1
|
import { requireChild, template } from "../lib/render.js";
|
|
5
2
|
import { bindDrag } from "../lib/pointer-drag.js";
|
|
6
|
-
import {
|
|
3
|
+
import { isUnder, optionValue } from "./radio-group.js";
|
|
4
|
+
import { SonicPositionGroupElement } from "./position-group.js";
|
|
7
5
|
|
|
8
6
|
//#region src/elements/switch.ts
|
|
9
7
|
const renderSwitch = template(`
|
|
@@ -19,165 +17,59 @@ const renderPosition = template(`
|
|
|
19
17
|
`, HTMLButtonElement);
|
|
20
18
|
const renderBare = template(`<button class="sonic-switch-position" role="switch" type="button"></button>`, HTMLButtonElement);
|
|
21
19
|
const pressCenterZone = .08;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const next = index + side;
|
|
26
|
-
return next < 0 || next >= count ? index - side : next;
|
|
27
|
-
}
|
|
28
|
-
var SonicSwitch = class extends SonicRadioGroupElement {
|
|
29
|
-
static observedAttributes = [
|
|
30
|
-
...SonicRadioGroupElement.observedAttributes,
|
|
31
|
-
"checked",
|
|
32
|
-
"orientation"
|
|
33
|
-
];
|
|
34
|
-
get checked() {
|
|
35
|
-
return this.#isDirty ? this.#checked : this.defaultChecked;
|
|
36
|
-
}
|
|
37
|
-
set checked(isChecked) {
|
|
38
|
-
this.#isDirty = true;
|
|
39
|
-
this.#checked = isChecked === true;
|
|
40
|
-
this.render();
|
|
41
|
-
}
|
|
42
|
-
get defaultChecked() {
|
|
43
|
-
return this.hasAttribute("checked");
|
|
44
|
-
}
|
|
45
|
-
set defaultChecked(isChecked) {
|
|
46
|
-
this.reflect("checked", isChecked);
|
|
47
|
-
}
|
|
48
|
-
get orientation() {
|
|
49
|
-
return this.getAttribute("orientation") === "horizontal" ? "horizontal" : "vertical";
|
|
50
|
-
}
|
|
51
|
-
set orientation(direction) {
|
|
52
|
-
this.reflect("orientation", direction);
|
|
53
|
-
}
|
|
54
|
-
get value() {
|
|
55
|
-
const value = super.value;
|
|
56
|
-
return value === "" && this.#isBare() ? "on" : value;
|
|
57
|
-
}
|
|
58
|
-
set value(next) {
|
|
59
|
-
super.value = next;
|
|
60
|
-
}
|
|
20
|
+
var SonicSwitch = class extends SonicPositionGroupElement {
|
|
21
|
+
bare = renderBare();
|
|
22
|
+
defaultOrientation = "vertical";
|
|
61
23
|
group = renderSwitch();
|
|
62
|
-
#bare = renderBare();
|
|
63
24
|
#bat = requireChild(this.group, ".sonic-switch-bat", HTMLSpanElement);
|
|
64
|
-
#checked = false;
|
|
65
|
-
#holding;
|
|
66
25
|
#isClickSwallowed = false;
|
|
67
|
-
#isDirty = false;
|
|
68
|
-
attributeChangedCallback(name) {
|
|
69
|
-
if (name === "checked") this.#isDirty = false;
|
|
70
|
-
if (this.isDisabled()) this.#holding?.release();
|
|
71
|
-
super.attributeChangedCallback(name);
|
|
72
|
-
}
|
|
73
|
-
formResetCallback() {
|
|
74
|
-
this.#isDirty = false;
|
|
75
|
-
super.formResetCallback();
|
|
76
|
-
}
|
|
77
|
-
activate() {
|
|
78
|
-
if (this.#isBare()) this.#bare.click();
|
|
79
|
-
else super.activate();
|
|
80
|
-
}
|
|
81
|
-
chooseByClick(option) {
|
|
82
|
-
super.chooseByClick(option);
|
|
83
|
-
if (this.#isMomentary(option)) this.#springBack();
|
|
84
|
-
}
|
|
85
|
-
chooseByKey(option, event) {
|
|
86
|
-
if (!this.#isMomentary(option)) {
|
|
87
|
-
super.chooseByKey(option, event);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (event.repeat) return;
|
|
91
|
-
this.#hold(option, event.key);
|
|
92
|
-
option.focus();
|
|
93
|
-
}
|
|
94
26
|
claimPress(option, event) {
|
|
95
27
|
if (this.#isOnBat(event)) return true;
|
|
96
|
-
|
|
28
|
+
const target = this.pressTarget(option);
|
|
29
|
+
if (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;
|
|
97
30
|
this.group.setPointerCapture(event.pointerId);
|
|
98
|
-
this
|
|
31
|
+
this.hold(target, event.pointerId);
|
|
99
32
|
return true;
|
|
100
33
|
}
|
|
101
34
|
connect(signal) {
|
|
102
35
|
const group = this.group;
|
|
103
|
-
this.upgradeProperties("defaultChecked", "checked", "orientation");
|
|
104
36
|
super.connect(signal);
|
|
105
37
|
this.checkStyles(group, "switch.css");
|
|
106
|
-
this
|
|
38
|
+
this.bare.addEventListener("click", (event) => {
|
|
107
39
|
const isSwallowed = this.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== "";
|
|
108
40
|
this.#isClickSwallowed = false;
|
|
109
|
-
if (!isSwallowed) this
|
|
41
|
+
if (!isSwallowed) this.setChecked(!this.checked);
|
|
110
42
|
}, { signal });
|
|
111
|
-
this
|
|
43
|
+
this.bare.addEventListener("pointerdown", () => {
|
|
112
44
|
this.#isClickSwallowed = false;
|
|
113
45
|
}, { signal });
|
|
114
46
|
this.#bindBat(group, signal);
|
|
115
|
-
this.#holding = bindHold(group, {
|
|
116
|
-
canHold: () => !this.isDisabled(),
|
|
117
|
-
leave: "focusout",
|
|
118
|
-
onHold: () => {
|
|
119
|
-
this.toggleState("held", true);
|
|
120
|
-
},
|
|
121
|
-
onRelease: () => {
|
|
122
|
-
this.toggleState("held", false);
|
|
123
|
-
this.#springBack();
|
|
124
|
-
}
|
|
125
|
-
}, signal);
|
|
126
|
-
}
|
|
127
|
-
focusTarget() {
|
|
128
|
-
return this.#isBare() ? this.#bare : super.focusTarget();
|
|
129
|
-
}
|
|
130
|
-
isWrapping() {
|
|
131
|
-
return false;
|
|
132
47
|
}
|
|
133
48
|
releaseTarget(option) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (options.length === 2 && optionValue(option) === this.value) return options.find((other) => other !== option);
|
|
137
|
-
return option;
|
|
49
|
+
const target = this.pressTarget(option);
|
|
50
|
+
return this.isMomentary(target) || this.isOptionDisabled(target) ? void 0 : target;
|
|
138
51
|
}
|
|
139
52
|
render() {
|
|
140
53
|
if (!this.isBound()) return;
|
|
141
|
-
|
|
54
|
+
super.render();
|
|
142
55
|
const options = this.options();
|
|
143
56
|
if (options.length > 0) {
|
|
144
|
-
this.#
|
|
145
|
-
writeAttribute(group, "role", "radiogroup");
|
|
146
|
-
writeAttribute(group, "data-sonic-position-count", String(options.length));
|
|
147
|
-
super.render();
|
|
148
|
-
this.#renderThrow(this.#checkedIndex(options));
|
|
57
|
+
this.#renderThrow(this.checkedIndex(options));
|
|
149
58
|
return;
|
|
150
59
|
}
|
|
151
|
-
const bare = this.#bare;
|
|
152
|
-
const isChecked = this.checked;
|
|
153
|
-
writeAttribute(group, "role", void 0);
|
|
154
|
-
delete group.dataset.sonicPositionCount;
|
|
155
|
-
if (bare.parentElement !== group) group.append(bare);
|
|
156
|
-
writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
|
|
157
|
-
writeAttribute(bare, "aria-checked", String(isChecked));
|
|
158
|
-
this.forwardNaming(group, false);
|
|
159
|
-
this.forwardNaming(bare, true);
|
|
160
60
|
const onSide = this.orientation === "horizontal" ? 1 : -1;
|
|
161
|
-
group.style.setProperty("--_sonic-switch-at", String(
|
|
162
|
-
this.writeFormValue(isChecked ? this.value : null, String(isChecked));
|
|
61
|
+
this.group.style.setProperty("--_sonic-switch-at", String(this.checked ? onSide : -onSide));
|
|
163
62
|
}
|
|
164
63
|
renderOption() {
|
|
165
64
|
return renderPosition();
|
|
166
65
|
}
|
|
167
|
-
restoreState(state) {
|
|
168
|
-
if (this.#isBare()) this.checked = state === "true";
|
|
169
|
-
else super.restoreState(state);
|
|
170
|
-
}
|
|
171
|
-
#along(event) {
|
|
172
|
-
return this.orientation === "horizontal" ? event.clientX : event.clientY;
|
|
173
|
-
}
|
|
174
66
|
#bindBat(group, signal) {
|
|
175
67
|
bindDrag(group, {
|
|
176
68
|
grab: (event) => {
|
|
177
|
-
if (!(event.target === this
|
|
69
|
+
if (!(event.target === this.bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
|
|
178
70
|
return {
|
|
179
71
|
hasMoved: false,
|
|
180
|
-
last: this
|
|
72
|
+
last: this.along(event),
|
|
181
73
|
width: this.#bat.getBoundingClientRect().width
|
|
182
74
|
};
|
|
183
75
|
},
|
|
@@ -187,83 +79,57 @@ var SonicSwitch = class extends SonicRadioGroupElement {
|
|
|
187
79
|
},
|
|
188
80
|
move: (press, event) => {
|
|
189
81
|
if (this.isDisabled()) return;
|
|
190
|
-
const delta = this
|
|
82
|
+
const delta = this.along(event) - press.last;
|
|
191
83
|
if (Math.abs(delta) < press.width / 4) return;
|
|
192
84
|
press.hasMoved = true;
|
|
193
|
-
press.last = this
|
|
85
|
+
press.last = this.along(event);
|
|
194
86
|
this.#throwBy(Math.sign(delta), event.pointerId);
|
|
195
87
|
}
|
|
196
88
|
}, signal);
|
|
197
89
|
}
|
|
198
|
-
#checkedIndex(options) {
|
|
199
|
-
return options.findIndex((option) => optionValue(option) === this.value);
|
|
200
|
-
}
|
|
201
90
|
#hasSprungFrom(option) {
|
|
202
|
-
if (!option || !this
|
|
203
|
-
this
|
|
91
|
+
if (!option || !this.isMomentary(option)) return false;
|
|
92
|
+
this.releaseHold();
|
|
204
93
|
return this.value !== optionValue(option);
|
|
205
94
|
}
|
|
206
|
-
#hold(option, by) {
|
|
207
|
-
if (this.#holding?.hold(by) === true) this.select(option);
|
|
208
|
-
}
|
|
209
|
-
#isBare() {
|
|
210
|
-
return this.options().length === 0;
|
|
211
|
-
}
|
|
212
|
-
#isMomentary(option) {
|
|
213
|
-
const options = this.options();
|
|
214
|
-
return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
|
|
215
|
-
}
|
|
216
95
|
#isOnBat(event) {
|
|
217
96
|
return isUnder(this.#bat, event.clientX, event.clientY);
|
|
218
97
|
}
|
|
219
98
|
#pressBat(event) {
|
|
220
99
|
const options = this.options();
|
|
221
100
|
if (options.length === 0) {
|
|
222
|
-
this
|
|
101
|
+
this.setChecked(!this.checked);
|
|
223
102
|
return;
|
|
224
103
|
}
|
|
225
104
|
const box = this.#bat.getBoundingClientRect();
|
|
226
105
|
const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
|
|
227
|
-
const offset = (this
|
|
228
|
-
const
|
|
106
|
+
const offset = (this.along(event) - start) / size - .5;
|
|
107
|
+
const index = this.checkedIndex(options);
|
|
108
|
+
const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
|
|
109
|
+
const toward = this.stepFrom(index, side);
|
|
110
|
+
const option = toward && toward !== options[index] ? toward : this.stepFrom(index, -side);
|
|
229
111
|
if (!option) return;
|
|
230
112
|
this.chooseByClick(option);
|
|
231
113
|
this.#refocus(options);
|
|
232
114
|
}
|
|
233
115
|
#refocus(options) {
|
|
234
|
-
if (this.group.matches(":focus-within")) options[this
|
|
116
|
+
if (this.group.matches(":focus-within")) options[this.checkedIndex(options)]?.focus();
|
|
235
117
|
}
|
|
236
118
|
#renderThrow(index) {
|
|
237
119
|
const count = this.options().length;
|
|
238
120
|
const at = index < 0 || count < 2 ? 0 : 2 * index / (count - 1) - 1;
|
|
239
121
|
this.group.style.setProperty("--_sonic-switch-at", String(at));
|
|
240
122
|
}
|
|
241
|
-
#setChecked(isChecked) {
|
|
242
|
-
if (isChecked === this.checked) return;
|
|
243
|
-
this.checked = isChecked;
|
|
244
|
-
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
245
|
-
}
|
|
246
|
-
#springBack() {
|
|
247
|
-
const options = this.options();
|
|
248
|
-
const index = this.#checkedIndex(options);
|
|
249
|
-
const held = options[index];
|
|
250
|
-
if (!held || !this.#isMomentary(held)) return;
|
|
251
|
-
const rest = options[index === 0 ? 1 : index - 1];
|
|
252
|
-
if (!rest) return;
|
|
253
|
-
const isFocused = this.group.contains(this.ownerDocument.activeElement);
|
|
254
|
-
this.select(rest);
|
|
255
|
-
if (isFocused) rest.focus();
|
|
256
|
-
}
|
|
257
123
|
#throwBy(direction, pointerId) {
|
|
258
124
|
const options = this.options();
|
|
259
125
|
if (options.length === 0) {
|
|
260
|
-
this
|
|
126
|
+
this.setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
|
|
261
127
|
return;
|
|
262
128
|
}
|
|
263
|
-
const index = this
|
|
264
|
-
const option =
|
|
129
|
+
const index = this.checkedIndex(options);
|
|
130
|
+
const option = this.stepFrom(index, direction);
|
|
265
131
|
if (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;
|
|
266
|
-
if (this
|
|
132
|
+
if (this.isMomentary(option)) this.hold(option, pointerId);
|
|
267
133
|
else this.select(option);
|
|
268
134
|
this.#refocus(options);
|
|
269
135
|
}
|