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
@@ -4,6 +4,9 @@ function isReservedArrowChord(event, allow = []) {
4
4
  if (!event.key.startsWith("Arrow")) return false;
5
5
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
6
6
  }
7
+ function hasModifierChord(event) {
8
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
9
+ }
7
10
 
8
11
  function ownerIndex(candidates, node) {
9
12
  if (!(node instanceof Node)) return -1;
@@ -37,6 +40,9 @@ var MicrotaskCoalescer = class {
37
40
  this.#queued = false;
38
41
  this.#generation += 1;
39
42
  }
43
+ get pending() {
44
+ return this.#queued;
45
+ }
40
46
  schedule() {
41
47
  if (!this.#active || this.#queued) return;
42
48
  this.#queued = true;
@@ -67,6 +73,9 @@ var MorphRenderWatcher = class {
67
73
  schedule() {
68
74
  this.#pass.schedule();
69
75
  }
76
+ get pending() {
77
+ return this.#pass.pending;
78
+ }
70
79
  disconnect() {
71
80
  this.#listeners.dispose();
72
81
  this.#pass.cancel();
@@ -119,6 +128,18 @@ var NumberValueReader = class {
119
128
  }
120
129
  };
121
130
 
131
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
132
+ if (element.getAttribute(name) === value) return false;
133
+ if (value === null) element.removeAttribute(name);
134
+ else element.setAttribute(name, value);
135
+ return true;
136
+ }
137
+ function deepActiveElement(ownerDocument) {
138
+ let active = ownerDocument.activeElement;
139
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
140
+ return active;
141
+ }
142
+
122
143
  function queryOpenDescendants(root, selector) {
123
144
  const matches = [];
124
145
  const roots = [root];
@@ -145,6 +166,9 @@ function sharedRegistry(key, create) {
145
166
  return registry;
146
167
  }
147
168
 
169
+ function holdsFocus(element) {
170
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
171
+ }
148
172
  var TabindexLoan = class _TabindexLoan {
149
173
  static #registry = sharedRegistry(
150
174
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -152,19 +176,22 @@ var TabindexLoan = class _TabindexLoan {
152
176
  );
153
177
  #value;
154
178
  #record;
179
+ #preserveFocus;
155
180
  #lent = /* @__PURE__ */ new Set();
156
- constructor(value, owner) {
181
+ constructor(value, owner, options = {}) {
157
182
  this.#value = value;
158
183
  this.#record = `data-${owner}-tabindex-loan`;
184
+ this.#preserveFocus = options.preserveFocus === true;
159
185
  _TabindexLoan.#registry.names.add(this.#record);
160
186
  }
161
- lend(element) {
187
+ lend(element, options = {}) {
162
188
  const stack = this.#stack(element);
163
189
  const current = element.getAttribute("tabindex");
164
190
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
165
191
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
166
192
  );
167
193
  const linked = stack.at(-1)?.value === current;
194
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
168
195
  if (!held && current !== null && !linked) return;
169
196
  this.#lent.add(element);
170
197
  if (held) held.loan = this;
@@ -174,19 +201,28 @@ var TabindexLoan = class _TabindexLoan {
174
201
  element.setAttribute("tabindex", this.#value);
175
202
  }
176
203
  }
177
- returnAll() {
178
- for (const element of this.#lent) {
179
- const stack = this.#stack(element);
180
- const hold = stack.find((held) => held.loan === this);
181
- if (hold) this.#release(element, stack, hold);
204
+ return(element) {
205
+ const stack = this.#stack(element);
206
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
207
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
208
+ );
209
+ if (!hold) return false;
210
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
211
+ hold.loan = this;
212
+ this.#lent.add(element);
213
+ return false;
182
214
  }
183
- this.#lent.clear();
215
+ this.#lent.delete(element);
216
+ return true;
217
+ }
218
+ returnAll() {
219
+ for (const element of Array.from(this.#lent)) this.return(element);
184
220
  }
185
221
  reclaim(element) {
186
222
  if (element.getAttribute(this.#record) !== this.#value) return;
187
223
  const stack = this.#stack(element);
188
224
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
189
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
225
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
190
226
  }
191
227
  reclaimWithin(root) {
192
228
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -210,26 +246,26 @@ var TabindexLoan = class _TabindexLoan {
210
246
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
211
247
  return holds;
212
248
  }
213
- #release(element, stack, hold) {
249
+ #release(element, stack, hold, preserveFocus = false) {
214
250
  const current = element.getAttribute("tabindex");
215
251
  const index = stack.indexOf(hold);
216
252
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
253
+ if (owns && preserveFocus && holdsFocus(element)) return false;
217
254
  stack.splice(index, 1);
218
255
  const above = stack[index];
219
256
  if (above) above.linked &&= hold.linked;
220
257
  this.#mark(element, stack);
221
- if (!owns) return;
258
+ if (!owns) return true;
222
259
  const below = hold.linked ? stack[index - 1] : void 0;
223
260
  if (!below) element.removeAttribute("tabindex");
224
261
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
262
+ return true;
225
263
  }
226
264
  #mark(element, stack) {
227
265
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
228
266
  for (const hold of stack) records.set(hold.record, hold.value);
229
267
  for (const [name, recorded] of records) {
230
- if (element.getAttribute(name) === recorded) continue;
231
- if (recorded === null) element.removeAttribute(name);
232
- else element.setAttribute(name, recorded);
268
+ setAttributeIfChanged(element, name, recorded);
233
269
  }
234
270
  }
235
271
  };
@@ -260,22 +296,18 @@ var ResizableController = class _ResizableController extends Controller {
260
296
  #confirmed = DEFAULT_VALUE;
261
297
  #drag = null;
262
298
  #paneTabindex = new TabindexLoan("-1", this.identifier);
263
- #cycleAbort = null;
299
+ #listeners = new ListenerSet();
264
300
  #repaint = new MorphRenderWatcher(() => this.#render());
265
301
  connect() {
266
302
  this.#paneTabindex.reclaimWithin(this.element);
267
303
  this.#repaint.observe(this.element);
268
304
  this.#confirmed = this.#position;
269
305
  this.#render();
270
- this.#cycleAbort = new AbortController();
271
- this.element.addEventListener("keydown", this.#onCycleKeydown, {
272
- signal: this.#cycleAbort.signal
273
- });
306
+ this.#listeners.add(this.element, "keydown", this.#onCycleKeydown);
274
307
  }
275
308
  disconnect() {
276
309
  this.#endDrag();
277
- this.#cycleAbort?.abort();
278
- this.#cycleAbort = null;
310
+ this.#listeners.dispose();
279
311
  this.#repaint.disconnect();
280
312
  this.#paneTabindex.returnAll();
281
313
  }
@@ -283,7 +315,7 @@ var ResizableController = class _ResizableController extends Controller {
283
315
  if (event.key !== "F6") return;
284
316
  if (event.isComposing) return;
285
317
  if (event.defaultPrevented) return;
286
- if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
318
+ if (hasModifierChord(event)) return;
287
319
  const panes = [];
288
320
  if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
289
321
  if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
@@ -329,6 +361,7 @@ var ResizableController = class _ResizableController extends Controller {
329
361
  }
330
362
  onKeydown(event) {
331
363
  if (isReservedArrowChord(event)) return;
364
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
332
365
  if (!this.hasSeparatorTarget) return;
333
366
  const { min, max } = this.#range;
334
367
  const isVertical = this.#isVertical;
@@ -54,6 +54,9 @@ var MicrotaskCoalescer = class {
54
54
  this.#queued = false;
55
55
  this.#generation += 1;
56
56
  }
57
+ get pending() {
58
+ return this.#queued;
59
+ }
57
60
  schedule() {
58
61
  if (!this.#active || this.#queued) return;
59
62
  this.#queued = true;
@@ -84,6 +87,9 @@ var MorphRenderWatcher = class {
84
87
  schedule() {
85
88
  this.#pass.schedule();
86
89
  }
90
+ get pending() {
91
+ return this.#pass.pending;
92
+ }
87
93
  disconnect() {
88
94
  this.#listeners.dispose();
89
95
  this.#pass.cancel();
@@ -128,10 +134,11 @@ var RovingController = class extends Controller {
128
134
  #reconcile = new MorphRenderWatcher(() => this.#ensureTabStop(true));
129
135
  #connected = false;
130
136
  #observer = null;
137
+ #listeners = new ListenerSet();
131
138
  connect() {
132
139
  this.#ensureTabStop(false);
133
- this.element.addEventListener("keydown", this.#onKeydown);
134
- this.element.addEventListener("focusin", this.#onFocusin);
140
+ this.#listeners.add(this.element, "keydown", this.#onKeydown);
141
+ this.#listeners.add(this.element, "focusin", this.#onFocusin);
135
142
  this.#watchState();
136
143
  this.#connected = true;
137
144
  this.#reconcile.observe(this.element);
@@ -139,8 +146,7 @@ var RovingController = class extends Controller {
139
146
  disconnect() {
140
147
  this.#connected = false;
141
148
  this.#reconcile.disconnect();
142
- this.element.removeEventListener("keydown", this.#onKeydown);
143
- this.element.removeEventListener("focusin", this.#onFocusin);
149
+ this.#listeners.dispose();
144
150
  this.#observer?.disconnect();
145
151
  this.#observer = null;
146
152
  }
@@ -1,15 +1,26 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
3
10
  function sharedRegistry(key, create) {
4
11
  const symbol = Symbol.for(key);
5
12
  const scope = globalThis;
6
13
  const existing = scope[symbol];
7
14
  if (existing !== void 0) return existing;
8
- const registry = create();
9
- Object.defineProperty(globalThis, symbol, { value: registry });
10
- return registry;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
11
18
  }
12
19
 
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
13
24
  function isAttributeValue(value) {
14
25
  return value === null || typeof value === "string";
15
26
  }
@@ -26,24 +37,46 @@ function readLeaseRecord(element, record) {
26
37
  return void 0;
27
38
  }
28
39
  }
29
- var AttributeLease = class _AttributeLease {
30
- static #registry = sharedRegistry(
31
- "stimeo-ui.attribute-lease.registry.v1",
32
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
- );
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
34
65
  #attribute;
35
66
  #record;
36
67
  #base;
68
+ #preserveAuthor;
37
69
  #held = /* @__PURE__ */ new Set();
38
70
  constructor(attribute, owner, options = {}) {
39
71
  this.#attribute = attribute;
40
72
  this.#record = `data-${owner}-${attribute}-lease`;
41
73
  this.#base = options.base === true;
42
- const names = _AttributeLease.#registry.names;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
43
76
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
77
  }
45
78
  write(element, value) {
46
- const stack = this.#stack(element);
79
+ const stack = attributeStack(element, this.#attribute);
47
80
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
81
  this.#held.add(element);
49
82
  const shows = !this.#covered(element, stack, hold);
@@ -53,8 +86,8 @@ var AttributeLease = class _AttributeLease {
53
86
  }
54
87
  return(element) {
55
88
  const held = this.#held.delete(element);
56
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
58
91
  const hold = this.#take(stack);
59
92
  if (!hold) return false;
60
93
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -72,31 +105,8 @@ var AttributeLease = class _AttributeLease {
72
105
  returnAll() {
73
106
  for (const element of Array.from(this.#held)) this.return(element);
74
107
  }
75
- #stack(element) {
76
- const stacks = _AttributeLease.#registry.stacks;
77
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
- stacks.set(element, byAttribute);
79
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
- byAttribute.set(this.#attribute, stack);
81
- return stack;
82
- }
83
- #seed(element) {
84
- const found = [];
85
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
- const read = readLeaseRecord(element, record);
87
- if (read) found.push({ ...read, record });
88
- }
89
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
- lease: null,
92
- record,
93
- written,
94
- original,
95
- linked: linked && index > 0
96
- }));
97
- }
98
108
  #take(stack) {
99
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
109
+ const hold = findHold(stack, this, this.#record);
100
110
  if (hold) hold.lease = this;
101
111
  return hold;
102
112
  }
@@ -105,7 +115,7 @@ var AttributeLease = class _AttributeLease {
105
115
  const top = stack.at(-1);
106
116
  const bottom = stack[0];
107
117
  const over = top?.written === current ? top : void 0;
108
- const from = this.#base ? bottom : over;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
109
119
  const hold = {
110
120
  lease: this,
111
121
  record: this.#record,
@@ -135,15 +145,11 @@ var AttributeLease = class _AttributeLease {
135
145
  );
136
146
  });
137
147
  for (const [name, recorded] of records) {
138
- if (element.getAttribute(name) === recorded) continue;
139
- if (recorded === null) element.removeAttribute(name);
140
- else element.setAttribute(name, recorded);
148
+ setAttributeIfChanged(element, name, recorded);
141
149
  }
142
150
  }
143
151
  #reflect(element, value) {
144
- if (element.getAttribute(this.#attribute) === value) return;
145
- if (value === null) element.removeAttribute(this.#attribute);
146
- else element.setAttribute(this.#attribute, value);
152
+ setAttributeIfChanged(element, this.#attribute, value);
147
153
  }
148
154
  };
149
155
 
@@ -234,6 +240,44 @@ function firstTabStop(root) {
234
240
  function hasTabStop(root) {
235
241
  return firstTabStop(root) !== null;
236
242
  }
243
+ function deepActiveElement(ownerDocument) {
244
+ let active = ownerDocument.activeElement;
245
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
246
+ return active;
247
+ }
248
+
249
+ var ListenerSet = class {
250
+ #abort = new AbortController();
251
+ add(target, type, handler, options) {
252
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
253
+ }
254
+ dispose() {
255
+ this.#abort.abort();
256
+ this.#abort = new AbortController();
257
+ }
258
+ };
259
+
260
+ var FontLoadWatcher = class {
261
+ #listeners = new ListenerSet();
262
+ #onSettled;
263
+ #fonts = null;
264
+ constructor(run) {
265
+ this.#onSettled = () => run();
266
+ }
267
+ observe(element) {
268
+ const fonts = element.ownerDocument.fonts ?? null;
269
+ if (fonts === this.#fonts) return;
270
+ this.disconnect();
271
+ if (!fonts) return;
272
+ this.#fonts = fonts;
273
+ this.#listeners.add(fonts, "loadingdone", this.#onSettled);
274
+ this.#listeners.add(fonts, "loadingerror", this.#onSettled);
275
+ }
276
+ disconnect() {
277
+ this.#listeners.dispose();
278
+ this.#fonts = null;
279
+ }
280
+ };
237
281
 
238
282
  var FrameCoalescer = class {
239
283
  #frame = null;
@@ -319,17 +363,6 @@ function logicalScrollMetrics(element, horizontal) {
319
363
  return { position: Math.min(max, Math.max(0, position)), max };
320
364
  }
321
365
 
322
- var ListenerSet = class {
323
- #abort = new AbortController();
324
- add(target, type, handler, options) {
325
- target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
326
- }
327
- dispose() {
328
- this.#abort.abort();
329
- this.#abort = new AbortController();
330
- }
331
- };
332
-
333
366
  var MicrotaskCoalescer = class {
334
367
  #run;
335
368
  #queued = false;
@@ -346,6 +379,9 @@ var MicrotaskCoalescer = class {
346
379
  this.#queued = false;
347
380
  this.#generation += 1;
348
381
  }
382
+ get pending() {
383
+ return this.#queued;
384
+ }
349
385
  schedule() {
350
386
  if (!this.#active || this.#queued) return;
351
387
  this.#queued = true;
@@ -376,6 +412,9 @@ var MorphRenderWatcher = class {
376
412
  schedule() {
377
413
  this.#pass.schedule();
378
414
  }
415
+ get pending() {
416
+ return this.#pass.pending;
417
+ }
379
418
  disconnect() {
380
419
  this.#listeners.dispose();
381
420
  this.#pass.cancel();
@@ -512,9 +551,7 @@ var StylePropertyLease = class _StylePropertyLease {
512
551
  );
513
552
  });
514
553
  for (const [name, recorded] of records) {
515
- if (element.getAttribute(name) === recorded) continue;
516
- if (recorded === null) element.removeAttribute(name);
517
- else element.setAttribute(name, recorded);
554
+ setAttributeIfChanged(element, name, recorded);
518
555
  }
519
556
  }
520
557
  #carries(element, hold) {
@@ -547,6 +584,9 @@ function queryOpenDescendants(root, selector) {
547
584
  return matches;
548
585
  }
549
586
 
587
+ function holdsFocus(element) {
588
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
589
+ }
550
590
  var TabindexLoan = class _TabindexLoan {
551
591
  static #registry = sharedRegistry(
552
592
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -554,19 +594,22 @@ var TabindexLoan = class _TabindexLoan {
554
594
  );
555
595
  #value;
556
596
  #record;
597
+ #preserveFocus;
557
598
  #lent = /* @__PURE__ */ new Set();
558
- constructor(value, owner) {
599
+ constructor(value, owner, options = {}) {
559
600
  this.#value = value;
560
601
  this.#record = `data-${owner}-tabindex-loan`;
602
+ this.#preserveFocus = options.preserveFocus === true;
561
603
  _TabindexLoan.#registry.names.add(this.#record);
562
604
  }
563
- lend(element) {
605
+ lend(element, options = {}) {
564
606
  const stack = this.#stack(element);
565
607
  const current = element.getAttribute("tabindex");
566
608
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
567
609
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
568
610
  );
569
611
  const linked = stack.at(-1)?.value === current;
612
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
570
613
  if (!held && current !== null && !linked) return;
571
614
  this.#lent.add(element);
572
615
  if (held) held.loan = this;
@@ -576,19 +619,28 @@ var TabindexLoan = class _TabindexLoan {
576
619
  element.setAttribute("tabindex", this.#value);
577
620
  }
578
621
  }
579
- returnAll() {
580
- for (const element of this.#lent) {
581
- const stack = this.#stack(element);
582
- const hold = stack.find((held) => held.loan === this);
583
- if (hold) this.#release(element, stack, hold);
622
+ return(element) {
623
+ const stack = this.#stack(element);
624
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
625
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
626
+ );
627
+ if (!hold) return false;
628
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
629
+ hold.loan = this;
630
+ this.#lent.add(element);
631
+ return false;
584
632
  }
585
- this.#lent.clear();
633
+ this.#lent.delete(element);
634
+ return true;
635
+ }
636
+ returnAll() {
637
+ for (const element of Array.from(this.#lent)) this.return(element);
586
638
  }
587
639
  reclaim(element) {
588
640
  if (element.getAttribute(this.#record) !== this.#value) return;
589
641
  const stack = this.#stack(element);
590
642
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
591
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
643
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
592
644
  }
593
645
  reclaimWithin(root) {
594
646
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -612,26 +664,26 @@ var TabindexLoan = class _TabindexLoan {
612
664
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
613
665
  return holds;
614
666
  }
615
- #release(element, stack, hold) {
667
+ #release(element, stack, hold, preserveFocus = false) {
616
668
  const current = element.getAttribute("tabindex");
617
669
  const index = stack.indexOf(hold);
618
670
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
671
+ if (owns && preserveFocus && holdsFocus(element)) return false;
619
672
  stack.splice(index, 1);
620
673
  const above = stack[index];
621
674
  if (above) above.linked &&= hold.linked;
622
675
  this.#mark(element, stack);
623
- if (!owns) return;
676
+ if (!owns) return true;
624
677
  const below = hold.linked ? stack[index - 1] : void 0;
625
678
  if (!below) element.removeAttribute("tabindex");
626
679
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
680
+ return true;
627
681
  }
628
682
  #mark(element, stack) {
629
683
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
630
684
  for (const hold of stack) records.set(hold.record, hold.value);
631
685
  for (const [name, recorded] of records) {
632
- if (element.getAttribute(name) === recorded) continue;
633
- if (recorded === null) element.removeAttribute(name);
634
- else element.setAttribute(name, recorded);
686
+ setAttributeIfChanged(element, name, recorded);
635
687
  }
636
688
  }
637
689
  };
@@ -654,12 +706,12 @@ var ScrollAreaController = class extends Controller {
654
706
  this.identifier
655
707
  );
656
708
  #viewport = null;
709
+ #viewportListeners = new ListenerSet();
657
710
  #content = null;
658
711
  #nameSources = null;
659
712
  #nameGraph = null;
660
713
  #observedNameIds = /* @__PURE__ */ new Set();
661
714
  #observedNameSources = [];
662
- #fonts = null;
663
715
  #frames = new FrameCoalescer();
664
716
  #overflowing = false;
665
717
  #lastEdge = null;
@@ -671,17 +723,18 @@ var ScrollAreaController = class extends Controller {
671
723
  #onFontsSettled = () => {
672
724
  this.#refresh();
673
725
  };
726
+ #fonts = new FontLoadWatcher(this.#onFontsSettled);
674
727
  connect() {
675
728
  this.#rebind.observe(this.element);
676
729
  this.#layout.observeViewport();
677
- this.#bindFonts();
730
+ this.#fonts.observe(this.element);
678
731
  this.#syncViewport();
679
732
  }
680
733
  disconnect() {
681
734
  this.#rebind.disconnect();
682
735
  if (this.#viewport) this.#unbindViewport(this.#viewport);
683
736
  this.#layout.disconnect();
684
- this.#unbindFonts();
737
+ this.#fonts.disconnect();
685
738
  this.#clearHostState();
686
739
  }
687
740
  orientationValueChanged() {
@@ -705,7 +758,7 @@ var ScrollAreaController = class extends Controller {
705
758
  this.#clearHostState();
706
759
  return;
707
760
  }
708
- next.addEventListener("scroll", this.#onScroll, { passive: true });
761
+ this.#viewportListeners.add(next, "scroll", this.#onScroll, { passive: true });
709
762
  this.#layout.observeDescendantLoads(next);
710
763
  this.#layout.observe(next);
711
764
  this.#bindContentObserver(next);
@@ -739,7 +792,7 @@ var ScrollAreaController = class extends Controller {
739
792
  }
740
793
  #unbindViewport(viewport) {
741
794
  this.#frames.cancel();
742
- viewport.removeEventListener("scroll", this.#onScroll);
795
+ this.#viewportListeners.dispose();
743
796
  this.#layout.unobserveDescendantLoads();
744
797
  this.#layout.unobserve(viewport);
745
798
  this.#content?.disconnect();
@@ -935,18 +988,6 @@ var ScrollAreaController = class extends Controller {
935
988
  this.#observedNameIds.clear();
936
989
  this.#observedNameSources = [];
937
990
  }
938
- #bindFonts() {
939
- const fonts = this.element.ownerDocument.fonts;
940
- if (!fonts || this.#fonts === fonts) return;
941
- this.#fonts = fonts;
942
- fonts.addEventListener("loadingdone", this.#onFontsSettled);
943
- fonts.addEventListener("loadingerror", this.#onFontsSettled);
944
- }
945
- #unbindFonts() {
946
- this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
947
- this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
948
- this.#fonts = null;
949
- }
950
991
  };
951
992
 
952
993
  export { ScrollAreaController };
@@ -16,6 +16,17 @@ var FrameCoalescer = class {
16
16
  }
17
17
  };
18
18
 
19
+ var ListenerSet = class {
20
+ #abort = new AbortController();
21
+ add(target, type, handler, options) {
22
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
23
+ }
24
+ dispose() {
25
+ this.#abort.abort();
26
+ this.#abort = new AbortController();
27
+ }
28
+ };
29
+
19
30
  function parseStored(raw) {
20
31
  if (raw === null) return null;
21
32
  try {
@@ -46,15 +57,16 @@ var ScrollRestoreController = class extends Controller {
46
57
  this.#capture();
47
58
  this.#frames.schedule(() => this.#persist());
48
59
  };
60
+ #listeners = new ListenerSet();
49
61
  connect() {
50
62
  this.#connected = true;
51
63
  this.#storageKey = this.#resolveKey();
52
- this.element.addEventListener("scroll", this.#onScroll, { passive: true });
64
+ this.#listeners.add(this.element, "scroll", this.#onScroll, { passive: true });
53
65
  if (this.#storageKey) this.#restore();
54
66
  }
55
67
  disconnect() {
56
68
  this.#connected = false;
57
- this.element.removeEventListener("scroll", this.#onScroll);
69
+ this.#listeners.dispose();
58
70
  this.#frames.cancel();
59
71
  this.#persist();
60
72
  }