stimeo-ui 0.13.0 → 0.14.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -5
  3. data/dist/controllers/calendar_controller.js +31 -7
  4. data/dist/controllers/carousel_controller.js +8 -6
  5. data/dist/controllers/conditional_fields_controller.js +7 -1
  6. data/dist/controllers/currency_input_controller.js +68 -23
  7. data/dist/controllers/date_range_picker_controller.js +31 -7
  8. data/dist/controllers/direct_upload_controller.js +21 -10
  9. data/dist/controllers/form_field_controller.js +7 -1
  10. data/dist/controllers/input_mask_controller.js +33 -12
  11. data/dist/controllers/intersection_controller.js +10 -5
  12. data/dist/controllers/lazy_frame_controller.js +10 -5
  13. data/dist/controllers/local_time_controller.js +34 -7
  14. data/dist/controllers/masonry_controller.js +1 -1
  15. data/dist/controllers/menu_controller.js +10 -2
  16. data/dist/controllers/menubar_controller.js +10 -2
  17. data/dist/controllers/navigation_menu_controller.js +8 -2
  18. data/dist/controllers/otp_controller.js +23 -10
  19. data/dist/controllers/overflow_menu_controller.js +10 -1
  20. data/dist/controllers/pointer_drag_controller.js +10 -3
  21. data/dist/controllers/portal_controller.js +21 -10
  22. data/dist/controllers/radio_group_controller.js +11 -3
  23. data/dist/controllers/relative_time_controller.js +31 -5
  24. data/dist/controllers/reset_before_cache_controller.js +23 -10
  25. data/dist/controllers/resizable_controller.js +7 -2
  26. data/dist/controllers/roving_controller.js +7 -3
  27. data/dist/controllers/scroll_visibility_controller.js +22 -13
  28. data/dist/controllers/scrollspy_controller.js +11 -6
  29. data/dist/controllers/smart_sticky_header_controller.js +23 -12
  30. data/dist/controllers/sticky_observer_controller.js +10 -5
  31. data/dist/controllers/theme_controller.js +23 -13
  32. data/dist/controllers/toggle_group_controller.js +11 -3
  33. data/dist/controllers/toolbar_controller.js +7 -3
  34. data/dist/index.js +165 -171
  35. data/lib/stimeo/ui/version.rb +1 -1
  36. metadata +2 -2
@@ -103,6 +103,12 @@ var EscapeLayer = class _EscapeLayer {
103
103
  }
104
104
  };
105
105
 
106
+ // src/utils/event_owner.ts
107
+ function ownerIndex(candidates, node) {
108
+ if (!(node instanceof Node)) return -1;
109
+ return candidates.findIndex((candidate) => candidate.contains(node));
110
+ }
111
+
106
112
  // src/utils/safe_timeout.ts
