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,8 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/active_option.ts
|
|
6
25
|
function syncActiveOption(options, active) {
|
|
7
26
|
let written = 0;
|
|
8
27
|
for (const option of options) {
|
|
@@ -14,13 +33,158 @@ function syncActiveOption(options, active) {
|
|
|
14
33
|
return written;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
|
-
// src/utils/arrow_step.ts
|
|
18
36
|
function isReservedArrowChord(event, allow = []) {
|
|
19
37
|
if (!event.key.startsWith("Arrow")) return false;
|
|
20
38
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
|
|
41
|
+
function sharedRegistry(key, create) {
|
|
42
|
+
const symbol = Symbol.for(key);
|
|
43
|
+
const scope = globalThis;
|
|
44
|
+
const existing = scope[symbol];
|
|
45
|
+
if (existing !== void 0) return existing;
|
|
46
|
+
const registry = create();
|
|
47
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAttributeValue(value) {
|
|
52
|
+
return value === null || typeof value === "string";
|
|
53
|
+
}
|
|
54
|
+
function readLeaseRecord(element, record) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
57
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
58
|
+
const [original, written, ...rest] = parsed;
|
|
59
|
+
const linked = rest[0] !== false;
|
|
60
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
61
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
62
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
63
|
+
} catch {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var AttributeLease = class _AttributeLease {
|
|
68
|
+
static #registry = sharedRegistry(
|
|
69
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
70
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
71
|
+
);
|
|
72
|
+
#attribute;
|
|
73
|
+
#record;
|
|
74
|
+
#base;
|
|
75
|
+
#held = /* @__PURE__ */ new Set();
|
|
76
|
+
constructor(attribute, owner, options = {}) {
|
|
77
|
+
this.#attribute = attribute;
|
|
78
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
79
|
+
this.#base = options.base === true;
|
|
80
|
+
const names = _AttributeLease.#registry.names;
|
|
81
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
82
|
+
}
|
|
83
|
+
write(element, value) {
|
|
84
|
+
const stack = this.#stack(element);
|
|
85
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
86
|
+
this.#held.add(element);
|
|
87
|
+
const shows = !this.#covered(element, stack, hold);
|
|
88
|
+
hold.written = value;
|
|
89
|
+
this.#mark(element, stack);
|
|
90
|
+
if (shows) this.#reflect(element, value);
|
|
91
|
+
}
|
|
92
|
+
return(element) {
|
|
93
|
+
const held = this.#held.delete(element);
|
|
94
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
95
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
96
|
+
const hold = this.#take(stack);
|
|
97
|
+
if (!hold) return false;
|
|
98
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
99
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
100
|
+
const index = stack.indexOf(hold);
|
|
101
|
+
stack.splice(index, 1);
|
|
102
|
+
const above = stack[index];
|
|
103
|
+
if (above) above.linked &&= hold.linked;
|
|
104
|
+
this.#mark(element, stack);
|
|
105
|
+
if (!owns) return true;
|
|
106
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
107
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
returnAll() {
|
|
111
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
112
|
+
}
|
|
113
|
+
#stack(element) {
|
|
114
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
115
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
116
|
+
stacks.set(element, byAttribute);
|
|
117
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
118
|
+
byAttribute.set(this.#attribute, stack);
|
|
119
|
+
return stack;
|
|
120
|
+
}
|
|
121
|
+
#seed(element) {
|
|
122
|
+
const found = [];
|
|
123
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
124
|
+
const read = readLeaseRecord(element, record);
|
|
125
|
+
if (read) found.push({ ...read, record });
|
|
126
|
+
}
|
|
127
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
128
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
129
|
+
lease: null,
|
|
130
|
+
record,
|
|
131
|
+
written,
|
|
132
|
+
original,
|
|
133
|
+
linked: linked && index > 0
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
#take(stack) {
|
|
137
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
138
|
+
if (hold) hold.lease = this;
|
|
139
|
+
return hold;
|
|
140
|
+
}
|
|
141
|
+
#acquire(element, stack) {
|
|
142
|
+
const current = element.getAttribute(this.#attribute);
|
|
143
|
+
const top = stack.at(-1);
|
|
144
|
+
const bottom = stack[0];
|
|
145
|
+
const over = top?.written === current ? top : void 0;
|
|
146
|
+
const from = this.#base ? bottom : over;
|
|
147
|
+
const hold = {
|
|
148
|
+
lease: this,
|
|
149
|
+
record: this.#record,
|
|
150
|
+
written: current,
|
|
151
|
+
original: from ? from.original : current,
|
|
152
|
+
linked: !this.#base && over !== void 0
|
|
153
|
+
};
|
|
154
|
+
if (this.#base && bottom) {
|
|
155
|
+
bottom.linked = true;
|
|
156
|
+
stack.unshift(hold);
|
|
157
|
+
} else stack.push(hold);
|
|
158
|
+
return hold;
|
|
159
|
+
}
|
|
160
|
+
#covered(element, stack, hold) {
|
|
161
|
+
const current = element.getAttribute(this.#attribute);
|
|
162
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
163
|
+
}
|
|
164
|
+
#mark(element, stack) {
|
|
165
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
166
|
+
stack.forEach((hold, index) => {
|
|
167
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
168
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
169
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
170
|
+
records.set(
|
|
171
|
+
hold.record,
|
|
172
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
for (const [name, recorded] of records) {
|
|
176
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
177
|
+
if (recorded === null) element.removeAttribute(name);
|
|
178
|
+
else element.setAttribute(name, recorded);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
#reflect(element, value) {
|
|
182
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
183
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
184
|
+
else element.setAttribute(this.#attribute, value);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
24
188
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
25
189
|
"insertCompositionText",
|
|
26
190
|
"insertFromComposition",
|
|
@@ -32,13 +196,11 @@ var CompositionTracker = class {
|
|
|
32
196
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
33
197
|
#onStart;
|
|
34
198
|
#onEnd;
|
|
35
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
36
199
|
#confirmedTarget = null;
|
|
37
200
|
constructor(options = {}) {
|
|
38
201
|
this.#onStart = options.onStart;
|
|
39
202
|
this.#onEnd = options.onEnd;
|
|
40
203
|
}
|
|
41
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
42
204
|
observe(target) {
|
|
43
205
|
if (this.#observedTargets.has(target)) return;
|
|
44
206
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -46,7 +208,6 @@ var CompositionTracker = class {
|
|
|
46
208
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
47
209
|
this.#observedTargets.add(target);
|
|
48
210
|
}
|
|
49
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
50
211
|
unobserve(target) {
|
|
51
212
|
if (!this.#observedTargets.delete(target)) return;
|
|
52
213
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -55,7 +216,6 @@ var CompositionTracker = class {
|
|
|
55
216
|
this.#activeTargets.delete(target);
|
|
56
217
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
57
218
|
}
|
|
58
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
59
219
|
disconnect() {
|
|
60
220
|
for (const target of this.#observedTargets) {
|
|
61
221
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -66,16 +226,9 @@ var CompositionTracker = class {
|
|
|
66
226
|
this.#activeTargets.clear();
|
|
67
227
|
this.#confirmedTarget = null;
|
|
68
228
|
}
|
|
69
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
70
229
|
isComposing(event) {
|
|
71
230
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
72
231
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
75
|
-
*
|
|
76
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
77
|
-
* once and a consumer asks once per `input`.
|
|
78
|
-
*/
|
|
79
232
|
consumesConfirmedInput(event) {
|
|
80
233
|
const confirmed = this.#confirmedTarget;
|
|
81
234
|
this.#confirmedTarget = null;
|
|
@@ -93,54 +246,54 @@ var CompositionTracker = class {
|
|
|
93
246
|
this.#confirmedTarget = event.target;
|
|
94
247
|
this.#onEnd?.(event);
|
|
95
248
|
};
|
|
96
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
97
249
|
#handleKeydown = () => {
|
|
98
250
|
this.#confirmedTarget = null;
|
|
99
251
|
};
|
|
100
252
|
};
|
|
101
253
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
static
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
activate() {
|
|
117
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
118
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
119
|
-
if (first) {
|
|
120
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
254
|
+
var DetachGate = class _DetachGate {
|
|
255
|
+
#pending = false;
|
|
256
|
+
get pending() {
|
|
257
|
+
return this.#pending;
|
|
258
|
+
}
|
|
259
|
+
static isDetached(host) {
|
|
260
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
261
|
+
}
|
|
262
|
+
disconnected(host, teardown) {
|
|
263
|
+
const carried = _DetachGate.#carried(host);
|
|
264
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
265
|
+
this.#pending = false;
|
|
266
|
+
teardown();
|
|
267
|
+
return;
|
|
121
268
|
}
|
|
269
|
+
this.#pending = true;
|
|
270
|
+
const probe = () => {
|
|
271
|
+
if (!this.#pending) return;
|
|
272
|
+
this.#pending = false;
|
|
273
|
+
teardown();
|
|
274
|
+
};
|
|
275
|
+
if (carried) setTimeout(probe, 0);
|
|
276
|
+
else queueMicrotask(probe);
|
|
122
277
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
278
|
+
static #carried(host) {
|
|
279
|
+
const element = host.element;
|
|
280
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
281
|
+
}
|
|
282
|
+
static #listed(host) {
|
|
283
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
284
|
+
return tokens.includes(host.identifier);
|
|
285
|
+
}
|
|
286
|
+
cancel() {
|
|
287
|
+
this.#pending = false;
|
|
128
288
|
}
|
|
129
289
|
};
|
|
130
290
|
|
|
131
|
-
// src/utils/escape_layer.ts
|
|
132
291
|
var EscapeLayer = class _EscapeLayer {
|
|
133
|
-
static #registries =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
#claims = null;
|
|
139
|
-
/**
|
|
140
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
141
|
-
* the document's shared resolver listener if this is its first layer.
|
|
142
|
-
* Re-activating an already-active layer moves it to the top.
|
|
143
|
-
*/
|
|
292
|
+
static #registries = sharedRegistry(
|
|
293
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
294
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
295
|
+
);
|
|
296
|
+
#active = null;
|
|
144
297
|
activate(ownerDocument = document, options) {
|
|
145
298
|
this.deactivate();
|
|
146
299
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -149,44 +302,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
149
302
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
150
303
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
151
304
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.#
|
|
155
|
-
this.#claims = options.claims ?? null;
|
|
305
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
306
|
+
registry.stack.push(entry);
|
|
307
|
+
this.#active = { document: ownerDocument, entry };
|
|
156
308
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
159
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
160
|
-
*/
|
|
161
309
|
deactivate() {
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
164
|
-
const registry = _EscapeLayer.#registries.get(
|
|
310
|
+
const active = this.#active;
|
|
311
|
+
if (!active) return;
|
|
312
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
165
313
|
if (registry) {
|
|
166
|
-
const index = registry.stack.lastIndexOf(
|
|
314
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
167
315
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
168
316
|
if (registry.stack.length === 0) {
|
|
169
|
-
|
|
170
|
-
_EscapeLayer.#registries.delete(
|
|
317
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
318
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
171
319
|
}
|
|
172
320
|
}
|
|
173
|
-
this.#
|
|
174
|
-
this.#onDismiss = null;
|
|
175
|
-
this.#claims = null;
|
|
321
|
+
this.#active = null;
|
|
176
322
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
179
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
180
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
181
|
-
*/
|
|
182
323
|
get ownsEscape() {
|
|
183
|
-
const
|
|
184
|
-
if (!
|
|
185
|
-
const registry = _EscapeLayer.#registries.get(
|
|
324
|
+
const active = this.#active;
|
|
325
|
+
if (!active) return false;
|
|
326
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
186
327
|
if (!registry) return false;
|
|
187
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
328
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
188
329
|
}
|
|
189
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
190
330
|
static #createRegistry() {
|
|
191
331
|
const registry = {
|
|
192
332
|
stack: [],
|
|
@@ -195,24 +335,22 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
195
335
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
196
336
|
if (!owner) return;
|
|
197
337
|
event.preventDefault();
|
|
198
|
-
owner
|
|
338
|
+
owner.onDismiss();
|
|
199
339
|
}
|
|
200
340
|
};
|
|
201
341
|
return registry;
|
|
202
342
|
}
|
|
203
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
204
343
|
static #resolveOwner(stack) {
|
|
205
344
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
206
345
|
const layer = stack[index];
|
|
207
346
|
if (!layer) continue;
|
|
208
|
-
if (layer
|
|
347
|
+
if (layer.claims && !layer.claims()) continue;
|
|
209
348
|
return layer;
|
|
210
349
|
}
|
|
211
350
|
return null;
|
|
212
351
|
}
|
|
213
352
|
};
|
|
214
353
|
|
|
215
|
-
// src/utils/focus_candidate.ts
|
|
216
354
|
function inheritsFieldsetDisabled(control) {
|
|
217
355
|
let fieldset = control.closest("fieldset[disabled]");
|
|
218
356
|
while (fieldset) {
|
|
@@ -222,8 +360,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
222
360
|
}
|
|
223
361
|
return false;
|
|
224
362
|
}
|
|
363
|
+
function unslotted(element) {
|
|
364
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
365
|
+
}
|
|
366
|
+
function flatTreeParent(element) {
|
|
367
|
+
if (unslotted(element)) return null;
|
|
368
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
369
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
370
|
+
return parent instanceof Element ? parent : null;
|
|
371
|
+
}
|
|
372
|
+
function flatTreeContains(ancestor, element) {
|
|
373
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
374
|
+
if (node === ancestor) return true;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
function closestInFlatTree(element, selector) {
|
|
379
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
380
|
+
if (node.matches(selector)) return node;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function flatTreeChildren(element) {
|
|
385
|
+
return ownedScope(element) ?? Array.from(element.children);
|
|
386
|
+
}
|
|
225
387
|
function canTakeFocus(element) {
|
|
226
|
-
|
|
388
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
389
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
390
|
+
}
|
|
227
391
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
228
392
|
if (!("disabled" in element)) return true;
|
|
229
393
|
if (element.disabled) return false;
|
|
@@ -280,222 +444,676 @@ function isTabStop(element) {
|
|
|
280
444
|
if (tabindex !== null) return tabindex >= 0;
|
|
281
445
|
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
282
446
|
}
|
|
283
|
-
function
|
|
284
|
-
return
|
|
285
|
-
|
|
447
|
+
function isRadio(element) {
|
|
448
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
449
|
+
}
|
|
450
|
+
function sameRadioGroup(a, b) {
|
|
451
|
+
return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
452
|
+
}
|
|
453
|
+
function isInEditableContent(element) {
|
|
454
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
455
|
+
if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
|
|
456
|
+
if (hasEditableTabStop(node)) return true;
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
461
|
+
function isSvgLink(element) {
|
|
462
|
+
return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
|
|
463
|
+
}
|
|
464
|
+
function hasRenderedImage(area) {
|
|
465
|
+
const map = area.closest("map");
|
|
466
|
+
const key = map?.name || map?.id;
|
|
467
|
+
if (!map || !key) return false;
|
|
468
|
+
const root = map.getRootNode();
|
|
469
|
+
return Array.from(root.querySelectorAll("img[usemap]")).some(
|
|
470
|
+
(image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
|
|
286
471
|
);
|
|
287
472
|
}
|
|
473
|
+
function isFocusableInOrder(element) {
|
|
474
|
+
const tabindex = authoredTabindex(element);
|
|
475
|
+
return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
|
|
476
|
+
}
|
|
477
|
+
function extraStop(element) {
|
|
478
|
+
if (element instanceof HTMLAreaElement) {
|
|
479
|
+
const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
|
|
480
|
+
return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
|
|
481
|
+
}
|
|
482
|
+
if (element instanceof HTMLObjectElement) {
|
|
483
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
|
|
484
|
+
}
|
|
485
|
+
if (isSvgLink(element))
|
|
486
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element);
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function isSequentialStop(element) {
|
|
490
|
+
const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
|
|
491
|
+
if (!stop) return false;
|
|
492
|
+
const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
|
|
493
|
+
return !(link && authoredTabindex(element) === null && isInEditableContent(element));
|
|
494
|
+
}
|
|
495
|
+
function ownedScope(element) {
|
|
496
|
+
if (element.shadowRoot) return Array.from(element.shadowRoot.children);
|
|
497
|
+
if (element instanceof HTMLSlotElement) {
|
|
498
|
+
const assigned = element.assignedElements();
|
|
499
|
+
return assigned.length > 0 ? assigned : Array.from(element.children);
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function orderScope(elements, walk) {
|
|
504
|
+
const entries = [];
|
|
505
|
+
const visit = (element) => {
|
|
506
|
+
if (element === walk.from) walk.fromAt = walk.tree.length;
|
|
507
|
+
const tabindex = authoredTabindex(element) ?? 0;
|
|
508
|
+
const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
|
|
509
|
+
if (stop) walk.tree.push(stop);
|
|
510
|
+
const owned = ownedScope(element);
|
|
511
|
+
if (owned) {
|
|
512
|
+
if (tabindex >= 0) {
|
|
513
|
+
entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (stop) entries.push({ tabindex, stops: [stop] });
|
|
518
|
+
for (const child of Array.from(element.children)) visit(child);
|
|
519
|
+
};
|
|
520
|
+
for (const element of elements) visit(element);
|
|
521
|
+
const positive = entries.filter((entry) => entry.tabindex > 0);
|
|
522
|
+
positive.sort((a, b) => a.tabindex - b.tabindex);
|
|
523
|
+
const rest = entries.filter((entry) => entry.tabindex === 0);
|
|
524
|
+
return [...positive, ...rest].flatMap((entry) => entry.stops);
|
|
525
|
+
}
|
|
526
|
+
function passesRadioRule(element, tree, focusedRadio) {
|
|
527
|
+
if (!isRadio(element) || element.checked) return true;
|
|
528
|
+
const group = tree.filter(
|
|
529
|
+
(stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
|
|
530
|
+
);
|
|
531
|
+
if (group.some((radio) => radio.checked)) return false;
|
|
532
|
+
if (group[0] !== element) return false;
|
|
533
|
+
return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
|
|
534
|
+
}
|
|
535
|
+
function startIndex(order, walk, backward, isStop) {
|
|
536
|
+
if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
|
|
537
|
+
const at = order.indexOf(walk.from);
|
|
538
|
+
if (at >= 0) return backward ? at - 1 : at + 1;
|
|
539
|
+
const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
|
|
540
|
+
if (neighbour) return order.indexOf(neighbour);
|
|
541
|
+
return backward ? order.length - 1 : 0;
|
|
542
|
+
}
|
|
543
|
+
function sequentialTabStops(root, from = null, backward = false) {
|
|
544
|
+
const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
|
|
545
|
+
const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
|
|
546
|
+
const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
|
|
547
|
+
const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
|
|
548
|
+
const start = startIndex(order, walk, backward, isStop);
|
|
549
|
+
const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
|
|
550
|
+
return visited.filter((element) => element !== from && isStop(element));
|
|
551
|
+
}
|
|
552
|
+
function deepActiveElement(ownerDocument) {
|
|
553
|
+
let active = ownerDocument.activeElement;
|
|
554
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
555
|
+
return active;
|
|
556
|
+
}
|
|
288
557
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
558
|
+
var MicrotaskCoalescer = class {
|
|
559
|
+
#run;
|
|
560
|
+
#queued = false;
|
|
561
|
+
#active = false;
|
|
562
|
+
#generation = 0;
|
|
563
|
+
constructor(run) {
|
|
564
|
+
this.#run = run;
|
|
565
|
+
}
|
|
566
|
+
activate() {
|
|
567
|
+
this.#active = true;
|
|
568
|
+
}
|
|
569
|
+
cancel() {
|
|
570
|
+
this.#active = false;
|
|
571
|
+
this.#queued = false;
|
|
572
|
+
this.#generation += 1;
|
|
573
|
+
}
|
|
574
|
+
schedule() {
|
|
575
|
+
if (!this.#active || this.#queued) return;
|
|
576
|
+
this.#queued = true;
|
|
577
|
+
const generation = this.#generation;
|
|
578
|
+
queueMicrotask(() => {
|
|
579
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
580
|
+
this.#queued = false;
|
|
581
|
+
this.#run();
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function queryOpenDescendants(root, selector) {
|
|
587
|
+
const matches = [];
|
|
588
|
+
const roots = [root];
|
|
589
|
+
for (const scope of roots) {
|
|
590
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
591
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
592
|
+
const shadow = scope.shadowRoot;
|
|
593
|
+
if (shadow) roots.push(shadow);
|
|
594
|
+
}
|
|
595
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
596
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return matches;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
var TabindexLoan = class _TabindexLoan {
|
|
603
|
+
static #registry = sharedRegistry(
|
|
604
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
605
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
606
|
+
);
|
|
607
|
+
#value;
|
|
608
|
+
#record;
|
|
609
|
+
#lent = /* @__PURE__ */ new Set();
|
|
610
|
+
constructor(value, owner) {
|
|
611
|
+
this.#value = value;
|
|
612
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
613
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
614
|
+
}
|
|
615
|
+
lend(element) {
|
|
616
|
+
const stack = this.#stack(element);
|
|
617
|
+
const current = element.getAttribute("tabindex");
|
|
618
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
619
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
620
|
+
);
|
|
621
|
+
const linked = stack.at(-1)?.value === current;
|
|
622
|
+
if (!held && current !== null && !linked) return;
|
|
623
|
+
this.#lent.add(element);
|
|
624
|
+
if (held) held.loan = this;
|
|
625
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
626
|
+
this.#mark(element, stack);
|
|
627
|
+
if (current === null || !held && current !== this.#value) {
|
|
628
|
+
element.setAttribute("tabindex", this.#value);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
returnAll() {
|
|
632
|
+
for (const element of this.#lent) {
|
|
633
|
+
const stack = this.#stack(element);
|
|
634
|
+
const hold = stack.find((held) => held.loan === this);
|
|
635
|
+
if (hold) this.#release(element, stack, hold);
|
|
636
|
+
}
|
|
637
|
+
this.#lent.clear();
|
|
638
|
+
}
|
|
639
|
+
reclaim(element) {
|
|
640
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
641
|
+
const stack = this.#stack(element);
|
|
642
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
643
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
644
|
+
}
|
|
645
|
+
reclaimWithin(root) {
|
|
646
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
647
|
+
this.reclaim(element);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
#stack(element) {
|
|
651
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
652
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
653
|
+
stacks.set(element, stack);
|
|
654
|
+
return stack;
|
|
655
|
+
}
|
|
656
|
+
#seed(element) {
|
|
657
|
+
const current = element.getAttribute("tabindex");
|
|
658
|
+
const holds = [];
|
|
659
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
660
|
+
const value = element.getAttribute(record);
|
|
661
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
662
|
+
}
|
|
663
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
664
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
665
|
+
return holds;
|
|
666
|
+
}
|
|
667
|
+
#release(element, stack, hold) {
|
|
668
|
+
const current = element.getAttribute("tabindex");
|
|
669
|
+
const index = stack.indexOf(hold);
|
|
670
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
671
|
+
stack.splice(index, 1);
|
|
672
|
+
const above = stack[index];
|
|
673
|
+
if (above) above.linked &&= hold.linked;
|
|
674
|
+
this.#mark(element, stack);
|
|
675
|
+
if (!owns) return;
|
|
676
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
677
|
+
if (!below) element.removeAttribute("tabindex");
|
|
678
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
679
|
+
}
|
|
680
|
+
#mark(element, stack) {
|
|
681
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
682
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
683
|
+
for (const [name, recorded] of records) {
|
|
684
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
685
|
+
if (recorded === null) element.removeAttribute(name);
|
|
686
|
+
else element.setAttribute(name, recorded);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
|
|
692
|
+
var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
|
|
693
|
+
var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
|
|
694
|
+
var FocusTrap = class _FocusTrap {
|
|
695
|
+
static #registries = sharedRegistry(
|
|
696
|
+
"stimeo-ui.focus-trap.registry.v2",
|
|
697
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
698
|
+
);
|
|
699
|
+
static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
700
|
+
"text",
|
|
701
|
+
"search",
|
|
702
|
+
"email",
|
|
703
|
+
"url",
|
|
704
|
+
"tel",
|
|
705
|
+
"password",
|
|
706
|
+
"number"
|
|
707
|
+
]);
|
|
708
|
+
#openers = [];
|
|
709
|
+
#entry;
|
|
710
|
+
#tabindexLoans = /* @__PURE__ */ new Map();
|
|
300
711
|
#activeState = false;
|
|
301
|
-
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
302
712
|
#escapeLayer = new EscapeLayer();
|
|
303
|
-
|
|
713
|
+
#gate = new DetachGate();
|
|
304
714
|
#getContainer;
|
|
305
|
-
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
306
715
|
#options;
|
|
307
|
-
/**
|
|
308
|
-
* @param getContainer - Returns the trapped element. Called on every operation
|
|
309
|
-
* so the live target is always used.
|
|
310
|
-
* @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
|
|
311
|
-
*/
|
|
312
716
|
constructor(getContainer, options = {}) {
|
|
313
717
|
this.#getContainer = getContainer;
|
|
314
718
|
this.#options = options;
|
|
719
|
+
this.#entry = {
|
|
720
|
+
container: () => this.#readContainer(),
|
|
721
|
+
moveFocus: (backward) => this.#moveFocus(backward),
|
|
722
|
+
refocus: () => this.#refocus(),
|
|
723
|
+
isolated: false,
|
|
724
|
+
lockedBody: null,
|
|
725
|
+
openedFrom: null,
|
|
726
|
+
handedBack: []
|
|
727
|
+
};
|
|
315
728
|
}
|
|
316
|
-
/** Whether the trap is currently active. */
|
|
317
729
|
get active() {
|
|
318
730
|
return this.#activeState;
|
|
319
731
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Applies the trap: records the current focus, optionally locks background scroll
|
|
322
|
-
* and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
|
|
323
|
-
* `autoFocus` is off) moves focus inside. No-ops if already active.
|
|
324
|
-
*/
|
|
325
732
|
activate() {
|
|
326
733
|
if (this.#activeState) return;
|
|
734
|
+
_FocusTrap.#sweep();
|
|
327
735
|
this.#activeState = true;
|
|
328
|
-
|
|
329
|
-
this.#
|
|
330
|
-
if (this.#flag(this.#options.lockScroll, true))
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
336
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
337
|
-
this.#beforeCache.activate();
|
|
736
|
+
this.#openers = _FocusTrap.#focusPath();
|
|
737
|
+
this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
|
|
738
|
+
if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
|
|
739
|
+
this.#entry.isolated = this.#flag(this.#options.isolate, true);
|
|
740
|
+
this.#joinStack();
|
|
741
|
+
_FocusTrap.#reconcileBackground(document);
|
|
338
742
|
const onEscape = this.#options.onEscape;
|
|
339
743
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
340
744
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
341
745
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Reverts every side effect applied by {@link activate}. No-ops if inactive, so
|
|
344
|
-
* a controller can call it defensively from both `close()` and `disconnect()`.
|
|
345
|
-
*
|
|
346
|
-
* @param restoreFocus - Move focus back to the opener (default `true`). Pass
|
|
347
|
-
* `false` on teardown (`disconnect`), where yanking focus is undesirable.
|
|
348
|
-
*/
|
|
349
746
|
deactivate({ restoreFocus = true } = {}) {
|
|
350
|
-
if (!this.#activeState)
|
|
747
|
+
if (!this.#activeState) {
|
|
748
|
+
this.#returnTabindexes();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
351
751
|
this.#activeState = false;
|
|
352
752
|
this.#escapeLayer.deactivate();
|
|
353
|
-
|
|
354
|
-
this.#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
753
|
+
const above = this.#leaveStack();
|
|
754
|
+
this.#unlockScroll();
|
|
755
|
+
_FocusTrap.#reconcileBackground(document);
|
|
756
|
+
const wayBack = this.#handOver(above);
|
|
757
|
+
if (restoreFocus) this.#restore(wayBack, above);
|
|
758
|
+
this.#returnTabindexes();
|
|
759
|
+
}
|
|
760
|
+
connect() {
|
|
761
|
+
this.#gate.cancel();
|
|
762
|
+
_FocusTrap.#sweep();
|
|
763
|
+
new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
|
|
764
|
+
}
|
|
765
|
+
disconnect(host) {
|
|
766
|
+
if (this.#activeState) this.deactivate({ restoreFocus: false });
|
|
767
|
+
this.#gate.disconnected(host, () => this.#returnTabindexes());
|
|
768
|
+
}
|
|
769
|
+
refreshIsolation() {
|
|
770
|
+
if (!this.#activeState) return;
|
|
771
|
+
const isolate = this.#flag(this.#options.isolate, true);
|
|
772
|
+
if (isolate === this.#entry.isolated) return;
|
|
773
|
+
this.#entry.isolated = isolate;
|
|
774
|
+
const stranded = _FocusTrap.#reconcileBackground(document);
|
|
775
|
+
const active = deepActiveElement(document);
|
|
776
|
+
if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
|
|
777
|
+
}
|
|
778
|
+
refreshContainer() {
|
|
779
|
+
if (!this.#activeState) return;
|
|
780
|
+
_FocusTrap.#reconcileBackground(document);
|
|
781
|
+
this.#returnTabindexes();
|
|
782
|
+
if (this.#ownsTab()) this.#refocus();
|
|
364
783
|
}
|
|
365
|
-
/** Resolves a boolean-or-getter option, defaulting when it was not provided. */
|
|
366
784
|
#flag(option, fallback) {
|
|
367
785
|
if (option === void 0) return fallback;
|
|
368
786
|
return typeof option === "function" ? option() : option;
|
|
369
787
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
* is left alone mid-navigation. The listener lives only while active.
|
|
377
|
-
*/
|
|
378
|
-
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
379
|
-
/**
|
|
380
|
-
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
381
|
-
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
382
|
-
* which layer currently owns Escape.
|
|
383
|
-
*/
|
|
384
|
-
#onKeydown = (event) => {
|
|
385
|
-
if (event.key === "Tab") this.#trapTab(event);
|
|
386
|
-
};
|
|
387
|
-
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
388
|
-
#trapTab(event) {
|
|
389
|
-
const focusable = this.#focusableElements();
|
|
390
|
-
if (focusable.length === 0) {
|
|
391
|
-
event.preventDefault();
|
|
392
|
-
return;
|
|
788
|
+
#joinStack() {
|
|
789
|
+
const registry = _FocusTrap.#registry(document);
|
|
790
|
+
if (registry.stack.length === 0) {
|
|
791
|
+
document.addEventListener("keydown", registry.onKeydown);
|
|
792
|
+
document.addEventListener("turbo:morph", registry.onMorph);
|
|
793
|
+
document.addEventListener("turbo:morph-element", registry.onMorph);
|
|
393
794
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
795
|
+
registry.stack.push(this.#entry);
|
|
796
|
+
}
|
|
797
|
+
#leaveStack() {
|
|
798
|
+
const registry = _FocusTrap.#registry(document);
|
|
799
|
+
const index = registry.stack.indexOf(this.#entry);
|
|
800
|
+
const above = registry.stack.slice(index + 1);
|
|
801
|
+
registry.stack.splice(index, 1);
|
|
802
|
+
if (registry.stack.length === 0) {
|
|
803
|
+
document.removeEventListener("keydown", registry.onKeydown);
|
|
804
|
+
document.removeEventListener("turbo:morph", registry.onMorph);
|
|
805
|
+
document.removeEventListener("turbo:morph-element", registry.onMorph);
|
|
401
806
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
807
|
+
return above;
|
|
808
|
+
}
|
|
809
|
+
static #focusPath() {
|
|
810
|
+
const path = [];
|
|
811
|
+
for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
|
|
812
|
+
if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
|
|
813
|
+
}
|
|
814
|
+
return path;
|
|
815
|
+
}
|
|
816
|
+
static #trapHolding(element) {
|
|
817
|
+
for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
|
|
818
|
+
const container = trap.container();
|
|
819
|
+
if (container && flatTreeContains(container, element)) return trap;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
#handOver(above) {
|
|
824
|
+
const entry = this.#entry;
|
|
825
|
+
const wayBack = [
|
|
826
|
+
...this.#openers,
|
|
827
|
+
this.#options.fallbackFocus?.() ?? null,
|
|
828
|
+
...entry.handedBack
|
|
829
|
+
].filter((element) => element !== null);
|
|
830
|
+
for (const trap of above) {
|
|
831
|
+
if (trap.openedFrom !== entry) continue;
|
|
832
|
+
trap.handedBack = [...trap.handedBack, ...wayBack];
|
|
833
|
+
trap.openedFrom = entry.openedFrom;
|
|
834
|
+
}
|
|
835
|
+
this.#openers = [];
|
|
836
|
+
entry.openedFrom = null;
|
|
837
|
+
entry.handedBack = [];
|
|
838
|
+
return wayBack;
|
|
839
|
+
}
|
|
840
|
+
#restore(wayBack, above) {
|
|
841
|
+
const active = deepActiveElement(document);
|
|
842
|
+
const released = this.#readContainer();
|
|
843
|
+
const holds = (trap) => {
|
|
844
|
+
const container = trap.container();
|
|
845
|
+
if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
|
|
849
|
+
};
|
|
850
|
+
if (above.some(holds)) return;
|
|
851
|
+
for (const element of wayBack) {
|
|
852
|
+
if (this.#activeState) return;
|
|
853
|
+
if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
|
|
854
|
+
const before = deepActiveElement(document);
|
|
855
|
+
element.focus();
|
|
856
|
+
const after = deepActiveElement(document);
|
|
857
|
+
if (after === element || after !== before) return;
|
|
858
|
+
}
|
|
859
|
+
_FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
|
|
860
|
+
}
|
|
861
|
+
#refocus() {
|
|
862
|
+
if (!this.#flag(this.#options.autoFocus, true)) return;
|
|
863
|
+
if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
|
|
864
|
+
this.#focusInitial();
|
|
865
|
+
}
|
|
866
|
+
#lendTabindex(container) {
|
|
867
|
+
const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
|
|
868
|
+
loan.lend(container);
|
|
869
|
+
this.#tabindexLoans.set(container, loan);
|
|
870
|
+
}
|
|
871
|
+
#returnTabindexes() {
|
|
872
|
+
const focused = deepActiveElement(document);
|
|
873
|
+
for (const [element, loan] of [...this.#tabindexLoans]) {
|
|
874
|
+
if (element === focused) continue;
|
|
875
|
+
loan.returnAll();
|
|
876
|
+
this.#tabindexLoans.delete(element);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
#lockScroll() {
|
|
880
|
+
const registry = _FocusTrap.#registry(document);
|
|
881
|
+
const body = document.body;
|
|
882
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
883
|
+
registry.scrollLocks.add(this.#entry);
|
|
884
|
+
this.#entry.lockedBody = body;
|
|
885
|
+
body.style.overflow = "hidden";
|
|
886
|
+
}
|
|
887
|
+
#unlockScroll() {
|
|
888
|
+
const registry = _FocusTrap.#registry(document);
|
|
889
|
+
const body = this.#entry.lockedBody;
|
|
890
|
+
this.#entry.lockedBody = null;
|
|
891
|
+
if (!registry.scrollLocks.delete(this.#entry) || !body) return;
|
|
892
|
+
if (!_FocusTrap.#isLocked(registry, body)) _FocusTrap.#unlock(body);
|
|
893
|
+
}
|
|
894
|
+
static #isLocked(registry, body) {
|
|
895
|
+
return [...registry.scrollLocks].some((trap) => trap.lockedBody === body);
|
|
896
|
+
}
|
|
897
|
+
static #unlock(body) {
|
|
898
|
+
const saved = body.getAttribute(OVERFLOW_MARK);
|
|
899
|
+
if (saved === null) return;
|
|
900
|
+
body.removeAttribute(OVERFLOW_MARK);
|
|
901
|
+
body.style.overflow = saved;
|
|
902
|
+
}
|
|
903
|
+
static #sweep() {
|
|
904
|
+
const registry = _FocusTrap.#registry(document);
|
|
905
|
+
for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
|
|
906
|
+
if (registry.inerted.has(element)) continue;
|
|
907
|
+
element.removeAttribute(INERT_MARK);
|
|
908
|
+
element.inert = false;
|
|
909
|
+
}
|
|
910
|
+
if (!_FocusTrap.#isLocked(registry, document.body)) _FocusTrap.#unlock(document.body);
|
|
911
|
+
}
|
|
912
|
+
static #registry(ownerDocument) {
|
|
913
|
+
const existing = _FocusTrap.#registries.get(ownerDocument);
|
|
914
|
+
if (existing) return existing;
|
|
915
|
+
const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry));
|
|
916
|
+
morphs.activate();
|
|
917
|
+
const registry = {
|
|
918
|
+
stack: [],
|
|
919
|
+
onKeydown: (event) => _FocusTrap.#resolveTab(registry.stack, event),
|
|
920
|
+
onMorph: () => morphs.schedule(),
|
|
921
|
+
inerted: /* @__PURE__ */ new Set(),
|
|
922
|
+
scrollLocks: /* @__PURE__ */ new Set()
|
|
923
|
+
};
|
|
924
|
+
_FocusTrap.#registries.set(ownerDocument, registry);
|
|
925
|
+
return registry;
|
|
926
|
+
}
|
|
927
|
+
static #reapply(registry) {
|
|
928
|
+
_FocusTrap.#reconcileBackground(document);
|
|
929
|
+
const body = document.body;
|
|
930
|
+
if (!_FocusTrap.#isLocked(registry, body)) return;
|
|
931
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
932
|
+
body.style.overflow = "hidden";
|
|
933
|
+
}
|
|
934
|
+
static #reconcileBackground(ownerDocument) {
|
|
935
|
+
const registry = _FocusTrap.#registry(ownerDocument);
|
|
936
|
+
for (const element of registry.inerted) {
|
|
937
|
+
if (!element.hasAttribute(INERT_MARK)) registry.inerted.delete(element);
|
|
938
|
+
}
|
|
939
|
+
const background = _FocusTrap.#backgroundOf(registry);
|
|
940
|
+
for (const element of registry.inerted) {
|
|
941
|
+
if (background.has(element)) continue;
|
|
942
|
+
element.removeAttribute(INERT_MARK);
|
|
943
|
+
element.inert = false;
|
|
944
|
+
registry.inerted.delete(element);
|
|
945
|
+
}
|
|
946
|
+
const added = [];
|
|
947
|
+
for (const element of background) {
|
|
948
|
+
if (registry.inerted.has(element)) continue;
|
|
949
|
+
element.setAttribute(INERT_MARK, "");
|
|
950
|
+
element.inert = true;
|
|
951
|
+
registry.inerted.add(element);
|
|
952
|
+
added.push(element);
|
|
953
|
+
}
|
|
954
|
+
return added;
|
|
955
|
+
}
|
|
956
|
+
static #backgroundOf(registry) {
|
|
957
|
+
const containers = registry.stack.map((trap) => trap.container());
|
|
958
|
+
let top = -1;
|
|
959
|
+
registry.stack.forEach((trap, index) => {
|
|
960
|
+
if (trap.isolated && containers[index]) top = index;
|
|
961
|
+
});
|
|
962
|
+
const background = /* @__PURE__ */ new Set();
|
|
963
|
+
if (top < 0) return background;
|
|
964
|
+
const kept = /* @__PURE__ */ new Set();
|
|
965
|
+
const keep = (path) => {
|
|
966
|
+
for (const node of path) {
|
|
967
|
+
kept.add(node);
|
|
968
|
+
background.delete(node);
|
|
432
969
|
}
|
|
433
|
-
node
|
|
970
|
+
for (const node of path) {
|
|
971
|
+
const parent = flatTreeParent(node);
|
|
972
|
+
for (const sibling of parent ? flatTreeChildren(parent) : []) {
|
|
973
|
+
if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
|
|
974
|
+
if (sibling.inert && !registry.inerted.has(sibling)) continue;
|
|
975
|
+
background.add(sibling);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
containers.slice(top).forEach((container, index) => {
|
|
980
|
+
const path = [];
|
|
981
|
+
for (let node = container; node && node !== document.body && !kept.has(node); ) {
|
|
982
|
+
path.push(node);
|
|
983
|
+
node = flatTreeParent(node);
|
|
984
|
+
}
|
|
985
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
986
|
+
});
|
|
987
|
+
return background;
|
|
988
|
+
}
|
|
989
|
+
#readContainer() {
|
|
990
|
+
try {
|
|
991
|
+
return this.#getContainer();
|
|
992
|
+
} catch {
|
|
993
|
+
return null;
|
|
434
994
|
}
|
|
435
995
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
996
|
+
static #resolveTab(stack, event) {
|
|
997
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
998
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
999
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
1000
|
+
const owner = _FocusTrap.#tabOwner(stack);
|
|
1001
|
+
if (!owner) return;
|
|
1002
|
+
event.preventDefault();
|
|
1003
|
+
owner.moveFocus(event.shiftKey);
|
|
1004
|
+
}
|
|
1005
|
+
static #tabOwner(stack) {
|
|
1006
|
+
for (const trap of [...stack].reverse()) {
|
|
1007
|
+
if (_FocusTrap.#takesTab(trap)) return trap;
|
|
1008
|
+
}
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
static #takesTab(trap) {
|
|
1012
|
+
const container = trap.container();
|
|
1013
|
+
if (!container) return false;
|
|
1014
|
+
return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
|
|
1015
|
+
}
|
|
1016
|
+
#ownsTab() {
|
|
1017
|
+
return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
|
|
1018
|
+
}
|
|
1019
|
+
#moveFocus(backward) {
|
|
1020
|
+
const from = deepActiveElement(document);
|
|
1021
|
+
for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
|
|
1022
|
+
if (!this.#ownsTab()) return;
|
|
1023
|
+
const before = deepActiveElement(document);
|
|
1024
|
+
stop.focus();
|
|
1025
|
+
const after = deepActiveElement(document);
|
|
1026
|
+
if (after === before) continue;
|
|
1027
|
+
if (after === stop) _FocusTrap.#selectOnEntry(stop);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
static #selectOnEntry(stop) {
|
|
1032
|
+
if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
|
|
1033
|
+
stop.select();
|
|
440
1034
|
}
|
|
441
|
-
this.#inertedSiblings = [];
|
|
442
1035
|
}
|
|
443
|
-
/** Moves focus to the initial target, the first focusable, or the container. */
|
|
444
1036
|
#focusInitial() {
|
|
445
1037
|
const preferred = this.#options.initialFocus?.();
|
|
446
1038
|
if (preferred) {
|
|
447
1039
|
preferred.focus();
|
|
448
1040
|
return;
|
|
449
1041
|
}
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
1042
|
+
const container = this.#getContainer();
|
|
1043
|
+
const [first] = sequentialTabStops(container);
|
|
1044
|
+
if (first) {
|
|
1045
|
+
first.focus();
|
|
453
1046
|
return;
|
|
454
1047
|
}
|
|
455
|
-
|
|
456
|
-
container.tabIndex = -1;
|
|
1048
|
+
this.#lendTabindex(container);
|
|
457
1049
|
container.focus();
|
|
458
1050
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
var ListenerSet = class {
|
|
1054
|
+
#abort = new AbortController();
|
|
1055
|
+
add(target, type, handler, options) {
|
|
1056
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
1057
|
+
}
|
|
1058
|
+
dispose() {
|
|
1059
|
+
this.#abort.abort();
|
|
1060
|
+
this.#abort = new AbortController();
|
|
462
1061
|
}
|
|
463
1062
|
};
|
|
464
1063
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
#
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#generation = 0;
|
|
471
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
1064
|
+
var MorphRenderWatcher = class {
|
|
1065
|
+
#listeners = new ListenerSet();
|
|
1066
|
+
#pass;
|
|
1067
|
+
#element = null;
|
|
1068
|
+
#onMorph = () => this.#pass.schedule();
|
|
472
1069
|
constructor(run) {
|
|
473
|
-
this.#
|
|
1070
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
474
1071
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this
|
|
1072
|
+
observe(element) {
|
|
1073
|
+
if (this.#element === element) return;
|
|
1074
|
+
this.disconnect();
|
|
1075
|
+
this.#element = element;
|
|
1076
|
+
this.#pass.activate();
|
|
1077
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
478
1078
|
}
|
|
479
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
480
|
-
cancel() {
|
|
481
|
-
this.#active = false;
|
|
482
|
-
this.#queued = false;
|
|
483
|
-
this.#generation += 1;
|
|
484
|
-
}
|
|
485
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
486
1079
|
schedule() {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1080
|
+
this.#pass.schedule();
|
|
1081
|
+
}
|
|
1082
|
+
disconnect() {
|
|
1083
|
+
this.#listeners.dispose();
|
|
1084
|
+
this.#pass.cancel();
|
|
1085
|
+
this.#element = null;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
var LivedMark = class {
|
|
1090
|
+
#name;
|
|
1091
|
+
#lived = false;
|
|
1092
|
+
#element = null;
|
|
1093
|
+
#morph = new MorphRenderWatcher(() => this.#write());
|
|
1094
|
+
constructor(identifier) {
|
|
1095
|
+
this.#name = `data-${identifier}-lived`;
|
|
1096
|
+
}
|
|
1097
|
+
connect(element) {
|
|
1098
|
+
const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
|
|
1099
|
+
this.#lived = true;
|
|
1100
|
+
this.#element = element;
|
|
1101
|
+
this.#write();
|
|
1102
|
+
this.#morph.observe(element);
|
|
1103
|
+
return connection;
|
|
1104
|
+
}
|
|
1105
|
+
disconnect() {
|
|
1106
|
+
this.#morph.disconnect();
|
|
1107
|
+
}
|
|
1108
|
+
#write() {
|
|
1109
|
+
this.#element?.setAttribute(this.#name, "");
|
|
495
1110
|
}
|
|
496
1111
|
};
|
|
497
1112
|
|
|
498
|
-
|
|
1113
|
+
function targetSelector(identifier, name) {
|
|
1114
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
499
1117
|
var CommandPaletteController = class _CommandPaletteController extends Controller {
|
|
500
1118
|
static #ORIGINAL_ARIA_DISABLED = "data-command-palette-original-aria-disabled";
|
|
501
1119
|
static #ABSENT_ARIA_DISABLED = "absent";
|
|
@@ -510,85 +1128,103 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
510
1128
|
"filter",
|
|
511
1129
|
"onKeydown",
|
|
512
1130
|
"open",
|
|
513
|
-
"
|
|
1131
|
+
"select",
|
|
514
1132
|
"toggle"
|
|
515
1133
|
];
|
|
516
1134
|
static events = ["select"];
|
|
517
|
-
/** Stable ID of the active option; the live target is resolved before every use. */
|
|
518
1135
|
#activeId = null;
|
|
519
|
-
/** Visible/selectable target ID order captured for removal fallback. */
|
|
520
1136
|
#activeOrder = [];
|
|
521
1137
|
#connected = false;
|
|
522
|
-
|
|
523
|
-
#
|
|
524
|
-
|
|
1138
|
+
#lived = new LivedMark(this.identifier);
|
|
1139
|
+
#dialog = null;
|
|
1140
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
1141
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
1142
|
+
#activeDescendant = new AttributeLease(
|
|
1143
|
+
"aria-activedescendant",
|
|
1144
|
+
this.identifier
|
|
1145
|
+
);
|
|
1146
|
+
#reconcile = new MicrotaskCoalescer(() => {
|
|
1147
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(this.#isOpen));
|
|
1148
|
+
this.#reconcileActive();
|
|
1149
|
+
});
|
|
1150
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
525
1151
|
#composition = new CompositionTracker({
|
|
526
1152
|
onEnd: () => {
|
|
527
1153
|
if (this.#isOpen) this.filter();
|
|
528
1154
|
}
|
|
529
1155
|
});
|
|
530
|
-
/**
|
|
531
|
-
* Owns the modal side effects (focus trap, scroll lock, background `inert`, focus
|
|
532
|
-
* restore). Escape closes; focus on open goes to the input, and is restored to
|
|
533
|
-
* whatever opened the palette on close.
|
|
534
|
-
*/
|
|
535
1156
|
#trap = new FocusTrap(() => this.dialogTarget, {
|
|
536
1157
|
onEscape: () => this.close(),
|
|
537
|
-
initialFocus: () =>
|
|
1158
|
+
initialFocus: () => {
|
|
1159
|
+
const dialog = this.dialogTarget;
|
|
1160
|
+
return this.inputTargets.find((input) => dialog.contains(input)) ?? null;
|
|
1161
|
+
}
|
|
538
1162
|
});
|
|
539
|
-
/**
|
|
540
|
-
* Initializes the global hotkey handler and establishes the initial open state.
|
|
541
|
-
*
|
|
542
|
-
* The DOM is the source of truth on reconnect (Turbo cache restore / morph): if
|
|
543
|
-
* the restored snapshot already shows the dialog open, honor that rather than
|
|
544
|
-
* re-deriving from the declarative `open` Value (which would slam a user-opened
|
|
545
|
-
* palette shut). The `open` Value only seeds the initial state of a genuinely
|
|
546
|
-
* fresh render. We normalize to a clean closed baseline first so
|
|
547
|
-
* {@link CommandPaletteController.open | open} runs its full setup — the
|
|
548
|
-
* `FocusTrap` is a fresh instance after a reconnect and must be re-activated.
|
|
549
|
-
*/
|
|
550
1163
|
connect() {
|
|
1164
|
+
const connection = this.#lived.connect(this.element);
|
|
1165
|
+
this.#trap.connect();
|
|
551
1166
|
document.addEventListener("keydown", this.#onGlobalKeydown);
|
|
552
1167
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
553
1168
|
this.#syncOptionSemantics();
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1169
|
+
if (connection === "reconnect" && this.#isOpen) {
|
|
1170
|
+
this.#trap.activate();
|
|
1171
|
+
} else {
|
|
1172
|
+
const shouldOpen = connection !== "restored" && (this.#isOpen || this.openValue);
|
|
1173
|
+
this.#resetToClosedState();
|
|
1174
|
+
if (shouldOpen) this.open();
|
|
1175
|
+
}
|
|
1176
|
+
this.#dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
557
1177
|
this.#connected = true;
|
|
558
1178
|
this.#reconcile.activate();
|
|
1179
|
+
this.#morphRender.observe(this.element);
|
|
559
1180
|
}
|
|
560
|
-
/** Tears down the global hotkey listener and reverts the modal side effects. */
|
|
561
1181
|
disconnect() {
|
|
562
1182
|
this.#connected = false;
|
|
1183
|
+
this.#lived.disconnect();
|
|
1184
|
+
this.#morphRender.disconnect();
|
|
563
1185
|
this.#reconcile.cancel();
|
|
564
1186
|
document.removeEventListener("keydown", this.#onGlobalKeydown);
|
|
565
1187
|
this.#composition.disconnect();
|
|
566
|
-
this.#trap.
|
|
567
|
-
this.#resetToClosedState();
|
|
1188
|
+
this.#trap.disconnect(this);
|
|
568
1189
|
}
|
|
569
|
-
/** Initializes semantics for an option added before or after the controller connects. */
|
|
570
1190
|
optionTargetConnected(option) {
|
|
571
1191
|
this.#syncOptionSemanticsFor(option);
|
|
572
1192
|
option.setAttribute("aria-selected", "false");
|
|
573
1193
|
option.removeAttribute("data-active");
|
|
574
1194
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
575
1195
|
}
|
|
576
|
-
/** Clears active residue on the old node and reconciles the surviving targets. */
|
|
577
1196
|
optionTargetDisconnected(option) {
|
|
578
1197
|
option.setAttribute("aria-selected", "false");
|
|
579
1198
|
option.removeAttribute("data-active");
|
|
580
1199
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
581
1200
|
}
|
|
582
|
-
/** Tracks an input added initially or after connect without extra consumer actions. */
|
|
583
1201
|
inputTargetConnected(input) {
|
|
584
1202
|
this.#composition.observe(input);
|
|
585
1203
|
if (this.#connected) this.#queueOptionReconciliation();
|
|
586
1204
|
}
|
|
587
|
-
/** Removes controller-owned listeners when the input target is replaced or removed. */
|
|
588
1205
|
inputTargetDisconnected(input) {
|
|
589
1206
|
this.#composition.unobserve(input);
|
|
1207
|
+
if (!this.inputTargets.includes(input)) {
|
|
1208
|
+
this.#expanded.return(input);
|
|
1209
|
+
this.#activeDescendant.return(input);
|
|
1210
|
+
}
|
|
1211
|
+
this.#queueOptionReconciliation();
|
|
1212
|
+
}
|
|
1213
|
+
dialogTargetConnected() {
|
|
1214
|
+
if (this.#connected) this.#adoptDialog();
|
|
1215
|
+
}
|
|
1216
|
+
dialogTargetDisconnected(dialog) {
|
|
1217
|
+
if (this.dialogTargets.includes(dialog)) return;
|
|
1218
|
+
this.#hidden.return(dialog);
|
|
1219
|
+
if (this.#connected) this.#adoptDialog();
|
|
1220
|
+
}
|
|
1221
|
+
emptyTargetConnected() {
|
|
1222
|
+
this.#queueOptionReconciliation();
|
|
1223
|
+
}
|
|
1224
|
+
emptyTargetDisconnected(empty) {
|
|
1225
|
+
if (!this.emptyTargets.includes(empty)) this.#hidden.return(empty);
|
|
1226
|
+
this.#queueOptionReconciliation();
|
|
590
1227
|
}
|
|
591
|
-
/** Toggles the open state of the command palette. */
|
|
592
1228
|
toggle() {
|
|
593
1229
|
if (this.#isOpen) {
|
|
594
1230
|
this.close();
|
|
@@ -596,25 +1232,43 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
596
1232
|
this.open();
|
|
597
1233
|
}
|
|
598
1234
|
}
|
|
599
|
-
/** Opens the palette, traps focus, and shifts focus to the input. */
|
|
600
1235
|
open() {
|
|
601
1236
|
if (!this.hasDialogTarget || this.#isOpen) return;
|
|
602
|
-
this.dialogTarget
|
|
1237
|
+
this.#hidden.write(this.dialogTarget, null);
|
|
603
1238
|
this.openValue = true;
|
|
604
|
-
if (this.hasInputTarget) this.inputTarget
|
|
1239
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "true");
|
|
605
1240
|
this.#resetFilter();
|
|
606
1241
|
this.#trap.activate();
|
|
607
1242
|
}
|
|
608
|
-
/** Closes the palette and restores focus back to the opener. */
|
|
609
1243
|
close() {
|
|
1244
|
+
this.#close(true);
|
|
1245
|
+
}
|
|
1246
|
+
#adoptDialog() {
|
|
1247
|
+
const dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
1248
|
+
if (dialog === this.#dialog) return;
|
|
1249
|
+
this.#dialog = dialog;
|
|
1250
|
+
if (!this.#trap.active) {
|
|
1251
|
+
if (dialog) this.#hidden.write(dialog, "");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (!dialog) {
|
|
1255
|
+
this.#resetToClosedState();
|
|
1256
|
+
this.#trap.deactivate();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
this.#hidden.write(dialog, null);
|
|
1260
|
+
this.#trap.refreshContainer();
|
|
1261
|
+
}
|
|
1262
|
+
#close(restoreFocus) {
|
|
610
1263
|
if (!this.hasDialogTarget || !this.#isOpen) return;
|
|
611
1264
|
this.#resetToClosedState();
|
|
612
|
-
this.#trap.deactivate();
|
|
1265
|
+
this.#trap.deactivate({ restoreFocus });
|
|
613
1266
|
}
|
|
614
|
-
/** Filters option elements in-memory matching the input value. Bound to input target. */
|
|
615
1267
|
filter(event) {
|
|
616
1268
|
if (!this.hasInputTarget || this.#composition.isComposing(event)) return;
|
|
617
|
-
this.#
|
|
1269
|
+
this.#setActiveIndex(this.#applyQuery() ? 0 : -1);
|
|
1270
|
+
}
|
|
1271
|
+
#applyQuery() {
|
|
618
1272
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
619
1273
|
let hasSelectableMatch = false;
|
|
620
1274
|
for (const option of this.optionTargets) {
|
|
@@ -628,31 +1282,35 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
628
1282
|
}
|
|
629
1283
|
}
|
|
630
1284
|
if (this.hasEmptyTarget) {
|
|
631
|
-
this.emptyTarget
|
|
1285
|
+
this.#hidden.write(this.emptyTarget, hasSelectableMatch ? "" : null);
|
|
632
1286
|
}
|
|
633
|
-
|
|
1287
|
+
return hasSelectableMatch;
|
|
1288
|
+
}
|
|
1289
|
+
#repair() {
|
|
1290
|
+
if (!this.hasDialogTarget) return;
|
|
1291
|
+
const open = this.#trap.active;
|
|
1292
|
+
this.#hidden.write(this.dialogTarget, open ? null : "");
|
|
1293
|
+
if (open && this.hasInputTarget) this.#applyQuery();
|
|
1294
|
+
this.#reconcile.schedule();
|
|
634
1295
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const option =
|
|
1296
|
+
select(source) {
|
|
1297
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
1298
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
1299
|
+
return;
|
|
1300
|
+
const option = host?.closest(targetSelector(this.identifier, "option"));
|
|
640
1301
|
if (!option || !this.optionTargets.includes(option)) return;
|
|
641
1302
|
this.#syncOptionSemanticsFor(option);
|
|
642
1303
|
if (!option.hasAttribute("hidden") && !this.#isDisabled(option)) {
|
|
643
|
-
this.#confirmSelection(
|
|
1304
|
+
this.#confirmSelection(
|
|
1305
|
+
option,
|
|
1306
|
+
reason,
|
|
1307
|
+
event !== null || this.element.contains(document.activeElement)
|
|
1308
|
+
);
|
|
644
1309
|
}
|
|
645
1310
|
}
|
|
646
|
-
/** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */
|
|
647
1311
|
closeOnBackdrop(event) {
|
|
648
1312
|
if (event.target === this.dialogTarget) this.close();
|
|
649
1313
|
}
|
|
650
|
-
/**
|
|
651
|
-
* Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
|
|
652
|
-
* `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
|
|
653
|
-
* the document level, so they work no matter which element inside the dialog has
|
|
654
|
-
* focus — not only the input.
|
|
655
|
-
*/
|
|
656
1314
|
onKeydown(event) {
|
|
657
1315
|
if (event.defaultPrevented) return;
|
|
658
1316
|
if (isReservedArrowChord(event)) return;
|
|
@@ -708,14 +1366,9 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
708
1366
|
this.#activeId = activeOption?.id || null;
|
|
709
1367
|
this.#activeOrder = activeOption ? visible.map((option) => option.id).filter(Boolean) : [];
|
|
710
1368
|
if (this.hasInputTarget) {
|
|
711
|
-
|
|
712
|
-
this.inputTarget.setAttribute("aria-activedescendant", activeOption.id);
|
|
713
|
-
} else {
|
|
714
|
-
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
715
|
-
}
|
|
1369
|
+
this.#activeDescendant.write(this.inputTarget, activeOption?.id || null);
|
|
716
1370
|
}
|
|
717
1371
|
}
|
|
718
|
-
/** Re-resolves active identity and component-specific fallback against live targets. */
|
|
719
1372
|
#reconcileActive() {
|
|
720
1373
|
if (!this.#isOpen) {
|
|
721
1374
|
this.#setActiveIndex(-1);
|
|
@@ -742,7 +1395,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
742
1395
|
this.#setActiveIndex(activeId ? this.#findFallbackIndex(visible, activeId) : -1);
|
|
743
1396
|
this.#reflectEmptyState();
|
|
744
1397
|
}
|
|
745
|
-
/** Resolves stable ID first, then active markers for the existing ID-less case. */
|
|
746
1398
|
#findActiveIndex(options) {
|
|
747
1399
|
const activeId = (this.hasInputTarget ? this.inputTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
|
|
748
1400
|
if (activeId) return options.findIndex((option) => option.id === activeId);
|
|
@@ -750,7 +1402,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
750
1402
|
(option) => option.hasAttribute("data-active") || option.getAttribute("aria-selected") === "true"
|
|
751
1403
|
);
|
|
752
1404
|
}
|
|
753
|
-
/** Chooses a surviving former successor, then predecessor, from selectable targets only. */
|
|
754
1405
|
#findFallbackIndex(options, activeId) {
|
|
755
1406
|
const oldIndex = this.#activeOrder.indexOf(activeId);
|
|
756
1407
|
if (oldIndex < 0) return -1;
|
|
@@ -765,28 +1416,21 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
765
1416
|
}
|
|
766
1417
|
return -1;
|
|
767
1418
|
}
|
|
768
|
-
/** Reflects whether any visible, navigable command remains. */
|
|
769
1419
|
#reflectEmptyState() {
|
|
770
|
-
if (this.hasEmptyTarget)
|
|
1420
|
+
if (this.hasEmptyTarget) {
|
|
1421
|
+
this.#hidden.write(this.emptyTarget, this.#visibleOptions.length > 0 ? "" : null);
|
|
1422
|
+
}
|
|
771
1423
|
}
|
|
772
|
-
/** Coalesces all target callbacks from one MutationObserver batch. */
|
|
773
1424
|
#queueOptionReconciliation() {
|
|
774
1425
|
this.#reconcile.schedule();
|
|
775
1426
|
}
|
|
776
|
-
|
|
777
|
-
* Runs `option`: dispatches `select` and closes.
|
|
778
|
-
*
|
|
779
|
-
* **Activation is refused here, not at the call sites.** Every path that runs a
|
|
780
|
-
* command funnels through this method, so one check covers keyboard, pointer,
|
|
781
|
-
* and anything added later. Per-call-site guards would leave `aria-disabled`
|
|
782
|
-
* options — which stay within the keyboard's reach — runnable from whichever
|
|
783
|
-
* path forgets to consult the predicate.
|
|
784
|
-
*/
|
|
785
|
-
#confirmSelection(option) {
|
|
1427
|
+
#confirmSelection(option, reason = "user", restoreFocus = true) {
|
|
786
1428
|
if (this.#isDisabled(option)) return;
|
|
787
1429
|
const value = option.dataset.value || option.textContent || "";
|
|
788
|
-
this.dispatch("select", { detail: { value, option } });
|
|
789
|
-
this
|
|
1430
|
+
this.dispatch("select", { detail: { value, option, reason } });
|
|
1431
|
+
this.#close(
|
|
1432
|
+
restoreFocus && (reason !== "api" || this.element.contains(document.activeElement))
|
|
1433
|
+
);
|
|
790
1434
|
}
|
|
791
1435
|
#resetFilter() {
|
|
792
1436
|
if (this.hasInputTarget) this.inputTarget.value = "";
|
|
@@ -802,43 +1446,15 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
802
1446
|
(option) => !option.hasAttribute("hidden") && this.#isNavigable(option)
|
|
803
1447
|
);
|
|
804
1448
|
}
|
|
805
|
-
/**
|
|
806
|
-
* Whether virtual focus may land on `option`.
|
|
807
|
-
*
|
|
808
|
-
* Only `data-disabled` disqualifies. That attribute is this controller's own —
|
|
809
|
-
* authors reach for it to mark a row that is not a destination at all, such as
|
|
810
|
-
* a group heading — so it can mean "skip entirely" without contradicting ARIA.
|
|
811
|
-
*
|
|
812
|
-
* **`aria-disabled` does not disqualify.** It marks a command that must stay
|
|
813
|
-
* *discoverable*: `aria-activedescendant` names it, so a reader announces it as
|
|
814
|
-
* unavailable rather than hiding its existence. Virtual focus does
|
|
815
|
-
* not change that — pointing `aria-activedescendant` at a disabled option is
|
|
816
|
-
* ordinary ARIA. Keeping the two attributes distinct is what leaves both
|
|
817
|
-
* meanings expressible; collapsing them would take "show it, say it is
|
|
818
|
-
* unavailable, do not run it" away from consumers.
|
|
819
|
-
*/
|
|
820
1449
|
#isNavigable(option) {
|
|
821
1450
|
return option.dataset.disabled !== "true";
|
|
822
1451
|
}
|
|
823
|
-
/** Whether activating `option` may run. Suppressed for either disabled marker. */
|
|
824
1452
|
#isDisabled(option) {
|
|
825
1453
|
return option.dataset.disabled === "true" || option.getAttribute("aria-disabled") === "true";
|
|
826
1454
|
}
|
|
827
|
-
/** Synchronizes every option's controller-owned selection and disabled semantics. */
|
|
828
1455
|
#syncOptionSemantics() {
|
|
829
1456
|
for (const option of this.optionTargets) this.#syncOptionSemanticsFor(option);
|
|
830
1457
|
}
|
|
831
|
-
/**
|
|
832
|
-
* Reflects `data-disabled` to ARIA without losing a pre-existing authored value,
|
|
833
|
-
* and puts `aria-selected` back to its baseline.
|
|
834
|
-
*
|
|
835
|
-
* Here `aria-selected` marks the *active* option, not a committed choice — the
|
|
836
|
-
* palette runs a command and keeps nothing selected — so an authored value is
|
|
837
|
-
* meaningless and is overwritten rather than preserved. Every caller either
|
|
838
|
-
* establishes the active option straight after (`filter`, `#setActiveIndex`) or
|
|
839
|
-
* is handling an option that cannot be the active one yet
|
|
840
|
-
* (`optionTargetConnected`).
|
|
841
|
-
*/
|
|
842
1458
|
#syncOptionSemanticsFor(option) {
|
|
843
1459
|
const originalAttribute = _CommandPaletteController.#ORIGINAL_ARIA_DISABLED;
|
|
844
1460
|
const originalValue = option.getAttribute(originalAttribute);
|
|
@@ -868,7 +1484,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
868
1484
|
event.preventDefault();
|
|
869
1485
|
this.toggle();
|
|
870
1486
|
};
|
|
871
|
-
/** Matches the configured `mod+key` or bare-key hotkey without extra modifiers. */
|
|
872
1487
|
#matchesHotkey(event) {
|
|
873
1488
|
const hotkey = this.#parseHotkey();
|
|
874
1489
|
if (!hotkey || event.altKey || event.shiftKey) return false;
|
|
@@ -880,7 +1495,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
880
1495
|
}
|
|
881
1496
|
return event.key.toLowerCase() === hotkey.key;
|
|
882
1497
|
}
|
|
883
|
-
/** Parses the intentionally small public hotkey grammar. */
|
|
884
1498
|
#parseHotkey() {
|
|
885
1499
|
const parts = this.hotkeyValue.toLowerCase().split("+");
|
|
886
1500
|
if (parts.length === 1 && parts[0] && parts[0] !== "mod") {
|
|
@@ -891,7 +1505,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
891
1505
|
}
|
|
892
1506
|
return null;
|
|
893
1507
|
}
|
|
894
|
-
/** Resets transient open state so reconnect starts from a predictable closed snapshot. */
|
|
895
1508
|
#resetToClosedState() {
|
|
896
1509
|
this.#activeId = null;
|
|
897
1510
|
this.#activeOrder = [];
|
|
@@ -901,15 +1514,14 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
901
1514
|
option.removeAttribute("data-active");
|
|
902
1515
|
}
|
|
903
1516
|
if (this.hasDialogTarget) {
|
|
904
|
-
this.dialogTarget
|
|
1517
|
+
this.#hidden.write(this.dialogTarget, "");
|
|
905
1518
|
}
|
|
906
1519
|
if (this.hasInputTarget) {
|
|
907
|
-
this.inputTarget
|
|
908
|
-
this.inputTarget
|
|
1520
|
+
this.#expanded.write(this.inputTarget, "false");
|
|
1521
|
+
this.#activeDescendant.write(this.inputTarget, null);
|
|
909
1522
|
}
|
|
910
1523
|
}
|
|
911
1524
|
};
|
|
912
1525
|
|
|
913
1526
|
export { CommandPaletteController };
|
|
914
|
-
//# sourceMappingURL=command_palette_controller.js.map
|
|
915
1527
|
//# sourceMappingURL=command_palette_controller.js.map
|