stimeo-ui 0.17.0 → 0.18.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -1
  3. data/README.md +19 -10
  4. data/dist/cable/index.js +22 -14
  5. data/dist/controllers/accordion_controller.js +14 -6
  6. data/dist/controllers/alert_dialog_controller.js +145 -118
  7. data/dist/controllers/announcer_controller.js +113 -17
  8. data/dist/controllers/auto_submit_controller.js +185 -60
  9. data/dist/controllers/avatar_controller.js +55 -43
  10. data/dist/controllers/bulk_select_controller.js +64 -50
  11. data/dist/controllers/calendar_controller.js +55 -53
  12. data/dist/controllers/carousel_controller.js +124 -86
  13. data/dist/controllers/character_counter_controller.js +291 -95
  14. data/dist/controllers/checkbox_controller.js +6 -0
  15. data/dist/controllers/clipboard_controller.js +118 -27
  16. data/dist/controllers/collapsible_controller.js +52 -49
  17. data/dist/controllers/color_picker_controller.js +29 -7
  18. data/dist/controllers/combobox_controller.js +210 -71
  19. data/dist/controllers/command_palette_controller.js +289 -140
  20. data/dist/controllers/conditional_fields_controller.js +145 -58
  21. data/dist/controllers/confirm_controller.js +145 -118
  22. data/dist/controllers/context_menu_controller.js +138 -73
  23. data/dist/controllers/countdown_controller.js +109 -21
  24. data/dist/controllers/currency_input_controller.js +446 -72
  25. data/dist/controllers/data_grid_controller.js +134 -28
  26. data/dist/controllers/date_range_picker_controller.js +93 -72
  27. data/dist/controllers/dialog_controller.js +145 -118
  28. data/dist/controllers/direct_upload_controller.js +24 -14
  29. data/dist/controllers/dirty_form_controller.js +68 -31
  30. data/dist/controllers/dismissible_controller.js +64 -46
  31. data/dist/controllers/drawer_controller.js +145 -118
  32. data/dist/controllers/dropdown_controller.js +76 -63
  33. data/dist/controllers/editable_controller.js +199 -62
  34. data/dist/controllers/empty_state_controller.js +13 -6
  35. data/dist/controllers/file_dropzone_controller.js +55 -48
  36. data/dist/controllers/filter_controller.js +9 -2
  37. data/dist/controllers/flash_controller.js +66 -33
  38. data/dist/controllers/focus_controller.js +39 -14
  39. data/dist/controllers/form_field_controller.js +52 -43
  40. data/dist/controllers/form_validation_controller.js +18 -11
  41. data/dist/controllers/frame_loading_controller.js +55 -48
  42. data/dist/controllers/hover_card_controller.js +67 -61
  43. data/dist/controllers/idle_controller.js +10 -6
  44. data/dist/controllers/input_mask_controller.js +411 -24
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +15 -3
  47. data/dist/controllers/listbox_controller.js +260 -79
  48. data/dist/controllers/local_time_controller.js +48 -13
  49. data/dist/controllers/masonry_controller.js +11 -4
  50. data/dist/controllers/menu_controller.js +138 -82
  51. data/dist/controllers/menubar_controller.js +65 -19
  52. data/dist/controllers/meter_controller.js +270 -24
  53. data/dist/controllers/multi_select_controller.js +251 -94
  54. data/dist/controllers/navigation_menu_controller.js +67 -19
  55. data/dist/controllers/nested_form_controller.js +98 -64
  56. data/dist/controllers/network_status_controller.js +70 -52
  57. data/dist/controllers/number_input_controller.js +344 -94
  58. data/dist/controllers/optimistic_controller.js +29 -11
  59. data/dist/controllers/otp_controller.js +502 -104
  60. data/dist/controllers/overflow_indicator_controller.js +6 -0
  61. data/dist/controllers/overflow_menu_controller.js +136 -21
  62. data/dist/controllers/pagination_controller.js +47 -14
  63. data/dist/controllers/password_reveal_controller.js +52 -49
  64. data/dist/controllers/password_strength_controller.js +13 -6
  65. data/dist/controllers/persist_controller.js +48 -128
  66. data/dist/controllers/pointer_drag_controller.js +249 -23
  67. data/dist/controllers/popover_controller.js +82 -65
  68. data/dist/controllers/portal_controller.js +6 -0
  69. data/dist/controllers/preview_guard_controller.js +14 -3
  70. data/dist/controllers/progress_controller.js +271 -27
  71. data/dist/controllers/radio_group_controller.js +167 -80
  72. data/dist/controllers/range_slider_controller.js +27 -7
  73. data/dist/controllers/rating_controller.js +93 -57
  74. data/dist/controllers/read_more_controller.js +10 -6
  75. data/dist/controllers/reading_progress_controller.js +22 -5
  76. data/dist/controllers/relative_time_controller.js +75 -11
  77. data/dist/controllers/reset_on_restore_controller.js +6 -0
  78. data/dist/controllers/resizable_controller.js +55 -22
  79. data/dist/controllers/roving_controller.js +10 -4
  80. data/dist/controllers/scroll_area_controller.js +129 -88
  81. data/dist/controllers/scroll_restore_controller.js +14 -2
  82. data/dist/controllers/scroll_visibility_controller.js +93 -60
  83. data/dist/controllers/scrollspy_controller.js +11 -5
  84. data/dist/controllers/separator_controller.js +59 -43
  85. data/dist/controllers/sidebar_controller.js +150 -122
  86. data/dist/controllers/skeleton_controller.js +55 -48
  87. data/dist/controllers/slider_controller.js +27 -6
  88. data/dist/controllers/smart_sticky_header_controller.js +13 -5
  89. data/dist/controllers/sortable_controller.js +27 -14
  90. data/dist/controllers/spinner_controller.js +55 -48
  91. data/dist/controllers/step_indicator_controller.js +6 -0
  92. data/dist/controllers/stepper_controller.js +6 -0
  93. data/dist/controllers/stick_to_bottom_controller.js +19 -8
  94. data/dist/controllers/sticky_observer_controller.js +49 -43
  95. data/dist/controllers/submit_once_controller.js +145 -85
  96. data/dist/controllers/switch_controller.js +172 -28
  97. data/dist/controllers/tabs_controller.js +6 -1
  98. data/dist/controllers/tags_input_controller.js +157 -28
  99. data/dist/controllers/textarea_autosize_controller.js +40 -19
  100. data/dist/controllers/theme_controller.js +10 -4
  101. data/dist/controllers/time_picker_controller.js +192 -54
  102. data/dist/controllers/toast_controller.js +58 -27
  103. data/dist/controllers/toggle_group_controller.js +158 -56
  104. data/dist/controllers/toolbar_controller.js +150 -21
  105. data/dist/controllers/tree_view_controller.js +134 -25
  106. data/dist/index.js +2451 -1312
  107. data/dist/positioning/index.js +6 -0
  108. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  109. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  110. data/lib/stimeo/ui/version.rb +1 -1
  111. metadata +2 -2