107
113
  var TimerRegistry = class {
108
114
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -395,7 +401,7 @@ var NavigationMenuController = class extends Controller {
395
401
  */
396
402
  #onPointerLeave = (event) => {
397
403
  const next = event instanceof MouseEvent ? event.relatedTarget : null;
398
- if (next instanceof Node && this.#hoverElements.some((el) => el.contains(next))) return;
404
+ if (ownerIndex(this.#hoverElements, next) !== -1) return;
399
405
  this.#hoveredTrigger = null;
400
406
  this.#hoverTimers.clearAll();
401
407
  this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);
@@ -465,7 +471,7 @@ var NavigationMenuController = class extends Controller {
465
471
  */
466
472
  get #hoverElements() {
467
473
  const areas = this.hoverAreaTargets;
468
- const covered = (element) => areas.some((area) => area.contains(element));
474
+ const covered = (element) => ownerIndex(areas, element) !== -1;
469
475
  return [
470
476
  ...areas,
471
477
  ...this.triggerTargets.filter((trigger) => !covered(trigger)),
@@ -152,6 +152,25 @@ var CompositionTracker = class {
152
152
  };
153
153
  };
154
154
 
155
+ // src/utils/declared_value.ts
156
+ function parseDeclared(raw, parse, fallback) {
157
+ try {
158
+ return parse(raw);
159
+ } catch {
160
+ return fallback;
161
+ }
162
+ }
163
+ function compileRegExp(source, anchor = "none") {
164
+ return parseDeclared(
165
+ source,
166
+ (text) => {
167
+ const bare = new RegExp(text);
168
+ return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
169
+ },
170
+ null
171
+ );
172
+ }
173
+
155
174
  // src/utils/half_width.ts
156
175
  var FULL_WIDTH_SHIFT = 65248;
157
176
  function halfWidthChar(char) {
@@ -201,13 +220,7 @@ var MicrotaskCoalescer = class {
201
220
 
202
221
  // src/controllers/otp_controller.ts
203
222
  var DEFAULT_PATTERN = "[0-9]";
204
- function compilePattern(source) {
205
- try {
206
- return new RegExp(`^${source}$`);
207
- } catch {
208
- return null;
209
- }
210
- }
223
+ var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
211
224
  function hasModifier(event) {
212
225
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
213
226
  }
@@ -222,7 +235,7 @@ var OtpController = class extends Controller {
222
235
  static actions = ["onInput", "onKeydown", "onPaste", "onPointerDown", "clear"];
223
236
  static events = ["change", "complete", "invalid", "reconcile"];
224
237
  /** Validated matcher; the hot path never compiles a raw declaration. */
225
- #pattern = new RegExp(`^${DEFAULT_PATTERN}$`);
238
+ #pattern = DEFAULT_MATCHER;
226
239
  /** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
227
240
  #patternSource = DEFAULT_PATTERN;
228
241
  /** Public state carried by the last dispatch; keeps a no-op sync silent. */
@@ -295,9 +308,9 @@ var OtpController = class extends Controller {
295
308
  * the new pattern no longer accepts, so the combined value stays interpretable.
296
309
  */
297
310
  patternValueChanged() {
298
- const compiled = compilePattern(this.patternValue);
311
+ const compiled = compileRegExp(this.patternValue, "exact");
299
312
  this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
300
- this.#pattern = compiled ?? new RegExp(`^${DEFAULT_PATTERN}$`);
313
+ this.#pattern = compiled ?? DEFAULT_MATCHER;
301
314
  if (!this.#connected) return;
302
315
  let dropped = false;
303
316
  for (const field of this.fieldTargets) {
@@ -31,6 +31,15 @@ var BeforeCacheReset = class _BeforeCacheReset {
31
31
  }
32
32
  };
33
33
 
34
+ // src/utils/event_owner.ts
35
+ function ownerIndex(candidates, node) {
36
+ if (!(node instanceof Node)) return -1;
37
+ return candidates.findIndex((candidate) => candidate.contains(node));
38
+ }
39
+ function ownerOf(candidates, node) {
40
+ return candidates[ownerIndex(candidates, node)] ?? null;
41
+ }
42
+
34
43
  // src/utils/focus_candidate.ts
35
44
  function inheritsFieldsetDisabled(control) {
36
45
  let fieldset = control.closest("fieldset[disabled]");
@@ -502,7 +511,7 @@ var OverflowMenuController = class extends Controller {
502
511
  }
503
512
  /** The managed item that is, or contains, `el` — focus rescue works on either. */
504
513
  #ownerOf(el) {
505
- return this.#items.find((item) => item === el || item.contains(el)) ?? null;
514
+ return ownerOf(this.#items, el);
506
515
  }
507
516
  /** Whether the item takes up room in the bar; an authored `hidden` one does not. */
508
517
  #rendered(item) {
@@ -63,6 +63,15 @@ var DetachGate = class _DetachGate {
63
63
  }
64
64
  };
65
65
 
66
+ // src/utils/event_owner.ts
67
+ function ownerIndex(candidates, node) {
68
+ if (!(node instanceof Node)) return -1;
69
+ return candidates.findIndex((candidate) => candidate.contains(node));
70
+ }
71
+ function ownerOf(candidates, node) {
72
+ return candidates[ownerIndex(candidates, node)] ?? null;
73
+ }
74
+
66
75
  // src/controllers/pointer_drag_controller.ts
67
76
  var NATIVE_KEY_OWNERS = "input, textarea, select, button, a[href], summary, [contenteditable]";
68
77
  var PointerDragController = class _PointerDragController extends Controller {
@@ -406,9 +415,7 @@ var PointerDragController = class _PointerDragController extends Controller {
406
415
  }
407
416
  /** Resolves the handle owning an event target (the handle or a descendant). */
408
417
  #handleFor(target) {
409
- const node = target;
410
- if (!node) return null;
411
- return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;
418
+ return ownerOf(this.#handles(), target);
412
419
  }
413
420
  #pointerTypeOf(event) {
414
421
  const type = event.pointerType;
@@ -2,6 +2,26 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/portal_controller.ts
4
4
 
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
7
+ try {
8
+ return parse(raw);
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ function validSelector(element, raw, fallback) {
14
+ if (raw.length === 0) return fallback;
15
+ return parseDeclared(
16
+ raw,
17
+ (selector) => {
18
+ element.matches(selector);
19
+ return selector;
20
+ },
21
+ fallback
22
+ );
23
+ }
24
+
5
25
  // src/utils/detach_gate.ts
6
26
  var DetachGate = class _DetachGate {
7
27
  /** Set while a probe is queued, waiting for a reconnect to cancel it. */
@@ -81,16 +101,7 @@ var PortalController = class extends Controller {
81
101
  * instead, which reads as a mistake and can be traced back to the declaration.
82
102
  */
83
103
  toValueChanged() {
84
- const selector = this.toValue;
85
- if (selector.length > 0) {
86
- try {
87
- this.element.matches(selector);
88
- this.#toSelector = selector;
89
- return;
90
- } catch {
91
- }
92
- }
93
- this.#toSelector = "body";
104
+ this.#toSelector = validSelector(this.element, this.toValue, "body");
94
105
  }
95
106
  connect() {
96
107
  this.#gate.cancel();
@@ -20,6 +20,15 @@ function isReservedArrowChord(event, allow = []) {
20
20
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
21
  }
22
22
 
23
+ // src/utils/event_owner.ts
24
+ function ownerIndex(candidates, node) {
25
+ if (!(node instanceof Node)) return -1;
26
+ return candidates.findIndex((candidate) => candidate.contains(node));
27
+ }
28
+ function ownerOf(candidates, node) {
29
+ return candidates[ownerIndex(candidates, node)] ?? null;
30
+ }
31
+
23
32
  // src/utils/focus_candidate.ts
24
33
  function inheritsFieldsetDisabled(control) {
25
34
  let fieldset = control.closest("fieldset[disabled]");
@@ -479,9 +488,8 @@ var RadioGroupController = class extends Controller {
479
488
  }
480
489
  /** Finds this group's radio containing an event target, excluding nested groups. */
481
490
  #radioForEventTarget(target) {
482
- if (!this.#ownsEventTarget(target)) return void 0;
483
- const node = target;
484
- return this.radioTargets.find((radio) => radio === node || radio.contains(node));
491
+ if (!this.#ownsEventTarget(target)) return null;
492
+ return ownerOf(this.radioTargets, target);
485
493
  }
486
494
  /** Whether the closest Radio Group scope around a target is this instance. */
487
495
  #ownsEventTarget(target) {
@@ -31,6 +31,35 @@ var BeforeCacheReset = class _BeforeCacheReset {
31
31
  }
32
32
  };
33
33
 
34
+ // src/utils/intl_format.ts
35
+ var formatters = /* @__PURE__ */ new WeakMap();
36
+ function intlFormatter(factory, locale, options, fallbackLocale) {
37
+ const cache = cacheFor(factory);
38
+ const key = cacheKey(locale, options, fallbackLocale);
39
+ if (cache.has(key)) return cache.get(key);
40
+ const formatter = construct(factory, locale, options);
41
+ cache.set(key, formatter);
42
+ return formatter;
43
+ }
44
+ function cacheFor(factory) {
45
+ const existing = formatters.get(factory);
46
+ if (existing) return existing;
47
+ const created = /* @__PURE__ */ new Map();
48
+ formatters.set(factory, created);
49
+ return created;
50
+ }
51
+ function cacheKey(locale, options, fallbackLocale) {
52
+ return JSON.stringify([locale, options, fallbackLocale]);
53
+ }
54
+ function construct(factory, locale, options, fallbackLocale) {
55
+ try {
56
+ return new factory(locale, options);
57
+ } catch (error) {
58
+ if (!(error instanceof RangeError)) throw error;
59
+ return null;
60
+ }
61
+ }
62
+
34
63
  // src/utils/microtask_coalescer.ts
35
64
  var MicrotaskCoalescer = class {
36
65
  #run;
@@ -128,6 +157,7 @@ var UNITS = [
128
157
  { limit: 31557600, unit: "month", ms: 26298e5 },
129
158
  YEAR_SCALE
130
159
  ];
160
+ var RELATIVE_OPTIONS = { numeric: "auto" };
131
161
  var RelativeTimeController = class extends Controller {
132
162
  static values = {
133
163
  locale: { type: String, default: "" },
@@ -235,11 +265,7 @@ var RelativeTimeController = class extends Controller {
235
265
  * when the runtime rejects that locale.
236
266
  */
237
267
  get #formatter() {
238
- try {
239
- return new Intl.RelativeTimeFormat(this.#locale, { numeric: "auto" });
240
- } catch {
241
- return null;
242
- }
268
+ return intlFormatter(Intl.RelativeTimeFormat, this.#locale, RELATIVE_OPTIONS);
243
269
  }
244
270
  /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
245
271
  get #locale() {
@@ -1,5 +1,27 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/reset_before_cache_controller.ts
4
+
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
7
+ try {
8
+ return parse(raw);
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ function validSelector(element, raw, fallback) {
14
+ if (raw.length === 0) return fallback;
15
+ return parseDeclared(
16
+ raw,
17
+ (selector) => {
18
+ element.matches(selector);
19
+ return selector;
20
+ },
21
+ fallback
22
+ );
23
+ }
24
+
3
25
  // src/controllers/reset_before_cache_controller.ts
4
26
  var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
5
27
  function restoreField(element) {
@@ -43,16 +65,7 @@ var ResetBeforeCacheController = class extends Controller {
43
65
  * silently taking the sweep down.
44
66
  */
45
67
  scopeValueChanged() {
46
- const selector = this.scopeValue;
47
- if (selector.length > 0) {
48
- try {
49
- this.element.matches(selector);
50
- this.#scopeSelector = selector;
51
- return;
52
- } catch {
53
- }
54
- }
55
- this.#scopeSelector = "";
68
+ this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
56
69
  }
57
70
  connect() {
58
71
  document.addEventListener("turbo:before-cache", this.#onBeforeCache);
@@ -8,6 +8,12 @@ function isReservedArrowChord(event, allow = []) {
8
8
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
9
  }
10
10
 
11
+ // src/utils/event_owner.ts
12
+ function ownerIndex(candidates, node) {
13
+ if (!(node instanceof Node)) return -1;
14
+ return candidates.findIndex((candidate) => candidate.contains(node));
15
+ }
16
+
11
17
  // src/utils/microtask_coalescer.ts
12
18
  var MicrotaskCoalescer = class {
13
19
  #run;
@@ -154,8 +160,7 @@ var ResizableController = class extends Controller {
154
160
  const panes = [];
155
161
  if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
156
162
  if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
157
- const target = event.target;
158
- const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
163
+ const current = ownerIndex(panes, event.target);
159
164
  const next = panes[(current + 1) % panes.length];
160
165
  if (!next) return;
161
166
  event.preventDefault();
@@ -16,6 +16,12 @@ function hasModifierChord(event) {
16
16
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
17
17
  }
18
18
 
19
+ // src/utils/event_owner.ts
20
+ function ownerIndex(candidates, node) {
21
+ if (!(node instanceof Node)) return -1;
22
+ return candidates.findIndex((candidate) => candidate.contains(node));
23
+ }
24
+
19
25
  // src/utils/focus_candidate.ts
20
26
  function inheritsFieldsetDisabled(control) {
21
27
  let fieldset = control.closest("fieldset[disabled]");
@@ -182,9 +188,7 @@ var RovingController = class extends Controller {
182
188
  };
183
189
  /** Resolves the item index owning an event target (the item or a descendant). */
184
190
  #indexOf(target) {
185
- const node = target;
186
- if (!node) return -1;
187
- return this.itemTargets.findIndex((item) => item === node || item.contains(node));
191
+ return ownerIndex(this.itemTargets, target);
188
192
  }
189
193
  /** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
190
194
  #activate(index, focus) {
@@ -57,6 +57,26 @@ var BlurDeferral = class {
57
57
  }
58
58
  };
59
59
 
60
+ // src/utils/declared_value.ts
61
+ function parseDeclared(raw, parse, fallback) {
62
+ try {
63
+ return parse(raw);
64
+ } catch {
65
+ return fallback;
66
+ }
67
+ }
68
+ function validSelector(element, raw, fallback) {
69
+ if (raw.length === 0) return fallback;
70
+ return parseDeclared(
71
+ raw,
72
+ (selector) => {
73
+ element.matches(selector);
74
+ return selector;
75
+ },
76
+ fallback
77
+ );
78
+ }
79
+
60
80
  // src/utils/reduced_motion.ts
61
81
  function prefersReducedMotion() {
62
82
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -223,11 +243,11 @@ var ScrollVisibilityController = class extends Controller {
223
243
  }
224
244
  /** Validates `root` once so connect never parses a selector that throws. */
225
245
  rootValueChanged() {
226
- this.#rootSelector = this.#validSelector(this.rootValue);
246
+ this.#rootSelector = validSelector(this.element, this.rootValue, "");
227
247
  }
228
248
  /** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
229
249
  focusSelectorValueChanged() {
230
- this.#focusSelector = this.#validSelector(this.focusSelectorValue);
250
+ this.#focusSelector = validSelector(this.element, this.focusSelectorValue, "");
231
251
  }
232
252
  /** Scrolls the source to the top and, optionally, moves focus to a safe target. */
233
253
  toTop() {
@@ -299,17 +319,6 @@ var ScrollVisibilityController = class extends Controller {
299
319
  if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
300
320
  return null;
301
321
  }
302
- /** Returns `declared` when it parses as a selector, and `""` when it does not. */
303
- #validSelector(declared) {
304
- if (declared.length > 0) {
305
- try {
306
- this.element.matches(declared);
307
- return declared;
308
- } catch {
309
- }
310
- }
311
- return "";
312
- }
313
322
  #scrollY() {
314
323
  if (this.#scrollSource === window) {
315
324
  return window.scrollY ?? window.pageYOffset ?? 0;
@@ -2,14 +2,19 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/scrollspy_controller.ts
4
4
 
5
- // src/utils/intersection_watcher.ts
6
- function queryRoot(selector) {
7
- if (!selector) return null;
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
8
7
  try {
9
- return document.querySelector(selector);
8
+ return parse(raw);
10
9
  } catch {
10
+ return fallback;
11
11
  }
12
- return null;
12
+ }
13
+
14
+ // src/utils/intersection_watcher.ts
15
+ function queryRoot(selector) {
16
+ if (!selector) return null;
17
+ return parseDeclared(selector, (raw) => document.querySelector(raw), null);
13
18
  }
14
19
  var IntersectionWatcher = class {
15
20
  #onEntries;
@@ -239,7 +244,7 @@ var ScrollspyController = class extends Controller {
239
244
  * only rewrites attributes, so Stimulus fires no target callback: without
240
245
  * this, a link re-pointed from `#intro` to `#faq` would keep the controller
241
246
  * observing `#intro` for the rest of the page's life. The filter is exactly
242
- * {@link ANCHOR_ATTRIBUTES}; guarding on `MutationObserver` keeps the
247
+ * `href` and `data-href`; guarding on `MutationObserver` keeps the
243
248
  * controller usable where the API is absent, matching `IntersectionWatcher`'s
244
249
  * own support guard.
245
250
  */
@@ -1,5 +1,27 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/smart_sticky_header_controller.ts
4
+
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
7
+ try {
8
+ return parse(raw);
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ function validSelector(element, raw, fallback) {
14
+ if (raw.length === 0) return fallback;
15
+ return parseDeclared(
16
+ raw,
17
+ (selector) => {
18
+ element.matches(selector);
19
+ return selector;
20
+ },
21
+ fallback
22
+ );
23
+ }
24
+
3
25
  // src/controllers/smart_sticky_header_controller.ts
4
26
  var DEFAULT_OFFSET = 80;
5
27
  var SmartStickyHeaderController = class extends Controller {
@@ -41,7 +63,7 @@ var SmartStickyHeaderController = class extends Controller {
41
63
  #onFocusin = () => this.#apply(false);
42
64
  /** Validates `containerSelector` once so connect never parses a selector that throws. */
43
65
  containerSelectorValueChanged() {
44
- this.#containerSelector = this.#validSelector(this.containerSelectorValue);
66
+ this.#containerSelector = validSelector(this.element, this.containerSelectorValue, "");
45
67
  }
46
68
  /** Re-decides when application code (or a Turbo morph) changes `offset` at runtime. */
47
69
  offsetValueChanged() {
@@ -71,17 +93,6 @@ var SmartStickyHeaderController = class extends Controller {
71
93
  }
72
94
  return window;
73
95
  }
74
- /** Returns `declared` when it parses as a selector, and `""` when it does not. */
75
- #validSelector(declared) {
76
- if (declared.length > 0) {
77
- try {
78
- this.element.matches(declared);
79
- return declared;
80
- } catch {
81
- }
82
- }
83
- return "";
84
- }
85
96
  get #scrollY() {
86
97
  const scroller = this.#scrollerEl;
87
98
  return scroller === window ? window.scrollY : scroller.scrollTop;
@@ -2,6 +2,15 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/sticky_observer_controller.ts
4
4
 
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
7
+ try {
8
+ return parse(raw);
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+
5
14
  // src/utils/intersection_watcher.ts
6
15
  function isBeforeRootStart(entry) {
7
16
  const rect = entry.boundingClientRect;
@@ -11,11 +20,7 @@ function isBeforeRootStart(entry) {
11
20
  }
12
21
  function queryRoot(selector) {
13
22
  if (!selector) return null;
14
- try {
15
- return document.querySelector(selector);
16
- } catch {
17
- }
18
- return null;
23
+ return parseDeclared(selector, (raw) => document.querySelector(raw), null);
19
24
  }
20
25
  var IntersectionWatcher = class {
21
26
  #onEntries;
@@ -23,6 +23,26 @@ function hasModifierChord(event) {
23
23
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
24
24
  }
25
25
 
26
+ // src/utils/declared_value.ts
27
+ function parseDeclared(raw, parse, fallback) {
28
+ try {
29
+ return parse(raw);
30
+ } catch {
31
+ return fallback;
32
+ }
33
+ }
34
+ function validSelector(element, raw, fallback) {
35
+ if (raw.length === 0) return fallback;
36
+ return parseDeclared(
37
+ raw,
38
+ (selector) => {
39
+ element.matches(selector);
40
+ return selector;
41
+ },
42
+ fallback
43
+ );
44
+ }
45
+
26
46
  // src/utils/roving_tabindex.ts
27
47
  var RovingTabindex = class {
28
48
  /** Returns the current ordered item elements; called on every operation. */
@@ -167,16 +187,7 @@ var ThemeController = class extends Controller {
167
187
  }
168
188
  /** Validates the `target` declaration once, so the render path never parses. */
169
189
  targetValueChanged() {
170
- const selector = this.targetValue;
171
- if (selector.length > 0) {
172
- try {
173
- this.element.matches(selector);
174
- this.#targetSelector = selector;
175
- return;
176
- } catch {
177
- }
178
- }
179
- this.#targetSelector = DEFAULT_TARGET;
190
+ this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
180
191
  }
181
192
  /** Re-derives the single Tab stop and ARIA for an option set that changed. */
182
193
  optionTargetConnected() {
@@ -189,9 +200,8 @@ var ThemeController = class extends Controller {
189
200
  /**
190
201
  * Selects the mode the activated option declares.
191
202
  *
192
- * Read through {@link ThemeController.#optionMode}, the same lane that decides
193
- * which option is checked, so the two can never disagree about what an option
194
- * declares.
203
+ * Read through the same lane that decides which option is checked, so the two
204
+ * can never disagree about what an option declares.
195
205
  */
196
206
  set(event) {
197
207
  const option = event.currentTarget;
@@ -20,6 +20,15 @@ function isReservedArrowChord(event, allow = []) {
20
20
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
21
  }
22
22
 
23
+ // src/utils/event_owner.ts
24
+ function ownerIndex(candidates, node) {
25
+ if (!(node instanceof Node)) return -1;
26
+ return candidates.findIndex((candidate) => candidate.contains(node));
27
+ }
28
+ function ownerOf(candidates, node) {
29
+ return candidates[ownerIndex(candidates, node)] ?? null;
30
+ }
31
+
23
32
  // src/utils/focus_candidate.ts
24
33
  function inheritsFieldsetDisabled(control) {
25
34
  let fieldset = control.closest("fieldset[disabled]");
@@ -363,9 +372,8 @@ var ToggleGroupController = class extends Controller {
363
372
  }
364
373
  /** Finds this group's item containing an event target, excluding nested groups. */
365
374
  #itemForEventTarget(target) {
366
- if (!this.#ownsEventTarget(target)) return void 0;
367
- const node = target;
368
- return this.itemTargets.find((item) => item === node || item.contains(node));
375
+ if (!this.#ownsEventTarget(target)) return null;
376
+ return ownerOf(this.itemTargets, target);
369
377
  }
370
378
  /** Whether the closest Toggle Group scope around a target is this instance. */
371
379
  #ownsEventTarget(target) {
@@ -60,6 +60,12 @@ var CompositionTracker = class {
60
60
  };
61
61
  };
62
62
 
63
+ // src/utils/event_owner.ts
64
+ function ownerIndex(candidates, node) {
65
+ if (!(node instanceof Node)) return -1;
66
+ return candidates.findIndex((candidate) => candidate.contains(node));
67
+ }
68
+
63
69
  // src/utils/focus_candidate.ts
64
70
  function inheritsFieldsetDisabled(control) {
65
71
  let fieldset = control.closest("fieldset[disabled]");
@@ -268,9 +274,7 @@ var ToolbarController = class extends Controller {
268
274
  }
269
275
  /** Index in `controlTargets` of the control owning `target` (it or a descendant). */
270
276
  #indexOf(target) {
271
- const node = target;
272
- if (!node) return -1;
273
- return this.controlTargets.findIndex((control) => control === node || control.contains(node));
277
+ return ownerIndex(this.controlTargets, target);
274
278
  }
275
279
  /** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
276
280
  get #navigableControls() {