stimeo-ui 0.14.0 → 0.16.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -192,6 +192,16 @@ function isScrollable(element) {
|
|
|
192
192
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// src/utils/state_reason.ts
|
|
196
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
197
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
198
|
+
function stateReasonFor(event) {
|
|
199
|
+
if (!event) return "api";
|
|
200
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
201
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
202
|
+
return "user";
|
|
203
|
+
}
|
|
204
|
+
|
|
195
205
|
// src/controllers/popover_controller.ts
|
|
196
206
|
var PopoverController = class extends Controller {
|
|
197
207
|
static targets = ["trigger", "panel"];
|
|
@@ -199,15 +209,19 @@ var PopoverController = class extends Controller {
|
|
|
199
209
|
closeOnScroll: { type: Boolean, default: false }
|
|
200
210
|
};
|
|
201
211
|
static actions = ["close", "open", "toggle"];
|
|
212
|
+
static events = ["close", "open"];
|
|
202
213
|
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
203
214
|
#stopScrollDismiss = null;
|
|
204
215
|
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
205
216
|
#escapeLayer = new EscapeLayer();
|
|
217
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
218
|
+
#reporting = false;
|
|
206
219
|
/** Starts closed and registers the standing dismissal listeners. */
|
|
207
220
|
connect() {
|
|
208
|
-
this
|
|
221
|
+
this.#close("api");
|
|
209
222
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
210
223
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
224
|
+
this.#reporting = true;
|
|
211
225
|
}
|
|
212
226
|
/**
|
|
213
227
|
* Removes every standing listener registered in {@link connect} plus any active
|
|
@@ -216,6 +230,7 @@ var PopoverController = class extends Controller {
|
|
|
216
230
|
* the element after a Turbo navigation.
|
|
217
231
|
*/
|
|
218
232
|
disconnect() {
|
|
233
|
+
this.#reporting = false;
|
|
219
234
|
this.#escapeLayer.deactivate();
|
|
220
235
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
221
236
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
@@ -223,34 +238,51 @@ var PopoverController = class extends Controller {
|
|
|
223
238
|
this.#stopScrollDismiss = null;
|
|
224
239
|
}
|
|
225
240
|
/** Toggles the popover. Bound via `data-action` (click on the trigger). */
|
|
226
|
-
toggle() {
|
|
241
|
+
toggle(event) {
|
|
227
242
|
if (this.#isOpen) {
|
|
228
|
-
this.close();
|
|
243
|
+
this.close(event);
|
|
229
244
|
} else {
|
|
230
|
-
this.open();
|
|
245
|
+
this.open(event);
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
/** Opens the panel, reflects state, and moves focus inside it. */
|
|
234
|
-
open() {
|
|
249
|
+
open(event) {
|
|
250
|
+
this.#open(stateReasonFor(event));
|
|
251
|
+
}
|
|
252
|
+
/** Closes the panel and reflects the collapsed state. Bound via `data-action`. */
|
|
253
|
+
close(event) {
|
|
254
|
+
this.#close(stateReasonFor(event));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Opens the panel, reflects state, reports a move, and moves focus inside it.
|
|
258
|
+
*
|
|
259
|
+
* @stimeoRuntimeOnly `closeOnScroll` decides whether this opening wires the scroll dismissal;
|
|
260
|
+
* what is shown does not depend on it.
|
|
261
|
+
*/
|
|
262
|
+
#open(reason) {
|
|
235
263
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
236
264
|
this.panelTarget.hidden = false;
|
|
237
265
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
266
|
+
if (this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
267
|
+
if (!this.#isOpen) return;
|
|
238
268
|
this.#escapeLayer.activate(document, {
|
|
239
269
|
onDismiss: () => this.#closeAndRestore(),
|
|
240
270
|
claims: claimsWhileFocusWithin(this.element)
|
|
241
271
|
});
|
|
242
272
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
243
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this
|
|
273
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#close("scroll"));
|
|
244
274
|
}
|
|
245
275
|
this.#focusFirst();
|
|
246
276
|
}
|
|
247
|
-
/** Closes the panel
|
|
248
|
-
close() {
|
|
277
|
+
/** Closes the panel, reflects the collapsed state, and reports a move. */
|
|
278
|
+
#close(reason) {
|
|
279
|
+
const was = this.#isOpen;
|
|
249
280
|
this.#escapeLayer.deactivate();
|
|
250
281
|
this.#stopScrollDismiss?.();
|
|
251
282
|
this.#stopScrollDismiss = null;
|
|
252
283
|
if (this.hasPanelTarget) this.panelTarget.hidden = true;
|
|
253
284
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
285
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
254
286
|
}
|
|
255
287
|
/** Moves focus to the first focusable element in the panel, or the panel itself. */
|
|
256
288
|
#focusFirst() {
|
|
@@ -264,13 +296,15 @@ var PopoverController = class extends Controller {
|
|
|
264
296
|
}
|
|
265
297
|
/** Closes and restores focus to the trigger for explicit keyboard dismissal. */
|
|
266
298
|
#closeAndRestore() {
|
|
267
|
-
this
|
|
299
|
+
this.#close("escape");
|
|
268
300
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
269
301
|
}
|
|
270
302
|
/** Closes without moving focus when a click lands outside the controller element. */
|
|
271
303
|
#onOutsideClick = (event) => {
|
|
272
304
|
const target = event.target;
|
|
273
|
-
if (this.#isOpen && target instanceof Node && !this.element.contains(target))
|
|
305
|
+
if (this.#isOpen && target instanceof Node && !this.element.contains(target)) {
|
|
306
|
+
this.#close("outside");
|
|
307
|
+
}
|
|
274
308
|
};
|
|
275
309
|
/**
|
|
276
310
|
* Closes when focus leaves the controller for a known external destination
|
|
@@ -284,7 +318,7 @@ var PopoverController = class extends Controller {
|
|
|
284
318
|
if (!this.#isOpen) return;
|
|
285
319
|
const next = event.relatedTarget;
|
|
286
320
|
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
287
|
-
this
|
|
321
|
+
this.#close("focus");
|
|
288
322
|
};
|
|
289
323
|
/** Whether the panel is currently visible. */
|
|
290
324
|
get #isOpen() {
|
|
@@ -122,7 +122,7 @@ var PortalController = class extends Controller {
|
|
|
122
122
|
destination.appendChild(node);
|
|
123
123
|
}
|
|
124
124
|
node.setAttribute("data-portaled", "true");
|
|
125
|
-
this.dispatch("mount", { detail: {
|
|
125
|
+
this.dispatch("mount", { detail: { destination } });
|
|
126
126
|
}
|
|
127
127
|
disconnect() {
|
|
128
128
|
const state = portalState.get(this.element);
|
|
@@ -137,7 +137,11 @@ var PortalController = class extends Controller {
|
|
|
137
137
|
if (current?.owner === this) this.#restore(current);
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Returns the node to its placeholder (or removes it) and clears the bookkeeping.
|
|
142
|
+
*
|
|
143
|
+
* @stimeoRuntimeOnly `restore` decides whether this one unmount puts the node back or drops it.
|
|
144
|
+
*/
|
|
141
145
|
#restore(state) {
|
|
142
146
|
portalState.delete(this.element);
|
|
143
147
|
const { node, placeholder } = state;
|
|
@@ -83,7 +83,20 @@ var StylePropertyLease = class {
|
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
// src/utils/transient_hooks.ts
|
|
87
|
+
var TransientHooks = class {
|
|
88
|
+
#attributes;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
this.#attributes = options.attributes;
|
|
91
|
+
}
|
|
92
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
93
|
+
reset(element) {
|
|
94
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
86
98
|
// src/controllers/preview_guard_controller.ts
|
|
99
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-preview-hidden"] });
|
|
87
100
|
var PreviewGuardController = class extends Controller {
|
|
88
101
|
static values = {
|
|
89
102
|
placeholder: { type: String, default: "" }
|
|
@@ -101,7 +114,7 @@ var PreviewGuardController = class extends Controller {
|
|
|
101
114
|
connect() {
|
|
102
115
|
this.#connected = true;
|
|
103
116
|
if (this.#hidden) this.#reguard();
|
|
104
|
-
else this.element
|
|
117
|
+
else TRANSIENT.reset(this.element);
|
|
105
118
|
this.#beforeCache.activate();
|
|
106
119
|
if (typeof MutationObserver !== "undefined") {
|
|
107
120
|
this.#observer = new MutationObserver(() => this.#sync());
|
|
@@ -161,6 +174,8 @@ var PreviewGuardController = class extends Controller {
|
|
|
161
174
|
* first would reconnect the whole subtree for an instant. Only a change of *form* —
|
|
162
175
|
* to or from the empty placeholder — has to revert, and the focus the guard is holding
|
|
163
176
|
* carries across it rather than being handed back and taken again.
|
|
177
|
+
*
|
|
178
|
+
* @stimeoRenderRoot
|
|
164
179
|
*/
|
|
165
180
|
#reguard() {
|
|
166
181
|
if (this.#savedNodes && this.placeholderValue !== "") {
|
|
@@ -76,8 +76,12 @@ function rangeFraction(value, min, max) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// src/controllers/progress_controller.ts
|
|
79
|
-
var OWNED_VALUE_TEXT = "
|
|
79
|
+
var OWNED_VALUE_TEXT = "owns-valuetext";
|
|
80
80
|
var ProgressController = class extends Controller {
|
|
81
|
+
/** The marker above, in the namespace this controller is registered under. */
|
|
82
|
+
get #ownedValueText() {
|
|
83
|
+
return `data-${this.identifier}-${OWNED_VALUE_TEXT}`;
|
|
84
|
+
}
|
|
81
85
|
static targets = ["bar"];
|
|
82
86
|
static values = {
|
|
83
87
|
value: { type: Number, default: 0 },
|
|
@@ -189,13 +193,13 @@ var ProgressController = class extends Controller {
|
|
|
189
193
|
const percent = Math.round(this.#ratio * 100);
|
|
190
194
|
const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent));
|
|
191
195
|
this.element.setAttribute("aria-valuetext", text);
|
|
192
|
-
this.element.setAttribute(
|
|
196
|
+
this.element.setAttribute(this.#ownedValueText, "");
|
|
193
197
|
}
|
|
194
198
|
/** Removes `aria-valuetext` only when this controller is the one that wrote it. */
|
|
195
199
|
#clearOwnValueText() {
|
|
196
|
-
if (!this.element.hasAttribute(
|
|
200
|
+
if (!this.element.hasAttribute(this.#ownedValueText)) return;
|
|
197
201
|
this.element.removeAttribute("aria-valuetext");
|
|
198
|
-
this.element.removeAttribute(
|
|
202
|
+
this.element.removeAttribute(this.#ownedValueText);
|
|
199
203
|
}
|
|
200
204
|
};
|
|
201
205
|
|
|
@@ -29,6 +29,16 @@ function ownerOf(candidates, node) {
|
|
|
29
29
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/utils/field_mirror.ts
|
|
33
|
+
function writeField(field, value) {
|
|
34
|
+
if (field.value === value) return false;
|
|
35
|
+
field.value = value;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function commitField(target) {
|
|
39
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// src/utils/focus_candidate.ts
|
|
33
43
|
function inheritsFieldsetDisabled(control) {
|
|
34
44
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -131,6 +141,7 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
// src/controllers/radio_group_controller.ts
|
|
144
|
+
var NO_SELECTION = { radio: null, value: "" };
|
|
134
145
|
var OBSERVED_ATTRIBUTES = [
|
|
135
146
|
"aria-checked",
|
|
136
147
|
"aria-disabled",
|
|
@@ -158,7 +169,8 @@ var RadioGroupController = class extends Controller {
|
|
|
158
169
|
#internalCheckedValues = /* @__PURE__ */ new Map();
|
|
159
170
|
#internalTabindexValues = /* @__PURE__ */ new Map();
|
|
160
171
|
#observer = null;
|
|
161
|
-
|
|
172
|
+
/** The selection last published: read on connect, then committed or reported. */
|
|
173
|
+
#committed = NO_SELECTION;
|
|
162
174
|
#connected = false;
|
|
163
175
|
#lastOrder = [];
|
|
164
176
|
#focusedRadio = null;
|
|
@@ -177,7 +189,7 @@ var RadioGroupController = class extends Controller {
|
|
|
177
189
|
this.#normalizeSelection();
|
|
178
190
|
this.#ensureTabStop(true);
|
|
179
191
|
this.#reflectField(this.#selectedRadio, { silent: true });
|
|
180
|
-
this.#
|
|
192
|
+
this.#committed = this.#settled;
|
|
181
193
|
this.#lastOrder = this.#managedTargets;
|
|
182
194
|
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
183
195
|
this.element.addEventListener("keydown", this.#onKeydownCapture, true);
|
|
@@ -208,7 +220,7 @@ var RadioGroupController = class extends Controller {
|
|
|
208
220
|
this.#internalTabindexValues.clear();
|
|
209
221
|
this.#focusedRadio = null;
|
|
210
222
|
this.#pendingFocusIndex = null;
|
|
211
|
-
this.#
|
|
223
|
+
this.#committed = NO_SELECTION;
|
|
212
224
|
this.#preferChecked = false;
|
|
213
225
|
}
|
|
214
226
|
/** Removes a newly connected radio from the Tab sequence before batch reconciliation. */
|
|
@@ -334,17 +346,23 @@ var RadioGroupController = class extends Controller {
|
|
|
334
346
|
if (this.#isActivationDisabled(destination)) this.#setActive(destination, true);
|
|
335
347
|
else this.#selectRadio(destination, { focus: true });
|
|
336
348
|
}
|
|
337
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Applies one user selection and emits only when selected identity changes. A
|
|
351
|
+
* new selection is taken as published before either report goes out, so a
|
|
352
|
+
* listener that selects again is measured from it. Activating the radio that is
|
|
353
|
+
* already checked publishes nothing: if the page checked it, the next pass
|
|
354
|
+
* reports that move.
|
|
355
|
+
*/
|
|
338
356
|
#selectRadio(radio, { focus }) {
|
|
339
357
|
if (!this.#isSupportedHost(radio) || this.#isActivationDisabled(radio)) return;
|
|
340
358
|
const previous = this.#selectedRadio;
|
|
341
359
|
const changed = previous !== radio;
|
|
342
360
|
if (changed) {
|
|
343
361
|
for (const item of this.#managedTargets) this.#setChecked(item, item === radio);
|
|
362
|
+
this.#committed = { radio, value: this.#radioValue(radio) };
|
|
344
363
|
}
|
|
345
364
|
this.#setActive(radio, focus);
|
|
346
365
|
this.#reflectField(radio, { silent: !changed });
|
|
347
|
-
this.#committedRadio = radio;
|
|
348
366
|
if (changed) {
|
|
349
367
|
this.dispatch("change", { detail: { value: this.#radioValue(radio), radio } });
|
|
350
368
|
}
|
|
@@ -372,16 +390,19 @@ var RadioGroupController = class extends Controller {
|
|
|
372
390
|
this.#reportReconciledSelection();
|
|
373
391
|
}
|
|
374
392
|
/**
|
|
375
|
-
* Announces a selection
|
|
376
|
-
*
|
|
393
|
+
* Announces a selection that moved since the one this group last published —
|
|
394
|
+
* another radio, or the same radio submitting another value. Dispatched after
|
|
395
|
+
* observation resumes so a consumer's own DOM edits are seen by the next pass,
|
|
396
|
+
* and only while connected: moving focus inside the pass runs listeners that
|
|
397
|
+
* can disconnect the group.
|
|
377
398
|
*/
|
|
378
399
|
#reportReconciledSelection() {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
});
|
|
400
|
+
if (!this.#connected) return;
|
|
401
|
+
const settled = this.#settled;
|
|
402
|
+
const committed = this.#committed;
|
|
403
|
+
if (settled.radio === committed.radio && settled.value === committed.value) return;
|
|
404
|
+
this.#committed = settled;
|
|
405
|
+
this.dispatch("reconcile", { detail: { value: settled.value, radio: settled.radio } });
|
|
385
406
|
}
|
|
386
407
|
/** Begins or ends ownership according to a radio's current host semantics. */
|
|
387
408
|
#reconcileHost(radio, dropFromTabSequence) {
|
|
@@ -493,7 +514,7 @@ var RadioGroupController = class extends Controller {
|
|
|
493
514
|
}
|
|
494
515
|
/** Whether the closest Radio Group scope around a target is this instance. */
|
|
495
516
|
#ownsEventTarget(target) {
|
|
496
|
-
return target instanceof Element && target.closest(
|
|
517
|
+
return target instanceof Element && target.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
497
518
|
}
|
|
498
519
|
/** Hosts whose activation model can be owned without conflicting native behavior. */
|
|
499
520
|
#isSupportedHost(radio) {
|
|
@@ -603,6 +624,11 @@ var RadioGroupController = class extends Controller {
|
|
|
603
624
|
get #selectedRadio() {
|
|
604
625
|
return this.#managedTargets.find((radio) => this.#isChecked(radio));
|
|
605
626
|
}
|
|
627
|
+
/** The current selection as the group publishes it. */
|
|
628
|
+
get #settled() {
|
|
629
|
+
const radio = this.#selectedRadio;
|
|
630
|
+
return radio ? { radio, value: this.#radioValue(radio) } : NO_SELECTION;
|
|
631
|
+
}
|
|
606
632
|
/** Whether a radio is currently checked. */
|
|
607
633
|
#isChecked(radio) {
|
|
608
634
|
return radio.getAttribute("aria-checked") === "true";
|
|
@@ -624,9 +650,8 @@ var RadioGroupController = class extends Controller {
|
|
|
624
650
|
#reflectField(radio, { silent = false } = {}) {
|
|
625
651
|
if (!this.hasFieldTarget) return;
|
|
626
652
|
const value = radio ? this.#radioValue(radio) : "";
|
|
627
|
-
if (this.fieldTarget
|
|
628
|
-
this.fieldTarget
|
|
629
|
-
if (!silent) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
653
|
+
if (!writeField(this.fieldTarget, value)) return;
|
|
654
|
+
if (!silent) commitField(this.fieldTarget);
|
|
630
655
|
}
|
|
631
656
|
/** A radio's submitted value (`data-value`, defaulting to empty). */
|
|
632
657
|
#radioValue(radio) {
|
|
@@ -18,6 +18,16 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/field_mirror.ts
|
|
22
|
+
function writeField(field, value) {
|
|
23
|
+
if (field.value === value) return false;
|
|
24
|
+
field.value = value;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function commitField(target) {
|
|
28
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// src/utils/microtask_coalescer.ts
|
|
22
32
|
var MicrotaskCoalescer = class {
|
|
23
33
|
#run;
|
|
@@ -53,54 +63,46 @@ var MicrotaskCoalescer = class {
|
|
|
53
63
|
|
|
54
64
|
// src/utils/owned_pointer_session.ts
|
|
55
65
|
var OwnedPointerSession = class {
|
|
56
|
-
pointerId;
|
|
66
|
+
#pointerId;
|
|
57
67
|
#owner;
|
|
58
68
|
#handlers;
|
|
59
69
|
#abort = new AbortController();
|
|
60
70
|
#active = true;
|
|
61
71
|
constructor(start, owner, handlers) {
|
|
62
|
-
this
|
|
72
|
+
this.#pointerId = start.pointerId;
|
|
63
73
|
this.#owner = owner;
|
|
64
74
|
this.#handlers = handlers;
|
|
65
75
|
const { signal } = this.#abort;
|
|
66
76
|
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
67
77
|
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
68
78
|
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
69
|
-
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
70
79
|
try {
|
|
71
|
-
owner.setPointerCapture?.(this
|
|
80
|
+
owner.setPointerCapture?.(this.#pointerId);
|
|
72
81
|
} catch {
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
/**
|
|
76
|
-
get active() {
|
|
77
|
-
return this.#active;
|
|
78
|
-
}
|
|
79
|
-
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
80
|
-
owns(event) {
|
|
81
|
-
return this.#active && event.pointerId === this.pointerId;
|
|
82
|
-
}
|
|
83
|
-
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
84
|
+
/** Releases capture and listeners, then reports the end exactly once. */
|
|
84
85
|
end() {
|
|
86
|
+
this.#finish("teardown");
|
|
87
|
+
}
|
|
88
|
+
#finish(kind) {
|
|
85
89
|
if (!this.#active) return;
|
|
86
90
|
this.#active = false;
|
|
87
91
|
this.#abort.abort();
|
|
88
92
|
try {
|
|
89
|
-
this.#owner.releasePointerCapture?.(this
|
|
93
|
+
this.#owner.releasePointerCapture?.(this.#pointerId);
|
|
90
94
|
} catch {
|
|
91
95
|
}
|
|
92
|
-
this.#handlers.end?.();
|
|
96
|
+
this.#handlers.end?.(kind);
|
|
97
|
+
}
|
|
98
|
+
#owns(event) {
|
|
99
|
+
return this.#active && event.pointerId === this.#pointerId;
|
|
93
100
|
}
|
|
94
101
|
#onMove = (event) => {
|
|
95
|
-
if (this
|
|
102
|
+
if (this.#owns(event)) this.#handlers.move(event);
|
|
96
103
|
};
|
|
97
104
|
#onEndEvent = (event) => {
|
|
98
|
-
if (this
|
|
99
|
-
};
|
|
100
|
-
#onLostCapture = (event) => {
|
|
101
|
-
const pointerId = event.pointerId;
|
|
102
|
-
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
103
|
-
this.end();
|
|
105
|
+
if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
|
|
104
106
|
};
|
|
105
107
|
};
|
|
106
108
|
|
|
@@ -232,7 +234,7 @@ var END_PROPERTY = "--stimeo--range-slider-end";
|
|
|
232
234
|
var DEFAULT_MIN = 0;
|
|
233
235
|
var DEFAULT_MAX = 100;
|
|
234
236
|
var RangeSliderController = class extends Controller {
|
|
235
|
-
static targets = ["track", "startThumb", "endThumb"];
|
|
237
|
+
static targets = ["track", "startThumb", "endThumb", "startField", "endField"];
|
|
236
238
|
static values = {
|
|
237
239
|
min: { type: Number, default: DEFAULT_MIN },
|
|
238
240
|
max: { type: Number, default: DEFAULT_MAX },
|
|
@@ -242,9 +244,14 @@ var RangeSliderController = class extends Controller {
|
|
|
242
244
|
logicalTrack: { type: Boolean, default: false }
|
|
243
245
|
};
|
|
244
246
|
static actions = ["onKeydown", "onPointerDown"];
|
|
245
|
-
static events = ["change"];
|
|
247
|
+
static events = ["change", "reconcile"];
|
|
246
248
|
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
247
249
|
#drag = null;
|
|
250
|
+
/**
|
|
251
|
+
* The pair last published: taken on connect, then moved by each user commit
|
|
252
|
+
* and by each repaint that reports a page-driven move.
|
|
253
|
+
*/
|
|
254
|
+
#settled = { start: DEFAULT_MIN, end: DEFAULT_MAX };
|
|
248
255
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
249
256
|
get #mirrored() {
|
|
250
257
|
return this.logicalTrackValue && isRtl(this.element);
|
|
@@ -254,11 +261,14 @@ var RangeSliderController = class extends Controller {
|
|
|
254
261
|
* pass Stimulus delivers before `connect()`.
|
|
255
262
|
*/
|
|
256
263
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
264
|
+
/**
|
|
265
|
+
* Renders the normalized, ordered pair without writing it back, and takes it
|
|
266
|
+
* as the baseline, so connecting reports nothing.
|
|
267
|
+
*/
|
|
257
268
|
connect() {
|
|
258
269
|
this.#repaint.activate();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.#commit(pair.start, pair.end, null, false);
|
|
270
|
+
this.#settled = this.#currentPair(this.#effectiveRange);
|
|
271
|
+
this.#render();
|
|
262
272
|
}
|
|
263
273
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
264
274
|
disconnect() {
|
|
@@ -313,6 +323,14 @@ var RangeSliderController = class extends Controller {
|
|
|
313
323
|
}
|
|
314
324
|
this.#repaint.schedule();
|
|
315
325
|
}
|
|
326
|
+
/** Fills a start field inserted or replaced at runtime on the next repaint. */
|
|
327
|
+
startFieldTargetConnected() {
|
|
328
|
+
this.#repaint.schedule();
|
|
329
|
+
}
|
|
330
|
+
/** Fills an end field inserted or replaced at runtime on the next repaint. */
|
|
331
|
+
endFieldTargetConnected() {
|
|
332
|
+
this.#repaint.schedule();
|
|
333
|
+
}
|
|
316
334
|
/** Drops a stale end-thumb reference without orphaning the track gesture. */
|
|
317
335
|
endThumbTargetDisconnected(thumb) {
|
|
318
336
|
if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
@@ -439,44 +457,72 @@ var RangeSliderController = class extends Controller {
|
|
|
439
457
|
#moveThumb(kind, raw) {
|
|
440
458
|
const pair = this.#currentPair(this.#effectiveRange);
|
|
441
459
|
if (kind === "start") {
|
|
442
|
-
this.#commit(raw, pair.end, "start"
|
|
460
|
+
this.#commit(raw, pair.end, "start");
|
|
443
461
|
} else {
|
|
444
|
-
this.#commit(pair.start, raw, "end"
|
|
462
|
+
this.#commit(pair.start, raw, "end");
|
|
445
463
|
}
|
|
446
464
|
}
|
|
447
465
|
/**
|
|
448
|
-
* Clamps and snaps
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
466
|
+
* Clamps and snaps the pair a user's move of the `moving` thumb produced,
|
|
467
|
+
* stops that thumb at its partner, stores the pair, and reflects it onto the
|
|
468
|
+
* thumbs' ARIA attributes and the range custom properties. The pair is
|
|
469
|
+
* reported as `change` when it differs from the pair last published — not from
|
|
470
|
+
* the pair the move started from — so a page write no repaint has published
|
|
471
|
+
* yet is reported once, as this change, and a move that ends on the pair last
|
|
472
|
+
* published reports nothing. The baseline moves before any report goes out,
|
|
473
|
+
* so the repaint the Value writes schedule — and any move a subscriber makes
|
|
474
|
+
* while the report is dispatched — is measured from this pair.
|
|
453
475
|
*/
|
|
454
|
-
#commit(start, end, moving
|
|
476
|
+
#commit(start, end, moving) {
|
|
455
477
|
const range = this.#effectiveRange;
|
|
456
|
-
const previous = this.#currentPair(range);
|
|
457
478
|
let nextStart = snapSteppedValue(start, range);
|
|
458
479
|
let nextEnd = snapSteppedValue(end, range);
|
|
459
480
|
if (nextStart > nextEnd) {
|
|
460
481
|
if (moving === "start") nextStart = nextEnd;
|
|
461
|
-
else
|
|
462
|
-
else [nextStart, nextEnd] = [nextEnd, nextStart];
|
|
482
|
+
else nextEnd = nextStart;
|
|
463
483
|
}
|
|
484
|
+
const pair = { start: nextStart, end: nextEnd };
|
|
485
|
+
const reported = this.#settled;
|
|
486
|
+
this.#settled = pair;
|
|
464
487
|
if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
|
|
465
488
|
if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
|
|
466
|
-
const pair = { start: nextStart, end: nextEnd };
|
|
467
489
|
this.#renderPair(pair, range);
|
|
468
|
-
|
|
490
|
+
this.#mirrorFields(pair, true);
|
|
491
|
+
if (nextStart !== reported.start || nextEnd !== reported.end) {
|
|
469
492
|
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
/**
|
|
473
|
-
* Reflects
|
|
496
|
+
* Reflects the Values the page supplied without writing them back. A published
|
|
497
|
+
* pair that moved from the last one is reported once as `reconcile`; the
|
|
498
|
+
* fields follow without a native `change`.
|
|
474
499
|
*
|
|
475
500
|
* @stimeoRenderRoot
|
|
476
501
|
*/
|
|
477
502
|
#render() {
|
|
478
503
|
const range = this.#effectiveRange;
|
|
479
|
-
this.#
|
|
504
|
+
const pair = this.#currentPair(range);
|
|
505
|
+
this.#renderPair(pair, range);
|
|
506
|
+
this.#mirrorFields(pair, false);
|
|
507
|
+
const settled = this.#settled;
|
|
508
|
+
if (pair.start === settled.start && pair.end === settled.end) return;
|
|
509
|
+
this.#settled = pair;
|
|
510
|
+
this.dispatch("reconcile", { detail: { start: pair.start, end: pair.end } });
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Mirrors the pair into the optional form fields, reporting only a user's
|
|
514
|
+
* move. Each end is compared on its own, so dragging one thumb never reports
|
|
515
|
+
* a commit from the field the other thumb owns.
|
|
516
|
+
*/
|
|
517
|
+
#mirrorFields(pair, notify) {
|
|
518
|
+
if (this.hasStartFieldTarget) {
|
|
519
|
+
const field = this.startFieldTarget;
|
|
520
|
+
if (writeField(field, String(pair.start)) && notify) commitField(field);
|
|
521
|
+
}
|
|
522
|
+
if (this.hasEndFieldTarget) {
|
|
523
|
+
const field = this.endFieldTarget;
|
|
524
|
+
if (writeField(field, String(pair.end)) && notify) commitField(field);
|
|
525
|
+
}
|
|
480
526
|
}
|
|
481
527
|
/** Reflects one normalized pair onto both thumbs and CSS properties. */
|
|
482
528
|
#renderPair(pair, range) {
|