@@ -22,6 +22,13 @@ function actionSource(source) {
22
22
  };
23
23
  }
24
24
 
25
+ function isAriaDisabled(element) {
26
+ for (let current = element; current; current = current.parentElement) {
27
+ if (current.getAttribute("aria-disabled") === "true") return true;
28
+ }
29
+ return false;
30
+ }
31
+
25
32
  function isRtl(element) {
26
33
  return window.getComputedStyle(element).direction === "rtl";
27
34
  }
@@ -31,6 +38,18 @@ function isReservedArrowChord(event, allow = []) {
31
38
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
32
39
  }
33
40
 
41
+ var ClickVerdicts = class {
42
+ #verdicts = /* @__PURE__ */ new WeakMap();
43
+ record(event, entries, refused) {
44
+ this.#verdicts.set(event, { entries, refused });
45
+ }
46
+ for(event, entry) {
47
+ const verdict = event && this.#verdicts.get(event);
48
+ if (!verdict?.entries.includes(entry)) return void 0;
49
+ return verdict.refused;
50
+ }
51
+ };
52
+
34
53
  function sharedRegistry(key, create) {
35
54
  const symbol = Symbol.for(key);
36
55
  const scope = globalThis;
@@ -114,6 +133,21 @@ function ownerIndex(candidates, node) {
114
133
  if (!(node instanceof Node)) return -1;
115
134
  return candidates.findIndex((candidate) => candidate.contains(node));
116
135
  }
136
+ function ownersOf(candidates, node) {
137
+ if (!(node instanceof Node)) return [];
138
+ return candidates.filter((candidate) => candidate.contains(node));
139
+ }
140
+
141
+ var ListenerSet = class {
142
+ #abort = new AbortController();
143
+ add(target, type, handler, options) {
144
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
145
+ }
146
+ dispose() {
147
+ this.#abort.abort();
148
+ this.#abort = new AbortController();
149
+ }
150
+ };
117
151
 
118
152
  var MAX_TIMER_DELAY_MS = 2147483647;
119
153
 
@@ -201,7 +235,7 @@ function targetSelector(identifier, name) {
201
235
 
202
236
  var NavigationMenuController = class _NavigationMenuController extends Controller {
203
237
  #numbers = new NumberValueReader();
204
- static targets = ["trigger", "panel", "hoverArea"];
238
+ static targets = ["trigger", "panel", "hoverArea", "list"];
205
239
  static values = {
206
240
  openOnHover: { type: Boolean, default: false },
207
241
  hoverDelay: { type: Number, default: 150 }
@@ -212,16 +246,19 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
212
246
  static actions = ["onTriggerKeydown", "toggle"];
213
247
  static events = ["close", "open"];
214
248
  #hoverTimers = new SafeTimeout();
215
- #hoverWired = /* @__PURE__ */ new Set();
249
+ #hoverListeners = new ListenerSet();
216
250
  #hoveredTrigger = null;
217
251
  #connected = false;
218
252
  #reporting = false;
219
253
  #escapeLayer = new EscapeLayer();
254
+ #listeners = new ListenerSet();
255
+ #clickVerdicts = new ClickVerdicts();
220
256
  connect() {
221
257
  this.#connected = true;
222
258
  this.#closeAll("api");
223
- document.addEventListener("click", this.#onOutsideClick, true);
224
- this.element.addEventListener("focusout", this.#onFocusOut);
259
+ this.#listeners.add(this.element, "click", this.#onClickCapture, { capture: true });
260
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
261
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
225
262
  if (this.openOnHoverValue) this.#addHoverListeners();
226
263
  this.#reporting = true;
227
264
  }
@@ -229,8 +266,7 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
229
266
  this.#connected = false;
230
267
  this.#reporting = false;
231
268
  this.#escapeLayer.deactivate();
232
- document.removeEventListener("click", this.#onOutsideClick, true);
233
- this.element.removeEventListener("focusout", this.#onFocusOut);
269
+ this.#listeners.dispose();
234
270
  this.#removeHoverListeners();
235
271
  this.#hoverTimers.clearAll();
236
272
  }
@@ -258,14 +294,14 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
258
294
  this.#rewireHoverListeners();
259
295
  }
260
296
  toggle(source) {
261
- const { host, origin, reason } = actionSource(source);
297
+ const { event, host, origin, reason } = actionSource(source);
262
298
  const trigger = host?.closest(targetSelector(this.identifier, "trigger"));
263
299
  if (!trigger || !this.triggerTargets.includes(trigger)) return;
264
300
  if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
265
301
  return;
266
302
  this.#hoverTimers.clearAll();
267
303
  if (!this.#isExpanded(trigger)) {
268
- this.#openPanel(trigger, reason);
304
+ if (!this.#refuses(trigger, event, origin)) this.#openPanel(trigger, reason);
269
305
  return;
270
306
  }
271
307
  if (this.openOnHoverValue && this.#hoveredTrigger === trigger) return;
@@ -282,12 +318,11 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
282
318
  const index = triggers.indexOf(current);
283
319
  if (index === -1 || triggers.length < 2) return;
284
320
  event.preventDefault();
285
- const forward = isRtl(this.element) ? "ArrowLeft" : "ArrowRight";
321
+ const forward = isRtl(this.hasListTarget ? this.listTarget : this.element) ? "ArrowLeft" : "ArrowRight";
286
322
  const step = event.key === forward ? 1 : -1;
287
323
  triggers[(index + step + triggers.length) % triggers.length]?.focus();
288
324
  }
289
325
  #openPanel(trigger, reason) {
290
- if (trigger.getAttribute("aria-disabled") === "true") return;
291
326
  if (this.#isExpanded(trigger)) {
292
327
  this.#syncEscapeLayer();
293
328
  return;
@@ -322,6 +357,16 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
322
357
  if (trigger.hasAttribute("hidden")) return false;
323
358
  return !trigger.disabled;
324
359
  }
360
+ #isActivationDisabled(trigger) {
361
+ return !this.#isNavigable(trigger) || isAriaDisabled(trigger);
362
+ }
363
+ #refuses(trigger, event, origin) {
364
+ const recorded = this.#clickVerdicts.for(event, trigger);
365
+ if (recorded !== void 0) return recorded;
366
+ if (this.#isActivationDisabled(trigger)) return true;
367
+ const target = event ? event.target : origin;
368
+ return ownersOf(this.triggerTargets, target).some((owner) => this.#isActivationDisabled(owner));
369
+ }
325
370
  #syncEscapeLayer() {
326
371
  if (this.#isAnyOpen) {
327
372
  this.#escapeLayer.activate(document, {
@@ -339,6 +384,14 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
339
384
  this.#closePanel(open, "escape");
340
385
  open.focus();
341
386
  }
387
+ #onClickCapture = (event) => {
388
+ const triggers = ownersOf(this.triggerTargets, event.target);
389
+ this.#clickVerdicts.record(
390
+ event,
391
+ triggers,
392
+ triggers.some((trigger) => this.#isActivationDisabled(trigger))
393
+ );
394
+ };
342
395
  #onOutsideClick = (event) => {
343
396
  if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAll("outside");
344
397
  };
@@ -362,7 +415,7 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
362
415
  this.#hoverTimers.set(() => this.#closeFromHover(), this.#safeHoverDelay);
363
416
  };
364
417
  #openFromHover(trigger) {
365
- if (trigger.getAttribute("aria-disabled") === "true") return;
418
+ if (this.#refuses(trigger, null, trigger)) return;
366
419
  const open = this.#openTrigger;
367
420
  if (open && open !== trigger && this.#holdsFocus(this.#panelFor(open))) open.focus();
368
421
  this.#openPanel(trigger, "pointer");
@@ -377,17 +430,12 @@ var NavigationMenuController = class _NavigationMenuController extends Controlle
377
430
  }
378
431
  #addHoverListeners() {
379
432
  for (const element of this.#hoverElements) {
380
- element.addEventListener("mouseenter", this.#onPointerEnter);
381
- element.addEventListener("mouseleave", this.#onPointerLeave);
382
- this.#hoverWired.add(element);
433
+ this.#hoverListeners.add(element, "mouseenter", this.#onPointerEnter);
434
+ this.#hoverListeners.add(element, "mouseleave", this.#onPointerLeave);
383
435
  }
384
436
  }
385
437
  #removeHoverListeners() {
386
- for (const element of this.#hoverWired) {
387
- element.removeEventListener("mouseenter", this.#onPointerEnter);
388
- element.removeEventListener("mouseleave", this.#onPointerLeave);
389
- }
390
- this.#hoverWired.clear();
438
+ this.#hoverListeners.dispose();
391
439
  this.#hoveredTrigger = null;
392
440
  }
393
441
  #rewireHoverListeners() {
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
9
+
3
10
  function announce(message, options = {}) {
4
11
  const text = message.trim();
5
12
  if (text.length === 0) return;
@@ -9,11 +16,12 @@ function announce(message, options = {}) {
9
16
  })
10
17
  );
11
18
  }
12
- function fillTemplate(template, values) {
13
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
14
- const replacement = values[name];
15
- return replacement === void 0 ? match : String(replacement);
16
- });
19
+
20
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
21
+ if (element.getAttribute(name) === value) return false;
22
+ if (value === null) element.removeAttribute(name);
23
+ else element.setAttribute(name, value);
24
+ return true;
17
25
  }
18
26
 
19
27
  function sharedRegistry(key, create) {
@@ -21,11 +29,15 @@ function sharedRegistry(key, create) {
21
29
  const scope = globalThis;
22
30
  const existing = scope[symbol];
23
31
  if (existing !== void 0) return existing;
24
- const registry = create();
25
- Object.defineProperty(globalThis, symbol, { value: registry });
26
- return registry;
32
+ const registry2 = create();
33
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
34
+ return registry2;
27
35
  }
28
36
 
37
+ var registry = sharedRegistry(
38
+ "stimeo-ui.attribute-lease.registry.v1",
39
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
40
+ );
29
41
  function isAttributeValue(value) {
30
42
  return value === null || typeof value === "string";
31
43
  }
@@ -42,24 +54,46 @@ function readLeaseRecord(element, record) {
42
54
  return void 0;
43
55
  }
44
56
  }
45
- var AttributeLease = class _AttributeLease {
46
- static #registry = sharedRegistry(
47
- "stimeo-ui.attribute-lease.registry.v1",
48
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
- );
57
+ function attributeStack(element, attribute) {
58
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
59
+ registry.stacks.set(element, byAttribute);
60
+ const known = byAttribute.get(attribute);
61
+ if (known) return known;
62
+ const found = [];
63
+ for (const record of registry.names.get(attribute) ?? []) {
64
+ const read = readLeaseRecord(element, record);
65
+ if (read) found.push({ ...read, record });
66
+ }
67
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
68
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
69
+ lease: null,
70
+ record,
71
+ written,
72
+ original,
73
+ linked: linked && index > 0
74
+ }));
75
+ byAttribute.set(attribute, stack);
76
+ return stack;
77
+ }
78
+ function findHold(stack, lease, record) {
79
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
80
+ }
81
+ var AttributeLease = class {
50
82
  #attribute;
51
83
  #record;
52
84
  #base;
85
+ #preserveAuthor;
53
86
  #held = /* @__PURE__ */ new Set();
54
87
  constructor(attribute, owner, options = {}) {
55
88
  this.#attribute = attribute;
56
89
  this.#record = `data-${owner}-${attribute}-lease`;
57
90
  this.#base = options.base === true;
58
- const names = _AttributeLease.#registry.names;
91
+ this.#preserveAuthor = options.preserveAuthor === true;
92
+ const names = registry.names;
59
93
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
94
  }
61
95
  write(element, value) {
62
- const stack = this.#stack(element);
96
+ const stack = attributeStack(element, this.#attribute);
63
97
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
98
  this.#held.add(element);
65
99
  const shows = !this.#covered(element, stack, hold);
@@ -69,8 +103,8 @@ var AttributeLease = class _AttributeLease {
69
103
  }
70
104
  return(element) {
71
105
  const held = this.#held.delete(element);
72
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
106
+ const known = registry.stacks.get(element)?.has(this.#attribute);
107
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
74
108
  const hold = this.#take(stack);
75
109
  if (!hold) return false;
76
110
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -88,31 +122,8 @@ var AttributeLease = class _AttributeLease {
88
122
  returnAll() {
89
123
  for (const element of Array.from(this.#held)) this.return(element);
90
124
  }
91
- #stack(element) {
92
- const stacks = _AttributeLease.#registry.stacks;
93
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
- stacks.set(element, byAttribute);
95
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
- byAttribute.set(this.#attribute, stack);
97
- return stack;
98
- }
99
- #seed(element) {
100
- const found = [];
101
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
- const read = readLeaseRecord(element, record);
103
- if (read) found.push({ ...read, record });
104
- }
105
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
- lease: null,
108
- record,
109
- written,
110
- original,
111
- linked: linked && index > 0
112
- }));
113
- }
114
125
  #take(stack) {
115
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
126
+ const hold = findHold(stack, this, this.#record);
116
127
  if (hold) hold.lease = this;
117
128
  return hold;
118
129
  }
@@ -121,7 +132,7 @@ var AttributeLease = class _AttributeLease {
121
132
  const top = stack.at(-1);
122
133
  const bottom = stack[0];
123
134
  const over = top?.written === current ? top : void 0;
124
- const from = this.#base ? bottom : over;
135
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
125
136
  const hold = {
126
137
  lease: this,
127
138
  record: this.#record,
@@ -151,15 +162,11 @@ var AttributeLease = class _AttributeLease {
151
162
  );
152
163
  });
153
164
  for (const [name, recorded] of records) {
154
- if (element.getAttribute(name) === recorded) continue;
155
- if (recorded === null) element.removeAttribute(name);
156
- else element.setAttribute(name, recorded);
165
+ setAttributeIfChanged(element, name, recorded);
157
166
  }
158
167
  }
159
168
  #reflect(element, value) {
160
- if (element.getAttribute(this.#attribute) === value) return;
161
- if (value === null) element.removeAttribute(this.#attribute);
162
- else element.setAttribute(this.#attribute, value);
169
+ setAttributeIfChanged(element, this.#attribute, value);
163
170
  }
164
171
  };
165
172
 
@@ -247,6 +254,11 @@ function firstTabStop(root) {
247
254
  }
248
255
  return null;
249
256
  }
257
+ function deepActiveElement(ownerDocument) {
258
+ let active = ownerDocument.activeElement;
259
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
260
+ return active;
261
+ }
250
262
 
251
263
  var ListenerSet = class {
252
264
  #abort = new AbortController();
@@ -275,6 +287,9 @@ var MicrotaskCoalescer = class {
275
287
  this.#queued = false;
276
288
  this.#generation += 1;
277
289
  }
290
+ get pending() {
291
+ return this.#queued;
292
+ }
278
293
  schedule() {
279
294
  if (!this.#active || this.#queued) return;
280
295
  this.#queued = true;
@@ -305,6 +320,9 @@ var MorphRenderWatcher = class {
305
320
  schedule() {
306
321
  this.#pass.schedule();
307
322
  }
323
+ get pending() {
324
+ return this.#pass.pending;
325
+ }
308
326
  disconnect() {
309
327
  this.#listeners.dispose();
310
328
  this.#pass.cancel();
@@ -372,6 +390,9 @@ function queryOpenDescendants(root, selector) {
372
390
  return matches;
373
391
  }
374
392
 
393
+ function holdsFocus(element) {
394
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
395
+ }
375
396
  var TabindexLoan = class _TabindexLoan {
376
397
  static #registry = sharedRegistry(
377
398
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -379,19 +400,22 @@ var TabindexLoan = class _TabindexLoan {
379
400
  );
380
401
  #value;
381
402
  #record;
403
+ #preserveFocus;
382
404
  #lent = /* @__PURE__ */ new Set();
383
- constructor(value, owner) {
405
+ constructor(value, owner, options = {}) {
384
406
  this.#value = value;
385
407
  this.#record = `data-${owner}-tabindex-loan`;
408
+ this.#preserveFocus = options.preserveFocus === true;
386
409
  _TabindexLoan.#registry.names.add(this.#record);
387
410
  }
388
- lend(element) {
411
+ lend(element, options = {}) {
389
412
  const stack = this.#stack(element);
390
413
  const current = element.getAttribute("tabindex");
391
414
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
392
415
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
393
416
  );
394
417
  const linked = stack.at(-1)?.value === current;
418
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
395
419
  if (!held && current !== null && !linked) return;
396
420
  this.#lent.add(element);
397
421
  if (held) held.loan = this;
@@ -401,19 +425,28 @@ var TabindexLoan = class _TabindexLoan {
401
425
  element.setAttribute("tabindex", this.#value);
402
426
  }
403
427
  }
404
- returnAll() {
405
- for (const element of this.#lent) {
406
- const stack = this.#stack(element);
407
- const hold = stack.find((held) => held.loan === this);
408
- if (hold) this.#release(element, stack, hold);
428
+ return(element) {
429
+ const stack = this.#stack(element);
430
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
431
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
432
+ );
433
+ if (!hold) return false;
434
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
435
+ hold.loan = this;
436
+ this.#lent.add(element);
437
+ return false;
409
438
  }
410
- this.#lent.clear();
439
+ this.#lent.delete(element);
440
+ return true;
441
+ }
442
+ returnAll() {
443
+ for (const element of Array.from(this.#lent)) this.return(element);
411
444
  }
412
445
  reclaim(element) {
413
446
  if (element.getAttribute(this.#record) !== this.#value) return;
414
447
  const stack = this.#stack(element);
415
448
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
416
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
449
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
417
450
  }
418
451
  reclaimWithin(root) {
419
452
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -437,26 +470,26 @@ var TabindexLoan = class _TabindexLoan {
437
470
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
438
471
  return holds;
439
472
  }
440
- #release(element, stack, hold) {
473
+ #release(element, stack, hold, preserveFocus = false) {
441
474
  const current = element.getAttribute("tabindex");
442
475
  const index = stack.indexOf(hold);
443
476
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
477
+ if (owns && preserveFocus && holdsFocus(element)) return false;
444
478
  stack.splice(index, 1);
445
479
  const above = stack[index];
446
480
  if (above) above.linked &&= hold.linked;
447
481
  this.#mark(element, stack);
448
- if (!owns) return;
482
+ if (!owns) return true;
449
483
  const below = hold.linked ? stack[index - 1] : void 0;
450
484
  if (!below) element.removeAttribute("tabindex");
451
485
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
486
+ return true;
452
487
  }
453
488
  #mark(element, stack) {
454
489
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
455
490
  for (const hold of stack) records.set(hold.record, hold.value);
456
491
  for (const [name, recorded] of records) {
457
- if (element.getAttribute(name) === recorded) continue;
458
- if (recorded === null) element.removeAttribute(name);
459
- else element.setAttribute(name, recorded);
492
+ setAttributeIfChanged(element, name, recorded);
460
493
  }
461
494
  }
462
495
  };
@@ -508,17 +541,18 @@ var NestedFormController = class _NestedFormController extends Controller {
508
541
  const row = this.#rowContaining(button);
509
542
  if (row) this.#removeRow(row);
510
543
  };
544
+ #listeners = new ListenerSet();
511
545
  connect() {
512
546
  this.#tabindex.reclaim(this.element);
513
547
  this.#warnedMissing = false;
514
548
  this.#warnedTemplate = false;
515
- this.element.addEventListener("click", this.#onClick);
549
+ this.#listeners.add(this.element, "click", this.#onClick);
516
550
  this.#reconcile.observe(this.element);
517
551
  if (!this.hasListTarget || !this.hasTemplateTarget) this.#warnMissing();
518
552
  this.#refresh();
519
553
  }
520
554
  disconnect() {
521
- this.element.removeEventListener("click", this.#onClick);
555
+ this.#listeners.dispose();
522
556
  this.#observer?.disconnect();
523
557
  this.#observer = null;
524
558
  this.#reconcile.disconnect();