stimeo-ui 0.15.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 +178 -0
- data/README.md +120 -0
- data/dist/cable/index.js +63 -15
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +8 -1
- data/dist/controllers/auto_submit_controller.js +43 -2
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +9 -8
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +323 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +54 -10
- 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 +56 -11
- 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 +205 -58
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +139 -27
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +14 -1
- 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 +305 -46
- 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 +16 -2
- data/dist/controllers/highlight_controller.js +19 -2
- 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/masonry_controller.js +0 -2
- 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 +14 -1
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +84 -13
- data/dist/controllers/overflow_menu_controller.js +112 -41
- 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 +69 -2
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +5 -1
- data/dist/controllers/scroll_visibility_controller.js +0 -1
- data/dist/controllers/scrollspy_controller.js +6 -0
- 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 +11 -1
- 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 +75 -5
- data/dist/controllers/submit_once_controller.js +16 -3
- 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/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +337 -54
- 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 +4588 -1535
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -72,7 +72,66 @@ function ownerOf(candidates, node) {
|
|
|
72
72
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/utils/owned_pointer_session.ts
|
|
76
|
+
var OwnedPointerSession = class {
|
|
77
|
+
#pointerId;
|
|
78
|
+
#owner;
|
|
79
|
+
#handlers;
|
|
80
|
+
#abort = new AbortController();
|
|
81
|
+
#active = true;
|
|
82
|
+
constructor(start, owner, handlers) {
|
|
83
|
+
this.#pointerId = start.pointerId;
|
|
84
|
+
this.#owner = owner;
|
|
85
|
+
this.#handlers = handlers;
|
|
86
|
+
const { signal } = this.#abort;
|
|
87
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
88
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
89
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
90
|
+
try {
|
|
91
|
+
owner.setPointerCapture?.(this.#pointerId);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Releases capture and listeners, then reports the end exactly once. */
|
|
96
|
+
end() {
|
|
97
|
+
this.#finish("teardown");
|
|
98
|
+
}
|
|
99
|
+
#finish(kind) {
|
|
100
|
+
if (!this.#active) return;
|
|
101
|
+
this.#active = false;
|
|
102
|
+
this.#abort.abort();
|
|
103
|
+
try {
|
|
104
|
+
this.#owner.releasePointerCapture?.(this.#pointerId);
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
this.#handlers.end?.(kind);
|
|
108
|
+
}
|
|
109
|
+
#owns(event) {
|
|
110
|
+
return this.#active && event.pointerId === this.#pointerId;
|
|
111
|
+
}
|
|
112
|
+
#onMove = (event) => {
|
|
113
|
+
if (this.#owns(event)) this.#handlers.move(event);
|
|
114
|
+
};
|
|
115
|
+
#onEndEvent = (event) => {
|
|
116
|
+
if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/utils/transient_hooks.ts
|
|
121
|
+
var TransientHooks = class {
|
|
122
|
+
#attributes;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.#attributes = options.attributes;
|
|
125
|
+
}
|
|
126
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
127
|
+
reset(element) {
|
|
128
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
75
132
|
// src/controllers/pointer_drag_controller.ts
|
|
133
|
+
var TRANSIENT_DRAG = new TransientHooks({ attributes: ["data-dragging"] });
|
|
134
|
+
var TRANSIENT_GRAB = new TransientHooks({ attributes: ["data-grabbed"] });
|
|
76
135
|
var NATIVE_KEY_OWNERS = "input, textarea, select, button, a[href], summary, [contenteditable]";
|
|
77
136
|
var PointerDragController = class _PointerDragController extends Controller {
|
|
78
137
|
static targets = ["handle"];
|
|
@@ -93,14 +152,12 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
93
152
|
#keyboard = null;
|
|
94
153
|
/** Committed follow offset from past drops; a new drag's deltas add onto it. */
|
|
95
154
|
#followBase = { x: 0, y: 0 };
|
|
96
|
-
/** Aborts in-progress pointer-drag listeners on drag end / teardown. */
|
|
97
|
-
#dragAbort = null;
|
|
98
155
|
/** Decides whether a mid-session disconnect() is an in-page move or a detach. */
|
|
99
156
|
#gate = new DetachGate();
|
|
100
157
|
connect() {
|
|
101
158
|
this.#gate.cancel();
|
|
102
|
-
if (!this.#pointer) this.element
|
|
103
|
-
if (!this.#keyboard) this.element
|
|
159
|
+
if (!this.#pointer) TRANSIENT_DRAG.reset(this.element);
|
|
160
|
+
if (!this.#keyboard) TRANSIENT_GRAB.reset(this.element);
|
|
104
161
|
if (this.followValue && !this.#pointer && !this.#keyboard) {
|
|
105
162
|
this.#followBase = this.#parseFollowBase();
|
|
106
163
|
}
|
|
@@ -170,15 +227,19 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
170
227
|
}
|
|
171
228
|
this.#teardownSessions();
|
|
172
229
|
}
|
|
173
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Arms a pointer drag on a handle; `start` waits for the threshold.
|
|
232
|
+
*
|
|
233
|
+
* @stimeoRuntimeOnly `disabled` decides whether this one pointer press starts a gesture.
|
|
234
|
+
*/
|
|
174
235
|
#onPointerDown = (event) => {
|
|
175
236
|
if (this.disabledValue || this.#pointer || this.#keyboard || event.button !== 0) return;
|
|
176
237
|
const handle = this.#handleFor(event.target);
|
|
177
238
|
if (!handle) return;
|
|
178
239
|
event.preventDefault();
|
|
179
240
|
handle.focus();
|
|
180
|
-
|
|
181
|
-
|
|
241
|
+
const session = {
|
|
242
|
+
pointer: null,
|
|
182
243
|
handle,
|
|
183
244
|
originX: event.clientX,
|
|
184
245
|
originY: event.clientY,
|
|
@@ -187,17 +248,18 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
187
248
|
dy: 0,
|
|
188
249
|
pointerType: this.#pointerTypeOf(event)
|
|
189
250
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
document.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
|
|
196
|
-
document.addEventListener("pointercancel", this.#onPointerCancel, { signal: abort.signal });
|
|
251
|
+
session.pointer = new OwnedPointerSession(event, handle, {
|
|
252
|
+
move: (move) => this.#trackMove(session, move),
|
|
253
|
+
end: (kind) => this.#finishPointer(session, kind)
|
|
254
|
+
});
|
|
255
|
+
this.#pointer = session;
|
|
197
256
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Applies one tracked move: threshold, hooks, follow offset, events.
|
|
259
|
+
*
|
|
260
|
+
* @stimeoRuntimeOnly `threshold` decides when this one pointer move starts the drag.
|
|
261
|
+
*/
|
|
262
|
+
#trackMove(session, event) {
|
|
201
263
|
const [dx, dy] = this.#filterAxis(
|
|
202
264
|
event.clientX - session.originX,
|
|
203
265
|
event.clientY - session.originY
|
|
@@ -216,25 +278,32 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
216
278
|
this.dispatch("move", {
|
|
217
279
|
detail: { dx, dy, x: event.clientX, y: event.clientY, pointerType: session.pointerType }
|
|
218
280
|
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Closes the session and reports only the outcome the caller does not own.
|
|
284
|
+
*
|
|
285
|
+
* A teardown (detach, Escape, mid-drag disable, `reset`) is announced by the
|
|
286
|
+
* path that asked for it, so this one stays silent; `pointerup` below the
|
|
287
|
+
* threshold was a plain click, and `pointercancel` is an OS gesture takeover.
|
|
288
|
+
*/
|
|
289
|
+
#finishPointer(session, kind) {
|
|
223
290
|
const { started, dx, dy, pointerType } = session;
|
|
224
|
-
this.#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
291
|
+
this.#pointer = null;
|
|
292
|
+
this.element.removeAttribute("data-dragging");
|
|
293
|
+
if (kind === "teardown" || !started) return;
|
|
294
|
+
if (kind === "up") {
|
|
295
|
+
this.#followCommit(dx, dy);
|
|
296
|
+
this.dispatch("end", { detail: { dx, dy, pointerType } });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.#dispatchCancel(pointerType);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Keyboard alternative: Space/Enter grab & drop, arrows move, Escape cancels.
|
|
303
|
+
*
|
|
304
|
+
* @stimeoRuntimeOnly `disabled` decides whether this one key is taken and `keyboardStep` is the
|
|
305
|
+
* distance of one keyboard move.
|
|
306
|
+
*/
|
|
238
307
|
#onKeydown = (event) => {
|
|
239
308
|
if (this.disabledValue) return;
|
|
240
309
|
const handle = this.#handleFor(event.target);
|
|
@@ -316,26 +385,19 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
316
385
|
return null;
|
|
317
386
|
}
|
|
318
387
|
}
|
|
319
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Zeroes the delta on the locked axis (`axis` = x | y | both).
|
|
390
|
+
*
|
|
391
|
+
* @stimeoRuntimeOnly `axis` filters the offset of one move to the allowed direction.
|
|
392
|
+
*/
|
|
320
393
|
#filterAxis(dx, dy) {
|
|
321
394
|
if (this.axisValue === "x") return [dx, 0];
|
|
322
395
|
if (this.axisValue === "y") return [0, dy];
|
|
323
396
|
return [dx, dy];
|
|
324
397
|
}
|
|
325
|
-
/**
|
|
398
|
+
/** Silently closes a live pointer session; the caller announces the outcome. */
|
|
326
399
|
#endPointerSession() {
|
|
327
|
-
this.#
|
|
328
|
-
this.#pointer = null;
|
|
329
|
-
this.#dragAbort?.abort();
|
|
330
|
-
this.#dragAbort = null;
|
|
331
|
-
this.element.removeAttribute("data-dragging");
|
|
332
|
-
}
|
|
333
|
-
/** Releases the pointer capture the active session set (idempotent, safe). */
|
|
334
|
-
#releasePointerCapture() {
|
|
335
|
-
const session = this.#pointer;
|
|
336
|
-
if (session?.handle.hasPointerCapture(session.pointerId)) {
|
|
337
|
-
session.handle.releasePointerCapture(session.pointerId);
|
|
338
|
-
}
|
|
400
|
+
this.#pointer?.pointer?.end();
|
|
339
401
|
}
|
|
340
402
|
#clearKeyboardSession() {
|
|
341
403
|
this.#keyboard?.handle.removeAttribute("data-grabbed");
|
|
@@ -372,18 +434,33 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
372
434
|
this.#followReset();
|
|
373
435
|
this.dispatch("cancel", { detail: { pointerType } });
|
|
374
436
|
}
|
|
375
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Applies the in-flight offset to the element's `translate` (follow only).
|
|
439
|
+
*
|
|
440
|
+
* @stimeoRuntimeOnly `follow` decides whether this one move writes the offset as a translate; the
|
|
441
|
+
* offset itself comes from the gesture.
|
|
442
|
+
*/
|
|
376
443
|
#followMove(dx, dy) {
|
|
377
444
|
if (!this.followValue) return;
|
|
378
445
|
this.#applyFollow(this.#followBase.x + dx, this.#followBase.y + dy);
|
|
379
446
|
}
|
|
380
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Folds a drop's deltas into the committed base offset (follow only).
|
|
449
|
+
*
|
|
450
|
+
* @stimeoRuntimeOnly `follow` decides whether the end of this one gesture keeps the offset as a
|
|
451
|
+
* translate.
|
|
452
|
+
*/
|
|
381
453
|
#followCommit(dx, dy) {
|
|
382
454
|
if (!this.followValue) return;
|
|
383
455
|
this.#followBase = { x: this.#followBase.x + dx, y: this.#followBase.y + dy };
|
|
384
456
|
this.#applyFollow(this.#followBase.x, this.#followBase.y);
|
|
385
457
|
}
|
|
386
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* Snaps back to the committed position (follow only) — the cancel contract.
|
|
460
|
+
*
|
|
461
|
+
* @stimeoRuntimeOnly `follow` decides whether cancelling this one gesture clears the translate it
|
|
462
|
+
* wrote.
|
|
463
|
+
*/
|
|
387
464
|
#followReset() {
|
|
388
465
|
if (!this.followValue) return;
|
|
389
466
|
this.#applyFollow(this.#followBase.x, this.#followBase.y);
|
|
@@ -438,6 +515,8 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
438
515
|
* idempotent) and focusability for the keyboard path (`tabindex="0"` only when
|
|
439
516
|
* the author supplied none — prefer a real `<button>` handle). Both additions
|
|
440
517
|
* are marker-owned so `#restoreHandle` reverts them symmetrically on teardown.
|
|
518
|
+
*
|
|
519
|
+
* @stimeoRenderRoot
|
|
441
520
|
*/
|
|
442
521
|
#prepareHandle(handle) {
|
|
443
522
|
const lent = this.#touchActionForAxis();
|
|
@@ -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) {
|