stimeo-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,8 +1,27 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/pagination_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/focus_candidate.ts
6
25
  function inheritsFieldsetDisabled(control) {
7
26
  let fieldset = control.closest("fieldset[disabled]");
8
27
  while (fieldset) {
@@ -12,35 +31,52 @@ function inheritsFieldsetDisabled(control) {
12
31
  }
13
32
  return false;
14
33
  }
34
+ function unslotted(element) {
35
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
36
+ }
37
+ function flatTreeParent(element) {
38
+ if (unslotted(element)) return null;
39
+ const parent = element.assignedSlot ?? element.parentNode;
40
+ if (parent instanceof ShadowRoot) return parent.host;
41
+ return parent instanceof Element ? parent : null;
42
+ }
15
43
  function canTakeFocus(element) {
16
- if (element.closest("[hidden], [inert]")) return false;
44
+ for (let node = element; node; node = flatTreeParent(node)) {
45
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
46
+ }
17
47
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
18
48
  if (!("disabled" in element)) return true;
19
49
  if (element.disabled) return false;
20
50
  return !inheritsFieldsetDisabled(element);
21
51
  }
22
52
 
23
- // src/utils/microtask_coalescer.ts
53
+ var ListenerSet = class {
54
+ #abort = new AbortController();
55
+ add(target, type, handler, options) {
56
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
57
+ }
58
+ dispose() {
59
+ this.#abort.abort();
60
+ this.#abort = new AbortController();
61
+ }
62
+ };
63
+
24
64
  var MicrotaskCoalescer = class {
25
65
  #run;
26
66
  #queued = false;
27
67
  #active = false;
28
68
  #generation = 0;
29
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
30
69
  constructor(run) {
31
70
  this.#run = run;
32
71
  }
33
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
34
72
  activate() {
35
73
  this.#active = true;
36
74
  }
37
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
38
75
  cancel() {
39
76
  this.#active = false;
40
77
  this.#queued = false;
41
78
  this.#generation += 1;
42
79
  }
43
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
44
80
  schedule() {
45
81
  if (!this.#active || this.#queued) return;
46
82
  this.#queued = true;
@@ -53,171 +89,304 @@ var MicrotaskCoalescer = class {
53
89
  }
54
90
  };
55
91
 
56
- // src/utils/before_cache_reset.ts
57
- var BeforeCacheReset = class _BeforeCacheReset {
58
- /** Every subscribed instance, iterated by the one shared document listener. */
59
- static #subscribers = /* @__PURE__ */ new Set();
60
- /** The shared listener; installed while at least one instance is subscribed. */
61
- static #onBeforeCache = () => {
62
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
63
- };
64
- #rewind;
65
- /** @param rewind - the pass that returns this controller's state to its initial form. */
66
- constructor(rewind) {
67
- this.#rewind = rewind;
92
+ var MorphRenderWatcher = class {
93
+ #listeners = new ListenerSet();
94
+ #pass;
95
+ #element = null;
96
+ #onMorph = () => this.#pass.schedule();
97
+ constructor(run) {
98
+ this.#pass = new MicrotaskCoalescer(run);
68
99
  }
69
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
70
- activate() {
71
- const first = _BeforeCacheReset.#subscribers.size === 0;
72
- _BeforeCacheReset.#subscribers.add(this);
73
- if (first) {
74
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
75
- }
100
+ observe(element) {
101
+ if (this.#element === element) return;
102
+ this.disconnect();
103
+ this.#element = element;
104
+ this.#pass.activate();
105
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
76
106
  }
77
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
78
- deactivate() {
79
- _BeforeCacheReset.#subscribers.delete(this);
80
- if (_BeforeCacheReset.#subscribers.size > 0) return;
81
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
107
+ schedule() {
108
+ this.#pass.schedule();
109
+ }
110
+ disconnect() {
111
+ this.#listeners.dispose();
112
+ this.#pass.cancel();
113
+ this.#element = null;
114
+ }
115
+ };
116
+
117
+ var MoveCounter = class {
118
+ #sequence = 0;
119
+ record() {
120
+ this.#sequence += 1;
121
+ return this.#sequence;
122
+ }
123
+ isLatest(token) {
124
+ return token === this.#sequence;
82
125
  }
83
126
  };
84
127
 
85
- // src/utils/tabindex_loan.ts
86
- var TabindexLoan = class {
128
+ var NUMBER_BOUNDS = {
129
+ finite: { finite: true }};
130
+ function matchesNumberBounds(value, bounds) {
131
+ if (!Number.isFinite(value)) {
132
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
133
+ if (direction === null) return false;
134
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
135
+ }
136
+ if (bounds.min !== void 0 && value < bounds.min) return false;
137
+ if (bounds.max !== void 0 && value > bounds.max) return false;
138
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
139
+ if (bounds.integer && !Number.isInteger(value)) return false;
140
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
141
+ return true;
142
+ }
143
+
144
+ function readNumber(raw, fallback, bounds) {
145
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
146
+ }
147
+
148
+ var NumberValueReader = class {
149
+ #lastRejected = /* @__PURE__ */ new Map();
150
+ read(owner, name, raw, fallback, bounds) {
151
+ const resolved = readNumber(raw, fallback, bounds);
152
+ if (matchesNumberBounds(raw, bounds)) {
153
+ this.#lastRejected.delete(name);
154
+ return resolved;
155
+ }
156
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
157
+ const literal = owner.element.getAttribute(attribute);
158
+ if (literal === null) {
159
+ this.#lastRejected.delete(name);
160
+ return resolved;
161
+ }
162
+ if (this.#lastRejected.get(name) !== literal) {
163
+ this.#lastRejected.set(name, literal);
164
+ console.warn(
165
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
166
+ );
167
+ }
168
+ return resolved;
169
+ }
170
+ };
171
+
172
+ function queryOpenDescendants(root, selector) {
173
+ const matches = [];
174
+ const roots = [root];
175
+ for (const scope of roots) {
176
+ matches.push(...scope.querySelectorAll(selector));
177
+ if (scope.nodeType === Node.ELEMENT_NODE) {
178
+ const shadow = scope.shadowRoot;
179
+ if (shadow) roots.push(shadow);
180
+ }
181
+ for (const element of scope.querySelectorAll("*")) {
182
+ if (element.shadowRoot) roots.push(element.shadowRoot);
183
+ }
184
+ }
185
+ return matches;
186
+ }
187
+
188
+ function sharedRegistry(key, create) {
189
+ const symbol = Symbol.for(key);
190
+ const scope = globalThis;
191
+ const existing = scope[symbol];
192
+ if (existing !== void 0) return existing;
193
+ const registry = create();
194
+ Object.defineProperty(globalThis, symbol, { value: registry });
195
+ return registry;
196
+ }
197
+
198
+ var TabindexLoan = class _TabindexLoan {
199
+ static #registry = sharedRegistry(
200
+ "stimeo-ui.tabindex-loan.registry.v1",
201
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
202
+ );
87
203
  #value;
204
+ #record;
88
205
  #lent = /* @__PURE__ */ new Set();
89
- /** Returns live loans before Turbo can copy them into its page snapshot. */
90
- #beforeCache = new BeforeCacheReset(() => this.returnAll());
91
- /**
92
- * @param value - the `tabindex` to lend. `"-1"` (the default) is
93
- * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
94
- * which a scroll region with no focusable content of its own needs.
95
- */
96
- constructor(value = "-1") {
206
+ constructor(value, owner) {
97
207
  this.#value = value;
208
+ this.#record = `data-${owner}-tabindex-loan`;
209
+ _TabindexLoan.#registry.names.add(this.#record);
98
210
  }
99
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
100
211
  lend(element) {
101
- if (element.hasAttribute("tabindex")) return;
102
- element.setAttribute("tabindex", this.#value);
212
+ const stack = this.#stack(element);
213
+ const current = element.getAttribute("tabindex");
214
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
215
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
216
+ );
217
+ const linked = stack.at(-1)?.value === current;
218
+ if (!held && current !== null && !linked) return;
103
219
  this.#lent.add(element);
104
- this.#beforeCache.activate();
220
+ if (held) held.loan = this;
221
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
222
+ this.#mark(element, stack);
223
+ if (current === null || !held && current !== this.#value) {
224
+ element.setAttribute("tabindex", this.#value);
225
+ }
105
226
  }
106
- /** Takes back every loan whose value is still the one that was lent. */
107
227
  returnAll() {
108
228
  for (const element of this.#lent) {
109
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
229
+ const stack = this.#stack(element);
230
+ const hold = stack.find((held) => held.loan === this);
231
+ if (hold) this.#release(element, stack, hold);
110
232
  }
111
233
  this.#lent.clear();
112
- this.#beforeCache.deactivate();
234
+ }
235
+ reclaim(element) {
236
+ if (element.getAttribute(this.#record) !== this.#value) return;
237
+ const stack = this.#stack(element);
238
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
239
+ if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
240
+ }
241
+ reclaimWithin(root) {
242
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
243
+ this.reclaim(element);
244
+ }
245
+ }
246
+ #stack(element) {
247
+ const stacks = _TabindexLoan.#registry.stacks;
248
+ const stack = stacks.get(element) ?? this.#seed(element);
249
+ stacks.set(element, stack);
250
+ return stack;
251
+ }
252
+ #seed(element) {
253
+ const current = element.getAttribute("tabindex");
254
+ const holds = [];
255
+ for (const record of _TabindexLoan.#registry.names) {
256
+ const value = element.getAttribute(record);
257
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
258
+ }
259
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
260
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
261
+ return holds;
262
+ }
263
+ #release(element, stack, hold) {
264
+ const current = element.getAttribute("tabindex");
265
+ const index = stack.indexOf(hold);
266
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
267
+ stack.splice(index, 1);
268
+ const above = stack[index];
269
+ if (above) above.linked &&= hold.linked;
270
+ this.#mark(element, stack);
271
+ if (!owns) return;
272
+ const below = hold.linked ? stack[index - 1] : void 0;
273
+ if (!below) element.removeAttribute("tabindex");
274
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
275
+ }
276
+ #mark(element, stack) {
277
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
278
+ for (const hold of stack) records.set(hold.record, hold.value);
279
+ for (const [name, recorded] of records) {
280
+ if (element.getAttribute(name) === recorded) continue;
281
+ if (recorded === null) element.removeAttribute(name);
282
+ else element.setAttribute(name, recorded);
283
+ }
113
284
  }
114
285
  };
115
286
 
116
- // src/controllers/pagination_controller.ts
117
- var PaginationController = class extends Controller {
287
+ function targetSelector(identifier, name) {
288
+ return `[data-${identifier}-target~="${name}"]`;
289
+ }
290
+
291
+ var PaginationController = class _PaginationController extends Controller {
292
+ #numbers = new NumberValueReader();
293
+ #moves = new MoveCounter();
294
+ #moveToken = 0;
118
295
  static targets = ["page", "prev", "next"];
119
296
  static values = {
120
297
  page: { type: Number, default: 1 },
121
298
  total: { type: Number, default: 1 }
122
299
  };
300
+ static valueConstraints = {
301
+ page: NUMBER_BOUNDS.finite,
302
+ total: NUMBER_BOUNDS.finite
303
+ };
123
304
  static actions = ["next", "prev", "select"];
124
305
  static events = ["change", "reconcile"];
125
- /** Marks a `disabled` this controller applied at a boundary, in its own namespace. */
126
306
  get #boundaryAttribute() {
127
307
  return `data-${this.identifier}-boundary-disabled`;
128
308
  }
129
- /** The `tabindex` this instance lends the root for the focus fallback. */
130
- #tabindex = new TabindexLoan();
131
- /**
132
- * Collapses the Value and target callbacks of one mutation into one pass, and
133
- * refuses the ones Stimulus delivers before `connect()`, which renders itself.
134
- */
135
- #repaint = new MicrotaskCoalescer(() => this.#reconcilePage());
136
- /** The page shown last, which the next move of the current page is measured from. */
309
+ #tabindex = new TabindexLoan("-1", this.identifier);
310
+ #repaint = new MorphRenderWatcher(() => this.#reconcilePage());
137
311
  #shown = 1;
138
- /** Renders the initial state from the clamped `page` and `total`. */
139
312
  connect() {
140
- this.#repaint.activate();
313
+ this.#tabindex.reclaim(this.element);
314
+ this.#repaint.observe(this.element);
141
315
  this.#shown = this.#page;
142
316
  this.#render();
143
317
  }
144
- /** Drops a pending pass and reverts the one attribute added outside the state hooks. */
145
318
  disconnect() {
146
- this.#repaint.cancel();
319
+ this.#repaint.disconnect();
147
320
  this.#tabindex.returnAll();
148
321
  }
149
- /** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
150
322
  pageValueChanged() {
151
323
  this.#repaint.schedule();
152
324
  }
153
- /** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
154
325
  totalValueChanged() {
155
326
  this.#repaint.schedule();
156
327
  }
157
- /** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
158
328
  pageTargetConnected() {
159
329
  this.#repaint.schedule();
160
330
  }
161
- /** Syncs a `prev` button appended/replaced at runtime. */
162
331
  prevTargetConnected() {
163
332
  this.#repaint.schedule();
164
333
  }
165
- /** Syncs a `next` button appended/replaced at runtime. */
334
+ prevTargetDisconnected(button) {
335
+ this.#releaseDeparted(button, this.prevTargets);
336
+ this.#repaint.schedule();
337
+ }
166
338
  nextTargetConnected() {
167
339
  this.#repaint.schedule();
168
340
  }
169
- /** Makes the clicked page button (its `data-page`) current. */
170
- select(event) {
171
- const button = event.currentTarget;
341
+ nextTargetDisconnected(button) {
342
+ this.#releaseDeparted(button, this.nextTargets);
343
+ this.#repaint.schedule();
344
+ }
345
+ select(source) {
346
+ const { host, origin, reason } = actionSource(source);
347
+ const button = host?.closest(targetSelector(this.identifier, "page"));
348
+ if (!button || !this.pageTargets.includes(button)) return;
349
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
350
+ return;
172
351
  const raw = button.dataset.page;
173
352
  if (raw === void 0 || raw.trim() === "") return;
174
353
  const page = Number(raw);
175
354
  if (!Number.isInteger(page)) return;
176
- this.#goto(page);
355
+ this.#goto(page, reason);
177
356
  }
178
- /** Steps to the previous page. */
179
- prev() {
180
- this.#goto(this.#page - 1);
357
+ prev(event) {
358
+ this.#goto(this.#page - 1, stateReasonFor(event));
181
359
  }
182
- /** Steps to the next page. */
183
- next() {
184
- this.#goto(this.#page + 1);
360
+ next(event) {
361
+ this.#goto(this.#page + 1, stateReasonFor(event));
185
362
  }
186
- /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */
187
- #goto(page) {
188
- if (!Number.isFinite(page)) return;
189
- const previous = this.#page;
363
+ #goto(page, reason) {
364
+ const previous = this.#shown;
190
365
  const target = this.#clamp(page);
191
- if (target === previous) return;
366
+ if (target === previous && target === this.#page) return;
192
367
  this.pageValue = target;
193
368
  this.#shown = target;
369
+ const changed = target !== previous;
370
+ if (changed) this.#moveToken = this.#moves.record();
371
+ const token = this.#moveToken;
194
372
  this.#render();
373
+ if (!changed || !this.#moves.isLatest(token)) return;
195
374
  this.dispatch("change", {
196
- detail: { page: target, total: this.#total, previous }
375
+ detail: { page: target, total: this.#total, previous, reason }
197
376
  });
198
377
  }
199
- /**
200
- * Renders one settled batch of Value and target changes, and reports a current
201
- * page that moved from the one shown before as `reconcile`.
202
- */
203
378
  #reconcilePage() {
204
379
  const previous = this.#shown;
205
380
  const page = this.#page;
206
381
  this.#shown = page;
382
+ const token = this.#moveToken;
207
383
  this.#render();
208
- if (page !== previous) {
384
+ if (page !== previous && this.#moves.isLatest(token)) {
209
385
  this.dispatch("reconcile", { detail: { page, total: this.#total, previous } });
210
386
  }
211
387
  }
212
- /**
213
- * Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
214
- *
215
- * It reads `page` and `total` through their clamps and never writes either Value,
216
- * so a declaration outside the range stays in the attributes as the page wrote it.
217
- *
218
- * @stimeoRenderRoot
219
- */
220
388
  #render() {
389
+ const token = this.#moveToken;
221
390
  const page = this.#page;
222
391
  for (const button of this.pageTargets) {
223
392
  if (Number(button.dataset.page) === page) {
@@ -230,40 +399,35 @@ var PaginationController = class extends Controller {
230
399
  const next = this.hasNextTarget ? this.nextTarget : null;
231
400
  const atStart = page <= 1;
232
401
  const atEnd = page >= this.#total;
233
- const prevStaysDisabled = this.#disabledAfter(prev, atStart);
234
402
  const nextStaysDisabled = this.#disabledAfter(next, atEnd);
235
403
  this.#release(prev, atStart);
236
404
  this.#release(next, atEnd);
237
405
  this.#disable(prev, atStart, nextStaysDisabled ? null : next);
238
- this.#disable(next, atEnd, prevStaysDisabled ? null : prev);
406
+ if (!this.#moves.isLatest(token)) return;
407
+ this.#disable(next, atEnd, prev);
239
408
  }
240
- /** Whether a boundary button will still be `disabled` once this render applies. */
241
409
  #disabledAfter(button, atBoundary) {
242
410
  if (!button) return true;
243
411
  if (atBoundary) return true;
244
412
  return button.disabled && !this.#owns(button);
245
413
  }
246
- /** Releases the boundary `disabled` this controller owns, once away from it. */
414
+ #releaseDeparted(button, targets) {
415
+ if (!targets.includes(button)) this.#release(button, false);
416
+ }
247
417
  #release(button, atBoundary) {
248
418
  if (!button || atBoundary || !this.#owns(button)) return;
249
419
  button.disabled = false;
250
420
  button.removeAttribute(this.#boundaryAttribute);
251
421
  }
252
- /**
253
- * Disables a boundary button, first moving focus off it when it is the active
254
- * element so disabling never strands focus.
255
- */
256
422
  #disable(button, atBoundary, opposite) {
257
423
  if (!button || !atBoundary) return;
258
424
  if (button.disabled && !this.#owns(button)) return;
425
+ const token = this.#moveToken;
259
426
  if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
427
+ if (!this.#moves.isLatest(token)) return;
260
428
  button.disabled = true;
261
429
  button.setAttribute(this.#boundaryAttribute, "");
262
430
  }
263
- /**
264
- * Moves focus to `opposite` (already resolved to `null` when it will stay
265
- * disabled), else to the current page button, else to the landmark itself.
266
- */
267
431
  #moveFocusAwayFrom(opposite) {
268
432
  const page = this.#page;
269
433
  const currentPage = this.pageTargets.find(
@@ -279,26 +443,38 @@ var PaginationController = class extends Controller {
279
443
  this.#tabindex.lend(this.element);
280
444
  this.element.focus();
281
445
  }
282
- /** Total pages, normalized to a finite integer >= 1. */
283
446
  get #total() {
284
- const total = this.totalValue;
285
- return Number.isFinite(total) ? Math.max(1, Math.trunc(total)) : 1;
447
+ const total = this.#safeTotal;
448
+ return Math.max(1, Math.trunc(total));
286
449
  }
287
- /** The current page, normalized into `[1, total]`. */
288
450
  get #page() {
289
- return this.#clamp(this.pageValue);
451
+ return this.#clamp(this.#safePage);
290
452
  }
291
- /** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */
292
453
  #clamp(page) {
293
- if (!Number.isFinite(page)) return 1;
294
454
  return Math.min(this.#total, Math.max(1, Math.trunc(page)));
295
455
  }
296
- /** Whether the button's current `disabled` was applied by boundary control. */
297
456
  #owns(button) {
298
457
  return button.hasAttribute(this.#boundaryAttribute);
299
458
  }
459
+ get #safePage() {
460
+ return this.#numbers.read(
461
+ this,
462
+ "page",
463
+ this.pageValue,
464
+ _PaginationController.values.page.default,
465
+ _PaginationController.valueConstraints.page
466
+ );
467
+ }
468
+ get #safeTotal() {
469
+ return this.#numbers.read(
470
+ this,
471
+ "total",
472
+ this.totalValue,
473
+ _PaginationController.values.total.default,
474
+ _PaginationController.valueConstraints.total
475
+ );
476
+ }
300
477
  };
301
478
 
302
479
  export { PaginationController };
303
- //# sourceMappingURL=pagination_controller.js.map
304
480
  //# sourceMappingURL=pagination_controller.js.map