stimeo-ui 0.1.0.pre.beta.3 → 0.2.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 +90 -0
- data/README.md +6 -6
- data/dist/controllers/auto_submit_controller.js +56 -21
- data/dist/controllers/character_counter_controller.js +52 -15
- data/dist/controllers/combobox_controller.js +64 -3
- data/dist/controllers/command_palette_controller.js +243 -39
- data/dist/controllers/confirm_controller.js +94 -8
- data/dist/controllers/context_menu_controller.js +191 -13
- data/dist/controllers/countdown_controller.js +2 -2
- data/dist/controllers/dialog_controller.js +94 -8
- data/dist/controllers/dropdown_controller.js +139 -16
- data/dist/controllers/focus_controller.js +94 -8
- data/dist/controllers/form_validation_controller.js +2 -2
- data/dist/controllers/hover_card_controller.js +103 -27
- data/dist/controllers/listbox_controller.js +1 -0
- data/dist/controllers/menu_controller.js +181 -8
- data/dist/controllers/otp_controller.js +62 -22
- data/dist/controllers/pointer_drag_controller.js +3 -3
- data/dist/controllers/popover_controller.js +118 -27
- data/dist/controllers/toast_controller.js +108 -29
- data/dist/controllers/tooltip_controller.js +148 -42
- data/dist/index.js +833 -348
- data/lib/stimeo/ui/version.rb +5 -4
- metadata +2 -2
data/dist/index.js
CHANGED
|
@@ -71,6 +71,96 @@ var AccordionController = class extends Controller {
|
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
// src/utils/escape_layer.ts
|
|
75
|
+
function claimsWhileFocusWithin(element) {
|
|
76
|
+
return () => {
|
|
77
|
+
const active = element.ownerDocument.activeElement;
|
|
78
|
+
return active === null || active === element.ownerDocument.body || element.contains(active);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
var EscapeLayer = class _EscapeLayer {
|
|
82
|
+
static #registries = /* @__PURE__ */ new WeakMap();
|
|
83
|
+
#ownerDocument = null;
|
|
84
|
+
/** Dismissal callback while active; `null` when inactive. */
|
|
85
|
+
#onDismiss = null;
|
|
86
|
+
/** Live predicate deciding whether the layer claims a press; `null` = always. */
|
|
87
|
+
#claims = null;
|
|
88
|
+
/**
|
|
89
|
+
* Activates this layer at the top of its document's Escape stack, installing
|
|
90
|
+
* the document's shared resolver listener if this is its first layer.
|
|
91
|
+
* Re-activating an already-active layer moves it to the top.
|
|
92
|
+
*/
|
|
93
|
+
activate(ownerDocument = document, options) {
|
|
94
|
+
this.deactivate();
|
|
95
|
+
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
96
|
+
if (!registry) {
|
|
97
|
+
registry = _EscapeLayer.#createRegistry();
|
|
98
|
+
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
99
|
+
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
100
|
+
}
|
|
101
|
+
registry.stack.push(this);
|
|
102
|
+
this.#ownerDocument = ownerDocument;
|
|
103
|
+
this.#onDismiss = options.onDismiss;
|
|
104
|
+
this.#claims = options.claims ?? null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Removes this layer from its document's Escape stack, uninstalling the
|
|
108
|
+
* shared listener when the stack empties. Safe to call when inactive.
|
|
109
|
+
*/
|
|
110
|
+
deactivate() {
|
|
111
|
+
const ownerDocument = this.#ownerDocument;
|
|
112
|
+
if (!ownerDocument) return;
|
|
113
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
114
|
+
if (registry) {
|
|
115
|
+
const index = registry.stack.lastIndexOf(this);
|
|
116
|
+
if (index >= 0) registry.stack.splice(index, 1);
|
|
117
|
+
if (registry.stack.length === 0) {
|
|
118
|
+
ownerDocument.removeEventListener("keydown", registry.onKeydown);
|
|
119
|
+
_EscapeLayer.#registries.delete(ownerDocument);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.#ownerDocument = null;
|
|
123
|
+
this.#onDismiss = null;
|
|
124
|
+
this.#claims = null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether this active layer would own a press right now: it is the topmost
|
|
128
|
+
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
129
|
+
* and diagnostics — production dismissal goes through the shared listener.
|
|
130
|
+
*/
|
|
131
|
+
get ownsEscape() {
|
|
132
|
+
const ownerDocument = this.#ownerDocument;
|
|
133
|
+
if (!ownerDocument) return false;
|
|
134
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
135
|
+
if (!registry) return false;
|
|
136
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === this;
|
|
137
|
+
}
|
|
138
|
+
/** Builds a document's registry with its shared resolver listener. */
|
|
139
|
+
static #createRegistry() {
|
|
140
|
+
const registry = {
|
|
141
|
+
stack: [],
|
|
142
|
+
onKeydown: (event) => {
|
|
143
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
144
|
+
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
145
|
+
if (!owner) return;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
owner.#onDismiss?.();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return registry;
|
|
151
|
+
}
|
|
152
|
+
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
153
|
+
static #resolveOwner(stack) {
|
|
154
|
+
for (let index = stack.length - 1; index >= 0; index--) {
|
|
155
|
+
const layer = stack[index];
|
|
156
|
+
if (!layer) continue;
|
|
157
|
+
if (layer.#claims && !layer.#claims()) continue;
|
|
158
|
+
return layer;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
74
164
|
// src/utils/focus_trap.ts
|
|
75
165
|
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
76
166
|
var FocusTrap = class {
|
|
@@ -84,6 +174,8 @@ var FocusTrap = class {
|
|
|
84
174
|
#inertedSiblings = [];
|
|
85
175
|
/** Whether the modal side effects are currently applied. */
|
|
86
176
|
#activeState = false;
|
|
177
|
+
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
178
|
+
#escapeLayer = new EscapeLayer();
|
|
87
179
|
/** Returns the trapped element; called on every operation for the live target. */
|
|
88
180
|
#getContainer;
|
|
89
181
|
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
@@ -119,6 +211,8 @@ var FocusTrap = class {
|
|
|
119
211
|
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
120
212
|
document.addEventListener("keydown", this.#onKeydown);
|
|
121
213
|
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
214
|
+
const onEscape = this.#options.onEscape;
|
|
215
|
+
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
122
216
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
123
217
|
}
|
|
124
218
|
/**
|
|
@@ -131,6 +225,7 @@ var FocusTrap = class {
|
|
|
131
225
|
deactivate({ restoreFocus = true } = {}) {
|
|
132
226
|
if (!this.#activeState) return;
|
|
133
227
|
this.#activeState = false;
|
|
228
|
+
this.#escapeLayer.deactivate();
|
|
134
229
|
document.removeEventListener("keydown", this.#onKeydown);
|
|
135
230
|
document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
136
231
|
if (this.#scrollLocked) {
|
|
@@ -159,15 +254,12 @@ var FocusTrap = class {
|
|
|
159
254
|
#onBeforeCache = () => {
|
|
160
255
|
this.deactivate({ restoreFocus: false });
|
|
161
256
|
};
|
|
162
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
259
|
+
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
260
|
+
* which layer currently owns Escape.
|
|
261
|
+
*/
|
|
163
262
|
#onKeydown = (event) => {
|
|
164
|
-
if (event.key === "Escape") {
|
|
165
|
-
if (this.#options.onEscape) {
|
|
166
|
-
event.preventDefault();
|
|
167
|
-
this.#options.onEscape();
|
|
168
|
-
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
263
|
if (event.key === "Tab") this.#trapTab(event);
|
|
172
264
|
};
|
|
173
265
|
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
@@ -515,6 +607,55 @@ var AspectRatioController = class extends Controller {
|
|
|
515
607
|
return value !== void 0 && Number.isFinite(value) && value > 0;
|
|
516
608
|
}
|
|
517
609
|
};
|
|
610
|
+
|
|
611
|
+
// src/utils/composition_tracker.ts
|
|
612
|
+
var CompositionTracker = class {
|
|
613
|
+
#observedTargets = /* @__PURE__ */ new Set();
|
|
614
|
+
#activeTargets = /* @__PURE__ */ new Set();
|
|
615
|
+
#onStart;
|
|
616
|
+
#onEnd;
|
|
617
|
+
constructor(options = {}) {
|
|
618
|
+
this.#onStart = options.onStart;
|
|
619
|
+
this.#onEnd = options.onEnd;
|
|
620
|
+
}
|
|
621
|
+
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
622
|
+
observe(target) {
|
|
623
|
+
if (this.#observedTargets.has(target)) return;
|
|
624
|
+
target.addEventListener("compositionstart", this.#handleStart);
|
|
625
|
+
target.addEventListener("compositionend", this.#handleEnd);
|
|
626
|
+
this.#observedTargets.add(target);
|
|
627
|
+
}
|
|
628
|
+
/** Stops tracking one target and clears any active composition it owned. */
|
|
629
|
+
unobserve(target) {
|
|
630
|
+
if (!this.#observedTargets.delete(target)) return;
|
|
631
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
632
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
633
|
+
this.#activeTargets.delete(target);
|
|
634
|
+
}
|
|
635
|
+
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
636
|
+
disconnect() {
|
|
637
|
+
for (const target of this.#observedTargets) {
|
|
638
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
639
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
640
|
+
}
|
|
641
|
+
this.#observedTargets.clear();
|
|
642
|
+
this.#activeTargets.clear();
|
|
643
|
+
}
|
|
644
|
+
/** True when lifecycle tracking or the current event reports composition. */
|
|
645
|
+
isComposing(event) {
|
|
646
|
+
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
647
|
+
}
|
|
648
|
+
#handleStart = (event) => {
|
|
649
|
+
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
650
|
+
this.#onStart?.(event);
|
|
651
|
+
};
|
|
652
|
+
#handleEnd = (event) => {
|
|
653
|
+
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
654
|
+
this.#onEnd?.(event);
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/controllers/auto_submit_controller.ts
|
|
518
659
|
var AutoSubmitController = class extends Controller {
|
|
519
660
|
static targets = ["form"];
|
|
520
661
|
static values = {
|
|
@@ -538,34 +679,22 @@ var AutoSubmitController = class extends Controller {
|
|
|
538
679
|
window.dispatchEvent(new CustomEvent("stimeo--announcer:announce", { detail: { message } }));
|
|
539
680
|
}
|
|
540
681
|
};
|
|
541
|
-
/**
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
};
|
|
547
|
-
/**
|
|
548
|
-
* Composition finished (the IME conversion is confirmed): clear the flag and
|
|
549
|
-
* schedule a submit as if `input` fired, so the settled text triggers a submit
|
|
550
|
-
* even on browsers whose post-composition `input` still reads `isComposing`.
|
|
551
|
-
*/
|
|
552
|
-
#onCompositionEnd = (event) => {
|
|
553
|
-
this.#composing = false;
|
|
554
|
-
if (this.#triggers("input")) this.#schedule(event.target ?? null);
|
|
555
|
-
};
|
|
682
|
+
/** Owns delegated IME lifecycle state and submits confirmed input text. */
|
|
683
|
+
#composition = new CompositionTracker({
|
|
684
|
+
onEnd: (event) => {
|
|
685
|
+
if (this.#triggers("input")) this.#schedule(event.target ?? null);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
556
688
|
connect() {
|
|
557
689
|
this.#form.addEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
558
|
-
this.#
|
|
559
|
-
this.#form.addEventListener("compositionend", this.#onCompositionEnd);
|
|
690
|
+
this.#composition.observe(this.#form);
|
|
560
691
|
}
|
|
561
692
|
disconnect() {
|
|
562
693
|
this.#timers.clearAll();
|
|
563
694
|
this.#pendingId = 0;
|
|
564
|
-
this.#
|
|
695
|
+
this.#composition.disconnect();
|
|
565
696
|
this.#form.removeAttribute("data-auto-submit-pending");
|
|
566
697
|
this.#form.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
567
|
-
this.#form.removeEventListener("compositionstart", this.#onCompositionStart);
|
|
568
|
-
this.#form.removeEventListener("compositionend", this.#onCompositionEnd);
|
|
569
698
|
}
|
|
570
699
|
/**
|
|
571
700
|
* Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an
|
|
@@ -574,7 +703,7 @@ var AutoSubmitController = class extends Controller {
|
|
|
574
703
|
*/
|
|
575
704
|
submit(event) {
|
|
576
705
|
if (!this.#triggers(event.type)) return;
|
|
577
|
-
if (event.type === "input" &&
|
|
706
|
+
if (event.type === "input" && this.#composition.isComposing(event)) return;
|
|
578
707
|
this.#schedule(event.target ?? null);
|
|
579
708
|
}
|
|
580
709
|
/** Schedules (and coalesces) the debounced submit for the given trigger. */
|
|
@@ -1443,35 +1572,25 @@ var CharacterCounterController = class _CharacterCounterController extends Contr
|
|
|
1443
1572
|
static #announceDelay = 200;
|
|
1444
1573
|
#timeouts = new SafeTimeout();
|
|
1445
1574
|
#announceId = null;
|
|
1446
|
-
/**
|
|
1447
|
-
#
|
|
1575
|
+
/** Owns IME lifecycle state and applies the confirmed count once. */
|
|
1576
|
+
#composition = new CompositionTracker({ onEnd: () => this.#update() });
|
|
1448
1577
|
#onInput = (event) => {
|
|
1449
|
-
if (this.#
|
|
1450
|
-
this.#update();
|
|
1451
|
-
};
|
|
1452
|
-
#onCompositionStart = () => {
|
|
1453
|
-
this.#composing = true;
|
|
1454
|
-
};
|
|
1455
|
-
#onCompositionEnd = () => {
|
|
1456
|
-
this.#composing = false;
|
|
1578
|
+
if (this.#composition.isComposing(event)) return;
|
|
1457
1579
|
this.#update();
|
|
1458
1580
|
};
|
|
1459
1581
|
connect() {
|
|
1460
1582
|
const field = this.#field;
|
|
1461
1583
|
if (!field) return;
|
|
1462
1584
|
field.addEventListener("input", this.#onInput);
|
|
1463
|
-
|
|
1464
|
-
field.addEventListener("compositionend", this.#onCompositionEnd);
|
|
1585
|
+
this.#composition.observe(field);
|
|
1465
1586
|
this.#update({ announce: false });
|
|
1466
1587
|
}
|
|
1467
1588
|
disconnect() {
|
|
1468
1589
|
const field = this.#field;
|
|
1469
1590
|
field?.removeEventListener("input", this.#onInput);
|
|
1470
|
-
|
|
1471
|
-
field?.removeEventListener("compositionend", this.#onCompositionEnd);
|
|
1591
|
+
this.#composition.disconnect();
|
|
1472
1592
|
this.#timeouts.clearAll();
|
|
1473
1593
|
this.#announceId = null;
|
|
1474
|
-
this.#composing = false;
|
|
1475
1594
|
}
|
|
1476
1595
|
/**
|
|
1477
1596
|
* Recomputes length-derived state. Non-text state (data hooks, `aria-invalid`)
|
|
@@ -2045,17 +2164,30 @@ var ComboboxController = class extends Controller {
|
|
|
2045
2164
|
* does not immediately re-open the listbox via a `focus`-bound action.
|
|
2046
2165
|
*/
|
|
2047
2166
|
#suppressOpen = false;
|
|
2167
|
+
/** Owns IME lifecycle state; confirmed text re-filters the list once. */
|
|
2168
|
+
#composition = new CompositionTracker({ onEnd: () => this.filter() });
|
|
2048
2169
|
/** Starts closed with no active option and registers the outside-click listener. */
|
|
2049
2170
|
connect() {
|
|
2171
|
+
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
2050
2172
|
this.close();
|
|
2051
2173
|
document.addEventListener("click", this.#onOutsideClick);
|
|
2052
2174
|
}
|
|
2053
2175
|
/** Removes the document-level listener registered in {@link connect}. */
|
|
2054
2176
|
disconnect() {
|
|
2177
|
+
this.#composition.disconnect();
|
|
2055
2178
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
2056
2179
|
}
|
|
2057
|
-
/**
|
|
2058
|
-
|
|
2180
|
+
/** Tracks an input added initially or after connect. */
|
|
2181
|
+
inputTargetConnected(input) {
|
|
2182
|
+
this.#composition.observe(input);
|
|
2183
|
+
}
|
|
2184
|
+
/** Removes composition listeners when the active input is replaced or removed. */
|
|
2185
|
+
inputTargetDisconnected(input) {
|
|
2186
|
+
this.#composition.unobserve(input);
|
|
2187
|
+
}
|
|
2188
|
+
/** Filters confirmed input text and opens the listbox. */
|
|
2189
|
+
filter(event) {
|
|
2190
|
+
if (this.#composition.isComposing(event)) return;
|
|
2059
2191
|
this.open();
|
|
2060
2192
|
}
|
|
2061
2193
|
/**
|
|
@@ -2092,7 +2224,7 @@ var ComboboxController = class extends Controller {
|
|
|
2092
2224
|
}
|
|
2093
2225
|
/** Routes keyboard interaction per the APG combobox model. */
|
|
2094
2226
|
onKeydown(event) {
|
|
2095
|
-
if (
|
|
2227
|
+
if (this.#composition.isComposing(event)) return;
|
|
2096
2228
|
switch (event.key) {
|
|
2097
2229
|
case "ArrowDown": {
|
|
2098
2230
|
event.preventDefault();
|
|
@@ -2138,6 +2270,7 @@ var ComboboxController = class extends Controller {
|
|
|
2138
2270
|
break;
|
|
2139
2271
|
}
|
|
2140
2272
|
case "Escape":
|
|
2273
|
+
if (event.defaultPrevented || this.#isClosed) break;
|
|
2141
2274
|
event.preventDefault();
|
|
2142
2275
|
this.close();
|
|
2143
2276
|
break;
|
|
@@ -2214,7 +2347,9 @@ var ComboboxController = class extends Controller {
|
|
|
2214
2347
|
return !this.hasListTarget || this.listTarget.hidden !== false;
|
|
2215
2348
|
}
|
|
2216
2349
|
};
|
|
2217
|
-
var CommandPaletteController = class extends Controller {
|
|
2350
|
+
var CommandPaletteController = class _CommandPaletteController extends Controller {
|
|
2351
|
+
static #ORIGINAL_ARIA_DISABLED = "data-command-palette-original-aria-disabled";
|
|
2352
|
+
static #ABSENT_ARIA_DISABLED = "absent";
|
|
2218
2353
|
static targets = ["dialog", "input", "list", "option", "empty"];
|
|
2219
2354
|
static values = {
|
|
2220
2355
|
hotkey: { type: String, default: "mod+k" },
|
|
@@ -2232,6 +2367,12 @@ var CommandPaletteController = class extends Controller {
|
|
|
2232
2367
|
static events = ["select"];
|
|
2233
2368
|
/** The index of the currently active option within the visible subset. */
|
|
2234
2369
|
#activeIndex = -1;
|
|
2370
|
+
/** Owns IME lifecycle state; confirmed text re-filters an open palette once. */
|
|
2371
|
+
#composition = new CompositionTracker({
|
|
2372
|
+
onEnd: () => {
|
|
2373
|
+
if (this.#isOpen) this.filter();
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2235
2376
|
/**
|
|
2236
2377
|
* Owns the modal side effects (focus trap, scroll lock, background `inert`, focus
|
|
2237
2378
|
* restore). Escape closes; focus on open goes to the input, and is restored to
|
|
@@ -2254,6 +2395,8 @@ var CommandPaletteController = class extends Controller {
|
|
|
2254
2395
|
*/
|
|
2255
2396
|
connect() {
|
|
2256
2397
|
document.addEventListener("keydown", this.#onGlobalKeydown);
|
|
2398
|
+
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
2399
|
+
this.#syncOptionSemantics();
|
|
2257
2400
|
const shouldOpen = this.#isOpen || this.openValue;
|
|
2258
2401
|
this.#resetToClosedState();
|
|
2259
2402
|
if (shouldOpen) this.open();
|
|
@@ -2261,9 +2404,22 @@ var CommandPaletteController = class extends Controller {
|
|
|
2261
2404
|
/** Tears down the global hotkey listener and reverts the modal side effects. */
|
|
2262
2405
|
disconnect() {
|
|
2263
2406
|
document.removeEventListener("keydown", this.#onGlobalKeydown);
|
|
2407
|
+
this.#composition.disconnect();
|
|
2264
2408
|
this.#trap.deactivate({ restoreFocus: false });
|
|
2265
2409
|
this.#resetToClosedState();
|
|
2266
2410
|
}
|
|
2411
|
+
/** Initializes semantics for an option added before or after the controller connects. */
|
|
2412
|
+
optionTargetConnected(option) {
|
|
2413
|
+
this.#syncOptionSemanticsFor(option);
|
|
2414
|
+
}
|
|
2415
|
+
/** Tracks an input added initially or after connect without extra consumer actions. */
|
|
2416
|
+
inputTargetConnected(input) {
|
|
2417
|
+
this.#composition.observe(input);
|
|
2418
|
+
}
|
|
2419
|
+
/** Removes controller-owned listeners when the input target is replaced or removed. */
|
|
2420
|
+
inputTargetDisconnected(input) {
|
|
2421
|
+
this.#composition.unobserve(input);
|
|
2422
|
+
}
|
|
2267
2423
|
/** Toggles the open state of the command palette. */
|
|
2268
2424
|
toggle() {
|
|
2269
2425
|
if (this.#isOpen) {
|
|
@@ -2288,8 +2444,9 @@ var CommandPaletteController = class extends Controller {
|
|
|
2288
2444
|
this.#trap.deactivate();
|
|
2289
2445
|
}
|
|
2290
2446
|
/** Filters option elements in-memory matching the input value. Bound to input target. */
|
|
2291
|
-
filter() {
|
|
2292
|
-
if (!this.hasInputTarget) return;
|
|
2447
|
+
filter(event) {
|
|
2448
|
+
if (!this.hasInputTarget || this.#composition.isComposing(event)) return;
|
|
2449
|
+
this.#syncOptionSemantics();
|
|
2293
2450
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
2294
2451
|
let hasSelectableMatch = false;
|
|
2295
2452
|
for (const option of this.optionTargets) {
|
|
@@ -2312,6 +2469,7 @@ var CommandPaletteController = class extends Controller {
|
|
|
2312
2469
|
const target = event.currentTarget;
|
|
2313
2470
|
if (!target) return;
|
|
2314
2471
|
const option = target.closest("[role='option']");
|
|
2472
|
+
if (option) this.#syncOptionSemanticsFor(option);
|
|
2315
2473
|
if (option && !this.#isDisabled(option)) {
|
|
2316
2474
|
this.#confirmSelection(option);
|
|
2317
2475
|
}
|
|
@@ -2327,7 +2485,7 @@ var CommandPaletteController = class extends Controller {
|
|
|
2327
2485
|
* focus — not only the input.
|
|
2328
2486
|
*/
|
|
2329
2487
|
onKeydown(event) {
|
|
2330
|
-
if (!this.#isOpen) return;
|
|
2488
|
+
if (this.#composition.isComposing(event) || !this.#isOpen) return;
|
|
2331
2489
|
switch (event.key) {
|
|
2332
2490
|
case "ArrowDown":
|
|
2333
2491
|
event.preventDefault();
|
|
@@ -2362,23 +2520,25 @@ var CommandPaletteController = class extends Controller {
|
|
|
2362
2520
|
this.#setActiveIndex(newIndex);
|
|
2363
2521
|
}
|
|
2364
2522
|
#setActiveIndex(index) {
|
|
2523
|
+
this.#syncOptionSemantics();
|
|
2365
2524
|
const visible = this.#visibleOptions;
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2525
|
+
const activeOption = visible[index] ?? null;
|
|
2526
|
+
this.#activeIndex = activeOption ? index : -1;
|
|
2527
|
+
for (const option of this.optionTargets) {
|
|
2528
|
+
option.setAttribute("aria-selected", "false");
|
|
2529
|
+
option.removeAttribute("data-active");
|
|
2530
|
+
}
|
|
2531
|
+
if (activeOption) {
|
|
2532
|
+
activeOption.setAttribute("aria-selected", "true");
|
|
2533
|
+
activeOption.setAttribute("data-active", "true");
|
|
2534
|
+
activeOption.scrollIntoView({ block: "nearest" });
|
|
2535
|
+
}
|
|
2536
|
+
if (this.hasInputTarget) {
|
|
2537
|
+
if (activeOption?.id) {
|
|
2538
|
+
this.inputTarget.setAttribute("aria-activedescendant", activeOption.id);
|
|
2375
2539
|
} else {
|
|
2376
|
-
|
|
2377
|
-
option.removeAttribute("data-active");
|
|
2540
|
+
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
2378
2541
|
}
|
|
2379
|
-
});
|
|
2380
|
-
if (index === -1 && this.hasInputTarget) {
|
|
2381
|
-
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
2382
2542
|
}
|
|
2383
2543
|
}
|
|
2384
2544
|
#confirmSelection(option) {
|
|
@@ -2391,8 +2551,9 @@ var CommandPaletteController = class extends Controller {
|
|
|
2391
2551
|
for (const option of this.optionTargets) {
|
|
2392
2552
|
option.removeAttribute("hidden");
|
|
2393
2553
|
}
|
|
2394
|
-
|
|
2395
|
-
this
|
|
2554
|
+
const hasSelectableOption = this.#visibleOptions.length > 0;
|
|
2555
|
+
if (this.hasEmptyTarget) this.emptyTarget.hidden = hasSelectableOption;
|
|
2556
|
+
this.#setActiveIndex(hasSelectableOption ? 0 : -1);
|
|
2396
2557
|
}
|
|
2397
2558
|
get #visibleOptions() {
|
|
2398
2559
|
return this.optionTargets.filter(
|
|
@@ -2400,23 +2561,66 @@ var CommandPaletteController = class extends Controller {
|
|
|
2400
2561
|
);
|
|
2401
2562
|
}
|
|
2402
2563
|
#isDisabled(option) {
|
|
2403
|
-
return option.dataset.disabled === "true";
|
|
2564
|
+
return option.dataset.disabled === "true" || option.getAttribute("aria-disabled") === "true";
|
|
2565
|
+
}
|
|
2566
|
+
/** Synchronizes every option's controller-owned selection and disabled semantics. */
|
|
2567
|
+
#syncOptionSemantics() {
|
|
2568
|
+
for (const option of this.optionTargets) this.#syncOptionSemanticsFor(option);
|
|
2569
|
+
}
|
|
2570
|
+
/** Reflects `data-disabled` to ARIA without losing a pre-existing authored value. */
|
|
2571
|
+
#syncOptionSemanticsFor(option) {
|
|
2572
|
+
if (!option.hasAttribute("aria-selected")) option.setAttribute("aria-selected", "false");
|
|
2573
|
+
const originalAttribute = _CommandPaletteController.#ORIGINAL_ARIA_DISABLED;
|
|
2574
|
+
const originalValue = option.getAttribute(originalAttribute);
|
|
2575
|
+
if (option.dataset.disabled === "true") {
|
|
2576
|
+
if (originalValue === null) {
|
|
2577
|
+
option.setAttribute(
|
|
2578
|
+
originalAttribute,
|
|
2579
|
+
option.getAttribute("aria-disabled") ?? _CommandPaletteController.#ABSENT_ARIA_DISABLED
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
option.setAttribute("aria-disabled", "true");
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (originalValue === null) return;
|
|
2586
|
+
if (originalValue === _CommandPaletteController.#ABSENT_ARIA_DISABLED) {
|
|
2587
|
+
option.removeAttribute("aria-disabled");
|
|
2588
|
+
} else {
|
|
2589
|
+
option.setAttribute("aria-disabled", originalValue);
|
|
2590
|
+
}
|
|
2591
|
+
option.removeAttribute(originalAttribute);
|
|
2404
2592
|
}
|
|
2405
2593
|
get #isOpen() {
|
|
2406
2594
|
return this.hasDialogTarget && !this.dialogTarget.hidden;
|
|
2407
2595
|
}
|
|
2408
2596
|
#onGlobalKeydown = (event) => {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
if (!key) return;
|
|
2413
|
-
const modPressed = isMod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;
|
|
2414
|
-
const keyMatch = event.key.toLowerCase() === key;
|
|
2415
|
-
if (modPressed && keyMatch) {
|
|
2416
|
-
event.preventDefault();
|
|
2417
|
-
this.toggle();
|
|
2418
|
-
}
|
|
2597
|
+
if (this.#composition.isComposing(event) || !this.#matchesHotkey(event)) return;
|
|
2598
|
+
event.preventDefault();
|
|
2599
|
+
this.toggle();
|
|
2419
2600
|
};
|
|
2601
|
+
/** Matches the configured `mod+key` or bare-key hotkey without extra modifiers. */
|
|
2602
|
+
#matchesHotkey(event) {
|
|
2603
|
+
const hotkey = this.#parseHotkey();
|
|
2604
|
+
if (!hotkey || event.altKey || event.shiftKey) return false;
|
|
2605
|
+
if (hotkey.requiresMod) {
|
|
2606
|
+
const hasExactlyOneModKey = event.metaKey !== event.ctrlKey;
|
|
2607
|
+
if (!hasExactlyOneModKey) return false;
|
|
2608
|
+
} else if (event.metaKey || event.ctrlKey) {
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2611
|
+
return event.key.toLowerCase() === hotkey.key;
|
|
2612
|
+
}
|
|
2613
|
+
/** Parses the intentionally small public hotkey grammar. */
|
|
2614
|
+
#parseHotkey() {
|
|
2615
|
+
const parts = this.hotkeyValue.toLowerCase().split("+");
|
|
2616
|
+
if (parts.length === 1 && parts[0] && parts[0] !== "mod") {
|
|
2617
|
+
return { key: parts[0], requiresMod: false };
|
|
2618
|
+
}
|
|
2619
|
+
if (parts.length === 2 && parts[0] === "mod" && parts[1]) {
|
|
2620
|
+
return { key: parts[1], requiresMod: true };
|
|
2621
|
+
}
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2420
2624
|
/** Resets transient open state so reconnect starts from a predictable closed snapshot. */
|
|
2421
2625
|
#resetToClosedState() {
|
|
2422
2626
|
this.#activeIndex = -1;
|
|
@@ -2649,14 +2853,23 @@ var ConfirmController = class extends Controller {
|
|
|
2649
2853
|
var ContextMenuController = class extends Controller {
|
|
2650
2854
|
static targets = ["region", "menu", "item"];
|
|
2651
2855
|
static actions = ["activate", "onItemKeydown", "onRegionKeydown", "open"];
|
|
2652
|
-
|
|
2856
|
+
#timers = new SafeTimeout();
|
|
2857
|
+
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
2858
|
+
#escapeLayer = new EscapeLayer();
|
|
2859
|
+
/** Starts closed and registers delegated activation and outside-pointer listeners. */
|
|
2653
2860
|
connect() {
|
|
2654
2861
|
this.#closeMenu();
|
|
2655
|
-
|
|
2862
|
+
this.element.addEventListener("click", this.#onItemClickCapture, true);
|
|
2863
|
+
document.addEventListener("click", this.#onOutsidePointer);
|
|
2864
|
+
document.addEventListener("contextmenu", this.#onOutsidePointer);
|
|
2656
2865
|
}
|
|
2657
|
-
/**
|
|
2866
|
+
/** Releases the listeners, stack membership, and pending Tab-close task. */
|
|
2658
2867
|
disconnect() {
|
|
2659
|
-
|
|
2868
|
+
this.#timers.clearAll();
|
|
2869
|
+
this.#escapeLayer.deactivate();
|
|
2870
|
+
this.element.removeEventListener("click", this.#onItemClickCapture, true);
|
|
2871
|
+
document.removeEventListener("click", this.#onOutsidePointer);
|
|
2872
|
+
document.removeEventListener("contextmenu", this.#onOutsidePointer);
|
|
2660
2873
|
}
|
|
2661
2874
|
/**
|
|
2662
2875
|
* Opens the menu from a `contextmenu` event: suppresses the native menu and
|
|
@@ -2681,11 +2894,17 @@ var ContextMenuController = class extends Controller {
|
|
|
2681
2894
|
switch (event.key) {
|
|
2682
2895
|
case "ArrowDown":
|
|
2683
2896
|
event.preventDefault();
|
|
2684
|
-
if (items.length > 0)
|
|
2897
|
+
if (items.length > 0) {
|
|
2898
|
+
const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;
|
|
2899
|
+
items[nextIndex]?.focus();
|
|
2900
|
+
}
|
|
2685
2901
|
break;
|
|
2686
2902
|
case "ArrowUp":
|
|
2687
2903
|
event.preventDefault();
|
|
2688
|
-
if (items.length > 0)
|
|
2904
|
+
if (items.length > 0) {
|
|
2905
|
+
const previousIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;
|
|
2906
|
+
items[(previousIndex + items.length) % items.length]?.focus();
|
|
2907
|
+
}
|
|
2689
2908
|
break;
|
|
2690
2909
|
case "Home":
|
|
2691
2910
|
event.preventDefault();
|
|
@@ -2695,12 +2914,9 @@ var ContextMenuController = class extends Controller {
|
|
|
2695
2914
|
event.preventDefault();
|
|
2696
2915
|
items[items.length - 1]?.focus();
|
|
2697
2916
|
break;
|
|
2698
|
-
case "Escape":
|
|
2699
|
-
event.preventDefault();
|
|
2700
|
-
this.#closeAndRestore();
|
|
2701
|
-
break;
|
|
2702
2917
|
case "Tab":
|
|
2703
|
-
this.#
|
|
2918
|
+
this.#timers.clearAll();
|
|
2919
|
+
this.#timers.set(() => this.#closeMenu(), 0);
|
|
2704
2920
|
break;
|
|
2705
2921
|
}
|
|
2706
2922
|
}
|
|
@@ -2711,6 +2927,11 @@ var ContextMenuController = class extends Controller {
|
|
|
2711
2927
|
/** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */
|
|
2712
2928
|
#openAt(x, y) {
|
|
2713
2929
|
if (!this.hasMenuTarget) return;
|
|
2930
|
+
this.#timers.clearAll();
|
|
2931
|
+
this.#escapeLayer.activate(document, {
|
|
2932
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
2933
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
2934
|
+
});
|
|
2714
2935
|
this.menuTarget.style.setProperty("--stimeo-context-menu-x", `${x}px`);
|
|
2715
2936
|
this.menuTarget.style.setProperty("--stimeo-context-menu-y", `${y}px`);
|
|
2716
2937
|
this.menuTarget.hidden = false;
|
|
@@ -2719,6 +2940,8 @@ var ContextMenuController = class extends Controller {
|
|
|
2719
2940
|
}
|
|
2720
2941
|
/** Hides the menu and reflects the collapsed state on the region. */
|
|
2721
2942
|
#closeMenu() {
|
|
2943
|
+
this.#timers.clearAll();
|
|
2944
|
+
this.#escapeLayer.deactivate();
|
|
2722
2945
|
if (!this.hasMenuTarget) return;
|
|
2723
2946
|
this.menuTarget.hidden = true;
|
|
2724
2947
|
if (this.hasRegionTarget) this.regionTarget.setAttribute("data-state", "closed");
|
|
@@ -2728,10 +2951,24 @@ var ContextMenuController = class extends Controller {
|
|
|
2728
2951
|
this.#closeMenu();
|
|
2729
2952
|
if (this.hasRegionTarget) this.regionTarget.focus();
|
|
2730
2953
|
}
|
|
2731
|
-
/** Closes
|
|
2732
|
-
#
|
|
2954
|
+
/** Closes when a click or context-menu invocation lands outside this instance. */
|
|
2955
|
+
#onOutsidePointer = (event) => {
|
|
2733
2956
|
if (this.#isOpen && !this.element.contains(event.target)) this.#closeMenu();
|
|
2734
2957
|
};
|
|
2958
|
+
/**
|
|
2959
|
+
* Captures clicks so `aria-disabled` commands cannot reach consumer handlers.
|
|
2960
|
+
* Native Enter/Space activation also synthesizes a click and is blocked here.
|
|
2961
|
+
*/
|
|
2962
|
+
#onItemClickCapture = (event) => {
|
|
2963
|
+
const target = event.target;
|
|
2964
|
+
if (!(target instanceof Node)) return;
|
|
2965
|
+
const disabled = this.itemTargets.some(
|
|
2966
|
+
(item) => item.getAttribute("aria-disabled") === "true" && item.contains(target)
|
|
2967
|
+
);
|
|
2968
|
+
if (!disabled) return;
|
|
2969
|
+
event.preventDefault();
|
|
2970
|
+
event.stopImmediatePropagation();
|
|
2971
|
+
};
|
|
2735
2972
|
/** Menu items eligible for roving focus (excludes disabled / hidden). */
|
|
2736
2973
|
get #navigableItems() {
|
|
2737
2974
|
return this.itemTargets.filter((item) => this.#isNavigable(item));
|
|
@@ -2889,8 +3126,8 @@ var CountdownController = class extends Controller {
|
|
|
2889
3126
|
* paused (or completed) one resets the displayed amount but stays paused until the
|
|
2890
3127
|
* user resumes — it never silently restarts. The run state is read from the DOM,
|
|
2891
3128
|
* not re-derived from the declarative `autostart` Value (which only governs the
|
|
2892
|
-
* initial state on connect); re-deriving it would override a user's pause
|
|
2893
|
-
*
|
|
3129
|
+
* initial state on connect); re-deriving it would override a user's pause —
|
|
3130
|
+
* the DOM, not a re-run of declarative config, is the source of truth.
|
|
2894
3131
|
*/
|
|
2895
3132
|
reset() {
|
|
2896
3133
|
const wasRunning = this.#state === "running";
|
|
@@ -3384,7 +3621,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
3384
3621
|
return;
|
|
3385
3622
|
}
|
|
3386
3623
|
if (event.key === "Escape") {
|
|
3387
|
-
if (this.#pendingStart) {
|
|
3624
|
+
if (this.#pendingStart && !event.defaultPrevented && !event.isComposing) {
|
|
3388
3625
|
event.preventDefault();
|
|
3389
3626
|
this.#pendingStart = "";
|
|
3390
3627
|
this.#previewDate = "";
|
|
@@ -3913,14 +4150,6 @@ var DirtyFormController = class extends Controller {
|
|
|
3913
4150
|
return null;
|
|
3914
4151
|
}
|
|
3915
4152
|
};
|
|
3916
|
-
var FOCUSABLE_SELECTOR = [
|
|
3917
|
-
"a[href]",
|
|
3918
|
-
"button:not([disabled])",
|
|
3919
|
-
"input:not([disabled])",
|
|
3920
|
-
"select:not([disabled])",
|
|
3921
|
-
"textarea:not([disabled])",
|
|
3922
|
-
'[tabindex]:not([tabindex="-1"])'
|
|
3923
|
-
].join(",");
|
|
3924
4153
|
var DismissibleController = class extends Controller {
|
|
3925
4154
|
static targets = ["root", "fallback"];
|
|
3926
4155
|
static values = {
|
|
@@ -3934,25 +4163,35 @@ var DismissibleController = class extends Controller {
|
|
|
3934
4163
|
if (!root.hasAttribute("data-state")) {
|
|
3935
4164
|
root.setAttribute("data-state", "open");
|
|
3936
4165
|
}
|
|
3937
|
-
|
|
3938
|
-
this.element.addEventListener("keydown", this.#onKeydown);
|
|
3939
|
-
}
|
|
4166
|
+
this.#syncEscapeListener();
|
|
3940
4167
|
}
|
|
3941
4168
|
disconnect() {
|
|
3942
4169
|
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
3943
4170
|
}
|
|
4171
|
+
/** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
|
|
4172
|
+
closeOnEscapeValueChanged() {
|
|
4173
|
+
this.#syncEscapeListener();
|
|
4174
|
+
}
|
|
3944
4175
|
/** Dismisses the element. Bound via `data-action` (click on the close button). */
|
|
3945
4176
|
dismiss() {
|
|
3946
4177
|
this.#performDismiss();
|
|
3947
4178
|
}
|
|
3948
4179
|
/** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */
|
|
3949
4180
|
#onKeydown = (event) => {
|
|
3950
|
-
if (event.key !== "Escape") return;
|
|
4181
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
4182
|
+
if (!this.closeOnEscapeValue) return;
|
|
3951
4183
|
const active = document.activeElement;
|
|
3952
4184
|
if (!active || !this.element.contains(active)) return;
|
|
3953
4185
|
event.preventDefault();
|
|
3954
4186
|
this.#performDismiss();
|
|
3955
4187
|
};
|
|
4188
|
+
/** Adds or removes the one stable listener reference without duplicating it. */
|
|
4189
|
+
#syncEscapeListener() {
|
|
4190
|
+
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
4191
|
+
if (this.closeOnEscapeValue) {
|
|
4192
|
+
this.element.addEventListener("keydown", this.#onKeydown);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
3956
4195
|
/** The element to dismiss: the explicit `root` target, or the host element. */
|
|
3957
4196
|
get #root() {
|
|
3958
4197
|
return this.hasRootTarget ? this.rootTarget : this.element;
|
|
@@ -3976,23 +4215,44 @@ var DismissibleController = class extends Controller {
|
|
|
3976
4215
|
#retreatFocus(root) {
|
|
3977
4216
|
const active = document.activeElement;
|
|
3978
4217
|
if (!(active instanceof HTMLElement) || !root.contains(active)) return;
|
|
3979
|
-
this.#
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
);
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4218
|
+
for (const candidate of this.#focusFallbacks(root)) {
|
|
4219
|
+
candidate.focus();
|
|
4220
|
+
if (document.activeElement === candidate) return;
|
|
4221
|
+
}
|
|
4222
|
+
document.body.focus();
|
|
4223
|
+
}
|
|
4224
|
+
/** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
|
|
4225
|
+
*#focusFallbacks(root) {
|
|
4226
|
+
const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
|
|
4227
|
+
if (explicitFallback && !root.contains(explicitFallback) && this.#isFocusAvailable(explicitFallback)) {
|
|
4228
|
+
yield explicitFallback;
|
|
4229
|
+
}
|
|
4230
|
+
const candidates = document.querySelectorAll(FOCUSABLE);
|
|
4231
|
+
for (const element of candidates) {
|
|
4232
|
+
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
4233
|
+
continue;
|
|
4234
|
+
}
|
|
4235
|
+
if (this.#isFocusAvailable(element)) yield element;
|
|
4236
|
+
}
|
|
4237
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
4238
|
+
const element = candidates.item(index);
|
|
4239
|
+
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)) {
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
if (this.#isFocusAvailable(element)) yield element;
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
/** Whether focus is allowed by the element and its semantic ancestors. */
|
|
4246
|
+
#isFocusAvailable(element) {
|
|
4247
|
+
if (!element.matches(FOCUSABLE) && !element.hasAttribute("tabindex") && !element.isContentEditable) {
|
|
4248
|
+
return false;
|
|
4249
|
+
}
|
|
4250
|
+
if (element.matches(":disabled")) return false;
|
|
4251
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
4252
|
+
for (let current = element; current; current = current.parentElement) {
|
|
4253
|
+
if (current.hidden || current.inert) return false;
|
|
4254
|
+
}
|
|
4255
|
+
return true;
|
|
3996
4256
|
}
|
|
3997
4257
|
};
|
|
3998
4258
|
function maxTransitionMs(value) {
|
|
@@ -4136,35 +4396,46 @@ var DrawerController = class extends Controller {
|
|
|
4136
4396
|
return this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
|
|
4137
4397
|
}
|
|
4138
4398
|
};
|
|
4399
|
+
|
|
4400
|
+
// src/utils/aria_ids.ts
|
|
4401
|
+
var counter = 0;
|
|
4402
|
+
function uniqueId(prefix = "stimeo") {
|
|
4403
|
+
let candidate;
|
|
4404
|
+
do {
|
|
4405
|
+
counter += 1;
|
|
4406
|
+
candidate = `${prefix}-${counter}`;
|
|
4407
|
+
} while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
|
|
4408
|
+
return candidate;
|
|
4409
|
+
}
|
|
4410
|
+
function ensureId(element, prefix = "stimeo") {
|
|
4411
|
+
if (element.id) return element.id;
|
|
4412
|
+
const id = uniqueId(prefix);
|
|
4413
|
+
element.id = id;
|
|
4414
|
+
return id;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
// src/controllers/dropdown_controller.ts
|
|
4139
4418
|
var DropdownController = class extends Controller {
|
|
4140
4419
|
static targets = ["trigger", "menu"];
|
|
4141
4420
|
static actions = ["close", "open", "toggle"];
|
|
4421
|
+
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
4422
|
+
#escapeLayer = new EscapeLayer();
|
|
4142
4423
|
/** Closes the menu when a click lands outside the controller's element. */
|
|
4143
4424
|
#onOutsideClick = (event) => {
|
|
4144
4425
|
if (!this.element.contains(event.target)) {
|
|
4145
4426
|
this.close();
|
|
4146
4427
|
}
|
|
4147
4428
|
};
|
|
4148
|
-
/**
|
|
4149
|
-
#onKeydown = (event) => {
|
|
4150
|
-
if (event.key === "Escape" && this.#isOpen) {
|
|
4151
|
-
this.close();
|
|
4152
|
-
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
4153
|
-
}
|
|
4154
|
-
};
|
|
4155
|
-
/**
|
|
4156
|
-
* Starts in the closed state and registers the document-level listeners that
|
|
4157
|
-
* power outside-click and `Escape` handling.
|
|
4158
|
-
*/
|
|
4429
|
+
/** Starts in the closed state and registers outside-click handling. */
|
|
4159
4430
|
connect() {
|
|
4431
|
+
this.#associateTriggerWithMenu();
|
|
4160
4432
|
this.close();
|
|
4161
|
-
document.addEventListener("click", this.#onOutsideClick);
|
|
4162
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
4433
|
+
document.addEventListener("click", this.#onOutsideClick, true);
|
|
4163
4434
|
}
|
|
4164
|
-
/** Removes the
|
|
4435
|
+
/** Removes the listeners registered in {@link connect}. */
|
|
4165
4436
|
disconnect() {
|
|
4166
|
-
|
|
4167
|
-
document.removeEventListener("
|
|
4437
|
+
this.#escapeLayer.deactivate();
|
|
4438
|
+
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
4168
4439
|
}
|
|
4169
4440
|
/** Toggles the menu between open and closed. Bound via `data-action`. */
|
|
4170
4441
|
toggle() {
|
|
@@ -4177,6 +4448,10 @@ var DropdownController = class extends Controller {
|
|
|
4177
4448
|
/** Reveals the menu and reflects the open state on the trigger. */
|
|
4178
4449
|
open() {
|
|
4179
4450
|
if (!this.hasMenuTarget) return;
|
|
4451
|
+
this.#escapeLayer.activate(document, {
|
|
4452
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
4453
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
4454
|
+
});
|
|
4180
4455
|
this.menuTarget.hidden = false;
|
|
4181
4456
|
if (this.hasTriggerTarget) {
|
|
4182
4457
|
this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
@@ -4184,16 +4459,34 @@ var DropdownController = class extends Controller {
|
|
|
4184
4459
|
}
|
|
4185
4460
|
/** Hides the menu and reflects the closed state on the trigger. */
|
|
4186
4461
|
close() {
|
|
4462
|
+
this.#escapeLayer.deactivate();
|
|
4187
4463
|
if (!this.hasMenuTarget) return;
|
|
4188
4464
|
this.menuTarget.hidden = true;
|
|
4189
4465
|
if (this.hasTriggerTarget) {
|
|
4190
4466
|
this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
4191
4467
|
}
|
|
4192
4468
|
}
|
|
4469
|
+
/** Closes and restores focus to the trigger (the keyboard-dismissal path). */
|
|
4470
|
+
#closeAndRestore() {
|
|
4471
|
+
this.close();
|
|
4472
|
+
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
4473
|
+
}
|
|
4193
4474
|
/** Whether the menu is currently visible. */
|
|
4194
4475
|
get #isOpen() {
|
|
4195
4476
|
return this.hasMenuTarget && !this.menuTarget.hidden;
|
|
4196
4477
|
}
|
|
4478
|
+
/**
|
|
4479
|
+
* Associates the disclosure trigger and controlled region without clobbering
|
|
4480
|
+
* authored markup.
|
|
4481
|
+
*/
|
|
4482
|
+
#associateTriggerWithMenu() {
|
|
4483
|
+
if (!this.hasTriggerTarget || !this.hasMenuTarget) return;
|
|
4484
|
+
if (this.triggerTarget.hasAttribute("aria-controls")) return;
|
|
4485
|
+
this.triggerTarget.setAttribute(
|
|
4486
|
+
"aria-controls",
|
|
4487
|
+
ensureId(this.menuTarget, "stimeo--dropdown-menu")
|
|
4488
|
+
);
|
|
4489
|
+
}
|
|
4197
4490
|
};
|
|
4198
4491
|
var EditableController = class extends Controller {
|
|
4199
4492
|
static targets = ["display", "input"];
|
|
@@ -4204,10 +4497,28 @@ var EditableController = class extends Controller {
|
|
|
4204
4497
|
static events = ["cancel", "change"];
|
|
4205
4498
|
/** The value captured when edit mode began, used to detect real changes. */
|
|
4206
4499
|
#previousValue = "";
|
|
4500
|
+
/**
|
|
4501
|
+
* Owns IME lifecycle state for the edit surface, so a keydown that belongs to
|
|
4502
|
+
* a composition (cancel or confirm) is never treated as an edit command.
|
|
4503
|
+
*/
|
|
4504
|
+
#composition = new CompositionTracker();
|
|
4207
4505
|
/** Establishes the initial display mode (display shown, input hidden). */
|
|
4208
4506
|
connect() {
|
|
4507
|
+
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
4209
4508
|
this.#setMode("display");
|
|
4210
4509
|
}
|
|
4510
|
+
/** Releases the composition listeners so nothing outlives the element. */
|
|
4511
|
+
disconnect() {
|
|
4512
|
+
this.#composition.disconnect();
|
|
4513
|
+
}
|
|
4514
|
+
/** Tracks an input added initially or after connect (e.g. a Turbo swap). */
|
|
4515
|
+
inputTargetConnected(input) {
|
|
4516
|
+
this.#composition.observe(input);
|
|
4517
|
+
}
|
|
4518
|
+
/** Removes composition listeners when the active input is replaced or removed. */
|
|
4519
|
+
inputTargetDisconnected(input) {
|
|
4520
|
+
this.#composition.unobserve(input);
|
|
4521
|
+
}
|
|
4211
4522
|
/** Enters edit mode: seeds the input from the display text, focuses, selects. */
|
|
4212
4523
|
edit() {
|
|
4213
4524
|
if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
|
|
@@ -4226,7 +4537,9 @@ var EditableController = class extends Controller {
|
|
|
4226
4537
|
}
|
|
4227
4538
|
/** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
|
|
4228
4539
|
onKeydown(event) {
|
|
4540
|
+
if (this.#composition.isComposing(event)) return;
|
|
4229
4541
|
if (event.key === "Escape") {
|
|
4542
|
+
if (event.defaultPrevented) return;
|
|
4230
4543
|
event.preventDefault();
|
|
4231
4544
|
this.#cancel();
|
|
4232
4545
|
return;
|
|
@@ -4794,25 +5107,6 @@ var FocusController = class extends Controller {
|
|
|
4794
5107
|
this.dispatch("deactivate", { detail: {} });
|
|
4795
5108
|
}
|
|
4796
5109
|
};
|
|
4797
|
-
|
|
4798
|
-
// src/utils/aria_ids.ts
|
|
4799
|
-
var counter = 0;
|
|
4800
|
-
function uniqueId(prefix = "stimeo") {
|
|
4801
|
-
let candidate;
|
|
4802
|
-
do {
|
|
4803
|
-
counter += 1;
|
|
4804
|
-
candidate = `${prefix}-${counter}`;
|
|
4805
|
-
} while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
|
|
4806
|
-
return candidate;
|
|
4807
|
-
}
|
|
4808
|
-
function ensureId(element, prefix = "stimeo") {
|
|
4809
|
-
if (element.id) return element.id;
|
|
4810
|
-
const id = uniqueId(prefix);
|
|
4811
|
-
element.id = id;
|
|
4812
|
-
return id;
|
|
4813
|
-
}
|
|
4814
|
-
|
|
4815
|
-
// src/controllers/form_field_controller.ts
|
|
4816
5110
|
var FormFieldController = class _FormFieldController extends Controller {
|
|
4817
5111
|
static targets = ["control", "description", "error"];
|
|
4818
5112
|
static values = {
|
|
@@ -5116,8 +5410,8 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
5116
5410
|
}
|
|
5117
5411
|
/**
|
|
5118
5412
|
* Applies (or clears) a declarative custom constraint via `setCustomValidity`,
|
|
5119
|
-
* for controls that opt in with `data-stimeo--form-field-disallow`. The
|
|
5120
|
-
*
|
|
5413
|
+
* for controls that opt in with `data-stimeo--form-field-disallow`. The supported
|
|
5414
|
+
* rule is `"whitespace"` — a value that is non-empty but blank
|
|
5121
5415
|
* after trimming (which slips past `required` / `minlength`); its message follows
|
|
5122
5416
|
* the per-constraint (`value-missing`) → generic → default chain.
|
|
5123
5417
|
*
|
|
@@ -5386,21 +5680,26 @@ var HoverCardController = class extends Controller {
|
|
|
5386
5680
|
closeDelay: { type: Number, default: 200 },
|
|
5387
5681
|
closeOnScroll: { type: Boolean, default: false }
|
|
5388
5682
|
};
|
|
5389
|
-
static actions = ["close", "
|
|
5390
|
-
/** Pending open/close timers,
|
|
5683
|
+
static actions = ["close", "open"];
|
|
5684
|
+
/** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
|
|
5391
5685
|
#timers = new SafeTimeout();
|
|
5686
|
+
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
5687
|
+
#escapeLayer = new EscapeLayer();
|
|
5392
5688
|
#pendingOpen = null;
|
|
5393
5689
|
#pendingClose = null;
|
|
5394
5690
|
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
5395
5691
|
#stopScrollDismiss = null;
|
|
5396
|
-
/** Starts closed. */
|
|
5692
|
+
/** Starts closed and discards any stale pending state from a prior connection. */
|
|
5397
5693
|
connect() {
|
|
5694
|
+
this.#cancelOpen();
|
|
5695
|
+
this.#cancelClose();
|
|
5398
5696
|
this.#conceal();
|
|
5399
5697
|
}
|
|
5400
|
-
/** Clears timers
|
|
5698
|
+
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
5401
5699
|
disconnect() {
|
|
5402
|
-
this.#
|
|
5403
|
-
|
|
5700
|
+
this.#cancelOpen();
|
|
5701
|
+
this.#cancelClose();
|
|
5702
|
+
this.#escapeLayer.deactivate();
|
|
5404
5703
|
this.#stopScrollDismiss?.();
|
|
5405
5704
|
this.#stopScrollDismiss = null;
|
|
5406
5705
|
}
|
|
@@ -5432,32 +5731,26 @@ var HoverCardController = class extends Controller {
|
|
|
5432
5731
|
this.#conceal();
|
|
5433
5732
|
}, this.closeDelayValue);
|
|
5434
5733
|
}
|
|
5435
|
-
/**
|
|
5436
|
-
onKeydown(event) {
|
|
5437
|
-
if (event.key === "Escape" && this.#isOpen) {
|
|
5438
|
-
event.preventDefault();
|
|
5439
|
-
this.#dismiss();
|
|
5440
|
-
}
|
|
5441
|
-
}
|
|
5442
|
-
/** Reveals the card, reflects state, and starts watching for a dismissing `Escape`/scroll. */
|
|
5734
|
+
/** Reveals the card, reflects state, and joins the Escape stack / scroll watcher. */
|
|
5443
5735
|
#reveal() {
|
|
5444
5736
|
if (!this.hasCardTarget) return;
|
|
5445
5737
|
this.cardTarget.hidden = false;
|
|
5446
5738
|
this.cardTarget.setAttribute("data-state", "open");
|
|
5447
5739
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
5448
|
-
|
|
5740
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss() });
|
|
5449
5741
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
5450
5742
|
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
5451
5743
|
}
|
|
5452
5744
|
}
|
|
5453
|
-
/** Hides the card, reflects state, and
|
|
5745
|
+
/** Hides the card, reflects state, and leaves the Escape stack / scroll watcher. */
|
|
5454
5746
|
#conceal() {
|
|
5455
|
-
|
|
5747
|
+
this.#escapeLayer.deactivate();
|
|
5456
5748
|
this.#stopScrollDismiss?.();
|
|
5457
5749
|
this.#stopScrollDismiss = null;
|
|
5458
|
-
if (
|
|
5459
|
-
|
|
5460
|
-
|
|
5750
|
+
if (this.hasCardTarget) {
|
|
5751
|
+
this.cardTarget.hidden = true;
|
|
5752
|
+
this.cardTarget.setAttribute("data-state", "closed");
|
|
5753
|
+
}
|
|
5461
5754
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
5462
5755
|
}
|
|
5463
5756
|
/** Cancels pending timers and conceals immediately (shared Escape path). */
|
|
@@ -5466,13 +5759,6 @@ var HoverCardController = class extends Controller {
|
|
|
5466
5759
|
this.#cancelClose();
|
|
5467
5760
|
this.#conceal();
|
|
5468
5761
|
}
|
|
5469
|
-
/** Document-level `Escape` watcher (active only while open). */
|
|
5470
|
-
#onDocumentKeydown = (event) => {
|
|
5471
|
-
if (event.key === "Escape") {
|
|
5472
|
-
event.preventDefault();
|
|
5473
|
-
this.#dismiss();
|
|
5474
|
-
}
|
|
5475
|
-
};
|
|
5476
5762
|
/** Cancels any pending open timer. */
|
|
5477
5763
|
#cancelOpen() {
|
|
5478
5764
|
if (this.#pendingOpen !== null) {
|
|
@@ -6029,6 +6315,7 @@ var ListboxController = class extends Controller {
|
|
|
6029
6315
|
this.#commitActive();
|
|
6030
6316
|
break;
|
|
6031
6317
|
case "Escape":
|
|
6318
|
+
if (event.defaultPrevented || event.isComposing) break;
|
|
6032
6319
|
event.preventDefault();
|
|
6033
6320
|
this.close();
|
|
6034
6321
|
this.triggerTarget.focus();
|
|
@@ -6308,13 +6595,20 @@ var MenuController = class extends Controller {
|
|
|
6308
6595
|
"open",
|
|
6309
6596
|
"toggle"
|
|
6310
6597
|
];
|
|
6311
|
-
|
|
6598
|
+
#timers = new SafeTimeout();
|
|
6599
|
+
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
6600
|
+
#escapeLayer = new EscapeLayer();
|
|
6601
|
+
/** Starts closed and registers delegated listeners. */
|
|
6312
6602
|
connect() {
|
|
6313
6603
|
this.close();
|
|
6604
|
+
this.element.addEventListener("click", this.#onItemClickCapture, true);
|
|
6314
6605
|
document.addEventListener("click", this.#onOutsideClick);
|
|
6315
6606
|
}
|
|
6316
|
-
/**
|
|
6607
|
+
/** Releases the listeners, stack membership, and any pending Tab-close task. */
|
|
6317
6608
|
disconnect() {
|
|
6609
|
+
this.#timers.clearAll();
|
|
6610
|
+
this.#escapeLayer.deactivate();
|
|
6611
|
+
this.element.removeEventListener("click", this.#onItemClickCapture, true);
|
|
6318
6612
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
6319
6613
|
}
|
|
6320
6614
|
/** Toggles the menu open/closed. Bound via `data-action` (click). */
|
|
@@ -6328,12 +6622,19 @@ var MenuController = class extends Controller {
|
|
|
6328
6622
|
}
|
|
6329
6623
|
/** Opens the menu and reflects the expanded state on the trigger. */
|
|
6330
6624
|
open() {
|
|
6625
|
+
this.#timers.clearAll();
|
|
6331
6626
|
if (!this.hasMenuTarget) return;
|
|
6627
|
+
this.#escapeLayer.activate(document, {
|
|
6628
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
6629
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
6630
|
+
});
|
|
6332
6631
|
this.menuTarget.hidden = false;
|
|
6333
6632
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
6334
6633
|
}
|
|
6335
6634
|
/** Closes the menu and reflects the collapsed state on the trigger. */
|
|
6336
6635
|
close() {
|
|
6636
|
+
this.#timers.clearAll();
|
|
6637
|
+
this.#escapeLayer.deactivate();
|
|
6337
6638
|
if (!this.hasMenuTarget) return;
|
|
6338
6639
|
this.menuTarget.hidden = true;
|
|
6339
6640
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
@@ -6378,18 +6679,18 @@ var MenuController = class extends Controller {
|
|
|
6378
6679
|
event.preventDefault();
|
|
6379
6680
|
items[items.length - 1]?.focus();
|
|
6380
6681
|
break;
|
|
6381
|
-
case "Escape":
|
|
6382
|
-
event.preventDefault();
|
|
6383
|
-
this.close();
|
|
6384
|
-
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
6385
|
-
break;
|
|
6386
6682
|
case "Tab":
|
|
6387
|
-
this.
|
|
6683
|
+
this.#timers.clearAll();
|
|
6684
|
+
this.#timers.set(() => this.close(), 0);
|
|
6388
6685
|
break;
|
|
6389
6686
|
}
|
|
6390
6687
|
}
|
|
6391
6688
|
/** Closes the menu after an item is activated. Bound via `data-action`. */
|
|
6392
6689
|
activate() {
|
|
6690
|
+
this.#closeAndRestore();
|
|
6691
|
+
}
|
|
6692
|
+
/** Closes and returns focus to the trigger (Escape / item-activation path). */
|
|
6693
|
+
#closeAndRestore() {
|
|
6393
6694
|
this.close();
|
|
6394
6695
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
6395
6696
|
}
|
|
@@ -6397,6 +6698,20 @@ var MenuController = class extends Controller {
|
|
|
6397
6698
|
#onOutsideClick = (event) => {
|
|
6398
6699
|
if (this.#isOpen && !this.element.contains(event.target)) this.close();
|
|
6399
6700
|
};
|
|
6701
|
+
/**
|
|
6702
|
+
* Captures clicks so `aria-disabled` commands cannot reach consumer handlers.
|
|
6703
|
+
* Native Enter/Space activation also synthesizes a click and is blocked here.
|
|
6704
|
+
*/
|
|
6705
|
+
#onItemClickCapture = (event) => {
|
|
6706
|
+
const target = event.target;
|
|
6707
|
+
if (!(target instanceof Node)) return;
|
|
6708
|
+
const disabled = this.itemTargets.some(
|
|
6709
|
+
(item) => item.getAttribute("aria-disabled") === "true" && item.contains(target)
|
|
6710
|
+
);
|
|
6711
|
+
if (!disabled) return;
|
|
6712
|
+
event.preventDefault();
|
|
6713
|
+
event.stopImmediatePropagation();
|
|
6714
|
+
};
|
|
6400
6715
|
/** Moves focus to the first navigable item (no-op if none). */
|
|
6401
6716
|
#focusFirst() {
|
|
6402
6717
|
this.#navigableItems[0]?.focus();
|
|
@@ -6435,6 +6750,8 @@ var MenubarController = class extends Controller {
|
|
|
6435
6750
|
#typeahead = "";
|
|
6436
6751
|
#typeaheadId = 0;
|
|
6437
6752
|
#timers = new SafeTimeout();
|
|
6753
|
+
/** Escape-stack membership while a menu is open; the shared resolver dismisses via it. */
|
|
6754
|
+
#escapeLayer = new EscapeLayer();
|
|
6438
6755
|
/** Establishes the single tab stop and the closed baseline. */
|
|
6439
6756
|
connect() {
|
|
6440
6757
|
const active = this.#roving.activeIndex;
|
|
@@ -6442,8 +6759,9 @@ var MenubarController = class extends Controller {
|
|
|
6442
6759
|
this.#closeAllMenus();
|
|
6443
6760
|
document.addEventListener("click", this.#onOutsideClick);
|
|
6444
6761
|
}
|
|
6445
|
-
/** Removes the document listener and any pending typeahead timer. */
|
|
6762
|
+
/** Removes the document listener, stack membership, and any pending typeahead timer. */
|
|
6446
6763
|
disconnect() {
|
|
6764
|
+
this.#escapeLayer.deactivate();
|
|
6447
6765
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
6448
6766
|
this.#timers.clearAll();
|
|
6449
6767
|
}
|
|
@@ -6488,12 +6806,6 @@ var MenubarController = class extends Controller {
|
|
|
6488
6806
|
event.preventDefault();
|
|
6489
6807
|
this.#gotoTop(length - 1, anyOpen);
|
|
6490
6808
|
break;
|
|
6491
|
-
case "Escape":
|
|
6492
|
-
if (anyOpen) {
|
|
6493
|
-
event.preventDefault();
|
|
6494
|
-
this.#closeAllMenus();
|
|
6495
|
-
}
|
|
6496
|
-
break;
|
|
6497
6809
|
}
|
|
6498
6810
|
}
|
|
6499
6811
|
/** Keyboard handling while focus is on a menu item. */
|
|
@@ -6529,11 +6841,6 @@ var MenubarController = class extends Controller {
|
|
|
6529
6841
|
event.preventDefault();
|
|
6530
6842
|
this.#moveToAdjacentMenu(menu, -1);
|
|
6531
6843
|
break;
|
|
6532
|
-
case "Escape":
|
|
6533
|
-
event.preventDefault();
|
|
6534
|
-
this.#closeMenu(this.#topFor(menu));
|
|
6535
|
-
this.#focusTop(this.#topFor(menu));
|
|
6536
|
-
break;
|
|
6537
6844
|
case "Tab":
|
|
6538
6845
|
this.#closeAllMenus();
|
|
6539
6846
|
break;
|
|
@@ -6571,6 +6878,10 @@ var MenubarController = class extends Controller {
|
|
|
6571
6878
|
if (!menu) return;
|
|
6572
6879
|
menu.hidden = false;
|
|
6573
6880
|
top.setAttribute("aria-expanded", "true");
|
|
6881
|
+
this.#escapeLayer.activate(document, {
|
|
6882
|
+
onDismiss: () => this.#dismissOpenMenu(),
|
|
6883
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
6884
|
+
});
|
|
6574
6885
|
this.#roving.setActive(this.topTargets.indexOf(top));
|
|
6575
6886
|
const items = this.#itemsIn(menu);
|
|
6576
6887
|
this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
|
|
@@ -6581,12 +6892,30 @@ var MenubarController = class extends Controller {
|
|
|
6581
6892
|
const menu = this.#menuFor(top);
|
|
6582
6893
|
if (menu) menu.hidden = true;
|
|
6583
6894
|
top.setAttribute("aria-expanded", "false");
|
|
6895
|
+
if (!this.#isAnyOpen) this.#escapeLayer.deactivate();
|
|
6584
6896
|
}
|
|
6585
6897
|
/** Closes every menu and resets the typeahead buffer. */
|
|
6586
6898
|
#closeAllMenus() {
|
|
6587
6899
|
for (const top of this.topTargets) this.#closeMenu(top);
|
|
6588
6900
|
this.#resetTypeahead();
|
|
6589
6901
|
}
|
|
6902
|
+
/**
|
|
6903
|
+
* Escape path, invoked by the shared resolver: pressed inside an open menu it
|
|
6904
|
+
* closes that menu and returns focus to its top item (the APG behavior);
|
|
6905
|
+
* pressed while focus is on a top item (or fell to the body) it closes every
|
|
6906
|
+
* menu without moving focus.
|
|
6907
|
+
*/
|
|
6908
|
+
#dismissOpenMenu() {
|
|
6909
|
+
const active = document.activeElement;
|
|
6910
|
+
const menu = this.menuTargets.find((candidate) => candidate.contains(active));
|
|
6911
|
+
if (menu) {
|
|
6912
|
+
const top = this.#topFor(menu);
|
|
6913
|
+
this.#closeMenu(top);
|
|
6914
|
+
this.#focusTop(top);
|
|
6915
|
+
return;
|
|
6916
|
+
}
|
|
6917
|
+
this.#closeAllMenus();
|
|
6918
|
+
}
|
|
6590
6919
|
/** Opens the menu of the top item `delta` steps from the one owning `menu`. */
|
|
6591
6920
|
#moveToAdjacentMenu(menu, delta) {
|
|
6592
6921
|
const top = this.#topFor(menu);
|
|
@@ -6768,9 +7097,25 @@ var MultiSelectController = class extends Controller {
|
|
|
6768
7097
|
static events = ["change", "filter"];
|
|
6769
7098
|
/** The active option (tracked via `aria-activedescendant`), or null. */
|
|
6770
7099
|
#activeOption = null;
|
|
7100
|
+
/** Absorbs the browser's redundant final input after compositionend. */
|
|
7101
|
+
#ignorePostCompositionInput = false;
|
|
7102
|
+
/** Owns IME lifecycle state; confirmed text emits one filter result. */
|
|
7103
|
+
#composition = new CompositionTracker({
|
|
7104
|
+
onStart: () => {
|
|
7105
|
+
this.#ignorePostCompositionInput = false;
|
|
7106
|
+
},
|
|
7107
|
+
onEnd: () => {
|
|
7108
|
+
this.#ignorePostCompositionInput = true;
|
|
7109
|
+
queueMicrotask(() => {
|
|
7110
|
+
this.#ignorePostCompositionInput = false;
|
|
7111
|
+
});
|
|
7112
|
+
this.filter();
|
|
7113
|
+
}
|
|
7114
|
+
});
|
|
6771
7115
|
#roving = new RovingTabindex(() => this.#removeButtons);
|
|
6772
7116
|
/** Starts closed, syncs chips for any pre-selected options, and listens out. */
|
|
6773
7117
|
connect() {
|
|
7118
|
+
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
6774
7119
|
this.close();
|
|
6775
7120
|
if (this.hasTagsTarget) {
|
|
6776
7121
|
this.tagsTarget.addEventListener("keydown", this.#onTagKeydown);
|
|
@@ -6784,14 +7129,30 @@ var MultiSelectController = class extends Controller {
|
|
|
6784
7129
|
}
|
|
6785
7130
|
/** Tears down document and chip listeners on disconnect (Turbo included). */
|
|
6786
7131
|
disconnect() {
|
|
7132
|
+
this.#composition.disconnect();
|
|
7133
|
+
this.#ignorePostCompositionInput = false;
|
|
6787
7134
|
if (this.hasTagsTarget) {
|
|
6788
7135
|
this.tagsTarget.removeEventListener("keydown", this.#onTagKeydown);
|
|
6789
7136
|
this.tagsTarget.removeEventListener("click", this.#onTagClick);
|
|
6790
7137
|
}
|
|
6791
7138
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
6792
7139
|
}
|
|
6793
|
-
/**
|
|
6794
|
-
|
|
7140
|
+
/** Tracks an input added initially or after connect. */
|
|
7141
|
+
inputTargetConnected(input) {
|
|
7142
|
+
this.#composition.observe(input);
|
|
7143
|
+
}
|
|
7144
|
+
/** Removes composition listeners when the active input is replaced or removed. */
|
|
7145
|
+
inputTargetDisconnected(input) {
|
|
7146
|
+
this.#composition.unobserve(input);
|
|
7147
|
+
this.#ignorePostCompositionInput = false;
|
|
7148
|
+
}
|
|
7149
|
+
/** Filters confirmed input text, opens, and re-seeds the active option. */
|
|
7150
|
+
filter(event) {
|
|
7151
|
+
if (event && this.#ignorePostCompositionInput) {
|
|
7152
|
+
this.#ignorePostCompositionInput = false;
|
|
7153
|
+
return;
|
|
7154
|
+
}
|
|
7155
|
+
if (this.#composition.isComposing(event)) return;
|
|
6795
7156
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
6796
7157
|
for (const option of this.optionTargets) {
|
|
6797
7158
|
const label = (option.textContent ?? "").trim().toLowerCase();
|
|
@@ -6819,7 +7180,7 @@ var MultiSelectController = class extends Controller {
|
|
|
6819
7180
|
}
|
|
6820
7181
|
/** Routes input keyboard interaction per the multi-select combobox model. */
|
|
6821
7182
|
onKeydown(event) {
|
|
6822
|
-
if (
|
|
7183
|
+
if (this.#composition.isComposing(event)) return;
|
|
6823
7184
|
switch (event.key) {
|
|
6824
7185
|
case "ArrowDown":
|
|
6825
7186
|
event.preventDefault();
|
|
@@ -6852,6 +7213,7 @@ var MultiSelectController = class extends Controller {
|
|
|
6852
7213
|
}
|
|
6853
7214
|
break;
|
|
6854
7215
|
case "Escape":
|
|
7216
|
+
if (event.defaultPrevented || this.#isClosed) break;
|
|
6855
7217
|
event.preventDefault();
|
|
6856
7218
|
this.close();
|
|
6857
7219
|
break;
|
|
@@ -7084,18 +7446,19 @@ var NavigationMenuController = class extends Controller {
|
|
|
7084
7446
|
* when targets churn between connect and disconnect.
|
|
7085
7447
|
*/
|
|
7086
7448
|
#hoverWired = /* @__PURE__ */ new Set();
|
|
7449
|
+
/** Escape-stack membership while any panel is open; the shared resolver dismisses via it. */
|
|
7450
|
+
#escapeLayer = new EscapeLayer();
|
|
7087
7451
|
/** Establishes the closed baseline and the dismissal listeners. */
|
|
7088
7452
|
connect() {
|
|
7089
7453
|
this.#closeAll();
|
|
7090
7454
|
document.addEventListener("click", this.#onOutsideClick);
|
|
7091
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
7092
7455
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
7093
7456
|
if (this.openOnHoverValue) this.#addHoverListeners();
|
|
7094
7457
|
}
|
|
7095
|
-
/** Removes every listener
|
|
7458
|
+
/** Removes every listener, pending hover timer, and stack membership registered while connected. */
|
|
7096
7459
|
disconnect() {
|
|
7460
|
+
this.#escapeLayer.deactivate();
|
|
7097
7461
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
7098
|
-
document.removeEventListener("keydown", this.#onKeydown);
|
|
7099
7462
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
7100
7463
|
this.#removeHoverListeners();
|
|
7101
7464
|
this.#hoverTimers.clearAll();
|
|
@@ -7154,17 +7517,34 @@ var NavigationMenuController = class extends Controller {
|
|
|
7154
7517
|
if (!panel) return;
|
|
7155
7518
|
panel.hidden = false;
|
|
7156
7519
|
trigger.setAttribute("aria-expanded", "true");
|
|
7520
|
+
this.#syncEscapeLayer();
|
|
7157
7521
|
}
|
|
7158
7522
|
/** Closes `trigger`'s panel and reflects the collapsed state. */
|
|
7159
7523
|
#closePanel(trigger) {
|
|
7160
7524
|
const panel = this.#panelFor(trigger);
|
|
7161
7525
|
if (panel) panel.hidden = true;
|
|
7162
7526
|
trigger.setAttribute("aria-expanded", "false");
|
|
7527
|
+
this.#syncEscapeLayer();
|
|
7163
7528
|
}
|
|
7164
7529
|
/** Closes every open panel. */
|
|
7165
7530
|
#closeAll() {
|
|
7166
7531
|
for (const trigger of this.triggerTargets) this.#closePanel(trigger);
|
|
7167
7532
|
}
|
|
7533
|
+
/**
|
|
7534
|
+
* Aligns Escape-stack membership with the open state: joins (or re-asserts to
|
|
7535
|
+
* the top) while a panel is open, leaves once none is. Re-asserting when the
|
|
7536
|
+
* user switches panels is deliberate — the nav is again the newest layer.
|
|
7537
|
+
*/
|
|
7538
|
+
#syncEscapeLayer() {
|
|
7539
|
+
if (this.#isAnyOpen) {
|
|
7540
|
+
this.#escapeLayer.activate(document, {
|
|
7541
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
7542
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
7543
|
+
});
|
|
7544
|
+
} else {
|
|
7545
|
+
this.#escapeLayer.deactivate();
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7168
7548
|
/** Closes any open panel and returns focus to its trigger (Escape path). */
|
|
7169
7549
|
#closeAndRestore() {
|
|
7170
7550
|
const open = this.#openTrigger;
|
|
@@ -7176,17 +7556,17 @@ var NavigationMenuController = class extends Controller {
|
|
|
7176
7556
|
#onOutsideClick = (event) => {
|
|
7177
7557
|
if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAll();
|
|
7178
7558
|
};
|
|
7179
|
-
/**
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7559
|
+
/**
|
|
7560
|
+
* Closes (without restoring focus) when focus leaves the nav for a known
|
|
7561
|
+
* external destination. A null/non-Node destination is indeterminate:
|
|
7562
|
+
* browsers use it for clicks on non-focusable content and for window
|
|
7563
|
+
* deactivation, so those never close the nav here (matching the popover
|
|
7564
|
+
* convention) — the outside-click handler decides pointer dismissal, and the
|
|
7565
|
+
* Escape stack's body-focus claim keeps keyboard dismissal working.
|
|
7566
|
+
*/
|
|
7187
7567
|
#onFocusOut = (event) => {
|
|
7188
7568
|
const next = event.relatedTarget;
|
|
7189
|
-
if (next
|
|
7569
|
+
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
7190
7570
|
this.#closeAll();
|
|
7191
7571
|
};
|
|
7192
7572
|
/** Opens a trigger's panel after the hover delay (hover mode). */
|
|
@@ -7760,21 +8140,24 @@ var OtpController = class extends Controller {
|
|
|
7760
8140
|
};
|
|
7761
8141
|
static actions = ["onInput", "onKeydown", "onPaste"];
|
|
7762
8142
|
static events = ["change", "complete", "invalid"];
|
|
7763
|
-
|
|
8143
|
+
/** Owns IME lifecycle state across every digit field. */
|
|
8144
|
+
#composition = new CompositionTracker({
|
|
8145
|
+
onEnd: (event) => {
|
|
8146
|
+
const input = event.currentTarget;
|
|
8147
|
+
if (input) this.#handleInputValidation(input);
|
|
8148
|
+
}
|
|
8149
|
+
});
|
|
7764
8150
|
connect() {
|
|
7765
8151
|
for (const field of this.fieldTargets) {
|
|
7766
8152
|
field.addEventListener("focus", this.#onFieldFocus);
|
|
7767
|
-
|
|
7768
|
-
field.addEventListener("compositionend", this.#onCompositionEnd);
|
|
8153
|
+
this.#composition.observe(field);
|
|
7769
8154
|
}
|
|
7770
8155
|
}
|
|
7771
8156
|
disconnect() {
|
|
7772
8157
|
for (const field of this.fieldTargets) {
|
|
7773
8158
|
field.removeEventListener("focus", this.#onFieldFocus);
|
|
7774
|
-
field.removeEventListener("compositionstart", this.#onCompositionStart);
|
|
7775
|
-
field.removeEventListener("compositionend", this.#onCompositionEnd);
|
|
7776
8159
|
}
|
|
7777
|
-
this.#
|
|
8160
|
+
this.#composition.disconnect();
|
|
7778
8161
|
}
|
|
7779
8162
|
/**
|
|
7780
8163
|
* Stimulus lifecycle callback when a new field target enters the DOM.
|
|
@@ -7782,21 +8165,18 @@ var OtpController = class extends Controller {
|
|
|
7782
8165
|
*/
|
|
7783
8166
|
fieldTargetConnected(element) {
|
|
7784
8167
|
element.addEventListener("focus", this.#onFieldFocus);
|
|
7785
|
-
|
|
7786
|
-
element.addEventListener("compositionend", this.#onCompositionEnd);
|
|
8168
|
+
this.#composition.observe(element);
|
|
7787
8169
|
}
|
|
7788
8170
|
/** Removes focus listeners when fields are dropped. */
|
|
7789
8171
|
fieldTargetDisconnected(element) {
|
|
7790
8172
|
element.removeEventListener("focus", this.#onFieldFocus);
|
|
7791
|
-
|
|
7792
|
-
element.removeEventListener("compositionend", this.#onCompositionEnd);
|
|
7793
|
-
this.#isComposing.delete(element);
|
|
8173
|
+
this.#composition.unobserve(element);
|
|
7794
8174
|
}
|
|
7795
8175
|
/** Handles keystroke inputs and advances focus to the next field. */
|
|
7796
8176
|
onInput(event) {
|
|
7797
8177
|
const input = event.currentTarget;
|
|
7798
8178
|
if (!input) return;
|
|
7799
|
-
if (this.#isComposing
|
|
8179
|
+
if (this.#composition.isComposing(event)) return;
|
|
7800
8180
|
this.#handleInputValidation(input);
|
|
7801
8181
|
}
|
|
7802
8182
|
/** Handles Backspace retreating, arrows, and home/end navigation. */
|
|
@@ -7805,7 +8185,7 @@ var OtpController = class extends Controller {
|
|
|
7805
8185
|
if (!input) return;
|
|
7806
8186
|
const index = this.fieldTargets.indexOf(input);
|
|
7807
8187
|
if (index === -1) return;
|
|
7808
|
-
if (this.#isComposing
|
|
8188
|
+
if (this.#composition.isComposing(event)) return;
|
|
7809
8189
|
switch (event.key) {
|
|
7810
8190
|
case "Backspace":
|
|
7811
8191
|
if (!input.value) {
|
|
@@ -7887,15 +8267,6 @@ var OtpController = class extends Controller {
|
|
|
7887
8267
|
input.select();
|
|
7888
8268
|
}
|
|
7889
8269
|
};
|
|
7890
|
-
#onCompositionStart = (event) => {
|
|
7891
|
-
const input = event.currentTarget;
|
|
7892
|
-
this.#isComposing.set(input, true);
|
|
7893
|
-
};
|
|
7894
|
-
#onCompositionEnd = (event) => {
|
|
7895
|
-
const input = event.currentTarget;
|
|
7896
|
-
this.#isComposing.set(input, false);
|
|
7897
|
-
this.#handleInputValidation(input);
|
|
7898
|
-
};
|
|
7899
8270
|
#handleInputValidation(input) {
|
|
7900
8271
|
const index = this.fieldTargets.indexOf(input);
|
|
7901
8272
|
if (index === -1) return;
|
|
@@ -8839,6 +9210,7 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
8839
9210
|
const handle = this.#handleFor(event.target);
|
|
8840
9211
|
if (!handle) return;
|
|
8841
9212
|
if (event.key === "Escape") {
|
|
9213
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
8842
9214
|
if (this.#pointer?.started) {
|
|
8843
9215
|
const { pointerType } = this.#pointer;
|
|
8844
9216
|
this.#teardownSessions();
|
|
@@ -8951,9 +9323,8 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
8951
9323
|
}
|
|
8952
9324
|
/**
|
|
8953
9325
|
* Silently tears down whatever session is live (disconnect / disabled /
|
|
8954
|
-
* Escape). Composed from the two single-session teardowns so
|
|
8955
|
-
*
|
|
8956
|
-
* the capture-release fix).
|
|
9326
|
+
* Escape). Composed from the two single-session teardowns so pointer and
|
|
9327
|
+
* keyboard cleanup cannot diverge as either path evolves.
|
|
8957
9328
|
*/
|
|
8958
9329
|
#teardownSessions() {
|
|
8959
9330
|
this.#endPointerSession();
|
|
@@ -9060,24 +9431,26 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
9060
9431
|
static actions = ["close", "open", "toggle"];
|
|
9061
9432
|
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
9062
9433
|
#stopScrollDismiss = null;
|
|
9434
|
+
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
9435
|
+
#escapeLayer = new EscapeLayer();
|
|
9063
9436
|
/** Selector for natively focusable elements used to find the first one. */
|
|
9064
9437
|
static #FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
9065
|
-
/** Starts closed and registers the
|
|
9438
|
+
/** Starts closed and registers the standing dismissal listeners. */
|
|
9066
9439
|
connect() {
|
|
9067
9440
|
this.close();
|
|
9068
9441
|
document.addEventListener("click", this.#onOutsideClick);
|
|
9069
|
-
|
|
9442
|
+
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
9070
9443
|
}
|
|
9071
9444
|
/**
|
|
9072
|
-
* Removes
|
|
9073
|
-
*
|
|
9074
|
-
*
|
|
9075
|
-
*
|
|
9445
|
+
* Removes every standing listener registered in {@link connect} plus any active
|
|
9446
|
+
* dismiss-on-scroll observers. `removeEventListener` is a no-op when it was
|
|
9447
|
+
* never added, so this is safe in the closed state too — no listener outlives
|
|
9448
|
+
* the element after a Turbo navigation.
|
|
9076
9449
|
*/
|
|
9077
9450
|
disconnect() {
|
|
9451
|
+
this.#escapeLayer.deactivate();
|
|
9078
9452
|
document.removeEventListener("click", this.#onOutsideClick);
|
|
9079
|
-
|
|
9080
|
-
if (this.hasPanelTarget) this.panelTarget.removeEventListener("focusout", this.#onFocusOut);
|
|
9453
|
+
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
9081
9454
|
this.#stopScrollDismiss?.();
|
|
9082
9455
|
this.#stopScrollDismiss = null;
|
|
9083
9456
|
}
|
|
@@ -9094,7 +9467,10 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
9094
9467
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
9095
9468
|
this.panelTarget.hidden = false;
|
|
9096
9469
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
9097
|
-
this.
|
|
9470
|
+
this.#escapeLayer.activate(document, {
|
|
9471
|
+
onDismiss: () => this.#closeAndRestore(),
|
|
9472
|
+
claims: claimsWhileFocusWithin(this.element)
|
|
9473
|
+
});
|
|
9098
9474
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
9099
9475
|
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.close());
|
|
9100
9476
|
}
|
|
@@ -9102,11 +9478,10 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
9102
9478
|
}
|
|
9103
9479
|
/** Closes the panel and reflects the collapsed state. Bound via `data-action`. */
|
|
9104
9480
|
close() {
|
|
9105
|
-
|
|
9106
|
-
this.panelTarget.removeEventListener("focusout", this.#onFocusOut);
|
|
9481
|
+
this.#escapeLayer.deactivate();
|
|
9107
9482
|
this.#stopScrollDismiss?.();
|
|
9108
9483
|
this.#stopScrollDismiss = null;
|
|
9109
|
-
this.panelTarget.hidden = true;
|
|
9484
|
+
if (this.hasPanelTarget) this.panelTarget.hidden = true;
|
|
9110
9485
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
9111
9486
|
}
|
|
9112
9487
|
/** Moves focus to the first focusable element in the panel, or the panel itself. */
|
|
@@ -9119,31 +9494,28 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
9119
9494
|
if (!this.panelTarget.hasAttribute("tabindex")) this.panelTarget.tabIndex = -1;
|
|
9120
9495
|
this.panelTarget.focus();
|
|
9121
9496
|
}
|
|
9122
|
-
/** Closes and restores focus to the trigger
|
|
9497
|
+
/** Closes and restores focus to the trigger for explicit keyboard dismissal. */
|
|
9123
9498
|
#closeAndRestore() {
|
|
9124
9499
|
this.close();
|
|
9125
9500
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
9126
9501
|
}
|
|
9127
|
-
/** Closes
|
|
9502
|
+
/** Closes without moving focus when a click lands outside the controller element. */
|
|
9128
9503
|
#onOutsideClick = (event) => {
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
/** Closes (restoring focus) on `Escape` while open. */
|
|
9132
|
-
#onKeydown = (event) => {
|
|
9133
|
-
if (event.key === "Escape" && this.#isOpen) {
|
|
9134
|
-
event.preventDefault();
|
|
9135
|
-
this.#closeAndRestore();
|
|
9136
|
-
}
|
|
9504
|
+
const target = event.target;
|
|
9505
|
+
if (this.#isOpen && target instanceof Node && !this.element.contains(target)) this.close();
|
|
9137
9506
|
};
|
|
9138
9507
|
/**
|
|
9139
|
-
* Closes when focus leaves the
|
|
9140
|
-
* (e.g.
|
|
9141
|
-
* destination is kept, which is the modeless
|
|
9142
|
-
*
|
|
9508
|
+
* Closes when focus leaves the controller for a known external destination
|
|
9509
|
+
* (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is
|
|
9510
|
+
* not restored — the natural destination is kept, which is the modeless
|
|
9511
|
+
* contract. A null/non-Node destination is indeterminate: browsers use it for
|
|
9512
|
+
* clicks on non-focusable content and window deactivation, so the later outside
|
|
9513
|
+
* click handler decides pointer dismissal instead.
|
|
9143
9514
|
*/
|
|
9144
9515
|
#onFocusOut = (event) => {
|
|
9516
|
+
if (!this.#isOpen) return;
|
|
9145
9517
|
const next = event.relatedTarget;
|
|
9146
|
-
if (next
|
|
9518
|
+
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
9147
9519
|
this.close();
|
|
9148
9520
|
};
|
|
9149
9521
|
/** Whether the panel is currently visible. */
|
|
@@ -10183,7 +10555,7 @@ var RovingController = class extends Controller {
|
|
|
10183
10555
|
}
|
|
10184
10556
|
}
|
|
10185
10557
|
};
|
|
10186
|
-
var
|
|
10558
|
+
var FOCUSABLE_SELECTOR = [
|
|
10187
10559
|
"a[href]",
|
|
10188
10560
|
"button:not([disabled])",
|
|
10189
10561
|
"input:not([disabled])",
|
|
@@ -10295,7 +10667,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
10295
10667
|
}
|
|
10296
10668
|
}
|
|
10297
10669
|
#hasFocusableContent(vp) {
|
|
10298
|
-
return vp.querySelector(
|
|
10670
|
+
return vp.querySelector(FOCUSABLE_SELECTOR) !== null;
|
|
10299
10671
|
}
|
|
10300
10672
|
#hasAccessibleName(vp) {
|
|
10301
10673
|
return vp.hasAttribute("aria-label") || vp.hasAttribute("aria-labelledby");
|
|
@@ -11956,20 +12328,32 @@ var TagsInputController = class extends Controller {
|
|
|
11956
12328
|
static actions = ["onKeydown"];
|
|
11957
12329
|
static events = ["change", "reject"];
|
|
11958
12330
|
#roving = new RovingTabindex(() => this.#removeButtons);
|
|
12331
|
+
/** Owns IME lifecycle state for commit-key guarding. */
|
|
12332
|
+
#composition = new CompositionTracker();
|
|
11959
12333
|
/** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
|
|
11960
12334
|
connect() {
|
|
12335
|
+
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
11961
12336
|
this.tagsTarget.addEventListener("keydown", this.#onTagKeydown);
|
|
11962
12337
|
this.tagsTarget.addEventListener("click", this.#onTagClick);
|
|
11963
12338
|
this.#syncState();
|
|
11964
12339
|
}
|
|
11965
12340
|
/** Releases the delegated listeners so no handler outlives the element. */
|
|
11966
12341
|
disconnect() {
|
|
12342
|
+
this.#composition.disconnect();
|
|
11967
12343
|
this.tagsTarget.removeEventListener("keydown", this.#onTagKeydown);
|
|
11968
12344
|
this.tagsTarget.removeEventListener("click", this.#onTagClick);
|
|
11969
12345
|
}
|
|
12346
|
+
/** Tracks an input added initially or after connect. */
|
|
12347
|
+
inputTargetConnected(input) {
|
|
12348
|
+
this.#composition.observe(input);
|
|
12349
|
+
}
|
|
12350
|
+
/** Removes composition listeners when the active input is replaced or removed. */
|
|
12351
|
+
inputTargetDisconnected(input) {
|
|
12352
|
+
this.#composition.unobserve(input);
|
|
12353
|
+
}
|
|
11970
12354
|
/** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
|
|
11971
12355
|
onKeydown(event) {
|
|
11972
|
-
if (
|
|
12356
|
+
if (this.#composition.isComposing(event)) return;
|
|
11973
12357
|
if (event.key === "Enter" || event.key === this.delimiterValue) {
|
|
11974
12358
|
event.preventDefault();
|
|
11975
12359
|
this.#commitInput();
|
|
@@ -12536,6 +12920,7 @@ var TimePickerController = class extends Controller {
|
|
|
12536
12920
|
function pad(value) {
|
|
12537
12921
|
return String(value).padStart(2, "0");
|
|
12538
12922
|
}
|
|
12923
|
+
var DELEGATED_EVENTS = ["click", "focusin", "focusout", "keydown", "mouseover", "mouseout"];
|
|
12539
12924
|
var ToastController = class extends Controller {
|
|
12540
12925
|
static targets = ["list", "template", "item"];
|
|
12541
12926
|
static values = {
|
|
@@ -12556,34 +12941,57 @@ var ToastController = class extends Controller {
|
|
|
12556
12941
|
* Tracked so {@link disconnect} can cancel any that have not fired, preventing a
|
|
12557
12942
|
* detached element from being mutated after it leaves the DOM (Turbo).
|
|
12558
12943
|
*/
|
|
12559
|
-
#rafHandles = /* @__PURE__ */ new
|
|
12944
|
+
#rafHandles = /* @__PURE__ */ new Map();
|
|
12560
12945
|
/** Track active timeouts mapped by each toast element for safe cancellation. */
|
|
12561
12946
|
#activeTimeouts = /* @__PURE__ */ new Map();
|
|
12562
12947
|
/** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */
|
|
12563
12948
|
#pauseReasons = /* @__PURE__ */ new Map();
|
|
12949
|
+
/** The stable list that owns delegated listeners for dynamically added items. */
|
|
12950
|
+
#delegatedList = null;
|
|
12564
12951
|
connect() {
|
|
12952
|
+
this.#connectDelegatedEvents();
|
|
12565
12953
|
this.enforceMaxLimit();
|
|
12566
12954
|
for (const item of this.itemTargets) {
|
|
12567
|
-
if (!this.#activeTimeouts.has(item)) {
|
|
12955
|
+
if (!this.#activeTimeouts.has(item) && item.dataset.state !== "leaving") {
|
|
12568
12956
|
this.#startTimer(item);
|
|
12569
12957
|
}
|
|
12570
12958
|
}
|
|
12571
12959
|
}
|
|
12572
12960
|
disconnect() {
|
|
12961
|
+
this.#disconnectDelegatedEvents();
|
|
12573
12962
|
this.#timers.clearAll();
|
|
12574
|
-
for (const handle of this.#rafHandles) {
|
|
12963
|
+
for (const handle of this.#rafHandles.values()) {
|
|
12575
12964
|
window.cancelAnimationFrame(handle);
|
|
12576
12965
|
}
|
|
12577
12966
|
this.#rafHandles.clear();
|
|
12578
12967
|
this.#activeTimeouts.clear();
|
|
12579
12968
|
this.#pauseReasons.clear();
|
|
12580
12969
|
}
|
|
12970
|
+
/** Rebinds delegated interaction when Turbo replaces the list target in place. */
|
|
12971
|
+
listTargetConnected(element) {
|
|
12972
|
+
if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);
|
|
12973
|
+
}
|
|
12974
|
+
/** Releases delegation only when the removed target is its current owner. */
|
|
12975
|
+
listTargetDisconnected(element) {
|
|
12976
|
+
if (this.#delegatedList === element) this.#disconnectDelegatedEvents();
|
|
12977
|
+
}
|
|
12581
12978
|
durationValueChanged() {
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12979
|
+
for (const item of this.itemTargets) {
|
|
12980
|
+
if (item.dataset.state === "leaving") continue;
|
|
12981
|
+
const pauseReasons = this.#pauseReasons.get(item);
|
|
12982
|
+
this.#clearTimer(item);
|
|
12983
|
+
if (this.durationValue <= 0) {
|
|
12984
|
+
item.removeAttribute("data-paused");
|
|
12985
|
+
} else if (pauseReasons && pauseReasons.size > 0) {
|
|
12986
|
+
this.#pauseReasons.set(item, pauseReasons);
|
|
12987
|
+
this.#activeTimeouts.set(item, {
|
|
12988
|
+
id: 0,
|
|
12989
|
+
startedAt: 0,
|
|
12990
|
+
remaining: this.durationValue
|
|
12991
|
+
});
|
|
12992
|
+
item.setAttribute("data-paused", "true");
|
|
12993
|
+
} else {
|
|
12994
|
+
this.#startTimer(item);
|
|
12587
12995
|
}
|
|
12588
12996
|
}
|
|
12589
12997
|
}
|
|
@@ -12597,17 +13005,21 @@ var ToastController = class extends Controller {
|
|
|
12597
13005
|
*/
|
|
12598
13006
|
itemTargetConnected(element) {
|
|
12599
13007
|
this.enforceMaxLimit();
|
|
13008
|
+
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
12600
13009
|
this.#startTimer(element);
|
|
12601
13010
|
element.setAttribute("data-state", "entering");
|
|
13011
|
+
this.#cancelAnimation(element);
|
|
12602
13012
|
const handle = window.requestAnimationFrame(() => {
|
|
12603
|
-
this.#rafHandles.delete(
|
|
13013
|
+
this.#rafHandles.delete(element);
|
|
13014
|
+
if (element.parentNode !== this.listTarget || element.dataset.state === "leaving") return;
|
|
12604
13015
|
element.setAttribute("data-state", "visible");
|
|
12605
13016
|
});
|
|
12606
|
-
this.#rafHandles.
|
|
13017
|
+
this.#rafHandles.set(element, handle);
|
|
12607
13018
|
}
|
|
12608
13019
|
/** Clears any active timer when a toast is removed from the DOM. */
|
|
12609
13020
|
itemTargetDisconnected(element) {
|
|
12610
13021
|
this.#clearTimer(element);
|
|
13022
|
+
this.#cancelAnimation(element);
|
|
12611
13023
|
}
|
|
12612
13024
|
/**
|
|
12613
13025
|
* Shows a new toast. Accepts its content from either a Stimulus action param
|
|
@@ -12630,10 +13042,9 @@ var ToastController = class extends Controller {
|
|
|
12630
13042
|
const item = clone.querySelector("[data-stimeo--toast-target='item']");
|
|
12631
13043
|
if (!item) return;
|
|
12632
13044
|
const bodySlot = item.querySelector("[data-toast-slot='body']");
|
|
12633
|
-
if (bodySlot)
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
item.setAttribute("role", this.#readField(event, "type") === "alert" ? "alert" : "status");
|
|
13045
|
+
if (!bodySlot) return;
|
|
13046
|
+
bodySlot.textContent = body;
|
|
13047
|
+
bodySlot.setAttribute("role", this.#readField(event, "type") === "alert" ? "alert" : "status");
|
|
12637
13048
|
this.listTarget.appendChild(item);
|
|
12638
13049
|
this.dispatch("show", { detail: { item } });
|
|
12639
13050
|
}
|
|
@@ -12655,18 +13066,15 @@ var ToastController = class extends Controller {
|
|
|
12655
13066
|
}
|
|
12656
13067
|
/** Dismisses the toast that contained the trigger. */
|
|
12657
13068
|
dismiss(event) {
|
|
12658
|
-
const
|
|
12659
|
-
if (!target) return;
|
|
12660
|
-
const item = target.closest("[data-stimeo--toast-target='item']");
|
|
13069
|
+
const item = this.#itemFromEvent(event);
|
|
12661
13070
|
if (!item) return;
|
|
12662
13071
|
this.#removeWithTransition(item, "user");
|
|
12663
13072
|
}
|
|
12664
13073
|
/** Dismisses the focused toast when Escape is pressed. */
|
|
12665
13074
|
onKeydown(event) {
|
|
12666
13075
|
if (event.key === "Escape") {
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
const item = target.closest("[data-stimeo--toast-target='item']");
|
|
13076
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
13077
|
+
const item = this.#itemFromEvent(event);
|
|
12670
13078
|
if (!item) return;
|
|
12671
13079
|
event.preventDefault();
|
|
12672
13080
|
this.#removeWithTransition(item, "user");
|
|
@@ -12692,6 +13100,10 @@ var ToastController = class extends Controller {
|
|
|
12692
13100
|
this.#timers.clear(timeout.id);
|
|
12693
13101
|
const elapsed = Date.now() - timeout.startedAt;
|
|
12694
13102
|
const remaining = Math.max(0, timeout.remaining - elapsed);
|
|
13103
|
+
if (remaining <= 0) {
|
|
13104
|
+
this.#removeWithTransition(item, "timeout");
|
|
13105
|
+
return;
|
|
13106
|
+
}
|
|
12695
13107
|
this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });
|
|
12696
13108
|
item.setAttribute("data-paused", "true");
|
|
12697
13109
|
}
|
|
@@ -12703,14 +13115,17 @@ var ToastController = class extends Controller {
|
|
|
12703
13115
|
reasons.delete(this.#pauseReason(event));
|
|
12704
13116
|
if (reasons.size > 0) return;
|
|
12705
13117
|
const timeout = this.#activeTimeouts.get(item);
|
|
12706
|
-
if (!timeout
|
|
13118
|
+
if (!timeout) return;
|
|
13119
|
+
if (timeout.id !== 0 || timeout.remaining <= 0) return;
|
|
12707
13120
|
item.removeAttribute("data-paused");
|
|
12708
13121
|
this.#startTimer(item, timeout.remaining);
|
|
12709
13122
|
}
|
|
12710
13123
|
/** Resolves the toast item element a pause/resume event targets. */
|
|
12711
13124
|
#itemFromEvent(event) {
|
|
12712
|
-
const target = event.
|
|
12713
|
-
|
|
13125
|
+
const target = event.target instanceof Element ? event.target : event.currentTarget;
|
|
13126
|
+
if (!(target instanceof Element) || !this.hasListTarget) return null;
|
|
13127
|
+
const item = target.closest("[data-stimeo--toast-target='item']");
|
|
13128
|
+
return item && this.listTarget.contains(item) ? item : null;
|
|
12714
13129
|
}
|
|
12715
13130
|
/** Classifies a pause/resume event as a hover or focus reason. */
|
|
12716
13131
|
#pauseReason(event) {
|
|
@@ -12726,7 +13141,9 @@ var ToastController = class extends Controller {
|
|
|
12726
13141
|
return reasons;
|
|
12727
13142
|
}
|
|
12728
13143
|
#startTimer(element, duration = this.durationValue) {
|
|
12729
|
-
if (duration <= 0)
|
|
13144
|
+
if (duration <= 0 || element.dataset.state === "leaving" || element.parentNode !== this.listTarget) {
|
|
13145
|
+
return;
|
|
13146
|
+
}
|
|
12730
13147
|
this.#clearTimer(element);
|
|
12731
13148
|
const id = this.#timers.set(() => {
|
|
12732
13149
|
this.#removeWithTransition(element, "timeout");
|
|
@@ -12742,12 +13159,13 @@ var ToastController = class extends Controller {
|
|
|
12742
13159
|
this.#pauseReasons.delete(element);
|
|
12743
13160
|
}
|
|
12744
13161
|
#removeWithTransition(element, reason) {
|
|
13162
|
+
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
12745
13163
|
this.#clearTimer(element);
|
|
13164
|
+
this.#cancelAnimation(element);
|
|
12746
13165
|
element.setAttribute("data-state", "leaving");
|
|
12747
13166
|
const finalize = () => {
|
|
12748
|
-
if (element.parentNode
|
|
12749
|
-
|
|
12750
|
-
}
|
|
13167
|
+
if (element.parentNode !== this.listTarget) return;
|
|
13168
|
+
this.listTarget.removeChild(element);
|
|
12751
13169
|
this.dispatch("dismiss", { detail: { item: element, reason } });
|
|
12752
13170
|
};
|
|
12753
13171
|
const transitions = window.getComputedStyle(element).transitionDuration;
|
|
@@ -12769,14 +13187,59 @@ var ToastController = class extends Controller {
|
|
|
12769
13187
|
*/
|
|
12770
13188
|
enforceMaxLimit() {
|
|
12771
13189
|
const currentItems = this.itemTargets;
|
|
12772
|
-
|
|
12773
|
-
|
|
13190
|
+
const max = Math.max(0, this.maxValue);
|
|
13191
|
+
if (currentItems.length > max) {
|
|
13192
|
+
const excessCount = currentItems.length - max;
|
|
12774
13193
|
for (let i = 0; i < excessCount; i++) {
|
|
12775
13194
|
const oldest = currentItems[i];
|
|
12776
13195
|
if (oldest) this.#removeWithTransition(oldest, "timeout");
|
|
12777
13196
|
}
|
|
12778
13197
|
}
|
|
12779
13198
|
}
|
|
13199
|
+
/** Wires stable-container delegation so newly appended items work immediately. */
|
|
13200
|
+
#connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null) {
|
|
13201
|
+
this.#disconnectDelegatedEvents();
|
|
13202
|
+
if (!list) return;
|
|
13203
|
+
this.#delegatedList = list;
|
|
13204
|
+
for (const type of DELEGATED_EVENTS) {
|
|
13205
|
+
this.#delegatedList.addEventListener(type, this.#onListEvent);
|
|
13206
|
+
}
|
|
13207
|
+
}
|
|
13208
|
+
/** Releases every delegated listener from the exact list that owns it. */
|
|
13209
|
+
#disconnectDelegatedEvents() {
|
|
13210
|
+
if (!this.#delegatedList) return;
|
|
13211
|
+
for (const type of DELEGATED_EVENTS) {
|
|
13212
|
+
this.#delegatedList.removeEventListener(type, this.#onListEvent);
|
|
13213
|
+
}
|
|
13214
|
+
this.#delegatedList = null;
|
|
13215
|
+
}
|
|
13216
|
+
/** Routes every delegated item interaction from the stable list. */
|
|
13217
|
+
#onListEvent = (event) => {
|
|
13218
|
+
if (event.type === "click") {
|
|
13219
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
13220
|
+
const trigger = target?.closest("[data-toast-dismiss]");
|
|
13221
|
+
if (trigger && this.#delegatedList?.contains(trigger)) this.dismiss(event);
|
|
13222
|
+
} else if (event.type === "keydown") {
|
|
13223
|
+
this.onKeydown(event);
|
|
13224
|
+
} else if (this.#crossesItemBoundary(event)) {
|
|
13225
|
+
if (event.type === "focusin" || event.type === "mouseover") this.pause(event);
|
|
13226
|
+
else this.resume(event);
|
|
13227
|
+
}
|
|
13228
|
+
};
|
|
13229
|
+
/** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */
|
|
13230
|
+
#crossesItemBoundary(event) {
|
|
13231
|
+
const item = this.#itemFromEvent(event);
|
|
13232
|
+
if (!item) return false;
|
|
13233
|
+
const related = "relatedTarget" in event ? event.relatedTarget : null;
|
|
13234
|
+
return !(related instanceof Node && item.contains(related));
|
|
13235
|
+
}
|
|
13236
|
+
/** Cancels the pending entering-to-visible frame owned by one item. */
|
|
13237
|
+
#cancelAnimation(element) {
|
|
13238
|
+
const handle = this.#rafHandles.get(element);
|
|
13239
|
+
if (handle === void 0) return;
|
|
13240
|
+
window.cancelAnimationFrame(handle);
|
|
13241
|
+
this.#rafHandles.delete(element);
|
|
13242
|
+
}
|
|
12780
13243
|
};
|
|
12781
13244
|
function cssTimeToMs(value) {
|
|
12782
13245
|
const first = value.split(",")[0]?.trim() ?? "";
|
|
@@ -12939,27 +13402,39 @@ var TooltipController = class extends Controller {
|
|
|
12939
13402
|
hideDelay: { type: Number, default: 0 },
|
|
12940
13403
|
closeOnScroll: { type: Boolean, default: false }
|
|
12941
13404
|
};
|
|
12942
|
-
static actions = ["hide", "
|
|
12943
|
-
/**
|
|
13405
|
+
static actions = ["hide", "show"];
|
|
13406
|
+
/** Registry whose pending timers are cancelled individually with their guard ids. */
|
|
12944
13407
|
#timers = new SafeTimeout();
|
|
12945
|
-
/**
|
|
13408
|
+
/** Escape-stack membership while shown; the shared resolver dismisses via it. */
|
|
13409
|
+
#escapeLayer = new EscapeLayer();
|
|
13410
|
+
/** The id of the currently pending show timer, if any. */
|
|
12946
13411
|
#pendingShow = null;
|
|
13412
|
+
/** The id of the currently pending hide timer, if any. */
|
|
12947
13413
|
#pendingHide = null;
|
|
13414
|
+
/** Whether focus or the pointer currently requires the tooltip to persist. */
|
|
13415
|
+
#focusActive = false;
|
|
13416
|
+
#pointerActive = false;
|
|
12948
13417
|
/** Cleanup for the dismiss-on-scroll listeners while shown, or `null`. */
|
|
12949
13418
|
#stopScrollDismiss = null;
|
|
12950
|
-
/** Starts hidden. */
|
|
13419
|
+
/** Starts hidden with no stale timer or interaction state from a prior connection. */
|
|
12951
13420
|
connect() {
|
|
13421
|
+
this.#cancelShow();
|
|
13422
|
+
this.#cancelHide();
|
|
13423
|
+
this.#resetInteractionState();
|
|
12952
13424
|
this.#conceal();
|
|
12953
13425
|
}
|
|
12954
|
-
/** Clears timers
|
|
13426
|
+
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
12955
13427
|
disconnect() {
|
|
12956
|
-
this.#
|
|
12957
|
-
|
|
13428
|
+
this.#cancelShow();
|
|
13429
|
+
this.#cancelHide();
|
|
13430
|
+
this.#resetInteractionState();
|
|
13431
|
+
this.#escapeLayer.deactivate();
|
|
12958
13432
|
this.#stopScrollDismiss?.();
|
|
12959
13433
|
this.#stopScrollDismiss = null;
|
|
12960
13434
|
}
|
|
12961
|
-
/** Shows
|
|
12962
|
-
show() {
|
|
13435
|
+
/** Shows after `showDelay`, recording the focus/pointer reason supplied by an action event. */
|
|
13436
|
+
show(event) {
|
|
13437
|
+
this.#activateInteraction(event);
|
|
12963
13438
|
this.#cancelHide();
|
|
12964
13439
|
if (this.#isVisible || this.#pendingShow !== null) return;
|
|
12965
13440
|
if (this.showDelayValue <= 0) {
|
|
@@ -12971,8 +13446,13 @@ var TooltipController = class extends Controller {
|
|
|
12971
13446
|
this.#reveal();
|
|
12972
13447
|
}, this.showDelayValue);
|
|
12973
13448
|
}
|
|
12974
|
-
/** Hides
|
|
12975
|
-
hide() {
|
|
13449
|
+
/** Hides after `hideDelay` once no focus/pointer reason remains; eventless calls are explicit. */
|
|
13450
|
+
hide(event) {
|
|
13451
|
+
const interactionEnded = this.#deactivateInteraction(event);
|
|
13452
|
+
if (interactionEnded && this.#hasActiveInteraction) {
|
|
13453
|
+
this.#cancelHide();
|
|
13454
|
+
return;
|
|
13455
|
+
}
|
|
12976
13456
|
this.#cancelShow();
|
|
12977
13457
|
if (!this.#isVisible || this.#pendingHide !== null) return;
|
|
12978
13458
|
if (this.hideDelayValue <= 0) {
|
|
@@ -12984,52 +13464,57 @@ var TooltipController = class extends Controller {
|
|
|
12984
13464
|
this.#conceal();
|
|
12985
13465
|
}, this.hideDelayValue);
|
|
12986
13466
|
}
|
|
12987
|
-
/**
|
|
12988
|
-
* Dismisses the tooltip on `Escape` from the trigger. The authoritative
|
|
12989
|
-
* dismissal path is the document-level listener (added while shown) so a
|
|
12990
|
-
* hover-triggered tooltip is dismissible regardless of focus; this handler
|
|
12991
|
-
* keeps the documented `keydown->#onKeydown` binding meaningful too.
|
|
12992
|
-
*/
|
|
12993
|
-
onKeydown(event) {
|
|
12994
|
-
if (event.key === "Escape" && this.#isVisible) {
|
|
12995
|
-
event.preventDefault();
|
|
12996
|
-
this.#cancelShow();
|
|
12997
|
-
this.#cancelHide();
|
|
12998
|
-
this.#conceal();
|
|
12999
|
-
}
|
|
13000
|
-
}
|
|
13001
|
-
/** Reveals the content and starts watching for a dismissing `Escape`/scroll. */
|
|
13467
|
+
/** Reveals the content and joins the Escape stack / starts the scroll watcher. */
|
|
13002
13468
|
#reveal() {
|
|
13003
13469
|
if (!this.hasContentTarget) return;
|
|
13004
13470
|
this.contentTarget.hidden = false;
|
|
13005
13471
|
this.contentTarget.setAttribute("data-state", "open");
|
|
13006
|
-
|
|
13472
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss() });
|
|
13007
13473
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
13008
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () =>
|
|
13009
|
-
this.#cancelShow();
|
|
13010
|
-
this.#cancelHide();
|
|
13011
|
-
this.#conceal();
|
|
13012
|
-
});
|
|
13474
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
13013
13475
|
}
|
|
13014
13476
|
}
|
|
13015
|
-
/** Hides the content and stops
|
|
13477
|
+
/** Hides the content and leaves the Escape stack / stops the scroll watcher. */
|
|
13016
13478
|
#conceal() {
|
|
13017
|
-
|
|
13479
|
+
this.#escapeLayer.deactivate();
|
|
13018
13480
|
this.#stopScrollDismiss?.();
|
|
13019
13481
|
this.#stopScrollDismiss = null;
|
|
13020
13482
|
if (!this.hasContentTarget) return;
|
|
13021
13483
|
this.contentTarget.hidden = true;
|
|
13022
13484
|
this.contentTarget.setAttribute("data-state", "closed");
|
|
13023
13485
|
}
|
|
13024
|
-
/**
|
|
13025
|
-
#
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13486
|
+
/** Cancels pending timers and conceals immediately (shared Escape / scroll path). */
|
|
13487
|
+
#dismiss() {
|
|
13488
|
+
this.#cancelShow();
|
|
13489
|
+
this.#cancelHide();
|
|
13490
|
+
this.#conceal();
|
|
13491
|
+
}
|
|
13492
|
+
/** Records the modality whose enter/focus event requires the tooltip to stay visible. */
|
|
13493
|
+
#activateInteraction(event) {
|
|
13494
|
+
if (event?.type === "mouseenter") this.#pointerActive = true;
|
|
13495
|
+
if (event?.type === "focusin") this.#focusActive = true;
|
|
13496
|
+
}
|
|
13497
|
+
/** Clears a modality on leave/blur and reports whether the event represented such a change. */
|
|
13498
|
+
#deactivateInteraction(event) {
|
|
13499
|
+
if (event?.type === "mouseleave") {
|
|
13500
|
+
this.#pointerActive = false;
|
|
13501
|
+
return true;
|
|
13031
13502
|
}
|
|
13032
|
-
|
|
13503
|
+
if (event?.type === "focusout") {
|
|
13504
|
+
this.#focusActive = false;
|
|
13505
|
+
return true;
|
|
13506
|
+
}
|
|
13507
|
+
return false;
|
|
13508
|
+
}
|
|
13509
|
+
/** Discards interaction reasons at a lifecycle boundary. */
|
|
13510
|
+
#resetInteractionState() {
|
|
13511
|
+
this.#focusActive = false;
|
|
13512
|
+
this.#pointerActive = false;
|
|
13513
|
+
}
|
|
13514
|
+
/** Whether focus or pointer presence still requires a persistent tooltip. */
|
|
13515
|
+
get #hasActiveInteraction() {
|
|
13516
|
+
return this.#focusActive || this.#pointerActive;
|
|
13517
|
+
}
|
|
13033
13518
|
/** Cancels any pending show timer. */
|
|
13034
13519
|
#cancelShow() {
|
|
13035
13520
|
if (this.#pendingShow !== null) {
|