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
|
@@ -365,18 +365,32 @@ var FocusTrap = class {
|
|
|
365
365
|
}
|
|
366
366
|
};
|
|
367
367
|
|
|
368
|
+
// src/utils/state_reason.ts
|
|
369
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
370
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
371
|
+
function stateReasonFor(event) {
|
|
372
|
+
if (!event) return "api";
|
|
373
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
374
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
375
|
+
return "user";
|
|
376
|
+
}
|
|
377
|
+
|
|
368
378
|
// src/controllers/dialog_controller.ts
|
|
369
379
|
var DialogController = class extends Controller {
|
|
370
380
|
static targets = ["trigger", "dialog"];
|
|
371
381
|
static actions = ["close", "closeOnBackdrop", "open"];
|
|
382
|
+
static events = ["close", "open"];
|
|
372
383
|
/** Owns the modal side effects; Escape closes, focus falls back to the trigger. */
|
|
373
384
|
#trap = new FocusTrap(() => this.dialogTarget, {
|
|
374
|
-
onEscape: () => this
|
|
385
|
+
onEscape: () => this.#close("escape"),
|
|
375
386
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
376
387
|
});
|
|
388
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
389
|
+
#reporting = false;
|
|
377
390
|
/** Starts closed (idempotently reflects the closed state on the markup). */
|
|
378
391
|
connect() {
|
|
379
392
|
if (this.hasDialogTarget) this.dialogTarget.hidden = true;
|
|
393
|
+
this.#reporting = true;
|
|
380
394
|
}
|
|
381
395
|
/**
|
|
382
396
|
* Reverts the modal side effects (scroll lock, background `inert`, keydown
|
|
@@ -385,24 +399,37 @@ var DialogController = class extends Controller {
|
|
|
385
399
|
* teardown.
|
|
386
400
|
*/
|
|
387
401
|
disconnect() {
|
|
402
|
+
this.#reporting = false;
|
|
388
403
|
this.#trap.deactivate({ restoreFocus: false });
|
|
389
404
|
}
|
|
390
405
|
/** Opens the dialog, traps focus, and locks background scroll. */
|
|
391
|
-
open() {
|
|
406
|
+
open(event) {
|
|
407
|
+
this.#open(stateReasonFor(event));
|
|
408
|
+
}
|
|
409
|
+
/** Closes the dialog, restores scroll, and returns focus to the opener. */
|
|
410
|
+
close(event) {
|
|
411
|
+
this.#close(stateReasonFor(event));
|
|
412
|
+
}
|
|
413
|
+
/** Closes when the backdrop (the dialog target itself) is clicked. */
|
|
414
|
+
closeOnBackdrop(event) {
|
|
415
|
+
if (event.target === this.dialogTarget) this.#close("outside");
|
|
416
|
+
}
|
|
417
|
+
/** Reveals the dialog, reports a move, then traps focus and locks scroll. */
|
|
418
|
+
#open(reason) {
|
|
392
419
|
if (!this.hasDialogTarget || this.#isOpen) return;
|
|
393
420
|
this.dialogTarget.hidden = false;
|
|
421
|
+
if (this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
422
|
+
if (!this.#isOpen) return;
|
|
394
423
|
this.#trap.activate();
|
|
395
424
|
}
|
|
396
|
-
/**
|
|
397
|
-
close() {
|
|
425
|
+
/** Hides the dialog, reports a move, then restores scroll and focus. */
|
|
426
|
+
#close(reason) {
|
|
398
427
|
if (!this.hasDialogTarget || !this.#isOpen) return;
|
|
399
428
|
this.dialogTarget.hidden = true;
|
|
429
|
+
if (this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
430
|
+
if (this.#isOpen) return;
|
|
400
431
|
this.#trap.deactivate();
|
|
401
432
|
}
|
|
402
|
-
/** Closes when the backdrop (the dialog target itself) is clicked. */
|
|
403
|
-
closeOnBackdrop(event) {
|
|
404
|
-
if (event.target === this.dialogTarget) this.close();
|
|
405
|
-
}
|
|
406
433
|
/** Whether the dialog is currently visible. */
|
|
407
434
|
get #isOpen() {
|
|
408
435
|
return this.hasDialogTarget && !this.dialogTarget.hidden;
|
|
@@ -121,6 +121,12 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
+
// src/utils/template_row.ts
|
|
125
|
+
function cloneTemplateRoot(template) {
|
|
126
|
+
const root = template.content.firstElementChild;
|
|
127
|
+
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
// src/controllers/direct_upload_controller.ts
|
|
125
131
|
var REMOVE_DELAY = 4e3;
|
|
126
132
|
var DirectUploadController = class extends Controller {
|
|
@@ -186,7 +192,12 @@ var DirectUploadController = class extends Controller {
|
|
|
186
192
|
this.#syncAggregate();
|
|
187
193
|
this.dispatch("progress", { detail: { id, percent: clamped } });
|
|
188
194
|
}
|
|
189
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Marks a not-yet-settled row done at 100%, announces it, and emits `done`.
|
|
197
|
+
*
|
|
198
|
+
* @stimeoRuntimeOnly `announceDoneText` words the one announcement of this completion and
|
|
199
|
+
* `removeOnDone` decides whether it arms the row's removal.
|
|
200
|
+
*/
|
|
190
201
|
#complete(id, name) {
|
|
191
202
|
const row = this.#rowFor(id, name);
|
|
192
203
|
if (row === null || this.#isSettled(row)) return;
|
|
@@ -204,6 +215,8 @@ var DirectUploadController = class extends Controller {
|
|
|
204
215
|
* Returns whether the failure is rendered by this widget (used to decide the
|
|
205
216
|
* `direct-upload:error` default), which also holds when the row already
|
|
206
217
|
* displays an earlier failure.
|
|
218
|
+
*
|
|
219
|
+
* @stimeoRuntimeOnly `announceErrorText` words the one announcement of this failure.
|
|
207
220
|
*/
|
|
208
221
|
#fail(id, error, name) {
|
|
209
222
|
const row = this.#rowFor(id, name);
|
|
@@ -219,7 +232,12 @@ var DirectUploadController = class extends Controller {
|
|
|
219
232
|
const state = row.getAttribute("data-upload-state");
|
|
220
233
|
return state === "done" || state === "error";
|
|
221
234
|
}
|
|
222
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Re-arms `removeOnDone` for completed rows after a reconnect.
|
|
237
|
+
*
|
|
238
|
+
* @stimeoRuntimeOnly `removeOnDone` decides whether reconnecting arms the removal of finished
|
|
239
|
+
* rows again.
|
|
240
|
+
*/
|
|
223
241
|
#rescheduleRemovals() {
|
|
224
242
|
if (!this.removeOnDoneValue) return;
|
|
225
243
|
this.#prune();
|
|
@@ -242,8 +260,8 @@ var DirectUploadController = class extends Controller {
|
|
|
242
260
|
this.#rows.delete(key);
|
|
243
261
|
}
|
|
244
262
|
if (!this.hasRowTarget || !this.hasListTarget) return null;
|
|
245
|
-
const clone = this.rowTarget
|
|
246
|
-
if (!
|
|
263
|
+
const clone = cloneTemplateRoot(this.rowTarget);
|
|
264
|
+
if (!clone) return null;
|
|
247
265
|
this.#applyName(clone, name);
|
|
248
266
|
clone.setAttribute("data-upload-state", "uploading");
|
|
249
267
|
this.#applyProgress(clone, 0);
|
|
@@ -79,7 +79,20 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
// src/utils/transient_hooks.ts
|
|
83
|
+
var TransientHooks = class {
|
|
84
|
+
#attributes;
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.#attributes = options.attributes;
|
|
87
|
+
}
|
|
88
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
89
|
+
reset(element) {
|
|
90
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
82
94
|
// src/controllers/dirty_form_controller.ts
|
|
95
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
|
|
83
96
|
var visitCoordinators = /* @__PURE__ */ new WeakMap();
|
|
84
97
|
var TurboVisitCoordinator = class {
|
|
85
98
|
#document;
|
|
@@ -206,7 +219,7 @@ var DirtyFormController = class extends Controller {
|
|
|
206
219
|
this.#resetSubmission();
|
|
207
220
|
this.#baseline = this.#serialize();
|
|
208
221
|
this.#setDirty(false);
|
|
209
|
-
this.element
|
|
222
|
+
TRANSIENT.reset(this.element);
|
|
210
223
|
this.#listeners.add(this.element, "input", this.#onFieldChange);
|
|
211
224
|
this.#listeners.add(this.element, "change", this.#onFieldChange);
|
|
212
225
|
this.#listeners.add(this.element, "submit", this.#onSubmit);
|
|
@@ -123,6 +123,7 @@ var DismissibleController = class extends Controller {
|
|
|
123
123
|
get #root() {
|
|
124
124
|
return this.hasRootTarget ? this.rootTarget : this.element;
|
|
125
125
|
}
|
|
126
|
+
/** @stimeoRuntimeOnly `mode` decides how this one dismissal takes the element away. */
|
|
126
127
|
#performDismiss() {
|
|
127
128
|
const root = this.#root;
|
|
128
129
|
const mode = this.modeValue === "hide" ? "hide" : "remove";
|
|
@@ -365,6 +365,16 @@ var FocusTrap = class {
|
|
|
365
365
|
}
|
|
366
366
|
};
|
|
367
367
|
|
|
368
|
+
// src/utils/state_reason.ts
|
|
369
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
370
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
371
|
+
function stateReasonFor(event) {
|
|
372
|
+
if (!event) return "api";
|
|
373
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
374
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
375
|
+
return "user";
|
|
376
|
+
}
|
|
377
|
+
|
|
368
378
|
// src/utils/safe_timeout.ts
|
|
369
379
|
var TimerRegistry = class {
|
|
370
380
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -567,17 +577,20 @@ var DrawerController = class extends Controller {
|
|
|
567
577
|
open: { type: Boolean, default: false }
|
|
568
578
|
};
|
|
569
579
|
static actions = ["close", "closeOnBackdrop", "open"];
|
|
580
|
+
static events = ["close", "open"];
|
|
570
581
|
/** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
|
|
571
582
|
#activePanel = null;
|
|
572
583
|
/** Owns the modal side effects; Escape closes, focus falls back to the trigger. */
|
|
573
584
|
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
574
|
-
onEscape: () => this
|
|
585
|
+
onEscape: () => this.#close("escape"),
|
|
575
586
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
576
587
|
});
|
|
577
588
|
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
578
589
|
#transition = new TransitionCompletion();
|
|
579
590
|
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
580
591
|
#connected = false;
|
|
592
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
593
|
+
#reporting = false;
|
|
581
594
|
/**
|
|
582
595
|
* Reflects placement and establishes the initial open/closed state.
|
|
583
596
|
*
|
|
@@ -585,9 +598,9 @@ var DrawerController = class extends Controller {
|
|
|
585
598
|
* restored snapshot whose panel is already `data-state="open"` stays open
|
|
586
599
|
* rather than being re-derived from the declarative `open` Value (which would
|
|
587
600
|
* close a user-opened drawer). The `open` Value only seeds a genuinely fresh
|
|
588
|
-
* render. We normalize to a clean closed baseline first so
|
|
589
|
-
* full reveal + trap activation —
|
|
590
|
-
* disconnect and must be re-activated.
|
|
601
|
+
* render. We normalize to a clean closed baseline first so
|
|
602
|
+
* {@link DrawerController.open | open} runs its full reveal + trap activation —
|
|
603
|
+
* the `FocusTrap` is inactive after a disconnect and must be re-activated.
|
|
591
604
|
*/
|
|
592
605
|
connect() {
|
|
593
606
|
this.#connected = true;
|
|
@@ -595,11 +608,13 @@ var DrawerController = class extends Controller {
|
|
|
595
608
|
this.#reflectPlacement();
|
|
596
609
|
const shouldOpen = this.#isOpen || this.openValue;
|
|
597
610
|
this.#applyClosedState();
|
|
598
|
-
if (shouldOpen) this
|
|
611
|
+
if (shouldOpen) this.#open("api");
|
|
612
|
+
this.#reporting = true;
|
|
599
613
|
}
|
|
600
614
|
/** Reverts the modal side effects and pending hide if torn down while open. */
|
|
601
615
|
disconnect() {
|
|
602
616
|
this.#connected = false;
|
|
617
|
+
this.#reporting = false;
|
|
603
618
|
this.#transition.cancel();
|
|
604
619
|
this.#trap.deactivate({ restoreFocus: false });
|
|
605
620
|
this.#activePanel = null;
|
|
@@ -630,7 +645,11 @@ var DrawerController = class extends Controller {
|
|
|
630
645
|
this.#reflectPlacement();
|
|
631
646
|
}
|
|
632
647
|
/** Opens the drawer: reveals it, syncs `data-state`, traps focus. */
|
|
633
|
-
open() {
|
|
648
|
+
open(event) {
|
|
649
|
+
this.#open(stateReasonFor(event));
|
|
650
|
+
}
|
|
651
|
+
/** Reveals the drawer, syncs `data-state`, reports a move, then traps focus. */
|
|
652
|
+
#open(reason) {
|
|
634
653
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
635
654
|
this.#transition.cancel();
|
|
636
655
|
this.#activePanel = this.panelTarget;
|
|
@@ -639,31 +658,46 @@ var DrawerController = class extends Controller {
|
|
|
639
658
|
void this.panelTarget.offsetWidth;
|
|
640
659
|
this.#setState("open");
|
|
641
660
|
this.openValue = true;
|
|
661
|
+
if (this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
662
|
+
if (!this.#isOpen) return;
|
|
642
663
|
this.#trap.activate();
|
|
643
664
|
}
|
|
644
665
|
/**
|
|
645
666
|
* Closes the drawer: syncs `data-state` to start the exit transition, then
|
|
646
667
|
* defers both `hidden` *and* the modal teardown (scroll lock / background
|
|
647
|
-
* `inert` / focus restore) until the transition finishes
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* the exit animation.
|
|
668
|
+
* `inert` / focus restore) until the transition finishes. This keeps the
|
|
669
|
+
* background inert and focus trapped while the drawer is still visually on
|
|
670
|
+
* screen, preserving the modal contract during the exit animation.
|
|
651
671
|
*/
|
|
652
|
-
close() {
|
|
672
|
+
close(event) {
|
|
673
|
+
this.#close(stateReasonFor(event));
|
|
674
|
+
}
|
|
675
|
+
/** Closes only when the overlay itself (not its contents) is clicked. */
|
|
676
|
+
closeOnBackdrop(event) {
|
|
677
|
+
if (this.hasOverlayTarget && event.target === this.overlayTarget) this.#close("outside");
|
|
678
|
+
}
|
|
679
|
+
/** Starts the exit transition, reports a move, then defers `hidden` and teardown. */
|
|
680
|
+
#close(reason) {
|
|
653
681
|
if (!this.hasPanelTarget || !this.#isOpen) return;
|
|
654
682
|
this.openValue = false;
|
|
655
683
|
this.#setState("closed");
|
|
684
|
+
if (this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
685
|
+
if (this.#isOpen) return;
|
|
656
686
|
this.#hideAfterTransition();
|
|
657
687
|
}
|
|
658
|
-
/**
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Writes `data-placement` from the current `placement` value.
|
|
690
|
+
*
|
|
691
|
+
* @stimeoRenderRoot
|
|
692
|
+
*/
|
|
663
693
|
#reflectPlacement() {
|
|
664
694
|
if (this.hasPanelTarget) this.panelTarget.setAttribute("data-placement", this.#placement);
|
|
665
695
|
}
|
|
666
|
-
/**
|
|
696
|
+
/**
|
|
697
|
+
* Reconciles a replacement panel and companion overlay from its explicit DOM state.
|
|
698
|
+
*
|
|
699
|
+
* @stimeoRenderRoot
|
|
700
|
+
*/
|
|
667
701
|
#adoptPanel(panel) {
|
|
668
702
|
this.#transition.cancel();
|
|
669
703
|
const trapWasActive = this.#trap.active;
|
|
@@ -719,8 +753,9 @@ var DrawerController = class extends Controller {
|
|
|
719
753
|
* Runs once the close transition has finished: applies `hidden` to the panel
|
|
720
754
|
* and overlay, then reverts the modal side effects (scroll lock, background
|
|
721
755
|
* `inert`, keydown listener) and restores focus to the opener. Deferring the
|
|
722
|
-
* `FocusTrap` teardown to here — rather than at
|
|
723
|
-
*
|
|
756
|
+
* `FocusTrap` teardown to here — rather than at
|
|
757
|
+
* {@link DrawerController.close | close} time — keeps the background unreachable
|
|
758
|
+
* and focus trapped for the whole exit animation.
|
|
724
759
|
*/
|
|
725
760
|
#applyHidden(panel) {
|
|
726
761
|
panel.hidden = true;
|
|
@@ -109,40 +109,64 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// src/utils/state_reason.ts
|
|
113
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
114
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
115
|
+
function stateReasonFor(event) {
|
|
116
|
+
if (!event) return "api";
|
|
117
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
118
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
119
|
+
return "user";
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
// src/controllers/dropdown_controller.ts
|
|
113
123
|
var DropdownController = class extends Controller {
|
|
114
124
|
static targets = ["trigger", "menu"];
|
|
115
125
|
static actions = ["close", "open", "toggle"];
|
|
126
|
+
static events = ["close", "open"];
|
|
116
127
|
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
117
128
|
#escapeLayer = new EscapeLayer();
|
|
129
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
130
|
+
#reporting = false;
|
|
118
131
|
/** Closes the menu when a click lands outside the controller's element. */
|
|
119
132
|
#onOutsideClick = (event) => {
|
|
120
133
|
if (!this.element.contains(event.target)) {
|
|
121
|
-
this
|
|
134
|
+
this.#close("outside");
|
|
122
135
|
}
|
|
123
136
|
};
|
|
124
137
|
/** Starts in the closed state and registers outside-click handling. */
|
|
125
138
|
connect() {
|
|
126
139
|
this.#associateTriggerWithMenu();
|
|
127
|
-
this
|
|
140
|
+
this.#close("api");
|
|
128
141
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
142
|
+
this.#reporting = true;
|
|
129
143
|
}
|
|
130
144
|
/** Removes the listeners registered in {@link connect}. */
|
|
131
145
|
disconnect() {
|
|
146
|
+
this.#reporting = false;
|
|
132
147
|
this.#escapeLayer.deactivate();
|
|
133
148
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
134
149
|
}
|
|
135
150
|
/** Toggles the menu between open and closed. Bound via `data-action`. */
|
|
136
|
-
toggle() {
|
|
151
|
+
toggle(event) {
|
|
137
152
|
if (this.#isOpen) {
|
|
138
|
-
this.close();
|
|
153
|
+
this.close(event);
|
|
139
154
|
} else {
|
|
140
|
-
this.open();
|
|
155
|
+
this.open(event);
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
/** Reveals the menu and reflects the open state on the trigger. */
|
|
144
|
-
open() {
|
|
159
|
+
open(event) {
|
|
160
|
+
this.#open(stateReasonFor(event));
|
|
161
|
+
}
|
|
162
|
+
/** Hides the menu and reflects the closed state on the trigger. */
|
|
163
|
+
close(event) {
|
|
164
|
+
this.#close(stateReasonFor(event));
|
|
165
|
+
}
|
|
166
|
+
/** Reveals the menu, reflects the open state, and reports a move. */
|
|
167
|
+
#open(reason) {
|
|
145
168
|
if (!this.hasMenuTarget) return;
|
|
169
|
+
const was = this.#isOpen;
|
|
146
170
|
this.#escapeLayer.activate(document, {
|
|
147
171
|
onDismiss: () => this.#closeAndRestore(),
|
|
148
172
|
claims: claimsWhileFocusWithin(this.element)
|
|
@@ -151,19 +175,22 @@ var DropdownController = class extends Controller {
|
|
|
151
175
|
if (this.hasTriggerTarget) {
|
|
152
176
|
this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
153
177
|
}
|
|
178
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
154
179
|
}
|
|
155
|
-
/** Hides the menu
|
|
156
|
-
close() {
|
|
180
|
+
/** Hides the menu, reflects the closed state, and reports a move. */
|
|
181
|
+
#close(reason) {
|
|
182
|
+
const was = this.#isOpen;
|
|
157
183
|
this.#escapeLayer.deactivate();
|
|
158
184
|
if (!this.hasMenuTarget) return;
|
|
159
185
|
this.menuTarget.hidden = true;
|
|
160
186
|
if (this.hasTriggerTarget) {
|
|
161
187
|
this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
162
188
|
}
|
|
189
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
163
190
|
}
|
|
164
191
|
/** Closes and restores focus to the trigger (the keyboard-dismissal path). */
|
|
165
192
|
#closeAndRestore() {
|
|
166
|
-
this
|
|
193
|
+
this.#close("escape");
|
|
167
194
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
168
195
|
}
|
|
169
196
|
/** Whether the menu is currently visible. */
|
|
@@ -3,11 +3,19 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
3
3
|
// src/controllers/editable_controller.ts
|
|
4
4
|
|
|
5
5
|
// src/utils/composition_tracker.ts
|
|
6
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"insertCompositionText",
|
|
8
|
+
"insertFromComposition",
|
|
9
|
+
"deleteCompositionText",
|
|
10
|
+
"deleteByComposition"
|
|
11
|
+
]);
|
|
6
12
|
var CompositionTracker = class {
|
|
7
13
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
8
14
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
9
15
|
#onStart;
|
|
10
16
|
#onEnd;
|
|
17
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
18
|
+
#confirmedTarget = null;
|
|
11
19
|
constructor(options = {}) {
|
|
12
20
|
this.#onStart = options.onStart;
|
|
13
21
|
this.#onEnd = options.onEnd;
|
|
@@ -17,6 +25,7 @@ var CompositionTracker = class {
|
|
|
17
25
|
if (this.#observedTargets.has(target)) return;
|
|
18
26
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
19
27
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
28
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
20
29
|
this.#observedTargets.add(target);
|
|
21
30
|
}
|
|
22
31
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -24,29 +33,52 @@ var CompositionTracker = class {
|
|
|
24
33
|
if (!this.#observedTargets.delete(target)) return;
|
|
25
34
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
26
35
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
36
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
27
37
|
this.#activeTargets.delete(target);
|
|
38
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
28
39
|
}
|
|
29
40
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
30
41
|
disconnect() {
|
|
31
42
|
for (const target of this.#observedTargets) {
|
|
32
43
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
33
44
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
45
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
34
46
|
}
|
|
35
47
|
this.#observedTargets.clear();
|
|
36
48
|
this.#activeTargets.clear();
|
|
49
|
+
this.#confirmedTarget = null;
|
|
37
50
|
}
|
|
38
51
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
39
52
|
isComposing(event) {
|
|
40
53
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
41
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
57
|
+
*
|
|
58
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
59
|
+
* once and a consumer asks once per `input`.
|
|
60
|
+
*/
|
|
61
|
+
consumesConfirmedInput(event) {
|
|
62
|
+
const confirmed = this.#confirmedTarget;
|
|
63
|
+
this.#confirmedTarget = null;
|
|
64
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
65
|
+
const inputType = event.inputType;
|
|
66
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
67
|
+
}
|
|
42
68
|
#handleStart = (event) => {
|
|
69
|
+
this.#confirmedTarget = null;
|
|
43
70
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
44
71
|
this.#onStart?.(event);
|
|
45
72
|
};
|
|
46
73
|
#handleEnd = (event) => {
|
|
47
74
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
75
|
+
this.#confirmedTarget = event.target;
|
|
48
76
|
this.#onEnd?.(event);
|
|
49
77
|
};
|
|
78
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
79
|
+
#handleKeydown = () => {
|
|
80
|
+
this.#confirmedTarget = null;
|
|
81
|
+
};
|
|
50
82
|
};
|
|
51
83
|
|
|
52
84
|
// src/controllers/editable_controller.ts
|
|
@@ -79,6 +111,8 @@ var EditableController = class extends Controller {
|
|
|
79
111
|
* "saves wherever focus moved" true only for focus that leaves the input
|
|
80
112
|
* itself, and tabbing straight past an inner button would strand the editor
|
|
81
113
|
* open.
|
|
114
|
+
*
|
|
115
|
+
* @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.
|
|
82
116
|
*/
|
|
83
117
|
#onFocusOut = (event) => {
|
|
84
118
|
const from = event.target;
|