stimeo-ui 0.16.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- 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 +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- 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)
|
|
367
|
+
);
|
|
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)
|
|
189
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;
|
|
190
496
|
}
|
|
191
497
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
};
|
|
586
|
+
|
|
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;
|
|
335
743
|
}
|
|
336
|
-
|
|
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);
|
|
337
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);
|
|
880
|
+
}
|
|
881
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
882
|
+
});
|
|
883
|
+
return background;
|
|
338
884
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
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,7 +1042,6 @@ function writeLocalStorage(key, value) {
|
|
|
382
1042
|
}
|
|
383
1043
|
}
|
|
384
1044
|
|
|
385
|
-
// src/utils/state_reason.ts
|
|
386
1045
|
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
387
1046
|
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
388
1047
|
function stateReasonFor(event) {
|
|
@@ -392,43 +1051,24 @@ function stateReasonFor(event) {
|
|
|
392
1051
|
return "user";
|
|
393
1052
|
}
|
|
394
1053
|
|
|
395
|
-
// src/utils/safe_timeout.ts
|
|
396
1054
|
var TimerRegistry = class {
|
|
397
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
398
1055
|
ids = /* @__PURE__ */ new Set();
|
|
399
|
-
/**
|
|
400
|
-
* Cancels a single tracked timer.
|
|
401
|
-
*
|
|
402
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
403
|
-
* registry), so callers can clear defensively without guarding.
|
|
404
|
-
*/
|
|
405
1056
|
clear(id) {
|
|
406
1057
|
if (this.ids.delete(id)) {
|
|
407
1058
|
this.cancel(id);
|
|
408
1059
|
}
|
|
409
1060
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
412
|
-
* to guarantee no timer outlives the element.
|
|
413
|
-
*/
|
|
414
1061
|
clearAll() {
|
|
415
1062
|
for (const id of this.ids) {
|
|
416
1063
|
this.cancel(id);
|
|
417
1064
|
}
|
|
418
1065
|
this.ids.clear();
|
|
419
1066
|
}
|
|
420
|
-
/** Number of timers currently tracked (pending). */
|
|
421
1067
|
get size() {
|
|
422
1068
|
return this.ids.size;
|
|
423
1069
|
}
|
|
424
1070
|
};
|
|
425
1071
|
var SafeTimeout = class extends TimerRegistry {
|
|
426
|
-
/**
|
|
427
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
428
|
-
*
|
|
429
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
430
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
431
|
-
*/
|
|
432
1072
|
set(callback, delay) {
|
|
433
1073
|
const id = this.schedule(() => {
|
|
434
1074
|
this.ids.delete(id);
|
|
@@ -445,7 +1085,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
445
1085
|
}
|
|
446
1086
|
};
|
|
447
1087
|
|
|
448
|
-
// src/utils/transition_completion.ts
|
|
449
1088
|
function timeMs(value) {
|
|
450
1089
|
const trimmed = value.trim();
|
|
451
1090
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -481,13 +1120,6 @@ var TransitionCompletion = class {
|
|
|
481
1120
|
#complete = null;
|
|
482
1121
|
#pendingProperties = null;
|
|
483
1122
|
#deadline = 0;
|
|
484
|
-
/**
|
|
485
|
-
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
486
|
-
* transition (including when `getComputedStyle` is unavailable).
|
|
487
|
-
*
|
|
488
|
-
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
489
|
-
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
490
|
-
*/
|
|
491
1123
|
wait(element, complete, options = {}) {
|
|
492
1124
|
this.cancel();
|
|
493
1125
|
const requested = options.timeoutMs ?? 0;
|
|
@@ -507,18 +1139,10 @@ var TransitionCompletion = class {
|
|
|
507
1139
|
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
508
1140
|
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
509
1141
|
}
|
|
510
|
-
/** Cancels the pending wait without invoking its completion callback. */
|
|
511
1142
|
cancel() {
|
|
512
1143
|
this.#complete = null;
|
|
513
1144
|
this.#teardown();
|
|
514
1145
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Handles terminal events from the observed element only.
|
|
517
|
-
*
|
|
518
|
-
* For explicit property lists, every declared positive-time property must
|
|
519
|
-
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
520
|
-
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
521
|
-
*/
|
|
522
1146
|
#onTerminal = (event) => {
|
|
523
1147
|
if (event.target !== this.#element) return;
|
|
524
1148
|
const transitionEvent = event;
|
|
@@ -538,10 +1162,6 @@ var TransitionCompletion = class {
|
|
|
538
1162
|
}
|
|
539
1163
|
if (Date.now() >= this.#deadline) this.#finish();
|
|
540
1164
|
};
|
|
541
|
-
/**
|
|
542
|
-
* Returns active CSS transition properties expanded to the names reported by
|
|
543
|
-
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
544
|
-
*/
|
|
545
1165
|
#activeTransitionProperties(element) {
|
|
546
1166
|
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
547
1167
|
try {
|
|
@@ -559,7 +1179,6 @@ var TransitionCompletion = class {
|
|
|
559
1179
|
return /* @__PURE__ */ new Set();
|
|
560
1180
|
}
|
|
561
1181
|
}
|
|
562
|
-
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
563
1182
|
#explicitPendingProperties(timings) {
|
|
564
1183
|
if (timings.some(({ property }) => property === "all")) return null;
|
|
565
1184
|
const pending = new Set(
|
|
@@ -567,7 +1186,6 @@ var TransitionCompletion = class {
|
|
|
567
1186
|
);
|
|
568
1187
|
return pending.size > 0 ? pending : null;
|
|
569
1188
|
}
|
|
570
|
-
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
571
1189
|
#finish() {
|
|
572
1190
|
const complete = this.#complete;
|
|
573
1191
|
if (!complete) return;
|
|
@@ -575,7 +1193,6 @@ var TransitionCompletion = class {
|
|
|
575
1193
|
this.#teardown();
|
|
576
1194
|
complete();
|
|
577
1195
|
}
|
|
578
|
-
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
579
1196
|
#teardown() {
|
|
580
1197
|
this.#timers.clearAll();
|
|
581
1198
|
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
@@ -586,84 +1203,112 @@ var TransitionCompletion = class {
|
|
|
586
1203
|
}
|
|
587
1204
|
};
|
|
588
1205
|
|
|
589
|
-
|
|
590
|
-
|
|
1206
|
+
var SidebarController = class _SidebarController extends Controller {
|
|
1207
|
+
#numbers = new NumberValueReader();
|
|
591
1208
|
static targets = ["trigger", "panel", "backdrop"];
|
|
592
1209
|
static values = {
|
|
593
1210
|
breakpoint: { type: Number, default: 768 },
|
|
594
1211
|
key: { type: String, default: "" },
|
|
595
1212
|
collapsed: { type: Boolean, default: false }
|
|
596
1213
|
};
|
|
1214
|
+
static valueConstraints = {
|
|
1215
|
+
breakpoint: NUMBER_BOUNDS.nonNegative
|
|
1216
|
+
};
|
|
597
1217
|
static actions = ["close", "open", "toggle"];
|
|
598
1218
|
static events = ["close", "open", "reconcile"];
|
|
599
|
-
/** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
|
|
600
1219
|
#activePanel = null;
|
|
601
|
-
/** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
|
|
602
1220
|
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
603
1221
|
onEscape: () => this.#closeOverlay("escape"),
|
|
604
1222
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
605
1223
|
});
|
|
606
|
-
/** Current responsive mode. */
|
|
607
1224
|
#mode = "inline";
|
|
608
|
-
/** Persisted inline preference: whether the rail is collapsed. */
|
|
609
1225
|
#collapsed = false;
|
|
610
|
-
/** The matched media query (`min-width: breakpoint`), watched for mode changes. */
|
|
611
1226
|
#mql = null;
|
|
612
|
-
/** Exact normalized query currently represented by the active media-query listener. */
|
|
613
1227
|
#mqlQuery = null;
|
|
614
|
-
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
615
1228
|
#transition = new TransitionCompletion();
|
|
616
|
-
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
617
1229
|
#connected = false;
|
|
618
|
-
|
|
619
|
-
#
|
|
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;
|
|
620
1242
|
connect() {
|
|
1243
|
+
this.#trap.connect();
|
|
621
1244
|
this.#connected = true;
|
|
622
|
-
this.#beforeCache.activate();
|
|
623
1245
|
this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
|
|
1246
|
+
this.#storageKeyRead = this.#storageKey;
|
|
624
1247
|
this.#collapsed = this.#restoreCollapsed();
|
|
625
1248
|
this.#mqlQuery = this.#breakpointQuery;
|
|
626
1249
|
this.#mql = this.#matchBreakpoint(this.#mqlQuery);
|
|
627
1250
|
this.#mql?.addEventListener("change", this.#onMediaChange);
|
|
628
|
-
this.#
|
|
629
|
-
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);
|
|
630
1257
|
}
|
|
631
1258
|
disconnect() {
|
|
632
1259
|
this.#connected = false;
|
|
633
|
-
this.#
|
|
634
|
-
this.#beforeCache.deactivate();
|
|
1260
|
+
this.#morphRender.disconnect();
|
|
635
1261
|
this.#mql?.removeEventListener("change", this.#onMediaChange);
|
|
636
1262
|
this.#mql = null;
|
|
637
1263
|
this.#mqlQuery = null;
|
|
638
1264
|
this.#transition.cancel();
|
|
639
|
-
this.#trap.
|
|
1265
|
+
this.#trap.disconnect(this);
|
|
640
1266
|
this.#activePanel = null;
|
|
641
1267
|
}
|
|
642
|
-
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
643
1268
|
panelTargetConnected(panel) {
|
|
644
|
-
if (!this.#connected || this.#activePanel?.isConnected
|
|
1269
|
+
if (!this.#connected || this.#activePanel?.isConnected) return;
|
|
645
1270
|
this.#adoptPanel(panel);
|
|
646
1271
|
}
|
|
647
|
-
/** Closes and releases overlay side effects when the actively trapped panel disappears. */
|
|
648
1272
|
panelTargetDisconnected(panel) {
|
|
649
|
-
|
|
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;
|
|
650
1280
|
this.#transition.cancel();
|
|
651
1281
|
this.#activePanel = null;
|
|
652
|
-
if (
|
|
653
|
-
|
|
654
|
-
|
|
1282
|
+
if (stays) {
|
|
1283
|
+
this.#writeState(panel, "closed");
|
|
1284
|
+
this.#writeHidden(panel, true);
|
|
1285
|
+
}
|
|
655
1286
|
this.#hideBackdrop();
|
|
656
1287
|
this.#setExpandedAttr(false);
|
|
657
|
-
this.#
|
|
1288
|
+
if (!this.#connected) {
|
|
1289
|
+
this.#trap.deactivate({ restoreFocus: false });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
658
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();
|
|
659
1311
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Rebinds responsive observation when `breakpoint` changes at runtime.
|
|
662
|
-
*
|
|
663
|
-
* Stimulus calls value callbacks before `connect()`, so the connected guard
|
|
664
|
-
* prevents an eager subscription. Equivalent normalized queries retain the
|
|
665
|
-
* existing listener instead of allocating duplicate `MediaQueryList` objects.
|
|
666
|
-
*/
|
|
667
1312
|
breakpointValueChanged() {
|
|
668
1313
|
if (!this.#connected) return;
|
|
669
1314
|
const query = this.#breakpointQuery;
|
|
@@ -675,22 +1320,20 @@ var SidebarController = class extends Controller {
|
|
|
675
1320
|
const next = this.#computeMode();
|
|
676
1321
|
if (next !== this.#mode) this.#applyMode(next, true);
|
|
677
1322
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.#
|
|
689
|
-
|
|
690
|
-
this.#setOverlayClosedImmediate();
|
|
691
|
-
});
|
|
692
|
-
/** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
|
|
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
|
+
}
|
|
693
1335
|
toggle(event) {
|
|
1336
|
+
if (!this.#connected) return;
|
|
694
1337
|
const reason = stateReasonFor(event);
|
|
695
1338
|
if (this.#isOverlay) {
|
|
696
1339
|
this.#isOverlayOpen ? this.#closeOverlay(reason) : this.#openOverlay(reason);
|
|
@@ -698,83 +1341,78 @@ var SidebarController = class extends Controller {
|
|
|
698
1341
|
this.#setCollapsed(!this.#collapsed, reason);
|
|
699
1342
|
}
|
|
700
1343
|
}
|
|
701
|
-
/** Shows the panel (inline: expand; overlay: open). */
|
|
702
1344
|
open(event) {
|
|
1345
|
+
if (!this.#connected) return;
|
|
703
1346
|
const reason = stateReasonFor(event);
|
|
704
1347
|
if (this.#isOverlay) this.#openOverlay(reason);
|
|
705
1348
|
else this.#setCollapsed(false, reason);
|
|
706
1349
|
}
|
|
707
|
-
/** Hides the panel (inline: collapse; overlay: close). */
|
|
708
1350
|
close(event) {
|
|
1351
|
+
if (!this.#connected) return;
|
|
709
1352
|
const reason = this.#closeReason(event);
|
|
710
1353
|
if (this.#isOverlay) this.#closeOverlay(reason);
|
|
711
1354
|
else this.#setCollapsed(true, reason);
|
|
712
1355
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Reads why a close happened. The backdrop has no action of its own — the
|
|
715
|
-
* consumer wires this one onto it — so the shared `"outside"` vocabulary is
|
|
716
|
-
* only reachable by recognising that target here.
|
|
717
|
-
*/
|
|
718
1356
|
#closeReason(event) {
|
|
719
1357
|
if (this.hasBackdropTarget && event?.currentTarget === this.backdropTarget) return "outside";
|
|
720
1358
|
return stateReasonFor(event);
|
|
721
1359
|
}
|
|
722
|
-
// --- Mode handling ---------------------------------------------------------
|
|
723
|
-
/**
|
|
724
|
-
* Re-renders the closed/default state for `mode` and records it.
|
|
725
|
-
*
|
|
726
|
-
* @param report - Whether the mode change is one the consumer can observe: a
|
|
727
|
-
* viewport change re-derives the state under them, the connect-time baseline
|
|
728
|
-
* does not.
|
|
729
|
-
*/
|
|
730
1360
|
#applyMode(mode, report) {
|
|
731
1361
|
this.#mode = mode;
|
|
732
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1362
|
+
if (this.hasPanelTarget) this.#modeLease.write(this.panelTarget, mode);
|
|
733
1363
|
if (mode === "inline") {
|
|
734
1364
|
this.#transition.cancel();
|
|
735
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
736
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1365
|
+
if (report) this.#trap.deactivate({ restoreFocus: false });
|
|
1366
|
+
if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, false);
|
|
737
1367
|
this.#hideBackdrop();
|
|
738
1368
|
this.#applyInlineState(this.#collapsed);
|
|
739
1369
|
} else {
|
|
740
|
-
this.#transition.cancel();
|
|
741
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
742
1370
|
this.#setOverlayClosedImmediate();
|
|
743
1371
|
}
|
|
744
|
-
if (report
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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;
|
|
749
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
|
+
});
|
|
750
1393
|
}
|
|
751
|
-
/** Reconciles a replacement panel with the current responsive mode and DOM state. */
|
|
752
1394
|
#adoptPanel(panel) {
|
|
753
1395
|
this.#transition.cancel();
|
|
754
|
-
const trapWasActive = this.#trap.active;
|
|
755
1396
|
this.#activePanel = panel;
|
|
756
|
-
|
|
1397
|
+
this.#modeLease.write(panel, this.#mode);
|
|
757
1398
|
if (this.#mode === "inline") {
|
|
758
|
-
panel
|
|
759
|
-
panel
|
|
1399
|
+
this.#writeHidden(panel, false);
|
|
1400
|
+
this.#writeState(panel, this.#collapsed ? "collapsed" : "expanded");
|
|
760
1401
|
this.#hideBackdrop();
|
|
761
1402
|
this.#setExpandedAttr(!this.#collapsed);
|
|
762
|
-
this.#trap.deactivate({ restoreFocus: false });
|
|
763
1403
|
return;
|
|
764
1404
|
}
|
|
765
1405
|
if (panel.getAttribute("data-state") === "open") {
|
|
766
|
-
panel
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
this.backdropTarget.hidden = false;
|
|
770
|
-
}
|
|
1406
|
+
this.#writeHidden(panel, false);
|
|
1407
|
+
this.#writeBackdropState("open");
|
|
1408
|
+
this.#writeBackdropHidden(false);
|
|
771
1409
|
this.#setExpandedAttr(true);
|
|
772
|
-
if (
|
|
773
|
-
this.#trap.activate();
|
|
1410
|
+
if (this.#trap.active) this.#trap.refreshContainer();
|
|
1411
|
+
else this.#trap.activate();
|
|
774
1412
|
return;
|
|
775
1413
|
}
|
|
776
|
-
panel
|
|
777
|
-
panel
|
|
1414
|
+
this.#writeState(panel, "closed");
|
|
1415
|
+
this.#writeHidden(panel, true);
|
|
778
1416
|
this.#hideBackdrop();
|
|
779
1417
|
this.#setExpandedAttr(false);
|
|
780
1418
|
this.#trap.deactivate();
|
|
@@ -790,8 +1428,6 @@ var SidebarController = class extends Controller {
|
|
|
790
1428
|
if (typeof window.matchMedia !== "function") return null;
|
|
791
1429
|
return window.matchMedia(query);
|
|
792
1430
|
}
|
|
793
|
-
// --- Inline (rail) ---------------------------------------------------------
|
|
794
|
-
/** Sets, reflects, and persists the inline collapsed preference. */
|
|
795
1431
|
#setCollapsed(collapsed, reason) {
|
|
796
1432
|
if (collapsed === this.#collapsed) return;
|
|
797
1433
|
this.#collapsed = collapsed;
|
|
@@ -799,21 +1435,18 @@ var SidebarController = class extends Controller {
|
|
|
799
1435
|
this.#report(!collapsed, reason);
|
|
800
1436
|
this.#persistCollapsed(collapsed);
|
|
801
1437
|
}
|
|
802
|
-
/** Reflects the inline rail state onto the panel and trigger. */
|
|
803
1438
|
#applyInlineState(collapsed) {
|
|
804
1439
|
if (this.hasPanelTarget) {
|
|
805
|
-
this.panelTarget
|
|
1440
|
+
this.#writeState(this.panelTarget, collapsed ? "collapsed" : "expanded");
|
|
806
1441
|
}
|
|
807
1442
|
this.#setExpandedAttr(!collapsed);
|
|
808
1443
|
}
|
|
809
|
-
// --- Overlay (off-canvas modal) -------------------------------------------
|
|
810
|
-
/** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
|
|
811
1444
|
#openOverlay(reason) {
|
|
812
1445
|
if (!this.hasPanelTarget || this.#isOverlayOpen) return;
|
|
813
1446
|
this.#transition.cancel();
|
|
814
1447
|
this.#activePanel = this.panelTarget;
|
|
815
|
-
this.panelTarget
|
|
816
|
-
|
|
1448
|
+
this.#writeHidden(this.panelTarget, false);
|
|
1449
|
+
this.#writeBackdropHidden(false);
|
|
817
1450
|
void this.panelTarget.offsetWidth;
|
|
818
1451
|
this.#setOverlayState("open");
|
|
819
1452
|
this.#setExpandedAttr(true);
|
|
@@ -821,7 +1454,6 @@ var SidebarController = class extends Controller {
|
|
|
821
1454
|
if (!this.#isOverlayOpen) return;
|
|
822
1455
|
this.#trap.activate();
|
|
823
1456
|
}
|
|
824
|
-
/** Closes the overlay: start the exit transition, defer hide + trap teardown. */
|
|
825
1457
|
#closeOverlay(reason) {
|
|
826
1458
|
if (!this.hasPanelTarget || !this.#isOverlayOpen) return;
|
|
827
1459
|
this.#setOverlayState("closed");
|
|
@@ -830,93 +1462,90 @@ var SidebarController = class extends Controller {
|
|
|
830
1462
|
if (this.#isOverlayOpen) return;
|
|
831
1463
|
this.#hideAfterTransition();
|
|
832
1464
|
}
|
|
833
|
-
/** Renders the overlay closed state up front (used when entering overlay mode). */
|
|
834
1465
|
#setOverlayClosedImmediate() {
|
|
835
1466
|
this.#setOverlayState("closed");
|
|
836
1467
|
this.#setExpandedAttr(false);
|
|
837
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1468
|
+
if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, true);
|
|
838
1469
|
this.#hideBackdrop();
|
|
839
1470
|
}
|
|
840
|
-
/** Syncs `data-state` on the panel and backdrop together (overlay). */
|
|
841
1471
|
#setOverlayState(state) {
|
|
842
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Applies `hidden` once the close transition ends so the exit slide can play,
|
|
847
|
-
* then reverts the modal side effects. The shared waiter completes
|
|
848
|
-
* synchronously for 0ms transitions and supplies a bounded fallback when the
|
|
849
|
-
* browser emits no terminal event.
|
|
850
|
-
*/
|
|
1472
|
+
if (this.hasPanelTarget) this.#writeState(this.panelTarget, state);
|
|
1473
|
+
this.#writeBackdropState(state);
|
|
1474
|
+
}
|
|
851
1475
|
#hideAfterTransition() {
|
|
852
1476
|
const panel = this.panelTarget;
|
|
853
1477
|
this.#activePanel = panel;
|
|
854
1478
|
this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));
|
|
855
1479
|
}
|
|
856
|
-
/** Hides the panel/backdrop and tears down the trap after the exit transition. */
|
|
857
1480
|
#applyOverlayHidden(panel) {
|
|
858
|
-
panel
|
|
1481
|
+
this.#writeHidden(panel, true);
|
|
859
1482
|
this.#hideBackdrop();
|
|
860
1483
|
this.#trap.deactivate();
|
|
861
1484
|
}
|
|
862
1485
|
#hideBackdrop() {
|
|
863
|
-
|
|
864
|
-
this
|
|
865
|
-
|
|
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);
|
|
866
1506
|
}
|
|
867
|
-
// --- Shared helpers --------------------------------------------------------
|
|
868
1507
|
#setExpandedAttr(expanded) {
|
|
1508
|
+
this.#expanded = expanded;
|
|
1509
|
+
this.#reflectExpanded();
|
|
1510
|
+
}
|
|
1511
|
+
#reflectExpanded() {
|
|
869
1512
|
if (this.hasTriggerTarget) {
|
|
870
|
-
this.triggerTarget
|
|
1513
|
+
this.#expandedLease.write(this.triggerTarget, this.#expanded ? "true" : "false");
|
|
871
1514
|
}
|
|
872
1515
|
}
|
|
873
|
-
/** Reports a move of the expanded state, naming the mode it happened in. */
|
|
874
1516
|
#report(open, reason) {
|
|
875
|
-
if (!this.#reporting) return;
|
|
876
1517
|
const detail = { reason, mode: this.#mode };
|
|
877
1518
|
if (open) this.dispatch("open", { detail, cancelable: false });
|
|
878
1519
|
else this.dispatch("close", { detail, cancelable: false });
|
|
879
1520
|
}
|
|
880
|
-
/** Whether the panel currently counts as expanded in the mode it is in. */
|
|
881
1521
|
get #isExpanded() {
|
|
882
1522
|
return this.#isOverlay ? this.#isOverlayOpen : !this.#collapsed;
|
|
883
1523
|
}
|
|
884
|
-
/**
|
|
885
|
-
* Resolves the inline collapsed preference, in priority order:
|
|
886
|
-
* 1. the persisted value (when a `key` is set and storage is readable),
|
|
887
|
-
* 2. the current DOM `data-state` — so a Turbo cache restore / morph that
|
|
888
|
-
* reconnects over already-rendered markup keeps the live state (the DOM is
|
|
889
|
-
* the source of truth) even when no `key` / localStorage is available,
|
|
890
|
-
* 3. the declared `collapsed` value.
|
|
891
|
-
*/
|
|
892
1524
|
#restoreCollapsed() {
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
895
|
-
const result = readLocalStorage(key);
|
|
896
|
-
if (result.ok && result.value !== null) return result.value === "1";
|
|
897
|
-
}
|
|
1525
|
+
const stored = this.#readStoredCollapsed(this.#storageKey);
|
|
1526
|
+
if (stored !== null) return stored;
|
|
898
1527
|
const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
|
|
899
1528
|
if (domState === "collapsed") return true;
|
|
900
1529
|
if (domState === "expanded") return false;
|
|
901
1530
|
return this.collapsedValue;
|
|
902
1531
|
}
|
|
903
|
-
|
|
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
|
+
}
|
|
904
1538
|
#persistCollapsed(collapsed) {
|
|
905
1539
|
const key = this.#storageKey;
|
|
906
1540
|
if (!key) return;
|
|
907
1541
|
writeLocalStorage(key, collapsed ? "1" : "0");
|
|
908
1542
|
}
|
|
909
|
-
/**
|
|
910
|
-
* @stimeoRuntimeOnly `key` names the storage slot the collapsed state is saved under; the state
|
|
911
|
-
* itself comes from the user.
|
|
912
|
-
*/
|
|
913
1543
|
get #storageKey() {
|
|
914
1544
|
return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
|
|
915
1545
|
}
|
|
916
|
-
/** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */
|
|
917
1546
|
get #breakpointQuery() {
|
|
918
|
-
const value = this
|
|
919
|
-
const breakpoint =
|
|
1547
|
+
const value = this.#safeBreakpoint;
|
|
1548
|
+
const breakpoint = value;
|
|
920
1549
|
return `(min-width: ${breakpoint}px)`;
|
|
921
1550
|
}
|
|
922
1551
|
get #isOverlay() {
|
|
@@ -925,8 +1554,16 @@ var SidebarController = class extends Controller {
|
|
|
925
1554
|
get #isOverlayOpen() {
|
|
926
1555
|
return this.#isOverlay && this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
|
|
927
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
|
+
}
|
|
928
1566
|
};
|
|
929
1567
|
|
|
930
1568
|
export { SidebarController };
|
|
931
|
-
//# sourceMappingURL=sidebar_controller.js.map
|
|
932
1569
|
//# sourceMappingURL=sidebar_controller.js.map
|