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
|
@@ -29,6 +29,29 @@ function ownerOf(candidates, node) {
|
|
|
29
29
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/utils/field_mirror.ts
|
|
33
|
+
function writeFields(container, values, { name, form = "" }) {
|
|
34
|
+
const current = [...container.children];
|
|
35
|
+
const submits = current.length === values.length && current.every(
|
|
36
|
+
(child, index) => child instanceof HTMLInputElement && child.type === "hidden" && child.value === values[index] && child.name === name && (child.getAttribute("form") ?? "") === form
|
|
37
|
+
);
|
|
38
|
+
if (submits) return false;
|
|
39
|
+
container.replaceChildren(
|
|
40
|
+
...values.map((value) => {
|
|
41
|
+
const input = document.createElement("input");
|
|
42
|
+
input.type = "hidden";
|
|
43
|
+
input.name = name;
|
|
44
|
+
input.value = value;
|
|
45
|
+
if (form !== "") input.setAttribute("form", form);
|
|
46
|
+
return input;
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function commitField(target) {
|
|
52
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
// src/utils/focus_candidate.ts
|
|
33
56
|
function inheritsFieldsetDisabled(control) {
|
|
34
57
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -57,6 +80,39 @@ function isInteractiveHost(element) {
|
|
|
57
80
|
return false;
|
|
58
81
|
}
|
|
59
82
|
|
|
83
|
+
// src/utils/microtask_coalescer.ts
|
|
84
|
+
var MicrotaskCoalescer = class {
|
|
85
|
+
#run;
|
|
86
|
+
#queued = false;
|
|
87
|
+
#active = false;
|
|
88
|
+
#generation = 0;
|
|
89
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
90
|
+
constructor(run) {
|
|
91
|
+
this.#run = run;
|
|
92
|
+
}
|
|
93
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
94
|
+
activate() {
|
|
95
|
+
this.#active = true;
|
|
96
|
+
}
|
|
97
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
98
|
+
cancel() {
|
|
99
|
+
this.#active = false;
|
|
100
|
+
this.#queued = false;
|
|
101
|
+
this.#generation += 1;
|
|
102
|
+
}
|
|
103
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
104
|
+
schedule() {
|
|
105
|
+
if (!this.#active || this.#queued) return;
|
|
106
|
+
this.#queued = true;
|
|
107
|
+
const generation = this.#generation;
|
|
108
|
+
queueMicrotask(() => {
|
|
109
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
110
|
+
this.#queued = false;
|
|
111
|
+
this.#run();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
60
116
|
// src/utils/roving_tabindex.ts
|
|
61
117
|
var RovingTabindex = class {
|
|
62
118
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -100,6 +156,7 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
100
156
|
// src/controllers/toggle_group_controller.ts
|
|
101
157
|
var ITEM_ATTRIBUTES = [
|
|
102
158
|
"aria-pressed",
|
|
159
|
+
"data-value",
|
|
103
160
|
"disabled",
|
|
104
161
|
"hidden",
|
|
105
162
|
"type",
|
|
@@ -109,24 +166,37 @@ var ITEM_ATTRIBUTES = [
|
|
|
109
166
|
];
|
|
110
167
|
var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
111
168
|
var ToggleGroupController = class extends Controller {
|
|
112
|
-
static targets = ["item"];
|
|
169
|
+
static targets = ["item", "fields"];
|
|
113
170
|
static values = {
|
|
114
|
-
mode: { type: String, default: "multiple" }
|
|
171
|
+
mode: { type: String, default: "multiple" },
|
|
172
|
+
name: { type: String, default: "values[]" },
|
|
173
|
+
form: { type: String, default: "" }
|
|
115
174
|
};
|
|
116
175
|
static actions = ["onKeydown", "toggle"];
|
|
117
|
-
static events = ["change"];
|
|
176
|
+
static events = ["change", "reconcile"];
|
|
118
177
|
#roving = new RovingTabindex(() => this.#managedTargets);
|
|
119
178
|
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
120
179
|
#managedItems = /* @__PURE__ */ new Set();
|
|
121
180
|
#originalTabindex = /* @__PURE__ */ new Map();
|
|
122
181
|
#ownedPressed = /* @__PURE__ */ new Set();
|
|
123
182
|
#internalPressedValues = /* @__PURE__ */ new Map();
|
|
183
|
+
/** One pass per batch of item, Value, and retained-element changes. */
|
|
184
|
+
#pass = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
124
185
|
#observer = null;
|
|
125
186
|
#connected = false;
|
|
187
|
+
/** The pressed set last published: read on connect, then committed or reported. */
|
|
188
|
+
#committedValues = [];
|
|
189
|
+
/**
|
|
190
|
+
* How many presses the user has committed, so a press whose reports are still
|
|
191
|
+
* going out can tell that a listener has committed a newer one meanwhile.
|
|
192
|
+
*/
|
|
193
|
+
#presses = 0;
|
|
126
194
|
/**
|
|
127
195
|
* Normalizes state and establishes one roving entry point. A single authored
|
|
128
196
|
* `tabindex="0"` survives reconnect; otherwise the first pressed, navigable
|
|
129
|
-
* item wins, falling back to the first navigable item.
|
|
197
|
+
* item wins, falling back to the first navigable item. The pressed set it
|
|
198
|
+
* settles on is the one a later move is measured from; connecting reports
|
|
199
|
+
* nothing.
|
|
130
200
|
*/
|
|
131
201
|
connect() {
|
|
132
202
|
const currentItems = new Set(this.itemTargets);
|
|
@@ -140,6 +210,8 @@ var ToggleGroupController = class extends Controller {
|
|
|
140
210
|
for (const item of this.itemTargets) this.#reconcileHost(item, false);
|
|
141
211
|
this.#normalizeSelection();
|
|
142
212
|
this.#ensureTabStop(preferred);
|
|
213
|
+
this.#mirrorFields(false);
|
|
214
|
+
this.#committedValues = this.#pressedValues();
|
|
143
215
|
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
144
216
|
this.element.addEventListener("keydown", this.#onKeydownCapture, true);
|
|
145
217
|
this.element.addEventListener("click", this.#onClick);
|
|
@@ -147,11 +219,16 @@ var ToggleGroupController = class extends Controller {
|
|
|
147
219
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
148
220
|
this.#connected = true;
|
|
149
221
|
this.#internalPressedValues.clear();
|
|
222
|
+
this.#pass.activate();
|
|
150
223
|
this.#observeMutations();
|
|
151
224
|
}
|
|
152
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Releases every listener, the observer, and a pass still queued, while
|
|
227
|
+
* retaining DOM state for Turbo cache/reconnect.
|
|
228
|
+
*/
|
|
153
229
|
disconnect() {
|
|
154
230
|
this.#connected = false;
|
|
231
|
+
this.#pass.cancel();
|
|
155
232
|
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
156
233
|
this.element.removeEventListener("keydown", this.#onKeydownCapture, true);
|
|
157
234
|
this.element.removeEventListener("click", this.#onClick);
|
|
@@ -161,25 +238,29 @@ var ToggleGroupController = class extends Controller {
|
|
|
161
238
|
this.#observer = null;
|
|
162
239
|
this.#internalPressedValues.clear();
|
|
163
240
|
}
|
|
164
|
-
/** Drops a newly connected item from the Tab sequence before
|
|
241
|
+
/** Drops a newly connected item from the Tab sequence before the batch is reconciled. */
|
|
165
242
|
itemTargetConnected(item) {
|
|
166
243
|
if (!this.#connected) return;
|
|
167
244
|
this.#reconcileHost(item, true);
|
|
168
|
-
this.#
|
|
169
|
-
this.#ensureTabStop();
|
|
245
|
+
this.#pass.schedule();
|
|
170
246
|
}
|
|
171
|
-
/** Restores attributes owned only while an element is an item
|
|
247
|
+
/** Restores attributes owned only while an element is an item, then reconciles the batch. */
|
|
172
248
|
itemTargetDisconnected(item) {
|
|
173
249
|
if (!this.#connected) return;
|
|
174
250
|
this.#releaseItem(item);
|
|
175
|
-
this.#
|
|
176
|
-
this.#ensureTabStop();
|
|
251
|
+
this.#pass.schedule();
|
|
177
252
|
}
|
|
178
253
|
/** Reconciles the single-selection invariant when the Stimulus Value changes. */
|
|
179
254
|
modeValueChanged() {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
255
|
+
this.#pass.schedule();
|
|
256
|
+
}
|
|
257
|
+
/** Rebuilds the submitted fields when the public name changes at runtime. */
|
|
258
|
+
nameValueChanged() {
|
|
259
|
+
this.#pass.schedule();
|
|
260
|
+
}
|
|
261
|
+
/** Repoints the submitted fields when the owning form changes at runtime. */
|
|
262
|
+
formValueChanged() {
|
|
263
|
+
this.#pass.schedule();
|
|
183
264
|
}
|
|
184
265
|
/**
|
|
185
266
|
* Toggles the action's item. Per-item action wiring is optional because click
|
|
@@ -272,7 +353,17 @@ var ToggleGroupController = class extends Controller {
|
|
|
272
353
|
event.preventDefault();
|
|
273
354
|
if (destination) this.#setActive(destination, true);
|
|
274
355
|
}
|
|
275
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Applies one user toggle and dispatches the documented change detail. The set
|
|
358
|
+
* it leaves is taken as published before either report goes out, so a
|
|
359
|
+
* listener that presses again or writes `aria-pressed` is measured from it. A
|
|
360
|
+
* listener of the native `change` that presses again has had that newer press
|
|
361
|
+
* reported, so this press's `change` would describe a set that is gone and is
|
|
362
|
+
* not dispatched.
|
|
363
|
+
*
|
|
364
|
+
* @stimeoRuntimeOnly `mode` is the rule this one press follows; the lasting single-press
|
|
365
|
+
* normalisation is a render root of its own.
|
|
366
|
+
*/
|
|
276
367
|
#toggleItem(item) {
|
|
277
368
|
if (!this.#isSupportedHost(item) || this.#isActivationDisabled(item)) return;
|
|
278
369
|
if (!this.#managedItems.has(item)) this.#reconcileHost(item, true);
|
|
@@ -284,15 +375,58 @@ var ToggleGroupController = class extends Controller {
|
|
|
284
375
|
this.#setPressed(item, willPress);
|
|
285
376
|
}
|
|
286
377
|
this.#setActive(item);
|
|
378
|
+
const values = this.#pressedValues();
|
|
379
|
+
this.#committedValues = values;
|
|
380
|
+
this.#presses += 1;
|
|
381
|
+
const press = this.#presses;
|
|
382
|
+
this.#mirrorFields(true);
|
|
383
|
+
if (press !== this.#presses) return;
|
|
287
384
|
this.dispatch("change", {
|
|
288
|
-
detail: { value: this.#itemValue(item), pressed: willPress, values:
|
|
385
|
+
detail: { value: this.#itemValue(item), pressed: willPress, values: [...values] }
|
|
289
386
|
});
|
|
290
387
|
}
|
|
291
|
-
/**
|
|
292
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Mirrors the pressed values into the optional fields container.
|
|
390
|
+
*
|
|
391
|
+
* @stimeoRenderRoot
|
|
392
|
+
*/
|
|
393
|
+
#mirrorFields(notify) {
|
|
394
|
+
if (!this.hasFieldsTarget) return;
|
|
395
|
+
const options = { name: this.nameValue, form: this.formValue };
|
|
396
|
+
if (writeFields(this.fieldsTarget, this.#pressedValues(), options) && notify) {
|
|
397
|
+
commitField(this.fieldsTarget);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Reconciles supported hosts, pressed state, the roving invariant, and the
|
|
402
|
+
* fields after the page changed the group, then reports a pressed set that
|
|
403
|
+
* moved. Records still queued are read first, so an author's write is not lost
|
|
404
|
+
* when observation is suspended for this pass's own writes; observation resumes
|
|
405
|
+
* before the report, so an edit a listener makes is seen by the next pass.
|
|
406
|
+
*/
|
|
407
|
+
#reconcileDom() {
|
|
408
|
+
const observer = this.#observer;
|
|
409
|
+
if (observer) {
|
|
410
|
+
this.#releaseAuthoredPressed(observer.takeRecords());
|
|
411
|
+
observer.disconnect();
|
|
412
|
+
}
|
|
293
413
|
for (const item of this.itemTargets) this.#reconcileHost(item, true);
|
|
294
414
|
this.#normalizeSelection();
|
|
295
415
|
this.#ensureTabStop();
|
|
416
|
+
this.#mirrorFields(false);
|
|
417
|
+
this.#internalPressedValues.clear();
|
|
418
|
+
if (observer) this.#observeWith(observer);
|
|
419
|
+
this.#reportMove();
|
|
420
|
+
}
|
|
421
|
+
/** Reports a pressed set that moved since the one this group last published. */
|
|
422
|
+
#reportMove() {
|
|
423
|
+
const values = this.#pressedValues();
|
|
424
|
+
const previous = this.#committedValues;
|
|
425
|
+
if (values.length === previous.length && values.every((value, i) => value === previous[i])) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this.#committedValues = values;
|
|
429
|
+
this.dispatch("reconcile", { detail: { values: [...values] } });
|
|
296
430
|
}
|
|
297
431
|
/** Begins or ends ownership according to the item's current host semantics. */
|
|
298
432
|
#reconcileHost(item, dropFromTabSequence) {
|
|
@@ -328,7 +462,11 @@ var ToggleGroupController = class extends Controller {
|
|
|
328
462
|
item.setAttribute("aria-pressed", "false");
|
|
329
463
|
}
|
|
330
464
|
}
|
|
331
|
-
/**
|
|
465
|
+
/**
|
|
466
|
+
* Makes the first DOM-ordered pressed item the sole pressed item in single mode.
|
|
467
|
+
*
|
|
468
|
+
* @stimeoRenderRoot
|
|
469
|
+
*/
|
|
332
470
|
#normalizeSelection() {
|
|
333
471
|
for (const item of this.#managedTargets) this.#normalizePressed(item);
|
|
334
472
|
if (this.modeValue !== "single") return;
|
|
@@ -377,7 +515,7 @@ var ToggleGroupController = class extends Controller {
|
|
|
377
515
|
}
|
|
378
516
|
/** Whether the closest Toggle Group scope around a target is this instance. */
|
|
379
517
|
#ownsEventTarget(target) {
|
|
380
|
-
return target instanceof Element && target.closest(
|
|
518
|
+
return target instanceof Element && target.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
381
519
|
}
|
|
382
520
|
/** Hosts whose activation model can be owned without conflicting native behavior. */
|
|
383
521
|
#isSupportedHost(item) {
|
|
@@ -425,9 +563,7 @@ var ToggleGroupController = class extends Controller {
|
|
|
425
563
|
#observeMutations() {
|
|
426
564
|
const observer = new MutationObserver((records) => {
|
|
427
565
|
this.#releaseAuthoredPressed(records);
|
|
428
|
-
|
|
429
|
-
this.#reconcile();
|
|
430
|
-
this.#observeWith(observer);
|
|
566
|
+
this.#pass.schedule();
|
|
431
567
|
});
|
|
432
568
|
this.#observer = observer;
|
|
433
569
|
this.#observeWith(observer);
|
|
@@ -14,11 +14,19 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// src/utils/composition_tracker.ts
|
|
17
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
"insertCompositionText",
|
|
19
|
+
"insertFromComposition",
|
|
20
|
+
"deleteCompositionText",
|
|
21
|
+
"deleteByComposition"
|
|
22
|
+
]);
|
|
17
23
|
var CompositionTracker = class {
|
|
18
24
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
19
25
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
20
26
|
#onStart;
|
|
21
27
|
#onEnd;
|
|
28
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
29
|
+
#confirmedTarget = null;
|
|
22
30
|
constructor(options = {}) {
|
|
23
31
|
this.#onStart = options.onStart;
|
|
24
32
|
this.#onEnd = options.onEnd;
|
|
@@ -28,6 +36,7 @@ var CompositionTracker = class {
|
|
|
28
36
|
if (this.#observedTargets.has(target)) return;
|
|
29
37
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
30
38
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
39
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
31
40
|
this.#observedTargets.add(target);
|
|
32
41
|
}
|
|
33
42
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -35,29 +44,52 @@ var CompositionTracker = class {
|
|
|
35
44
|
if (!this.#observedTargets.delete(target)) return;
|
|
36
45
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
37
46
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
47
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
38
48
|
this.#activeTargets.delete(target);
|
|
49
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
39
50
|
}
|
|
40
51
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
41
52
|
disconnect() {
|
|
42
53
|
for (const target of this.#observedTargets) {
|
|
43
54
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
44
55
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
56
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
45
57
|
}
|
|
46
58
|
this.#observedTargets.clear();
|
|
47
59
|
this.#activeTargets.clear();
|
|
60
|
+
this.#confirmedTarget = null;
|
|
48
61
|
}
|
|
49
62
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
50
63
|
isComposing(event) {
|
|
51
64
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
52
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
68
|
+
*
|
|
69
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
70
|
+
* once and a consumer asks once per `input`.
|
|
71
|
+
*/
|
|
72
|
+
consumesConfirmedInput(event) {
|
|
73
|
+
const confirmed = this.#confirmedTarget;
|
|
74
|
+
this.#confirmedTarget = null;
|
|
75
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
76
|
+
const inputType = event.inputType;
|
|
77
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
78
|
+
}
|
|
53
79
|
#handleStart = (event) => {
|
|
80
|
+
this.#confirmedTarget = null;
|
|
54
81
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
55
82
|
this.#onStart?.(event);
|
|
56
83
|
};
|
|
57
84
|
#handleEnd = (event) => {
|
|
58
85
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
86
|
+
this.#confirmedTarget = event.target;
|
|
59
87
|
this.#onEnd?.(event);
|
|
60
88
|
};
|
|
89
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
90
|
+
#handleKeydown = () => {
|
|
91
|
+
this.#confirmedTarget = null;
|
|
92
|
+
};
|
|
61
93
|
};
|
|
62
94
|
|
|
63
95
|
// src/utils/event_owner.ts
|
|
@@ -164,6 +164,16 @@ function isScrollable(element) {
|
|
|
164
164
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/utils/state_reason.ts
|
|
168
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
169
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
170
|
+
function stateReasonFor(event) {
|
|
171
|
+
if (!event) return "api";
|
|
172
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
173
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
174
|
+
return "user";
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
// src/controllers/tooltip_controller.ts
|
|
168
178
|
var TooltipController = class extends Controller {
|
|
169
179
|
static targets = ["trigger", "content"];
|
|
@@ -173,6 +183,7 @@ var TooltipController = class extends Controller {
|
|
|
173
183
|
closeOnScroll: { type: Boolean, default: false }
|
|
174
184
|
};
|
|
175
185
|
static actions = ["hide", "show"];
|
|
186
|
+
static events = ["close", "open"];
|
|
176
187
|
/** Registry whose pending timers are cancelled individually with their guard ids. */
|
|
177
188
|
#timers = new SafeTimeout();
|
|
178
189
|
/** Escape-stack membership while shown; the shared resolver dismisses via it. */
|
|
@@ -186,15 +197,19 @@ var TooltipController = class extends Controller {
|
|
|
186
197
|
#pointerActive = false;
|
|
187
198
|
/** Cleanup for the dismiss-on-scroll listeners while shown, or `null`. */
|
|
188
199
|
#stopScrollDismiss = null;
|
|
200
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
201
|
+
#reporting = false;
|
|
189
202
|
/** Starts hidden with no stale timer or interaction state from a prior connection. */
|
|
190
203
|
connect() {
|
|
191
204
|
this.#cancelShow();
|
|
192
205
|
this.#cancelHide();
|
|
193
206
|
this.#resetInteractionState();
|
|
194
|
-
this.#conceal();
|
|
207
|
+
this.#conceal("api");
|
|
208
|
+
this.#reporting = true;
|
|
195
209
|
}
|
|
196
210
|
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
197
211
|
disconnect() {
|
|
212
|
+
this.#reporting = false;
|
|
198
213
|
this.#cancelShow();
|
|
199
214
|
this.#cancelHide();
|
|
200
215
|
this.#resetInteractionState();
|
|
@@ -204,20 +219,22 @@ var TooltipController = class extends Controller {
|
|
|
204
219
|
}
|
|
205
220
|
/** Shows after `showDelay`, recording the focus/pointer reason supplied by an action event. */
|
|
206
221
|
show(event) {
|
|
222
|
+
const reason = stateReasonFor(event);
|
|
207
223
|
this.#activateInteraction(event);
|
|
208
224
|
this.#cancelHide();
|
|
209
225
|
if (this.#isVisible || this.#pendingShow !== null) return;
|
|
210
226
|
if (this.showDelayValue <= 0) {
|
|
211
|
-
this.#reveal();
|
|
227
|
+
this.#reveal(reason);
|
|
212
228
|
return;
|
|
213
229
|
}
|
|
214
230
|
this.#pendingShow = this.#timers.set(() => {
|
|
215
231
|
this.#pendingShow = null;
|
|
216
|
-
this.#reveal();
|
|
232
|
+
this.#reveal(reason);
|
|
217
233
|
}, this.showDelayValue);
|
|
218
234
|
}
|
|
219
235
|
/** Hides after `hideDelay` once no focus/pointer reason remains; eventless calls are explicit. */
|
|
220
236
|
hide(event) {
|
|
237
|
+
const reason = stateReasonFor(event);
|
|
221
238
|
const interactionEnded = this.#deactivateInteraction(event);
|
|
222
239
|
if (interactionEnded && this.#hasActiveInteraction) {
|
|
223
240
|
this.#cancelHide();
|
|
@@ -226,38 +243,47 @@ var TooltipController = class extends Controller {
|
|
|
226
243
|
this.#cancelShow();
|
|
227
244
|
if (!this.#isVisible || this.#pendingHide !== null) return;
|
|
228
245
|
if (this.hideDelayValue <= 0) {
|
|
229
|
-
this.#conceal();
|
|
246
|
+
this.#conceal(reason);
|
|
230
247
|
return;
|
|
231
248
|
}
|
|
232
249
|
this.#pendingHide = this.#timers.set(() => {
|
|
233
250
|
this.#pendingHide = null;
|
|
234
|
-
this.#conceal();
|
|
251
|
+
this.#conceal(reason);
|
|
235
252
|
}, this.hideDelayValue);
|
|
236
253
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Reveals the content, reports a move, and joins the Escape stack / scroll watcher.
|
|
256
|
+
*
|
|
257
|
+
* @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what
|
|
258
|
+
* is shown does not depend on it.
|
|
259
|
+
*/
|
|
260
|
+
#reveal(reason) {
|
|
239
261
|
if (!this.hasContentTarget) return;
|
|
262
|
+
const was = this.#isVisible;
|
|
240
263
|
this.contentTarget.hidden = false;
|
|
241
264
|
this.contentTarget.setAttribute("data-state", "open");
|
|
242
|
-
this.#
|
|
265
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
266
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
243
267
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
244
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
268
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
|
-
/** Hides the content and leaves the Escape stack /
|
|
248
|
-
#conceal() {
|
|
271
|
+
/** Hides the content, reports a move, and leaves the Escape stack / scroll watcher. */
|
|
272
|
+
#conceal(reason) {
|
|
273
|
+
const was = this.#isVisible;
|
|
249
274
|
this.#escapeLayer.deactivate();
|
|
250
275
|
this.#stopScrollDismiss?.();
|
|
251
276
|
this.#stopScrollDismiss = null;
|
|
252
277
|
if (!this.hasContentTarget) return;
|
|
253
278
|
this.contentTarget.hidden = true;
|
|
254
279
|
this.contentTarget.setAttribute("data-state", "closed");
|
|
280
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
255
281
|
}
|
|
256
282
|
/** Cancels pending timers and conceals immediately (shared Escape / scroll path). */
|
|
257
|
-
#dismiss() {
|
|
283
|
+
#dismiss(reason) {
|
|
258
284
|
this.#cancelShow();
|
|
259
285
|
this.#cancelHide();
|
|
260
|
-
this.#conceal();
|
|
286
|
+
this.#conceal(reason);
|
|
261
287
|
}
|
|
262
288
|
/** Records the modality whose enter/focus event requires the tooltip to stay visible. */
|
|
263
289
|
#activateInteraction(event) {
|
|
@@ -277,6 +277,10 @@ var TransitionController = class extends Controller {
|
|
|
277
277
|
if (state === "entered" || state === "entering") this.leave();
|
|
278
278
|
else this.enter();
|
|
279
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* @stimeoRuntimeOnly The class lists and `timeout` shape this one transition; `#finish` strips
|
|
282
|
+
* the classes, so none of them stays at rest.
|
|
283
|
+
*/
|
|
280
284
|
#run(kind) {
|
|
281
285
|
this.#cancel();
|
|
282
286
|
const isEnter = kind === "enter";
|