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 });
|
|
24
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
|
+
}));
|
|
25
97
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
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,225 +340,699 @@ 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
|
+
}
|
|
335
873
|
}
|
|
336
|
-
|
|
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;
|
|
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();
|
|
365
957
|
}
|
|
366
958
|
};
|
|
367
959
|
|
|
368
|
-
|
|
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 LivedMark = class {
|
|
986
|
+
#name;
|
|
987
|
+
#lived = false;
|
|
988
|
+
#element = null;
|
|
989
|
+
#morph = new MorphRenderWatcher(() => this.#write());
|
|
990
|
+
constructor(identifier) {
|
|
991
|
+
this.#name = `data-${identifier}-lived`;
|
|
992
|
+
}
|
|
993
|
+
connect(element) {
|
|
994
|
+
const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
|
|
995
|
+
this.#lived = true;
|
|
996
|
+
this.#element = element;
|
|
997
|
+
this.#write();
|
|
998
|
+
this.#morph.observe(element);
|
|
999
|
+
return connection;
|
|
1000
|
+
}
|
|
1001
|
+
disconnect() {
|
|
1002
|
+
this.#morph.disconnect();
|
|
1003
|
+
}
|
|
1004
|
+
#write() {
|
|
1005
|
+
this.#element?.setAttribute(this.#name, "");
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
1010
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
1011
|
+
function stateReasonFor(event) {
|
|
1012
|
+
if (!event) return "api";
|
|
1013
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
1014
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
1015
|
+
return "user";
|
|
1016
|
+
}
|
|
1017
|
+
|
|
369
1018
|
var TimerRegistry = class {
|
|
370
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
371
1019
|
ids = /* @__PURE__ */ new Set();
|
|
372
|
-
/**
|
|
373
|
-
* Cancels a single tracked timer.
|
|
374
|
-
*
|
|
375
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
376
|
-
* registry), so callers can clear defensively without guarding.
|
|
377
|
-
*/
|
|
378
1020
|
clear(id) {
|
|
379
1021
|
if (this.ids.delete(id)) {
|
|
380
1022
|
this.cancel(id);
|
|
381
1023
|
}
|
|
382
1024
|
}
|
|
383
|
-
/**
|
|
384
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
385
|
-
* to guarantee no timer outlives the element.
|
|
386
|
-
*/
|
|
387
1025
|
clearAll() {
|
|
388
1026
|
for (const id of this.ids) {
|
|
389
1027
|
this.cancel(id);
|
|
390
1028
|
}
|
|
391
1029
|
this.ids.clear();
|
|
392
1030
|
}
|
|
393
|
-
/** Number of timers currently tracked (pending). */
|
|
394
1031
|
get size() {
|
|
395
1032
|
return this.ids.size;
|
|
396
1033
|
}
|
|
397
1034
|
};
|
|
398
1035
|
var SafeTimeout = class extends TimerRegistry {
|
|
399
|
-
/**
|
|
400
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
401
|
-
*
|
|
402
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
403
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
404
|
-
*/
|
|
405
1036
|
set(callback, delay) {
|
|
406
1037
|
const id = this.schedule(() => {
|
|
407
1038
|
this.ids.delete(id);
|
|
@@ -418,7 +1049,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
418
1049
|
}
|
|
419
1050
|
};
|
|
420
1051
|
|
|
421
|
-
// src/utils/transition_completion.ts
|
|
422
1052
|
function timeMs(value) {
|
|
423
1053
|
const trimmed = value.trim();
|
|
424
1054
|
const amount = Number.parseFloat(trimmed);
|
|
@@ -454,13 +1084,6 @@ var TransitionCompletion = class {
|
|
|
454
1084
|
#complete = null;
|
|
455
1085
|
#pendingProperties = null;
|
|
456
1086
|
#deadline = 0;
|
|
457
|
-
/**
|
|
458
|
-
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
459
|
-
* transition (including when `getComputedStyle` is unavailable).
|
|
460
|
-
*
|
|
461
|
-
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
462
|
-
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
463
|
-
*/
|
|
464
1087
|
wait(element, complete, options = {}) {
|
|
465
1088
|
this.cancel();
|
|
466
1089
|
const requested = options.timeoutMs ?? 0;
|
|
@@ -480,18 +1103,10 @@ var TransitionCompletion = class {
|
|
|
480
1103
|
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
481
1104
|
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
482
1105
|
}
|
|
483
|
-
/** Cancels the pending wait without invoking its completion callback. */
|
|
484
1106
|
cancel() {
|
|
485
1107
|
this.#complete = null;
|
|
486
1108
|
this.#teardown();
|
|
487
1109
|
}
|
|
488
|
-
/**
|
|
489
|
-
* Handles terminal events from the observed element only.
|
|
490
|
-
*
|
|
491
|
-
* For explicit property lists, every declared positive-time property must
|
|
492
|
-
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
493
|
-
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
494
|
-
*/
|
|
495
1110
|
#onTerminal = (event) => {
|
|
496
1111
|
if (event.target !== this.#element) return;
|
|
497
1112
|
const transitionEvent = event;
|
|
@@ -511,10 +1126,6 @@ var TransitionCompletion = class {
|
|
|
511
1126
|
}
|
|
512
1127
|
if (Date.now() >= this.#deadline) this.#finish();
|
|
513
1128
|
};
|
|
514
|
-
/**
|
|
515
|
-
* Returns active CSS transition properties expanded to the names reported by
|
|
516
|
-
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
517
|
-
*/
|
|
518
1129
|
#activeTransitionProperties(element) {
|
|
519
1130
|
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
520
1131
|
try {
|
|
@@ -532,7 +1143,6 @@ var TransitionCompletion = class {
|
|
|
532
1143
|
return /* @__PURE__ */ new Set();
|
|
533
1144
|
}
|
|
534
1145
|
}
|
|
535
|
-
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
536
1146
|
#explicitPendingProperties(timings) {
|
|
537
1147
|
if (timings.some(({ property }) => property === "all")) return null;
|
|
538
1148
|
const pending = new Set(
|
|
@@ -540,7 +1150,6 @@ var TransitionCompletion = class {
|
|
|
540
1150
|
);
|
|
541
1151
|
return pending.size > 0 ? pending : null;
|
|
542
1152
|
}
|
|
543
|
-
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
544
1153
|
#finish() {
|
|
545
1154
|
const complete = this.#complete;
|
|
546
1155
|
if (!complete) return;
|
|
@@ -548,7 +1157,6 @@ var TransitionCompletion = class {
|
|
|
548
1157
|
this.#teardown();
|
|
549
1158
|
complete();
|
|
550
1159
|
}
|
|
551
|
-
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
552
1160
|
#teardown() {
|
|
553
1161
|
this.#timers.clearAll();
|
|
554
1162
|
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
@@ -559,7 +1167,6 @@ var TransitionCompletion = class {
|
|
|
559
1167
|
}
|
|
560
1168
|
};
|
|
561
1169
|
|
|
562
|
-
// src/controllers/drawer_controller.ts
|
|
563
1170
|
var DrawerController = class extends Controller {
|
|
564
1171
|
static targets = ["trigger", "overlay", "panel"];
|
|
565
1172
|
static values = {
|
|
@@ -567,172 +1174,192 @@ var DrawerController = class extends Controller {
|
|
|
567
1174
|
open: { type: Boolean, default: false }
|
|
568
1175
|
};
|
|
569
1176
|
static actions = ["close", "closeOnBackdrop", "open"];
|
|
570
|
-
|
|
1177
|
+
static events = ["close", "open"];
|
|
571
1178
|
#activePanel = null;
|
|
572
|
-
|
|
1179
|
+
#openState = false;
|
|
573
1180
|
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
574
|
-
onEscape: () => this
|
|
1181
|
+
onEscape: () => this.#close("escape"),
|
|
575
1182
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
576
1183
|
});
|
|
577
|
-
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
578
1184
|
#transition = new TransitionCompletion();
|
|
579
|
-
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
580
1185
|
#connected = false;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* close a user-opened drawer). The `open` Value only seeds a genuinely fresh
|
|
588
|
-
* render. We normalize to a clean closed baseline first so {@link open} runs its
|
|
589
|
-
* full reveal + trap activation — the `FocusTrap` is inactive after a
|
|
590
|
-
* disconnect and must be re-activated.
|
|
591
|
-
*/
|
|
1186
|
+
#reporting = false;
|
|
1187
|
+
#lived = new LivedMark(this.identifier);
|
|
1188
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
1189
|
+
#stateLease = new AttributeLease("data-state", this.identifier);
|
|
1190
|
+
#hiddenLease = new AttributeLease("hidden", this.identifier);
|
|
1191
|
+
#placementLease = new AttributeLease("data-placement", this.identifier);
|
|
592
1192
|
connect() {
|
|
1193
|
+
const restored = this.#lived.connect(this.element) === "restored";
|
|
1194
|
+
this.#trap.connect();
|
|
1195
|
+
this.#morphRender.observe(this.element);
|
|
593
1196
|
this.#connected = true;
|
|
594
1197
|
this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
|
|
595
1198
|
this.#reflectPlacement();
|
|
596
|
-
const shouldOpen = this.#isOpen || this.openValue;
|
|
1199
|
+
const shouldOpen = !restored && (this.#isOpen || this.openValue);
|
|
597
1200
|
this.#applyClosedState();
|
|
598
|
-
if (
|
|
1201
|
+
if (restored) this.openValue = false;
|
|
1202
|
+
if (shouldOpen) this.#open("api");
|
|
1203
|
+
this.#reporting = true;
|
|
599
1204
|
}
|
|
600
|
-
/** Reverts the modal side effects and pending hide if torn down while open. */
|
|
601
1205
|
disconnect() {
|
|
1206
|
+
this.#lived.disconnect();
|
|
1207
|
+
this.#morphRender.disconnect();
|
|
602
1208
|
this.#connected = false;
|
|
1209
|
+
this.#reporting = false;
|
|
603
1210
|
this.#transition.cancel();
|
|
604
|
-
this.#trap.
|
|
1211
|
+
this.#trap.disconnect(this);
|
|
605
1212
|
this.#activePanel = null;
|
|
606
1213
|
}
|
|
607
|
-
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
608
1214
|
panelTargetConnected(panel) {
|
|
609
|
-
if (!this.#connected || this.#activePanel?.isConnected
|
|
1215
|
+
if (!this.#connected || this.#activePanel?.isConnected) return;
|
|
610
1216
|
this.#adoptPanel(panel);
|
|
611
1217
|
}
|
|
612
|
-
/** Closes and releases modal side effects when the actively trapped panel disappears. */
|
|
613
1218
|
panelTargetDisconnected(panel) {
|
|
614
|
-
|
|
1219
|
+
const stays = this.panelTargets.includes(panel);
|
|
1220
|
+
if (!stays) {
|
|
1221
|
+
this.#stateLease.return(panel);
|
|
1222
|
+
this.#hiddenLease.return(panel);
|
|
1223
|
+
this.#placementLease.return(panel);
|
|
1224
|
+
}
|
|
1225
|
+
if (panel !== this.#activePanel || stays && this.panelTarget === panel) return;
|
|
615
1226
|
this.#transition.cancel();
|
|
616
1227
|
this.#activePanel = null;
|
|
617
|
-
if (!this.#connected)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1228
|
+
if (!this.#connected) {
|
|
1229
|
+
this.#trap.deactivate({ restoreFocus: false });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
this.#openState = false;
|
|
1233
|
+
if (stays) {
|
|
1234
|
+
this.#writeState(panel, "closed");
|
|
1235
|
+
this.#writeHidden(panel, true);
|
|
623
1236
|
}
|
|
1237
|
+
this.#writeOverlayState("closed");
|
|
1238
|
+
this.#writeOverlayHidden(true);
|
|
624
1239
|
this.openValue = false;
|
|
625
|
-
this.#trap.deactivate();
|
|
626
1240
|
if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
|
|
1241
|
+
else this.#trap.deactivate();
|
|
1242
|
+
}
|
|
1243
|
+
overlayTargetConnected() {
|
|
1244
|
+
this.#morphRender.schedule();
|
|
1245
|
+
}
|
|
1246
|
+
overlayTargetDisconnected(overlay) {
|
|
1247
|
+
if (!this.overlayTargets.includes(overlay)) {
|
|
1248
|
+
this.#stateLease.return(overlay);
|
|
1249
|
+
this.#hiddenLease.return(overlay);
|
|
1250
|
+
}
|
|
1251
|
+
this.#morphRender.schedule();
|
|
627
1252
|
}
|
|
628
|
-
/** Keeps `data-placement` in sync if the value changes at runtime. */
|
|
629
1253
|
placementValueChanged() {
|
|
630
|
-
this.#
|
|
1254
|
+
if (this.#connected) this.#repair();
|
|
1255
|
+
else this.#reflectPlacement();
|
|
1256
|
+
}
|
|
1257
|
+
open(event) {
|
|
1258
|
+
if (!this.#connected) return;
|
|
1259
|
+
this.#open(stateReasonFor(event));
|
|
631
1260
|
}
|
|
632
|
-
|
|
633
|
-
open() {
|
|
1261
|
+
#open(reason) {
|
|
634
1262
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
635
1263
|
this.#transition.cancel();
|
|
636
1264
|
this.#activePanel = this.panelTarget;
|
|
637
|
-
this.panelTarget
|
|
638
|
-
|
|
1265
|
+
this.#writeHidden(this.panelTarget, false);
|
|
1266
|
+
this.#writeOverlayHidden(false);
|
|
639
1267
|
void this.panelTarget.offsetWidth;
|
|
640
1268
|
this.#setState("open");
|
|
641
1269
|
this.openValue = true;
|
|
1270
|
+
if (this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
1271
|
+
if (!this.#isOpen) return;
|
|
642
1272
|
this.#trap.activate();
|
|
643
1273
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
close() {
|
|
1274
|
+
close(event) {
|
|
1275
|
+
if (!this.#connected) return;
|
|
1276
|
+
this.#close(stateReasonFor(event));
|
|
1277
|
+
}
|
|
1278
|
+
closeOnBackdrop(event) {
|
|
1279
|
+
if (!this.#connected) return;
|
|
1280
|
+
if (this.hasOverlayTarget && event.target === this.overlayTarget) this.#close("outside");
|
|
1281
|
+
}
|
|
1282
|
+
#close(reason) {
|
|
653
1283
|
if (!this.hasPanelTarget || !this.#isOpen) return;
|
|
654
1284
|
this.openValue = false;
|
|
655
1285
|
this.#setState("closed");
|
|
1286
|
+
if (this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
1287
|
+
if (this.#isOpen) return;
|
|
656
1288
|
this.#hideAfterTransition();
|
|
657
1289
|
}
|
|
658
|
-
/** Closes only when the overlay itself (not its contents) is clicked. */
|
|
659
|
-
closeOnBackdrop(event) {
|
|
660
|
-
if (this.hasOverlayTarget && event.target === this.overlayTarget) this.close();
|
|
661
|
-
}
|
|
662
|
-
/** Writes `data-placement` from the current `placement` value. */
|
|
663
1290
|
#reflectPlacement() {
|
|
664
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
1291
|
+
if (this.hasPanelTarget) this.#placementLease.write(this.panelTarget, this.#placement);
|
|
665
1292
|
}
|
|
666
|
-
/** Reconciles a replacement panel and companion overlay from its explicit DOM state. */
|
|
667
1293
|
#adoptPanel(panel) {
|
|
668
1294
|
this.#transition.cancel();
|
|
669
|
-
const trapWasActive = this.#trap.active;
|
|
670
1295
|
this.#activePanel = panel;
|
|
671
|
-
|
|
1296
|
+
this.#placementLease.write(panel, this.#placement);
|
|
672
1297
|
if (panel.getAttribute("data-state") === "open") {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
1298
|
+
this.#openState = true;
|
|
1299
|
+
this.#writeHidden(panel, false);
|
|
1300
|
+
this.#writeOverlayState("open");
|
|
1301
|
+
this.#writeOverlayHidden(false);
|
|
678
1302
|
this.openValue = true;
|
|
679
|
-
if (
|
|
680
|
-
this.#trap.activate();
|
|
1303
|
+
if (this.#trap.active) this.#trap.refreshContainer();
|
|
1304
|
+
else this.#trap.activate();
|
|
681
1305
|
return;
|
|
682
1306
|
}
|
|
683
|
-
|
|
684
|
-
panel
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
1307
|
+
this.#openState = false;
|
|
1308
|
+
this.#writeState(panel, "closed");
|
|
1309
|
+
this.#writeHidden(panel, true);
|
|
1310
|
+
this.#writeOverlayState("closed");
|
|
1311
|
+
this.#writeOverlayHidden(true);
|
|
689
1312
|
this.openValue = false;
|
|
690
1313
|
this.#trap.deactivate();
|
|
691
1314
|
}
|
|
692
|
-
|
|
1315
|
+
#repair() {
|
|
1316
|
+
this.#reflectPlacement();
|
|
1317
|
+
if (!this.hasPanelTarget) return;
|
|
1318
|
+
this.#setState(this.#openState ? "open" : "closed");
|
|
1319
|
+
const hidden = !this.#openState && !this.#trap.active;
|
|
1320
|
+
this.#writeHidden(this.panelTarget, hidden);
|
|
1321
|
+
this.#writeOverlayHidden(hidden);
|
|
1322
|
+
}
|
|
693
1323
|
get #placement() {
|
|
694
1324
|
const value = this.placementValue;
|
|
695
1325
|
return value === "left" || value === "top" || value === "bottom" ? value : "right";
|
|
696
1326
|
}
|
|
697
|
-
/** Syncs `data-state` on the panel and overlay together. */
|
|
698
1327
|
#setState(state) {
|
|
699
|
-
|
|
700
|
-
if (this.
|
|
1328
|
+
this.#openState = state === "open";
|
|
1329
|
+
if (this.hasPanelTarget) this.#writeState(this.panelTarget, state);
|
|
1330
|
+
this.#writeOverlayState(state);
|
|
1331
|
+
}
|
|
1332
|
+
#writeState(element, state) {
|
|
1333
|
+
this.#stateLease.write(element, state);
|
|
1334
|
+
}
|
|
1335
|
+
#writeHidden(element, hidden) {
|
|
1336
|
+
this.#hiddenLease.write(element, hidden ? "" : null);
|
|
1337
|
+
}
|
|
1338
|
+
#writeOverlayState(state) {
|
|
1339
|
+
if (this.hasOverlayTarget) this.#writeState(this.overlayTarget, state);
|
|
1340
|
+
}
|
|
1341
|
+
#writeOverlayHidden(hidden) {
|
|
1342
|
+
if (this.hasOverlayTarget) this.#writeHidden(this.overlayTarget, hidden);
|
|
701
1343
|
}
|
|
702
|
-
/** Fully reflects the closed state up front (used on connect when not open). */
|
|
703
1344
|
#applyClosedState() {
|
|
704
1345
|
this.#setState("closed");
|
|
705
|
-
if (this.hasPanelTarget) this.panelTarget
|
|
706
|
-
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Applies `hidden` once the panel's close transition ends, so the exit slide
|
|
710
|
-
* can play. The shared waiter hides synchronously for 0ms transitions and
|
|
711
|
-
* supplies a bounded fallback when the browser emits no terminal event.
|
|
712
|
-
*/
|
|
1346
|
+
if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, true);
|
|
1347
|
+
this.#writeOverlayHidden(true);
|
|
1348
|
+
}
|
|
713
1349
|
#hideAfterTransition() {
|
|
714
1350
|
const panel = this.panelTarget;
|
|
715
1351
|
this.#activePanel = panel;
|
|
716
1352
|
this.#transition.wait(panel, () => this.#applyHidden(panel));
|
|
717
1353
|
}
|
|
718
|
-
/**
|
|
719
|
-
* Runs once the close transition has finished: applies `hidden` to the panel
|
|
720
|
-
* and overlay, then reverts the modal side effects (scroll lock, background
|
|
721
|
-
* `inert`, keydown listener) and restores focus to the opener. Deferring the
|
|
722
|
-
* `FocusTrap` teardown to here — rather than at {@link close} time — keeps
|
|
723
|
-
* the background unreachable and focus trapped for the whole exit animation.
|
|
724
|
-
*/
|
|
725
1354
|
#applyHidden(panel) {
|
|
726
|
-
panel
|
|
727
|
-
|
|
1355
|
+
this.#writeHidden(panel, true);
|
|
1356
|
+
this.#writeOverlayHidden(true);
|
|
728
1357
|
this.#trap.deactivate();
|
|
729
1358
|
}
|
|
730
|
-
/** Whether the drawer is open (tracked via `data-state`, not `hidden`). */
|
|
731
1359
|
get #isOpen() {
|
|
732
1360
|
return this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
|
|
733
1361
|
}
|
|
734
1362
|
};
|
|
735
1363
|
|
|
736
1364
|
export { DrawerController };
|
|
737
|
-
//# sourceMappingURL=drawer_controller.js.map
|
|
738
1365
|
//# sourceMappingURL=drawer_controller.js.map
|