stimeo-ui 0.15.0 → 0.17.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 +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,49 +1,195 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
17
27
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
88
|
+
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
24
141
|
}
|
|
25
142
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
var DetachGate = class _DetachGate {
|
|
151
|
+
#pending = false;
|
|
152
|
+
get pending() {
|
|
153
|
+
return this.#pending;
|
|
154
|
+
}
|
|
155
|
+
static isDetached(host) {
|
|
156
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
157
|
+
}
|
|
158
|
+
disconnected(host, teardown) {
|
|
159
|
+
const carried = _DetachGate.#carried(host);
|
|
160
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
161
|
+
this.#pending = false;
|
|
162
|
+
teardown();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.#pending = true;
|
|
166
|
+
const probe = () => {
|
|
167
|
+
if (!this.#pending) return;
|
|
168
|
+
this.#pending = false;
|
|
169
|
+
teardown();
|
|
170
|
+
};
|
|
171
|
+
if (carried) setTimeout(probe, 0);
|
|
172
|
+
else queueMicrotask(probe);
|
|
173
|
+
}
|
|
174
|
+
static #carried(host) {
|
|
175
|
+
const element = host.element;
|
|
176
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
177
|
+
}
|
|
178
|
+
static #listed(host) {
|
|
179
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
180
|
+
return tokens.includes(host.identifier);
|
|
181
|
+
}
|
|
182
|
+
cancel() {
|
|
183
|
+
this.#pending = false;
|
|
31
184
|
}
|
|
32
185
|
};
|
|
33
186
|
|
|
34
|
-
// src/utils/escape_layer.ts
|
|
35
187
|
var EscapeLayer = class _EscapeLayer {
|
|
36
|
-
static #registries =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#claims = null;
|
|
42
|
-
/**
|
|
43
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
44
|
-
* the document's shared resolver listener if this is its first layer.
|
|
45
|
-
* Re-activating an already-active layer moves it to the top.
|
|
46
|
-
*/
|
|
188
|
+
static #registries = sharedRegistry(
|
|
189
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
190
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
191
|
+
);
|
|
192
|
+
#active = null;
|
|
47
193
|
activate(ownerDocument = document, options) {
|
|
48
194
|
this.deactivate();
|
|
49
195
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -52,44 +198,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
52
198
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
53
199
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
54
200
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.#
|
|
58
|
-
this.#claims = options.claims ?? null;
|
|
201
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
202
|
+
registry.stack.push(entry);
|
|
203
|
+
this.#active = { document: ownerDocument, entry };
|
|
59
204
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
62
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
63
|
-
*/
|
|
64
205
|
deactivate() {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
const registry = _EscapeLayer.#registries.get(
|
|
206
|
+
const active = this.#active;
|
|
207
|
+
if (!active) return;
|
|
208
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
68
209
|
if (registry) {
|
|
69
|
-
const index = registry.stack.lastIndexOf(
|
|
210
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
70
211
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
71
212
|
if (registry.stack.length === 0) {
|
|
72
|
-
|
|
73
|
-
_EscapeLayer.#registries.delete(
|
|
213
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
214
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
74
215
|
}
|
|
75
216
|
}
|
|
76
|
-
this.#
|
|
77
|
-
this.#onDismiss = null;
|
|
78
|
-
this.#claims = null;
|
|
217
|
+
this.#active = null;
|
|
79
218
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
82
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
83
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
84
|
-
*/
|
|
85
219
|
get ownsEscape() {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
const registry = _EscapeLayer.#registries.get(
|
|
220
|
+
const active = this.#active;
|
|
221
|
+
if (!active) return false;
|
|
222
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
89
223
|
if (!registry) return false;
|
|
90
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
224
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
91
225
|
}
|
|
92
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
93
226
|
static #createRegistry() {
|
|
94
227
|
const registry = {
|
|
95
228
|
stack: [],
|
|
@@ -98,24 +231,22 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
98
231
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
99
232
|
if (!owner) return;
|
|
100
233
|
event.preventDefault();
|
|
101
|
-
owner
|
|
234
|
+
owner.onDismiss();
|
|
102
235
|
}
|
|
103
236
|
};
|
|
104
237
|
return registry;
|
|
105
238
|
}
|
|
106
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
107
239
|
static #resolveOwner(stack) {
|
|
108
240
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
109
241
|
const layer = stack[index];
|
|
110
242
|
if (!layer) continue;
|
|
111
|
-
if (layer
|
|
243
|
+
if (layer.claims && !layer.claims()) continue;
|
|
112
244
|
return layer;
|
|
113
245
|
}
|
|
114
246
|
return null;
|
|
115
247
|
}
|
|
116
248
|
};
|
|
117
249
|
|
|
118
|
-
// src/utils/focus_candidate.ts
|
|
119
250
|
function inheritsFieldsetDisabled(control) {
|
|
120
251
|
let fieldset = control.closest("fieldset[disabled]");
|
|
121
252
|
while (fieldset) {
|
|
@@ -125,8 +256,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
125
256
|
}
|
|
126
257
|
return false;
|
|
127
258
|
}
|
|
259
|
+
function unslotted(element) {
|
|
260
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
261
|
+
}
|
|
262
|
+
function flatTreeParent(element) {
|
|
263
|
+
if (unslotted(element)) return null;
|
|
264
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
265
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
266
|
+
return parent instanceof Element ? parent : null;
|
|
267
|
+
}
|
|
268
|
+
function flatTreeContains(ancestor, element) {
|
|
269
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
270
|
+
if (node === ancestor) return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
function closestInFlatTree(element, selector) {
|
|
275
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
276
|
+
if (node.matches(selector)) return node;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function flatTreeChildren(element) {
|
|
281
|
+
return ownedScope(element) ?? Array.from(element.children);
|
|
282
|
+
}
|
|
128
283
|
function canTakeFocus(element) {
|
|
129
|
-
|
|
284
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
285
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
286
|
+
}
|
|
130
287
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
288
|
if (!("disabled" in element)) return true;
|
|
132
289
|
if (element.disabled) return false;
|
|
@@ -183,189 +340,692 @@ function isTabStop(element) {
|
|
|
183
340
|
if (tabindex !== null) return tabindex >= 0;
|
|
184
341
|
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
342
|
}
|
|
186
|
-
function
|
|
187
|
-
return
|
|
188
|
-
|
|
343
|
+
function isRadio(element) {
|
|
344
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
345
|
+
}
|
|
346
|
+
function sameRadioGroup(a, b) {
|
|
347
|
+
return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
348
|
+
}
|
|
349
|
+
function isInEditableContent(element) {
|
|
350
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
351
|
+
if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
|
|
352
|
+
if (hasEditableTabStop(node)) return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
357
|
+
function isSvgLink(element) {
|
|
358
|
+
return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
|
|
359
|
+
}
|
|
360
|
+
function hasRenderedImage(area) {
|
|
361
|
+
const map = area.closest("map");
|
|
362
|
+
const key = map?.name || map?.id;
|
|
363
|
+
if (!map || !key) return false;
|
|
364
|
+
const root = map.getRootNode();
|
|
365
|
+
return Array.from(root.querySelectorAll("img[usemap]")).some(
|
|
366
|
+
(image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
|
|
189
367
|
);
|
|
190
368
|
}
|
|
369
|
+
function isFocusableInOrder(element) {
|
|
370
|
+
const tabindex = authoredTabindex(element);
|
|
371
|
+
return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
|
|
372
|
+
}
|
|
373
|
+
function extraStop(element) {
|
|
374
|
+
if (element instanceof HTMLAreaElement) {
|
|
375
|
+
const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
|
|
376
|
+
return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
|
|
377
|
+
}
|
|
378
|
+
if (element instanceof HTMLObjectElement) {
|
|
379
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
|
|
380
|
+
}
|
|
381
|
+
if (isSvgLink(element))
|
|
382
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function isSequentialStop(element) {
|
|
386
|
+
const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
|
|
387
|
+
if (!stop) return false;
|
|
388
|
+
const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
|
|
389
|
+
return !(link && authoredTabindex(element) === null && isInEditableContent(element));
|
|
390
|
+
}
|
|
391
|
+
function ownedScope(element) {
|
|
392
|
+
if (element.shadowRoot) return Array.from(element.shadowRoot.children);
|
|
393
|
+
if (element instanceof HTMLSlotElement) {
|
|
394
|
+
const assigned = element.assignedElements();
|
|
395
|
+
return assigned.length > 0 ? assigned : Array.from(element.children);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function orderScope(elements, walk) {
|
|
400
|
+
const entries = [];
|
|
401
|
+
const visit = (element) => {
|
|
402
|
+
if (element === walk.from) walk.fromAt = walk.tree.length;
|
|
403
|
+
const tabindex = authoredTabindex(element) ?? 0;
|
|
404
|
+
const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
|
|
405
|
+
if (stop) walk.tree.push(stop);
|
|
406
|
+
const owned = ownedScope(element);
|
|
407
|
+
if (owned) {
|
|
408
|
+
if (tabindex >= 0) {
|
|
409
|
+
entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (stop) entries.push({ tabindex, stops: [stop] });
|
|
414
|
+
for (const child of Array.from(element.children)) visit(child);
|
|
415
|
+
};
|
|
416
|
+
for (const element of elements) visit(element);
|
|
417
|
+
const positive = entries.filter((entry) => entry.tabindex > 0);
|
|
418
|
+
positive.sort((a, b) => a.tabindex - b.tabindex);
|
|
419
|
+
const rest = entries.filter((entry) => entry.tabindex === 0);
|
|
420
|
+
return [...positive, ...rest].flatMap((entry) => entry.stops);
|
|
421
|
+
}
|
|
422
|
+
function passesRadioRule(element, tree, focusedRadio) {
|
|
423
|
+
if (!isRadio(element) || element.checked) return true;
|
|
424
|
+
const group = tree.filter(
|
|
425
|
+
(stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
|
|
426
|
+
);
|
|
427
|
+
if (group.some((radio) => radio.checked)) return false;
|
|
428
|
+
if (group[0] !== element) return false;
|
|
429
|
+
return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
|
|
430
|
+
}
|
|
431
|
+
function startIndex(order, walk, backward, isStop) {
|
|
432
|
+
if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
|
|
433
|
+
const at = order.indexOf(walk.from);
|
|
434
|
+
if (at >= 0) return backward ? at - 1 : at + 1;
|
|
435
|
+
const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
|
|
436
|
+
if (neighbour) return order.indexOf(neighbour);
|
|
437
|
+
return backward ? order.length - 1 : 0;
|
|
438
|
+
}
|
|
439
|
+
function sequentialTabStops(root, from = null, backward = false) {
|
|
440
|
+
const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
|
|
441
|
+
const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
|
|
442
|
+
const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
|
|
443
|
+
const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
|
|
444
|
+
const start = startIndex(order, walk, backward, isStop);
|
|
445
|
+
const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
|
|
446
|
+
return visited.filter((element) => element !== from && isStop(element));
|
|
447
|
+
}
|
|
448
|
+
function deepActiveElement(ownerDocument) {
|
|
449
|
+
let active = ownerDocument.activeElement;
|
|
450
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
451
|
+
return active;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
var MicrotaskCoalescer = class {
|
|
455
|
+
#run;
|
|
456
|
+
#queued = false;
|
|
457
|
+
#active = false;
|
|
458
|
+
#generation = 0;
|
|
459
|
+
constructor(run) {
|
|
460
|
+
this.#run = run;
|
|
461
|
+
}
|
|
462
|
+
activate() {
|
|
463
|
+
this.#active = true;
|
|
464
|
+
}
|
|
465
|
+
cancel() {
|
|
466
|
+
this.#active = false;
|
|
467
|
+
this.#queued = false;
|
|
468
|
+
this.#generation += 1;
|
|
469
|
+
}
|
|
470
|
+
schedule() {
|
|
471
|
+
if (!this.#active || this.#queued) return;
|
|
472
|
+
this.#queued = true;
|
|
473
|
+
const generation = this.#generation;
|
|
474
|
+
queueMicrotask(() => {
|
|
475
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
476
|
+
this.#queued = false;
|
|
477
|
+
this.#run();
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
function queryOpenDescendants(root, selector) {
|
|
483
|
+
const matches = [];
|
|
484
|
+
const roots = [root];
|
|
485
|
+
for (const scope of roots) {
|
|
486
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
487
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
488
|
+
const shadow = scope.shadowRoot;
|
|
489
|
+
if (shadow) roots.push(shadow);
|
|
490
|
+
}
|
|
491
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
492
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return matches;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
var TabindexLoan = class _TabindexLoan {
|
|
499
|
+
static #registry = sharedRegistry(
|
|
500
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
501
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
502
|
+
);
|
|
503
|
+
#value;
|
|
504
|
+
#record;
|
|
505
|
+
#lent = /* @__PURE__ */ new Set();
|
|
506
|
+
constructor(value, owner) {
|
|
507
|
+
this.#value = value;
|
|
508
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
509
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
510
|
+
}
|
|
511
|
+
lend(element) {
|
|
512
|
+
const stack = this.#stack(element);
|
|
513
|
+
const current = element.getAttribute("tabindex");
|
|
514
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
515
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
516
|
+
);
|
|
517
|
+
const linked = stack.at(-1)?.value === current;
|
|
518
|
+
if (!held && current !== null && !linked) return;
|
|
519
|
+
this.#lent.add(element);
|
|
520
|
+
if (held) held.loan = this;
|
|
521
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
522
|
+
this.#mark(element, stack);
|
|
523
|
+
if (current === null || !held && current !== this.#value) {
|
|
524
|
+
element.setAttribute("tabindex", this.#value);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
returnAll() {
|
|
528
|
+
for (const element of this.#lent) {
|
|
529
|
+
const stack = this.#stack(element);
|
|
530
|
+
const hold = stack.find((held) => held.loan === this);
|
|
531
|
+
if (hold) this.#release(element, stack, hold);
|
|
532
|
+
}
|
|
533
|
+
this.#lent.clear();
|
|
534
|
+
}
|
|
535
|
+
reclaim(element) {
|
|
536
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
537
|
+
const stack = this.#stack(element);
|
|
538
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
539
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
540
|
+
}
|
|
541
|
+
reclaimWithin(root) {
|
|
542
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
543
|
+
this.reclaim(element);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
#stack(element) {
|
|
547
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
548
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
549
|
+
stacks.set(element, stack);
|
|
550
|
+
return stack;
|
|
551
|
+
}
|
|
552
|
+
#seed(element) {
|
|
553
|
+
const current = element.getAttribute("tabindex");
|
|
554
|
+
const holds = [];
|
|
555
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
556
|
+
const value = element.getAttribute(record);
|
|
557
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
558
|
+
}
|
|
559
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
560
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
561
|
+
return holds;
|
|
562
|
+
}
|
|
563
|
+
#release(element, stack, hold) {
|
|
564
|
+
const current = element.getAttribute("tabindex");
|
|
565
|
+
const index = stack.indexOf(hold);
|
|
566
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
567
|
+
stack.splice(index, 1);
|
|
568
|
+
const above = stack[index];
|
|
569
|
+
if (above) above.linked &&= hold.linked;
|
|
570
|
+
this.#mark(element, stack);
|
|
571
|
+
if (!owns) return;
|
|
572
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
573
|
+
if (!below) element.removeAttribute("tabindex");
|
|
574
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
575
|
+
}
|
|
576
|
+
#mark(element, stack) {
|
|
577
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
578
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
579
|
+
for (const [name, recorded] of records) {
|
|
580
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
581
|
+
if (recorded === null) element.removeAttribute(name);
|
|
582
|
+
else element.setAttribute(name, recorded);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
191
586
|
|
|
192
|
-
|
|
193
|
-
var
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
587
|
+
var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
|
|
588
|
+
var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
|
|
589
|
+
var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
|
|
590
|
+
var FocusTrap = class _FocusTrap {
|
|
591
|
+
static #registries = sharedRegistry(
|
|
592
|
+
"stimeo-ui.focus-trap.registry.v2",
|
|
593
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
594
|
+
);
|
|
595
|
+
static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
596
|
+
"text",
|
|
597
|
+
"search",
|
|
598
|
+
"email",
|
|
599
|
+
"url",
|
|
600
|
+
"tel",
|
|
601
|
+
"password",
|
|
602
|
+
"number"
|
|
603
|
+
]);
|
|
604
|
+
#openers = [];
|
|
605
|
+
#entry;
|
|
606
|
+
#tabindexLoans = /* @__PURE__ */ new Map();
|
|
203
607
|
#activeState = false;
|
|
204
|
-
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
205
608
|
#escapeLayer = new EscapeLayer();
|
|
206
|
-
|
|
609
|
+
#gate = new DetachGate();
|
|
207
610
|
#getContainer;
|
|
208
|
-
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
209
611
|
#options;
|
|
210
|
-
/**
|
|
211
|
-
* @param getContainer - Returns the trapped element. Called on every operation
|
|
212
|
-
* so the live target is always used.
|
|
213
|
-
* @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
|
|
214
|
-
*/
|
|
215
612
|
constructor(getContainer, options = {}) {
|
|
216
613
|
this.#getContainer = getContainer;
|
|
217
614
|
this.#options = options;
|
|
615
|
+
this.#entry = {
|
|
616
|
+
container: () => this.#readContainer(),
|
|
617
|
+
moveFocus: (backward) => this.#moveFocus(backward),
|
|
618
|
+
refocus: () => this.#refocus(),
|
|
619
|
+
isolated: false,
|
|
620
|
+
lockedBody: null,
|
|
621
|
+
openedFrom: null,
|
|
622
|
+
handedBack: []
|
|
623
|
+
};
|
|
218
624
|
}
|
|
219
|
-
/** Whether the trap is currently active. */
|
|
220
625
|
get active() {
|
|
221
626
|
return this.#activeState;
|
|
222
627
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Applies the trap: records the current focus, optionally locks background scroll
|
|
225
|
-
* and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
|
|
226
|
-
* `autoFocus` is off) moves focus inside. No-ops if already active.
|
|
227
|
-
*/
|
|
228
628
|
activate() {
|
|
229
629
|
if (this.#activeState) return;
|
|
630
|
+
_FocusTrap.#sweep();
|
|
230
631
|
this.#activeState = true;
|
|
231
|
-
|
|
232
|
-
this.#
|
|
233
|
-
if (this.#flag(this.#options.lockScroll, true))
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
239
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
240
|
-
this.#beforeCache.activate();
|
|
632
|
+
this.#openers = _FocusTrap.#focusPath();
|
|
633
|
+
this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
|
|
634
|
+
if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
|
|
635
|
+
this.#entry.isolated = this.#flag(this.#options.isolate, true);
|
|
636
|
+
this.#joinStack();
|
|
637
|
+
_FocusTrap.#reconcileBackground(document);
|
|
241
638
|
const onEscape = this.#options.onEscape;
|
|
242
639
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
243
640
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
244
641
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Reverts every side effect applied by {@link activate}. No-ops if inactive, so
|
|
247
|
-
* a controller can call it defensively from both `close()` and `disconnect()`.
|
|
248
|
-
*
|
|
249
|
-
* @param restoreFocus - Move focus back to the opener (default `true`). Pass
|
|
250
|
-
* `false` on teardown (`disconnect`), where yanking focus is undesirable.
|
|
251
|
-
*/
|
|
252
642
|
deactivate({ restoreFocus = true } = {}) {
|
|
253
|
-
if (!this.#activeState)
|
|
643
|
+
if (!this.#activeState) {
|
|
644
|
+
this.#returnTabindexes();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
254
647
|
this.#activeState = false;
|
|
255
648
|
this.#escapeLayer.deactivate();
|
|
256
|
-
|
|
257
|
-
this.#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
649
|
+
const above = this.#leaveStack();
|
|
650
|
+
this.#unlockScroll();
|
|
651
|
+
_FocusTrap.#reconcileBackground(document);
|
|
652
|
+
const wayBack = this.#handOver(above);
|
|
653
|
+
if (restoreFocus) this.#restore(wayBack, above);
|
|
654
|
+
this.#returnTabindexes();
|
|
655
|
+
}
|
|
656
|
+
connect() {
|
|
657
|
+
this.#gate.cancel();
|
|
658
|
+
_FocusTrap.#sweep();
|
|
659
|
+
new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
|
|
660
|
+
}
|
|
661
|
+
disconnect(host) {
|
|
662
|
+
if (this.#activeState) this.deactivate({ restoreFocus: false });
|
|
663
|
+
this.#gate.disconnected(host, () => this.#returnTabindexes());
|
|
664
|
+
}
|
|
665
|
+
refreshIsolation() {
|
|
666
|
+
if (!this.#activeState) return;
|
|
667
|
+
const isolate = this.#flag(this.#options.isolate, true);
|
|
668
|
+
if (isolate === this.#entry.isolated) return;
|
|
669
|
+
this.#entry.isolated = isolate;
|
|
670
|
+
const stranded = _FocusTrap.#reconcileBackground(document);
|
|
671
|
+
const active = deepActiveElement(document);
|
|
672
|
+
if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
|
|
673
|
+
}
|
|
674
|
+
refreshContainer() {
|
|
675
|
+
if (!this.#activeState) return;
|
|
676
|
+
_FocusTrap.#reconcileBackground(document);
|
|
677
|
+
this.#returnTabindexes();
|
|
678
|
+
if (this.#ownsTab()) this.#refocus();
|
|
267
679
|
}
|
|
268
|
-
/** Resolves a boolean-or-getter option, defaulting when it was not provided. */
|
|
269
680
|
#flag(option, fallback) {
|
|
270
681
|
if (option === void 0) return fallback;
|
|
271
682
|
return typeof option === "function" ? option() : option;
|
|
272
683
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* is left alone mid-navigation. The listener lives only while active.
|
|
280
|
-
*/
|
|
281
|
-
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
282
|
-
/**
|
|
283
|
-
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
284
|
-
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
285
|
-
* which layer currently owns Escape.
|
|
286
|
-
*/
|
|
287
|
-
#onKeydown = (event) => {
|
|
288
|
-
if (event.key === "Tab") this.#trapTab(event);
|
|
289
|
-
};
|
|
290
|
-
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
291
|
-
#trapTab(event) {
|
|
292
|
-
const focusable = this.#focusableElements();
|
|
293
|
-
if (focusable.length === 0) {
|
|
294
|
-
event.preventDefault();
|
|
295
|
-
return;
|
|
684
|
+
#joinStack() {
|
|
685
|
+
const registry = _FocusTrap.#registry(document);
|
|
686
|
+
if (registry.stack.length === 0) {
|
|
687
|
+
document.addEventListener("keydown", registry.onKeydown);
|
|
688
|
+
document.addEventListener("turbo:morph", registry.onMorph);
|
|
689
|
+
document.addEventListener("turbo:morph-element", registry.onMorph);
|
|
296
690
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
691
|
+
registry.stack.push(this.#entry);
|
|
692
|
+
}
|
|
693
|
+
#leaveStack() {
|
|
694
|
+
const registry = _FocusTrap.#registry(document);
|
|
695
|
+
const index = registry.stack.indexOf(this.#entry);
|
|
696
|
+
const above = registry.stack.slice(index + 1);
|
|
697
|
+
registry.stack.splice(index, 1);
|
|
698
|
+
if (registry.stack.length === 0) {
|
|
699
|
+
document.removeEventListener("keydown", registry.onKeydown);
|
|
700
|
+
document.removeEventListener("turbo:morph", registry.onMorph);
|
|
701
|
+
document.removeEventListener("turbo:morph-element", registry.onMorph);
|
|
304
702
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
703
|
+
return above;
|
|
704
|
+
}
|
|
705
|
+
static #focusPath() {
|
|
706
|
+
const path = [];
|
|
707
|
+
for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
|
|
708
|
+
if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
|
|
709
|
+
}
|
|
710
|
+
return path;
|
|
711
|
+
}
|
|
712
|
+
static #trapHolding(element) {
|
|
713
|
+
for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
|
|
714
|
+
const container = trap.container();
|
|
715
|
+
if (container && flatTreeContains(container, element)) return trap;
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
#handOver(above) {
|
|
720
|
+
const entry = this.#entry;
|
|
721
|
+
const wayBack = [
|
|
722
|
+
...this.#openers,
|
|
723
|
+
this.#options.fallbackFocus?.() ?? null,
|
|
724
|
+
...entry.handedBack
|
|
725
|
+
].filter((element) => element !== null);
|
|
726
|
+
for (const trap of above) {
|
|
727
|
+
if (trap.openedFrom !== entry) continue;
|
|
728
|
+
trap.handedBack = [...trap.handedBack, ...wayBack];
|
|
729
|
+
trap.openedFrom = entry.openedFrom;
|
|
730
|
+
}
|
|
731
|
+
this.#openers = [];
|
|
732
|
+
entry.openedFrom = null;
|
|
733
|
+
entry.handedBack = [];
|
|
734
|
+
return wayBack;
|
|
735
|
+
}
|
|
736
|
+
#restore(wayBack, above) {
|
|
737
|
+
const active = deepActiveElement(document);
|
|
738
|
+
const released = this.#readContainer();
|
|
739
|
+
const holds = (trap) => {
|
|
740
|
+
const container = trap.container();
|
|
741
|
+
if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
|
|
745
|
+
};
|
|
746
|
+
if (above.some(holds)) return;
|
|
747
|
+
for (const element of wayBack) {
|
|
748
|
+
if (this.#activeState) return;
|
|
749
|
+
if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
|
|
750
|
+
const before = deepActiveElement(document);
|
|
751
|
+
element.focus();
|
|
752
|
+
const after = deepActiveElement(document);
|
|
753
|
+
if (after === element || after !== before) return;
|
|
754
|
+
}
|
|
755
|
+
_FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
|
|
756
|
+
}
|
|
757
|
+
#refocus() {
|
|
758
|
+
if (!this.#flag(this.#options.autoFocus, true)) return;
|
|
759
|
+
if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
|
|
760
|
+
this.#focusInitial();
|
|
761
|
+
}
|
|
762
|
+
#lendTabindex(container) {
|
|
763
|
+
const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
|
|
764
|
+
loan.lend(container);
|
|
765
|
+
this.#tabindexLoans.set(container, loan);
|
|
766
|
+
}
|
|
767
|
+
#returnTabindexes() {
|
|
768
|
+
const focused = deepActiveElement(document);
|
|
769
|
+
for (const [element, loan] of [...this.#tabindexLoans]) {
|
|
770
|
+
if (element === focused) continue;
|
|
771
|
+
loan.returnAll();
|
|
772
|
+
this.#tabindexLoans.delete(element);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
#lockScroll() {
|
|
776
|
+
const registry = _FocusTrap.#registry(document);
|
|
777
|
+
const body = document.body;
|
|
778
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
779
|
+
registry.scrollLocks.add(this.#entry);
|
|
780
|
+
this.#entry.lockedBody = body;
|
|
781
|
+
body.style.overflow = "hidden";
|
|
782
|
+
}
|
|
783
|
+
#unlockScroll() {
|
|
784
|
+
const registry = _FocusTrap.#registry(document);
|
|
785
|
+
const body = this.#entry.lockedBody;
|
|
786
|
+
this.#entry.lockedBody = null;
|
|
787
|
+
if (!registry.scrollLocks.delete(this.#entry) || !body) return;
|
|
788
|
+
if (!_FocusTrap.#isLocked(registry, body)) _FocusTrap.#unlock(body);
|
|
789
|
+
}
|
|
790
|
+
static #isLocked(registry, body) {
|
|
791
|
+
return [...registry.scrollLocks].some((trap) => trap.lockedBody === body);
|
|
792
|
+
}
|
|
793
|
+
static #unlock(body) {
|
|
794
|
+
const saved = body.getAttribute(OVERFLOW_MARK);
|
|
795
|
+
if (saved === null) return;
|
|
796
|
+
body.removeAttribute(OVERFLOW_MARK);
|
|
797
|
+
body.style.overflow = saved;
|
|
798
|
+
}
|
|
799
|
+
static #sweep() {
|
|
800
|
+
const registry = _FocusTrap.#registry(document);
|
|
801
|
+
for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
|
|
802
|
+
if (registry.inerted.has(element)) continue;
|
|
803
|
+
element.removeAttribute(INERT_MARK);
|
|
804
|
+
element.inert = false;
|
|
805
|
+
}
|
|
806
|
+
if (!_FocusTrap.#isLocked(registry, document.body)) _FocusTrap.#unlock(document.body);
|
|
807
|
+
}
|
|
808
|
+
static #registry(ownerDocument) {
|
|
809
|
+
const existing = _FocusTrap.#registries.get(ownerDocument);
|
|
810
|
+
if (existing) return existing;
|
|
811
|
+
const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry));
|
|
812
|
+
morphs.activate();
|
|
813
|
+
const registry = {
|
|
814
|
+
stack: [],
|
|
815
|
+
onKeydown: (event) => _FocusTrap.#resolveTab(registry.stack, event),
|
|
816
|
+
onMorph: () => morphs.schedule(),
|
|
817
|
+
inerted: /* @__PURE__ */ new Set(),
|
|
818
|
+
scrollLocks: /* @__PURE__ */ new Set()
|
|
819
|
+
};
|
|
820
|
+
_FocusTrap.#registries.set(ownerDocument, registry);
|
|
821
|
+
return registry;
|
|
822
|
+
}
|
|
823
|
+
static #reapply(registry) {
|
|
824
|
+
_FocusTrap.#reconcileBackground(document);
|
|
825
|
+
const body = document.body;
|
|
826
|
+
if (!_FocusTrap.#isLocked(registry, body)) return;
|
|
827
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
828
|
+
body.style.overflow = "hidden";
|
|
829
|
+
}
|
|
830
|
+
static #reconcileBackground(ownerDocument) {
|
|
831
|
+
const registry = _FocusTrap.#registry(ownerDocument);
|
|
832
|
+
for (const element of registry.inerted) {
|
|
833
|
+
if (!element.hasAttribute(INERT_MARK)) registry.inerted.delete(element);
|
|
834
|
+
}
|
|
835
|
+
const background = _FocusTrap.#backgroundOf(registry);
|
|
836
|
+
for (const element of registry.inerted) {
|
|
837
|
+
if (background.has(element)) continue;
|
|
838
|
+
element.removeAttribute(INERT_MARK);
|
|
839
|
+
element.inert = false;
|
|
840
|
+
registry.inerted.delete(element);
|
|
841
|
+
}
|
|
842
|
+
const added = [];
|
|
843
|
+
for (const element of background) {
|
|
844
|
+
if (registry.inerted.has(element)) continue;
|
|
845
|
+
element.setAttribute(INERT_MARK, "");
|
|
846
|
+
element.inert = true;
|
|
847
|
+
registry.inerted.add(element);
|
|
848
|
+
added.push(element);
|
|
849
|
+
}
|
|
850
|
+
return added;
|
|
851
|
+
}
|
|
852
|
+
static #backgroundOf(registry) {
|
|
853
|
+
const containers = registry.stack.map((trap) => trap.container());
|
|
854
|
+
let top = -1;
|
|
855
|
+
registry.stack.forEach((trap, index) => {
|
|
856
|
+
if (trap.isolated && containers[index]) top = index;
|
|
857
|
+
});
|
|
858
|
+
const background = /* @__PURE__ */ new Set();
|
|
859
|
+
if (top < 0) return background;
|
|
860
|
+
const kept = /* @__PURE__ */ new Set();
|
|
861
|
+
const keep = (path) => {
|
|
862
|
+
for (const node of path) {
|
|
863
|
+
kept.add(node);
|
|
864
|
+
background.delete(node);
|
|
865
|
+
}
|
|
866
|
+
for (const node of path) {
|
|
867
|
+
const parent = flatTreeParent(node);
|
|
868
|
+
for (const sibling of parent ? flatTreeChildren(parent) : []) {
|
|
869
|
+
if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
|
|
870
|
+
if (sibling.inert && !registry.inerted.has(sibling)) continue;
|
|
871
|
+
background.add(sibling);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
containers.slice(top).forEach((container, index) => {
|
|
876
|
+
const path = [];
|
|
877
|
+
for (let node = container; node && node !== document.body && !kept.has(node); ) {
|
|
878
|
+
path.push(node);
|
|
879
|
+
node = flatTreeParent(node);
|
|
335
880
|
}
|
|
336
|
-
node
|
|
881
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
882
|
+
});
|
|
883
|
+
return background;
|
|
884
|
+
}
|
|
885
|
+
#readContainer() {
|
|
886
|
+
try {
|
|
887
|
+
return this.#getContainer();
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
static #resolveTab(stack, event) {
|
|
893
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
894
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
895
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
896
|
+
const owner = _FocusTrap.#tabOwner(stack);
|
|
897
|
+
if (!owner) return;
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
owner.moveFocus(event.shiftKey);
|
|
900
|
+
}
|
|
901
|
+
static #tabOwner(stack) {
|
|
902
|
+
for (const trap of [...stack].reverse()) {
|
|
903
|
+
if (_FocusTrap.#takesTab(trap)) return trap;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
static #takesTab(trap) {
|
|
908
|
+
const container = trap.container();
|
|
909
|
+
if (!container) return false;
|
|
910
|
+
return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
|
|
911
|
+
}
|
|
912
|
+
#ownsTab() {
|
|
913
|
+
return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
|
|
914
|
+
}
|
|
915
|
+
#moveFocus(backward) {
|
|
916
|
+
const from = deepActiveElement(document);
|
|
917
|
+
for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
|
|
918
|
+
if (!this.#ownsTab()) return;
|
|
919
|
+
const before = deepActiveElement(document);
|
|
920
|
+
stop.focus();
|
|
921
|
+
const after = deepActiveElement(document);
|
|
922
|
+
if (after === before) continue;
|
|
923
|
+
if (after === stop) _FocusTrap.#selectOnEntry(stop);
|
|
924
|
+
return;
|
|
337
925
|
}
|
|
338
926
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
sibling.inert = false;
|
|
927
|
+
static #selectOnEntry(stop) {
|
|
928
|
+
if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
|
|
929
|
+
stop.select();
|
|
343
930
|
}
|
|
344
|
-
this.#inertedSiblings = [];
|
|
345
931
|
}
|
|
346
|
-
/** Moves focus to the initial target, the first focusable, or the container. */
|
|
347
932
|
#focusInitial() {
|
|
348
933
|
const preferred = this.#options.initialFocus?.();
|
|
349
934
|
if (preferred) {
|
|
350
935
|
preferred.focus();
|
|
351
936
|
return;
|
|
352
937
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
938
|
+
const container = this.#getContainer();
|
|
939
|
+
const [first] = sequentialTabStops(container);
|
|
940
|
+
if (first) {
|
|
941
|
+
first.focus();
|
|
356
942
|
return;
|
|
357
943
|
}
|
|
358
|
-
|
|
359
|
-
container.tabIndex = -1;
|
|
944
|
+
this.#lendTabindex(container);
|
|
360
945
|
container.focus();
|
|
361
946
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
var ListenerSet = class {
|
|
950
|
+
#abort = new AbortController();
|
|
951
|
+
add(target, type, handler, options) {
|
|
952
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
953
|
+
}
|
|
954
|
+
dispose() {
|
|
955
|
+
this.#abort.abort();
|
|
956
|
+
this.#abort = new AbortController();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
var MorphRenderWatcher = class {
|
|
961
|
+
#listeners = new ListenerSet();
|
|
962
|
+
#pass;
|
|
963
|
+
#element = null;
|
|
964
|
+
#onMorph = () => this.#pass.schedule();
|
|
965
|
+
constructor(run) {
|
|
966
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
967
|
+
}
|
|
968
|
+
observe(element) {
|
|
969
|
+
if (this.#element === element) return;
|
|
970
|
+
this.disconnect();
|
|
971
|
+
this.#element = element;
|
|
972
|
+
this.#pass.activate();
|
|
973
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
974
|
+
}
|
|
975
|
+
schedule() {
|
|
976
|
+
this.#pass.schedule();
|
|
977
|
+
}
|
|
978
|
+
disconnect() {
|
|
979
|
+
this.#listeners.dispose();
|
|
980
|
+
this.#pass.cancel();
|
|
981
|
+
this.#element = null;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
var NUMBER_BOUNDS = {
|
|
986
|
+
nonNegative: { finite: true, min: 0 }};
|
|
987
|
+
function matchesNumberBounds(value, bounds) {
|
|
988
|
+
if (!Number.isFinite(value)) {
|
|
989
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
990
|
+
if (direction === null) return false;
|
|
991
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
992
|
+
}
|
|
993
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
994
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
995
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
996
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
997
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function readNumber(raw, fallback, bounds) {
|
|
1002
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
var NumberValueReader = class {
|
|
1006
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
1007
|
+
read(owner, name, raw, fallback, bounds) {
|
|
1008
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
1009
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
1010
|
+
this.#lastRejected.delete(name);
|
|
1011
|
+
return resolved;
|
|
1012
|
+
}
|
|
1013
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
1014
|
+
const literal = owner.element.getAttribute(attribute);
|
|
1015
|
+
if (literal === null) {
|
|
1016
|
+
this.#lastRejected.delete(name);
|
|
1017
|
+
return resolved;
|
|
1018
|
+
}
|
|
1019
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
1020
|
+
this.#lastRejected.set(name, literal);
|
|
1021
|
+
console.warn(
|
|
1022
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return resolved;
|
|
365
1026
|
}
|
|
366
1027
|
};
|
|
367
1028
|
|
|
368
|
-
// src/utils/safe_storage.ts
|
|
369
1029
|
function readLocalStorage(key) {
|
|
370
1030
|
try {
|
|
371
1031
|
return { ok: true, value: window.localStorage.getItem(key) };
|
|
@@ -382,43 +1042,33 @@ function writeLocalStorage(key, value) {
|
|
|
382
1042
|
}
|
|
383
1043
|
}
|
|
384
1044
|
|
|
385
|
-
|
|
1045
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
1046
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
1047
|
+
function stateReasonFor(event) {
|
|
1048
|
+
if (!event) return "api";
|
|
1049
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
1050
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
1051
|
+
return "user";
|
|
1052
|
+
}
|
|
1053
|
+
|
|
386
1054
|
var TimerRegistry = class {
|
|
387
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
388
1055
|
ids = /* @__PURE__ */ new Set();
|
|
389
|
-
/**
|
|
390
|
-
* Cancels a single tracked timer.
|
|
391
|
-
*
|
|
392
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
393
|
-
* registry), so callers can clear defensively without guarding.
|
|
394
|
-
*/
|
|
395
1056
|
clear(id) {
|
|
396
1057
|
if (this.ids.delete(id)) {
|
|
397
1058
|
this.cancel(id);
|
|
398
1059
|
}
|
|
399
1060
|
}
|
|
400
|
-
/**
|
|
401
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
402
|
-
* to guarantee no timer outlives the element.
|
|
403
|
-
*/
|
|
404
1061
|
clearAll() {
|
|
405
1062
|
for (const id of this.ids) {
|
|
406
1063
|
this.cancel(id);
|
|
407
1064
|
}
|
|
408
1065
|
this.ids.clear();
|
|
409
1066
|
}
|
|
410
|
-
/** Number of timers currently tracked (pending). */
|
|
411
1067
|
get size() {
|
|
412
1068
|
return this.ids.size;
|
|
413
1069
|
}
|
|
414
1070
|
};
|
|
415
1071
|
var SafeTimeout = class extends TimerRegistry {
|
|
416
|
-
/**
|
|
417
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
418
|
-
*
|
|
419
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
420
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
421
|
-
*/
|
|
422
1072
|
set(callback, delay) {
|
|
423
1073
|
const id = this.schedule(() => {
|
|
424
1074
|
this.ids.delete(id);
|
|
@@ -435,7 +1085,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
435
1085
|
}
|
|
436
1086
|
};
|
|
437
1087
|
|
|
438
|
-
// src/utils/transition_completion.ts
|
|
439
1088
|
function timeMs(value) {
|
|
440
1089
|
const trimmed = value.trim();
|
|
441
1090
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -471,13 +1120,6 @@ var TransitionCompletion = class {
|
|
|
471
1120
|
#complete = null;
|
|
472
1121
|
#pendingProperties = null;
|
|
473
1122
|
#deadline = 0;
|
|
474
|
-
/**
|
|
475
|
-
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
476
|
-
* transition (including when `getComputedStyle` is unavailable).
|
|
477
|
-
*
|
|
478
|
-
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
479
|
-
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
480
|
-
*/
|
|
481
1123
|
wait(element, complete, options = {}) {
|
|
482
1124
|
this.cancel();
|
|
483
1125
|
const requested = options.timeoutMs ?? 0;
|
|
@@ -497,18 +1139,10 @@ var TransitionCompletion = class {
|
|
|
497
1139
|
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
498
1140
|
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
499
1141
|
}
|
|
500
|
-
/** Cancels the pending wait without invoking its completion callback. */
|
|
501
1142
|
cancel() {
|
|
502
1143
|
this.#complete = null;
|
|
503
1144
|
this.#teardown();
|
|
504
1145
|
}
|
|
505
|
-
/**
|
|
506
|
-
* Handles terminal events from the observed element only.
|
|
507
|
-
*
|
|
508
|
-
* For explicit property lists, every declared positive-time property must
|
|
509
|
-
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
510
|
-
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
511
|
-
*/
|
|
512
1146
|
#onTerminal = (event) => {
|
|
513
1147
|
if (event.target !== this.#element) return;
|
|
514
1148
|
const transitionEvent = event;
|
|
@@ -528,10 +1162,6 @@ var TransitionCompletion = class {
|
|
|
528
1162
|
}
|
|
529
1163
|
if (Date.now() >= this.#deadline) this.#finish();
|
|
530
1164
|
};
|
|
531
|
-
/**
|
|
532
|
-
* Returns active CSS transition properties expanded to the names reported by
|
|
533
|
-
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
534
|
-
*/
|
|
535
1165
|
#activeTransitionProperties(element) {
|
|
536
1166
|
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
537
1167
|
try {
|
|
@@ -549,7 +1179,6 @@ var TransitionCompletion = class {
|
|
|
549
1179
|
return /* @__PURE__ */ new Set();
|
|
550
1180
|
}
|
|
551
1181
|
}
|
|
552
|
-
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
553
1182
|
#explicitPendingProperties(timings) {
|
|
554
1183
|
if (timings.some(({ property }) => property === "all")) return null;
|
|
555
1184
|
const pending = new Set(
|
|
@@ -557,7 +1186,6 @@ var TransitionCompletion = class {
|
|
|
557
1186
|
);
|
|
558
1187
|
return pending.size > 0 ? pending : null;
|
|
559
1188
|
}
|
|
560
|
-
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
561
1189
|
#finish() {
|
|
562
1190
|
const complete = this.#complete;
|
|
563
1191
|
if (!complete) return;
|
|
@@ -565,7 +1193,6 @@ var TransitionCompletion = class {
|
|
|
565
1193
|
this.#teardown();
|
|
566
1194
|
complete();
|
|
567
1195
|
}
|
|
568
|
-
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
569
1196
|
#teardown() {
|
|
570
1197
|
this.#timers.clearAll();
|
|
571
1198
|
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
@@ -576,79 +1203,112 @@ var TransitionCompletion = class {
|
|
|
576
1203
|
}
|
|
577
1204
|
};
|
|
578
1205
|
|
|
579
|
-
|
|
580
|
-
|
|
1206
|
+
var SidebarController = class _SidebarController extends Controller {
|
|
1207
|
+
#numbers = new NumberValueReader();
|
|
581
1208
|
static targets = ["trigger", "panel", "backdrop"];
|
|
582
1209
|
static values = {
|
|
583
1210
|
breakpoint: { type: Number, default: 768 },
|
|
584
1211
|
key: { type: String, default: "" },
|
|
585
1212
|
collapsed: { type: Boolean, default: false }
|
|
586
1213
|
};
|
|
1214
|
+
static valueConstraints = {
|
|
1215
|
+
breakpoint: NUMBER_BOUNDS.nonNegative
|
|
1216
|
+
};
|
|
587
1217
|
static actions = ["close", "open", "toggle"];
|
|
588
|
-
|
|
1218
|
+
static events = ["close", "open", "reconcile"];
|
|
589
1219
|
#activePanel = null;
|
|
590
|
-
/** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
|
|
591
1220
|
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
592
|
-
onEscape: () => this
|
|
1221
|
+
onEscape: () => this.#closeOverlay("escape"),
|
|
593
1222
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
594
1223
|
});
|
|
595
|
-
/** Current responsive mode. */
|
|
596
1224
|
#mode = "inline";
|
|
597
|
-
/** Persisted inline preference: whether the rail is collapsed. */
|
|
598
1225
|
#collapsed = false;
|
|
599
|
-
/** The matched media query (`min-width: breakpoint`), watched for mode changes. */
|
|
600
1226
|
#mql = null;
|
|
601
|
-
/** Exact normalized query currently represented by the active media-query listener. */
|
|
602
1227
|
#mqlQuery = null;
|
|
603
|
-
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
604
1228
|
#transition = new TransitionCompletion();
|
|
605
|
-
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
606
1229
|
#connected = false;
|
|
1230
|
+
#storageKeyRead = null;
|
|
1231
|
+
#expanded = false;
|
|
1232
|
+
#backdrop = {
|
|
1233
|
+
state: "closed",
|
|
1234
|
+
hidden: true
|
|
1235
|
+
};
|
|
1236
|
+
#expandedLease = new AttributeLease("aria-expanded", this.identifier);
|
|
1237
|
+
#stateLease = new AttributeLease("data-state", this.identifier);
|
|
1238
|
+
#hiddenLease = new AttributeLease("hidden", this.identifier);
|
|
1239
|
+
#modeLease = new AttributeLease("data-mode", this.identifier);
|
|
1240
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
1241
|
+
#lived = false;
|
|
607
1242
|
connect() {
|
|
1243
|
+
this.#trap.connect();
|
|
608
1244
|
this.#connected = true;
|
|
609
|
-
this.#beforeCache.activate();
|
|
610
1245
|
this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
|
|
1246
|
+
this.#storageKeyRead = this.#storageKey;
|
|
611
1247
|
this.#collapsed = this.#restoreCollapsed();
|
|
612
1248
|
this.#mqlQuery = this.#breakpointQuery;
|
|
613
1249
|
this.#mql = this.#matchBreakpoint(this.#mqlQuery);
|
|
614
1250
|
this.#mql?.addEventListener("change", this.#onMediaChange);
|
|
615
|
-
this.#
|
|
1251
|
+
const mode = this.#computeMode();
|
|
1252
|
+
const resume = this.#lived && mode === "overlay" && this.#isOverlayOpen;
|
|
1253
|
+
this.#lived = true;
|
|
1254
|
+
if (resume) this.#trap.activate();
|
|
1255
|
+
else this.#applyMode(mode, false);
|
|
1256
|
+
this.#morphRender.observe(this.element);
|
|
616
1257
|
}
|
|
617
1258
|
disconnect() {
|
|
618
1259
|
this.#connected = false;
|
|
619
|
-
this.#
|
|
1260
|
+
this.#morphRender.disconnect();
|
|
620
1261
|
this.#mql?.removeEventListener("change", this.#onMediaChange);
|
|
621
1262
|
this.#mql = null;
|
|
622
1263
|
this.#mqlQuery = null;
|
|
623
1264
|
this.#transition.cancel();
|
|
624
|
-
this.#trap.
|
|
1265
|
+
this.#trap.disconnect(this);
|
|
625
1266
|
this.#activePanel = null;
|
|
626
1267
|
}
|
|
627
|
-
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
628
1268
|
panelTargetConnected(panel) {
|
|
629
|
-
if (!this.#connected || this.#activePanel?.isConnected
|
|
1269
|
+
if (!this.#connected || this.#activePanel?.isConnected) return;
|
|
630
1270
|
this.#adoptPanel(panel);
|
|
631
1271
|
}
|
|
632
|
-
/** Closes and releases overlay side effects when the actively trapped panel disappears. */
|
|
633
1272
|
panelTargetDisconnected(panel) {
|
|
634
|
-
|
|
1273
|
+
const stays = this.panelTargets.includes(panel);
|
|
1274
|
+
if (!stays) {
|
|
1275
|
+
this.#modeLease.return(panel);
|
|
1276
|
+
this.#stateLease.return(panel);
|
|
1277
|
+
this.#hiddenLease.return(panel);
|
|
1278
|
+
}
|
|
1279
|
+
if (panel !== this.#activePanel || stays && this.panelTarget === panel) return;
|
|
635
1280
|
this.#transition.cancel();
|
|
636
1281
|
this.#activePanel = null;
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
1282
|
+
if (stays) {
|
|
1283
|
+
this.#writeState(panel, "closed");
|
|
1284
|
+
this.#writeHidden(panel, true);
|
|
1285
|
+
}
|
|
640
1286
|
this.#hideBackdrop();
|
|
641
1287
|
this.#setExpandedAttr(false);
|
|
642
|
-
this.#
|
|
1288
|
+
if (!this.#connected) {
|
|
1289
|
+
this.#trap.deactivate({ restoreFocus: false });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
643
1292
|
if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
|
|
1293
|
+
else this.#trap.deactivate();
|
|
1294
|
+
}
|
|
1295
|
+
triggerTargetConnected() {
|
|
1296
|
+
if (this.#connected) this.#reflectExpanded();
|
|
1297
|
+
}
|
|
1298
|
+
triggerTargetDisconnected(trigger) {
|
|
1299
|
+
if (!this.triggerTargets.includes(trigger)) this.#expandedLease.return(trigger);
|
|
1300
|
+
if (this.#connected) this.#reflectExpanded();
|
|
1301
|
+
}
|
|
1302
|
+
backdropTargetConnected() {
|
|
1303
|
+
if (this.#connected) this.#reflectBackdrop();
|
|
1304
|
+
}
|
|
1305
|
+
backdropTargetDisconnected(backdrop) {
|
|
1306
|
+
if (!this.backdropTargets.includes(backdrop)) {
|
|
1307
|
+
this.#stateLease.return(backdrop);
|
|
1308
|
+
this.#hiddenLease.return(backdrop);
|
|
1309
|
+
}
|
|
1310
|
+
if (this.#connected) this.#reflectBackdrop();
|
|
644
1311
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Rebinds responsive observation when `breakpoint` changes at runtime.
|
|
647
|
-
*
|
|
648
|
-
* Stimulus calls value callbacks before `connect()`, so the connected guard
|
|
649
|
-
* prevents an eager subscription. Equivalent normalized queries retain the
|
|
650
|
-
* existing listener instead of allocating duplicate `MediaQueryList` objects.
|
|
651
|
-
*/
|
|
652
1312
|
breakpointValueChanged() {
|
|
653
1313
|
if (!this.#connected) return;
|
|
654
1314
|
const query = this.#breakpointQuery;
|
|
@@ -658,91 +1318,108 @@ var SidebarController = class extends Controller {
|
|
|
658
1318
|
this.#mql = this.#matchBreakpoint(query);
|
|
659
1319
|
this.#mql?.addEventListener("change", this.#onMediaChange);
|
|
660
1320
|
const next = this.#computeMode();
|
|
661
|
-
if (next !== this.#mode) this.#applyMode(next);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
this.#
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
toggle() {
|
|
1321
|
+
if (next !== this.#mode) this.#applyMode(next, true);
|
|
1322
|
+
}
|
|
1323
|
+
keyValueChanged() {
|
|
1324
|
+
if (!this.#connected) return;
|
|
1325
|
+
const key = this.#storageKey;
|
|
1326
|
+
if (key === this.#storageKeyRead) return;
|
|
1327
|
+
this.#storageKeyRead = key;
|
|
1328
|
+
const stored = this.#readStoredCollapsed(key);
|
|
1329
|
+
if (stored === null || stored === this.#collapsed) return;
|
|
1330
|
+
this.#collapsed = stored;
|
|
1331
|
+
if (this.#isOverlay) return;
|
|
1332
|
+
this.#applyInlineState(stored);
|
|
1333
|
+
this.#reportReconcile();
|
|
1334
|
+
}
|
|
1335
|
+
toggle(event) {
|
|
1336
|
+
if (!this.#connected) return;
|
|
1337
|
+
const reason = stateReasonFor(event);
|
|
679
1338
|
if (this.#isOverlay) {
|
|
680
|
-
this.#isOverlayOpen ? this.#closeOverlay() : this.#openOverlay();
|
|
1339
|
+
this.#isOverlayOpen ? this.#closeOverlay(reason) : this.#openOverlay(reason);
|
|
681
1340
|
} else {
|
|
682
|
-
this.#setCollapsed(!this.#collapsed);
|
|
1341
|
+
this.#setCollapsed(!this.#collapsed, reason);
|
|
683
1342
|
}
|
|
684
1343
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1344
|
+
open(event) {
|
|
1345
|
+
if (!this.#connected) return;
|
|
1346
|
+
const reason = stateReasonFor(event);
|
|
1347
|
+
if (this.#isOverlay) this.#openOverlay(reason);
|
|
1348
|
+
else this.#setCollapsed(false, reason);
|
|
1349
|
+
}
|
|
1350
|
+
close(event) {
|
|
1351
|
+
if (!this.#connected) return;
|
|
1352
|
+
const reason = this.#closeReason(event);
|
|
1353
|
+
if (this.#isOverlay) this.#closeOverlay(reason);
|
|
1354
|
+
else this.#setCollapsed(true, reason);
|
|
689
1355
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
else this.#setCollapsed(true);
|
|
1356
|
+
#closeReason(event) {
|
|
1357
|
+
if (this.hasBackdropTarget && event?.currentTarget === this.backdropTarget) return "outside";
|
|
1358
|
+
return stateReasonFor(event);
|
|
694
1359
|
}
|
|
695
|
-
|
|
696
|
-
/** Re-renders the closed/default state for `mode` and records it. */
|
|
697
|
-
#applyMode(mode) {
|
|
1360
|
+
#applyMode(mode, report) {
|
|
698
1361
|
this.#mode = mode;
|
|
699
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1362
|
+
if (this.hasPanelTarget) this.#modeLease.write(this.panelTarget, mode);
|
|
700
1363
|
if (mode === "inline") {
|
|
701
1364
|
this.#transition.cancel();
|
|
702
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
703
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1365
|
+
if (report) this.#trap.deactivate({ restoreFocus: false });
|
|
1366
|
+
if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, false);
|
|
704
1367
|
this.#hideBackdrop();
|
|
705
1368
|
this.#applyInlineState(this.#collapsed);
|
|
706
1369
|
} else {
|
|
707
|
-
this.#transition.cancel();
|
|
708
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
709
1370
|
this.#setOverlayClosedImmediate();
|
|
710
1371
|
}
|
|
1372
|
+
if (report) this.#reportReconcile();
|
|
1373
|
+
}
|
|
1374
|
+
#repair() {
|
|
1375
|
+
if (!this.hasPanelTarget) return;
|
|
1376
|
+
if (this.#mode === "inline") {
|
|
1377
|
+
this.#applyMode("inline", false);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const panel = this.panelTarget;
|
|
1381
|
+
this.#modeLease.write(panel, "overlay");
|
|
1382
|
+
this.#setOverlayState(this.#expanded ? "open" : "closed");
|
|
1383
|
+
const hidden = !this.#expanded && !this.#trap.active;
|
|
1384
|
+
this.#writeHidden(panel, hidden);
|
|
1385
|
+
this.#writeBackdropHidden(hidden);
|
|
1386
|
+
this.#setExpandedAttr(this.#expanded);
|
|
1387
|
+
}
|
|
1388
|
+
#reportReconcile() {
|
|
1389
|
+
this.dispatch("reconcile", {
|
|
1390
|
+
detail: { mode: this.#mode, open: this.#isExpanded },
|
|
1391
|
+
cancelable: false
|
|
1392
|
+
});
|
|
711
1393
|
}
|
|
712
|
-
/** Reconciles a replacement panel with the current responsive mode and DOM state. */
|
|
713
1394
|
#adoptPanel(panel) {
|
|
714
1395
|
this.#transition.cancel();
|
|
715
|
-
const trapWasActive = this.#trap.active;
|
|
716
1396
|
this.#activePanel = panel;
|
|
717
|
-
|
|
1397
|
+
this.#modeLease.write(panel, this.#mode);
|
|
718
1398
|
if (this.#mode === "inline") {
|
|
719
|
-
panel
|
|
720
|
-
panel
|
|
1399
|
+
this.#writeHidden(panel, false);
|
|
1400
|
+
this.#writeState(panel, this.#collapsed ? "collapsed" : "expanded");
|
|
721
1401
|
this.#hideBackdrop();
|
|
722
1402
|
this.#setExpandedAttr(!this.#collapsed);
|
|
723
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
724
1403
|
return;
|
|
725
1404
|
}
|
|
726
1405
|
if (panel.getAttribute("data-state") === "open") {
|
|
727
|
-
panel
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.backdropTarget.hidden = false;
|
|
731
|
-
}
|
|
1406
|
+
this.#writeHidden(panel, false);
|
|
1407
|
+
this.#writeBackdropState("open");
|
|
1408
|
+
this.#writeBackdropHidden(false);
|
|
732
1409
|
this.#setExpandedAttr(true);
|
|
733
|
-
if (
|
|
734
|
-
this.#trap.activate();
|
|
1410
|
+
if (this.#trap.active) this.#trap.refreshContainer();
|
|
1411
|
+
else this.#trap.activate();
|
|
735
1412
|
return;
|
|
736
1413
|
}
|
|
737
|
-
panel
|
|
738
|
-
panel
|
|
1414
|
+
this.#writeState(panel, "closed");
|
|
1415
|
+
this.#writeHidden(panel, true);
|
|
739
1416
|
this.#hideBackdrop();
|
|
740
1417
|
this.#setExpandedAttr(false);
|
|
741
1418
|
this.#trap.deactivate();
|
|
742
1419
|
}
|
|
743
1420
|
#onMediaChange = (event) => {
|
|
744
1421
|
const next = event.matches ? "inline" : "overlay";
|
|
745
|
-
if (next !== this.#mode) this.#applyMode(next);
|
|
1422
|
+
if (next !== this.#mode) this.#applyMode(next, true);
|
|
746
1423
|
};
|
|
747
1424
|
#computeMode() {
|
|
748
1425
|
return this.#mql?.matches ?? true ? "inline" : "overlay";
|
|
@@ -751,101 +1428,113 @@ var SidebarController = class extends Controller {
|
|
|
751
1428
|
if (typeof window.matchMedia !== "function") return null;
|
|
752
1429
|
return window.matchMedia(query);
|
|
753
1430
|
}
|
|
754
|
-
|
|
755
|
-
/** Sets, reflects, and persists the inline collapsed preference. */
|
|
756
|
-
#setCollapsed(collapsed) {
|
|
1431
|
+
#setCollapsed(collapsed, reason) {
|
|
757
1432
|
if (collapsed === this.#collapsed) return;
|
|
758
1433
|
this.#collapsed = collapsed;
|
|
759
1434
|
this.#applyInlineState(collapsed);
|
|
1435
|
+
this.#report(!collapsed, reason);
|
|
760
1436
|
this.#persistCollapsed(collapsed);
|
|
761
1437
|
}
|
|
762
|
-
/** Reflects the inline rail state onto the panel and trigger. */
|
|
763
1438
|
#applyInlineState(collapsed) {
|
|
764
1439
|
if (this.hasPanelTarget) {
|
|
765
|
-
this.panelTarget
|
|
1440
|
+
this.#writeState(this.panelTarget, collapsed ? "collapsed" : "expanded");
|
|
766
1441
|
}
|
|
767
1442
|
this.#setExpandedAttr(!collapsed);
|
|
768
1443
|
}
|
|
769
|
-
|
|
770
|
-
/** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
|
|
771
|
-
#openOverlay() {
|
|
1444
|
+
#openOverlay(reason) {
|
|
772
1445
|
if (!this.hasPanelTarget || this.#isOverlayOpen) return;
|
|
773
1446
|
this.#transition.cancel();
|
|
774
1447
|
this.#activePanel = this.panelTarget;
|
|
775
|
-
this.panelTarget
|
|
776
|
-
|
|
1448
|
+
this.#writeHidden(this.panelTarget, false);
|
|
1449
|
+
this.#writeBackdropHidden(false);
|
|
777
1450
|
void this.panelTarget.offsetWidth;
|
|
778
1451
|
this.#setOverlayState("open");
|
|
779
1452
|
this.#setExpandedAttr(true);
|
|
1453
|
+
this.#report(true, reason);
|
|
1454
|
+
if (!this.#isOverlayOpen) return;
|
|
780
1455
|
this.#trap.activate();
|
|
781
1456
|
}
|
|
782
|
-
|
|
783
|
-
#closeOverlay() {
|
|
1457
|
+
#closeOverlay(reason) {
|
|
784
1458
|
if (!this.hasPanelTarget || !this.#isOverlayOpen) return;
|
|
785
1459
|
this.#setOverlayState("closed");
|
|
786
1460
|
this.#setExpandedAttr(false);
|
|
1461
|
+
this.#report(false, reason);
|
|
1462
|
+
if (this.#isOverlayOpen) return;
|
|
787
1463
|
this.#hideAfterTransition();
|
|
788
1464
|
}
|
|
789
|
-
/** Renders the overlay closed state up front (used when entering overlay mode). */
|
|
790
1465
|
#setOverlayClosedImmediate() {
|
|
791
1466
|
this.#setOverlayState("closed");
|
|
792
1467
|
this.#setExpandedAttr(false);
|
|
793
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1468
|
+
if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, true);
|
|
794
1469
|
this.#hideBackdrop();
|
|
795
1470
|
}
|
|
796
|
-
/** Syncs `data-state` on the panel and backdrop together (overlay). */
|
|
797
1471
|
#setOverlayState(state) {
|
|
798
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Applies `hidden` once the close transition ends so the exit slide can play,
|
|
803
|
-
* then reverts the modal side effects. The shared waiter completes
|
|
804
|
-
* synchronously for 0ms transitions and supplies a bounded fallback when the
|
|
805
|
-
* browser emits no terminal event.
|
|
806
|
-
*/
|
|
1472
|
+
if (this.hasPanelTarget) this.#writeState(this.panelTarget, state);
|
|
1473
|
+
this.#writeBackdropState(state);
|
|
1474
|
+
}
|
|
807
1475
|
#hideAfterTransition() {
|
|
808
1476
|
const panel = this.panelTarget;
|
|
809
1477
|
this.#activePanel = panel;
|
|
810
1478
|
this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));
|
|
811
1479
|
}
|
|
812
|
-
/** Hides the panel/backdrop and tears down the trap after the exit transition. */
|
|
813
1480
|
#applyOverlayHidden(panel) {
|
|
814
|
-
panel
|
|
1481
|
+
this.#writeHidden(panel, true);
|
|
815
1482
|
this.#hideBackdrop();
|
|
816
1483
|
this.#trap.deactivate();
|
|
817
1484
|
}
|
|
818
1485
|
#hideBackdrop() {
|
|
819
|
-
|
|
820
|
-
this
|
|
821
|
-
|
|
1486
|
+
this.#writeBackdropState("closed");
|
|
1487
|
+
this.#writeBackdropHidden(true);
|
|
1488
|
+
}
|
|
1489
|
+
#writeState(element, state) {
|
|
1490
|
+
this.#stateLease.write(element, state);
|
|
1491
|
+
}
|
|
1492
|
+
#writeHidden(element, hidden) {
|
|
1493
|
+
this.#hiddenLease.write(element, hidden ? "" : null);
|
|
1494
|
+
}
|
|
1495
|
+
#writeBackdropState(state) {
|
|
1496
|
+
this.#backdrop.state = state;
|
|
1497
|
+
if (this.hasBackdropTarget) this.#writeState(this.backdropTarget, state);
|
|
1498
|
+
}
|
|
1499
|
+
#writeBackdropHidden(hidden) {
|
|
1500
|
+
this.#backdrop.hidden = hidden;
|
|
1501
|
+
if (this.hasBackdropTarget) this.#writeHidden(this.backdropTarget, hidden);
|
|
1502
|
+
}
|
|
1503
|
+
#reflectBackdrop() {
|
|
1504
|
+
this.#writeBackdropState(this.#backdrop.state);
|
|
1505
|
+
this.#writeBackdropHidden(this.#backdrop.hidden);
|
|
822
1506
|
}
|
|
823
|
-
// --- Shared helpers --------------------------------------------------------
|
|
824
1507
|
#setExpandedAttr(expanded) {
|
|
1508
|
+
this.#expanded = expanded;
|
|
1509
|
+
this.#reflectExpanded();
|
|
1510
|
+
}
|
|
1511
|
+
#reflectExpanded() {
|
|
825
1512
|
if (this.hasTriggerTarget) {
|
|
826
|
-
this.triggerTarget
|
|
1513
|
+
this.#expandedLease.write(this.triggerTarget, this.#expanded ? "true" : "false");
|
|
827
1514
|
}
|
|
828
1515
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1516
|
+
#report(open, reason) {
|
|
1517
|
+
const detail = { reason, mode: this.#mode };
|
|
1518
|
+
if (open) this.dispatch("open", { detail, cancelable: false });
|
|
1519
|
+
else this.dispatch("close", { detail, cancelable: false });
|
|
1520
|
+
}
|
|
1521
|
+
get #isExpanded() {
|
|
1522
|
+
return this.#isOverlay ? this.#isOverlayOpen : !this.#collapsed;
|
|
1523
|
+
}
|
|
837
1524
|
#restoreCollapsed() {
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
const result = readLocalStorage(key);
|
|
841
|
-
if (result.ok && result.value !== null) return result.value === "1";
|
|
842
|
-
}
|
|
1525
|
+
const stored = this.#readStoredCollapsed(this.#storageKey);
|
|
1526
|
+
if (stored !== null) return stored;
|
|
843
1527
|
const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
|
|
844
1528
|
if (domState === "collapsed") return true;
|
|
845
1529
|
if (domState === "expanded") return false;
|
|
846
1530
|
return this.collapsedValue;
|
|
847
1531
|
}
|
|
848
|
-
|
|
1532
|
+
#readStoredCollapsed(key) {
|
|
1533
|
+
if (!key) return null;
|
|
1534
|
+
const result = readLocalStorage(key);
|
|
1535
|
+
if (!result.ok || result.value === null) return null;
|
|
1536
|
+
return result.value === "1";
|
|
1537
|
+
}
|
|
849
1538
|
#persistCollapsed(collapsed) {
|
|
850
1539
|
const key = this.#storageKey;
|
|
851
1540
|
if (!key) return;
|
|
@@ -854,10 +1543,9 @@ var SidebarController = class extends Controller {
|
|
|
854
1543
|
get #storageKey() {
|
|
855
1544
|
return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
|
|
856
1545
|
}
|
|
857
|
-
/** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */
|
|
858
1546
|
get #breakpointQuery() {
|
|
859
|
-
const value = this
|
|
860
|
-
const breakpoint =
|
|
1547
|
+
const value = this.#safeBreakpoint;
|
|
1548
|
+
const breakpoint = value;
|
|
861
1549
|
return `(min-width: ${breakpoint}px)`;
|
|
862
1550
|
}
|
|
863
1551
|
get #isOverlay() {
|
|
@@ -866,8 +1554,16 @@ var SidebarController = class extends Controller {
|
|
|
866
1554
|
get #isOverlayOpen() {
|
|
867
1555
|
return this.#isOverlay && this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
|
|
868
1556
|
}
|
|
1557
|
+
get #safeBreakpoint() {
|
|
1558
|
+
return this.#numbers.read(
|
|
1559
|
+
this,
|
|
1560
|
+
"breakpoint",
|
|
1561
|
+
this.breakpointValue,
|
|
1562
|
+
_SidebarController.values.breakpoint.default,
|
|
1563
|
+
_SidebarController.valueConstraints.breakpoint
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
869
1566
|
};
|
|
870
1567
|
|
|
871
1568
|
export { SidebarController };
|
|
872
|
-
//# sourceMappingURL=sidebar_controller.js.map
|
|
873
1569
|
//# sourceMappingURL=sidebar_controller.js.map
|