stimeo-ui 0.17.0 → 0.18.1

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 +136 -2
  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 +290 -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 +66 -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 +200 -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 +434 -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 +272 -95
  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 +255 -24
  67. data/dist/controllers/popover_controller.js +91 -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 +2515 -1315
  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
@@ -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,12 +16,6 @@ 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
- });
17
- }
18
19
 
19
20
  var ListenerSet = class {
20
21
  #abort = new AbortController();
@@ -43,6 +44,9 @@ var MicrotaskCoalescer = class {
43
44
  this.#queued = false;
44
45
  this.#generation += 1;
45
46
  }
47
+ get pending() {
48
+ return this.#queued;
49
+ }
46
50
  schedule() {
47
51
  if (!this.#active || this.#queued) return;
48
52
  this.#queued = true;
@@ -73,6 +77,9 @@ var MorphRenderWatcher = class {
73
77
  schedule() {
74
78
  this.#pass.schedule();
75
79
  }
80
+ get pending() {
81
+ return this.#pass.pending;
82
+ }
76
83
  disconnect() {
77
84
  this.#listeners.dispose();
78
85
  this.#pass.cancel();
@@ -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
 
@@ -27,6 +27,9 @@ var MicrotaskCoalescer = class {
27
27
  this.#queued = false;
28
28
  this.#generation += 1;
29
29
  }
30
+ get pending() {
31
+ return this.#queued;
32
+ }
30
33
  schedule() {
31
34
  if (!this.#active || this.#queued) return;
32
35
  this.#queued = true;
@@ -57,6 +60,9 @@ var MorphRenderWatcher = class {
57
60
  schedule() {
58
61
  this.#pass.schedule();
59
62
  }
63
+ get pending() {
64
+ return this.#pass.pending;
65
+ }
60
66
  disconnect() {
61
67
  this.#listeners.dispose();
62
68
  this.#pass.cancel();
@@ -75,17 +81,18 @@ var FilterController = class extends Controller {
75
81
  #reported = null;
76
82
  #connected = false;
77
83
  #morphRender = new MorphRenderWatcher(() => this.#evaluate());
84
+ #listeners = new ListenerSet();
78
85
  connect() {
79
86
  this.#morphRender.observe(this.element);
80
87
  this.#reported = null;
81
88
  this.#evaluate();
82
- this.element.addEventListener("change", this.#onChange);
89
+ this.#listeners.add(this.element, "change", this.#onChange);
83
90
  this.#connected = true;
84
91
  }
85
92
  disconnect() {
86
93
  this.#morphRender.disconnect();
87
94
  this.#connected = false;
88
- this.element.removeEventListener("change", this.#onChange);
95
+ this.#listeners.dispose();
89
96
  }
90
97
  matchValueChanged() {
91
98
  if (!this.#connected) return;
@@ -40,6 +40,59 @@ function ownerOf(candidates, node) {
40
40
  return candidates[ownerIndex(candidates, node)] ?? null;
41
41
  }
42
42
 
43
+ var ListenerSet = class {
44
+ #abort = new AbortController();
45
+ add(target, type, handler, options) {
46
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
47
+ }
48
+ dispose() {
49
+ this.#abort.abort();
50
+ this.#abort = new AbortController();
51
+ }
52
+ };
53
+
54
+ var HoverRecheck = class {
55
+ #document;
56
+ #release;
57
+ #listeners = new ListenerSet();
58
+ #pending = /* @__PURE__ */ new Set();
59
+ #tickets = /* @__PURE__ */ new WeakMap();
60
+ constructor(document2, release) {
61
+ this.#document = document2;
62
+ this.#release = release;
63
+ }
64
+ wait(element) {
65
+ this.#pending.add(element);
66
+ this.#tickets.set(element, /* @__PURE__ */ Symbol());
67
+ this.#listeners.add(this.#document, "pointermove", this.#onPointerMove, {
68
+ capture: true,
69
+ passive: true
70
+ });
71
+ }
72
+ cancel(element) {
73
+ this.#tickets.delete(element);
74
+ this.#pending.delete(element);
75
+ if (this.#pending.size === 0) this.#listeners.dispose();
76
+ }
77
+ dispose() {
78
+ this.#listeners.dispose();
79
+ this.#pending.clear();
80
+ this.#tickets = /* @__PURE__ */ new WeakMap();
81
+ }
82
+ #onPointerMove = () => {
83
+ this.#listeners.dispose();
84
+ const waiting = [...this.#pending].map(
85
+ (element) => [element, this.#tickets.get(element)]
86
+ );
87
+ this.#pending.clear();
88
+ for (const [element, ticket] of waiting) {
89
+ if (this.#tickets.get(element) !== ticket) continue;
90
+ if (element.matches(":hover")) continue;
91
+ this.#release(element);
92
+ }
93
+ };
94
+ };
95
+
43
96
  var MAX_TIMER_DELAY_MS = 2147483647;
44
97
 
45
98
  var TimerRegistry = class {
@@ -100,17 +153,6 @@ var KeyedTimers = class {
100
153
  }
101
154
  };
102
155
 
103
- var ListenerSet = class {
104
- #abort = new AbortController();
105
- add(target, type, handler, options) {
106
- target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
107
- }
108
- dispose() {
109
- this.#abort.abort();
110
- this.#abort = new AbortController();
111
- }
112
- };
113
-
114
156
  var MicrotaskCoalescer = class {
115
157
  #run;
116
158
  #queued = false;
@@ -127,6 +169,9 @@ var MicrotaskCoalescer = class {
127
169
  this.#queued = false;
128
170
  this.#generation += 1;
129
171
  }
172
+ get pending() {
173
+ return this.#queued;
174
+ }
130
175
  schedule() {
131
176
  if (!this.#active || this.#queued) return;
132
177
  this.#queued = true;
@@ -157,6 +202,9 @@ var MorphRenderWatcher = class {
157
202
  schedule() {
158
203
  this.#pass.schedule();
159
204
  }
205
+ get pending() {
206
+ return this.#pass.pending;
207
+ }
160
208
  disconnect() {
161
209
  this.#listeners.dispose();
162
210
  this.#pass.cancel();
@@ -332,8 +380,10 @@ var FlashController = class _FlashController extends Controller {
332
380
  #fixedDismiss = new KeyedTimers();
333
381
  #reapply = new MorphRenderWatcher(() => this.#reapplyCap());
334
382
  #spared = [];
335
- #pointer = new ListenerSet();
336
- #awaitingPointer = /* @__PURE__ */ new Set();
383
+ #pointer = new HoverRecheck(
384
+ this.element.ownerDocument,
385
+ (message) => this.#resume(message, "hover", null)
386
+ );
337
387
  #observer = null;
338
388
  #connected = false;
339
389
  #arrivals = /* @__PURE__ */ new Set();
@@ -376,7 +426,6 @@ var FlashController = class _FlashController extends Controller {
376
426
  this.#reapply.disconnect();
377
427
  this.#spared = [];
378
428
  this.#pointer.dispose();
379
- this.#awaitingPointer.clear();
380
429
  for (const message of this.#order) this.#unbindPause(message);
381
430
  this.#order.length = 0;
382
431
  this.#leaving.clear();
@@ -534,29 +583,14 @@ var FlashController = class _FlashController extends Controller {
534
583
  }
535
584
  #afterMove(message) {
536
585
  if (!message.contains(document.activeElement)) this.#resume(message, "focus", null);
537
- if (this.#dismiss.isHeld(message)) this.#awaitPointer(message);
586
+ if (this.#dismiss.isHeld(message)) this.#pointer.wait(message);
538
587
  }
539
588
  #readHolds(message) {
540
589
  if (message.contains(document.activeElement)) this.#pause(message, "focus");
541
590
  if (!message.matches(":hover")) return;
542
591
  this.#pause(message, "hover");
543
- this.#awaitPointer(message);
544
- }
545
- #awaitPointer(message) {
546
- this.#awaitingPointer.add(message);
547
- this.#pointer.add(document, "pointermove", this.#onPointerMove, {
548
- capture: true,
549
- passive: true
550
- });
592
+ this.#pointer.wait(message);
551
593
  }
552
- #onPointerMove = () => {
553
- this.#pointer.dispose();
554
- const waiting = [...this.#awaitingPointer];
555
- this.#awaitingPointer.clear();
556
- for (const message of waiting) {
557
- if (!message.matches(":hover")) this.#resume(message, "hover", null);
558
- }
559
- };
560
594
  #forget(message) {
561
595
  if (this.#dismiss.isHeld(message)) this.#capLater(null);
562
596
  this.#dismiss.clear(message);
@@ -564,8 +598,7 @@ var FlashController = class _FlashController extends Controller {
564
598
  const index = this.#order.indexOf(message);
565
599
  if (index !== -1) this.#order.splice(index, 1);
566
600
  this.#unbindPause(message);
567
- this.#awaitingPointer.delete(message);
568
- if (this.#awaitingPointer.size === 0) this.#pointer.dispose();
601
+ this.#pointer.cancel(message);
569
602
  }
570
603
  #beginDismiss(message, reason) {
571
604
  if (!this.#order.includes(message)) return;
@@ -330,6 +330,9 @@ var MicrotaskCoalescer = class {
330
330
  this.#queued = false;
331
331
  this.#generation += 1;
332
332
  }
333
+ get pending() {
334
+ return this.#queued;
335
+ }
333
336
  schedule() {
334
337
  if (!this.#active || this.#queued) return;
335
338
  this.#queued = true;
@@ -358,6 +361,16 @@ function queryOpenDescendants(root, selector) {
358
361
  return matches;
359
362
  }
360
363
 
364
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
365
+ if (element.getAttribute(name) === value) return false;
366
+ if (value === null) element.removeAttribute(name);
367
+ else element.setAttribute(name, value);
368
+ return true;
369
+ }
370
+
371
+ function holdsFocus(element) {
372
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
373
+ }
361
374
  var TabindexLoan = class _TabindexLoan {
362
375
  static #registry = sharedRegistry(
363
376
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -365,19 +378,22 @@ var TabindexLoan = class _TabindexLoan {
365
378
  );
366
379
  #value;
367
380
  #record;
381
+ #preserveFocus;
368
382
  #lent = /* @__PURE__ */ new Set();
369
- constructor(value, owner) {
383
+ constructor(value, owner, options = {}) {
370
384
  this.#value = value;
371
385
  this.#record = `data-${owner}-tabindex-loan`;
386
+ this.#preserveFocus = options.preserveFocus === true;
372
387
  _TabindexLoan.#registry.names.add(this.#record);
373
388
  }
374
- lend(element) {
389
+ lend(element, options = {}) {
375
390
  const stack = this.#stack(element);
376
391
  const current = element.getAttribute("tabindex");
377
392
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
378
393
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
379
394
  );
380
395
  const linked = stack.at(-1)?.value === current;
396
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
381
397
  if (!held && current !== null && !linked) return;
382
398
  this.#lent.add(element);
383
399
  if (held) held.loan = this;
@@ -387,19 +403,28 @@ var TabindexLoan = class _TabindexLoan {
387
403
  element.setAttribute("tabindex", this.#value);
388
404
  }
389
405
  }
390
- returnAll() {
391
- for (const element of this.#lent) {
392
- const stack = this.#stack(element);
393
- const hold = stack.find((held) => held.loan === this);
394
- if (hold) this.#release(element, stack, hold);
406
+ return(element) {
407
+ const stack = this.#stack(element);
408
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
409
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
410
+ );
411
+ if (!hold) return false;
412
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
413
+ hold.loan = this;
414
+ this.#lent.add(element);
415
+ return false;
395
416
  }
396
- this.#lent.clear();
417
+ this.#lent.delete(element);
418
+ return true;
419
+ }
420
+ returnAll() {
421
+ for (const element of Array.from(this.#lent)) this.return(element);
397
422
  }
398
423
  reclaim(element) {
399
424
  if (element.getAttribute(this.#record) !== this.#value) return;
400
425
  const stack = this.#stack(element);
401
426
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
402
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
427
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
403
428
  }
404
429
  reclaimWithin(root) {
405
430
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -423,26 +448,26 @@ var TabindexLoan = class _TabindexLoan {
423
448
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
424
449
  return holds;
425
450
  }
426
- #release(element, stack, hold) {
451
+ #release(element, stack, hold, preserveFocus = false) {
427
452
  const current = element.getAttribute("tabindex");
428
453
  const index = stack.indexOf(hold);
429
454
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
455
+ if (owns && preserveFocus && holdsFocus(element)) return false;
430
456
  stack.splice(index, 1);
431
457
  const above = stack[index];
432
458
  if (above) above.linked &&= hold.linked;
433
459
  this.#mark(element, stack);
434
- if (!owns) return;
460
+ if (!owns) return true;
435
461
  const below = hold.linked ? stack[index - 1] : void 0;
436
462
  if (!below) element.removeAttribute("tabindex");
437
463
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
464
+ return true;
438
465
  }
439
466
  #mark(element, stack) {
440
467
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
441
468
  for (const hold of stack) records.set(hold.record, hold.value);
442
469
  for (const [name, recorded] of records) {
443
- if (element.getAttribute(name) === recorded) continue;
444
- if (recorded === null) element.removeAttribute(name);
445
- else element.setAttribute(name, recorded);
470
+ setAttributeIfChanged(element, name, recorded);
446
471
  }
447
472
  }
448
473
  };