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,14 +1,10 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/conditional_fields_controller.ts
4
-
5
- // src/utils/event_owner.ts
6
3
  function ownerIndex(candidates, node) {
7
4
  if (!(node instanceof Node)) return -1;
8
5
  return candidates.findIndex((candidate) => candidate.contains(node));
9
6
  }
10
7
 
11
- // src/utils/focus_candidate.ts
12
8
  function inheritsFieldsetDisabled(control) {
13
9
  let fieldset = control.closest("fieldset[disabled]");
14
10
  while (fieldset) {
@@ -18,33 +14,37 @@ function inheritsFieldsetDisabled(control) {
18
14
  }
19
15
  return false;
20
16
  }
17
+ function unslotted(element) {
18
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
19
+ }
20
+ function flatTreeParent(element) {
21
+ if (unslotted(element)) return null;
22
+ const parent = element.assignedSlot ?? element.parentNode;
23
+ if (parent instanceof ShadowRoot) return parent.host;
24
+ return parent instanceof Element ? parent : null;
25
+ }
21
26
  function canTakeFocus(element) {
22
- if (element.closest("[hidden], [inert]")) return false;
27
+ for (let node = element; node; node = flatTreeParent(node)) {
28
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
29
+ }
23
30
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
24
31
  if (!("disabled" in element)) return true;
25
32
  if (element.disabled) return false;
26
33
  return !inheritsFieldsetDisabled(element);
27
34
  }
28
35
 
29
- // src/utils/form_reset_watcher.ts
30
36
  var FormResetWatcher = class {
31
37
  #owns;
32
38
  #onReset;
33
39
  #frames = /* @__PURE__ */ new Set();
34
40
  #generation = 0;
35
- /**
36
- * @param owns - whether `form` owns a control this controller derives from.
37
- * @param onReset - the reconciliation pass, run once per honoured reset.
38
- */
39
41
  constructor(owns, onReset) {
40
42
  this.#owns = owns;
41
43
  this.#onReset = onReset;
42
44
  }
43
- /** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
44
45
  observe() {
45
46
  document.addEventListener("reset", this.#handleReset, true);
46
47
  }
47
- /** Unsubscribes and drops every pending pass; call from `disconnect()`. */
48
48
  disconnect() {
49
49
  document.removeEventListener("reset", this.#handleReset, true);
50
50
  this.#generation += 1;
@@ -68,57 +68,38 @@ var FormResetWatcher = class {
68
68
  this.#frames.add(frame);
69
69
  });
70
70
  };
71
- /** Runs the pass unless a listener cancelled the reset before it landed. */
72
71
  #settle(event) {
73
72
  if (!event.defaultPrevented) this.#onReset();
74
73
  }
75
74
  };
76
75
 
77
- // src/utils/listener_set.ts
78
76
  var ListenerSet = class {
79
- /** The generation every `add` joins until the next `dispose()`. */
80
77
  #abort = new AbortController();
81
- /**
82
- * Attaches `handler` to the open generation, exactly as the caller spelled it.
83
- *
84
- * The set supplies the signal, so `options` carries everything else the DOM
85
- * accepts — `capture` included, which has to match at release time and no
86
- * longer has a second place to drift from.
87
- */
88
78
  add(target, type, handler, options) {
89
79
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
90
80
  }
91
- /**
92
- * Releases every listener of the open generation, synchronously, and opens the
93
- * next one. Idempotent, and safe before anything has been added.
94
- */
95
81
  dispose() {
96
82
  this.#abort.abort();
97
83
  this.#abort = new AbortController();
98
84
  }
99
85
  };
100
86
 
101
- // src/utils/microtask_coalescer.ts
102
87
  var MicrotaskCoalescer = class {
103
88
  #run;
104
89
  #queued = false;
105
90
  #active = false;
106
91
  #generation = 0;
107
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
108
92
  constructor(run) {
109
93
  this.#run = run;
110
94
  }
111
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
112
95
  activate() {
113
96
  this.#active = true;
114
97
  }
115
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
116
98
  cancel() {
117
99
  this.#active = false;
118
100
  this.#queued = false;
119
101
  this.#generation += 1;
120
102
  }
121
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
122
103
  schedule() {
123
104
  if (!this.#active || this.#queued) return;
124
105
  this.#queued = true;
@@ -131,68 +112,148 @@ var MicrotaskCoalescer = class {
131
112
  }
132
113
  };
133
114
 
134
- // src/utils/before_cache_reset.ts
135
- var BeforeCacheReset = class _BeforeCacheReset {
136
- /** Every subscribed instance, iterated by the one shared document listener. */
137
- static #subscribers = /* @__PURE__ */ new Set();
138
- /** The shared listener; installed while at least one instance is subscribed. */
139
- static #onBeforeCache = () => {
140
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
141
- };
142
- #rewind;
143
- /** @param rewind - the pass that returns this controller's state to its initial form. */
144
- constructor(rewind) {
145
- this.#rewind = rewind;
115
+ var MorphRenderWatcher = class {
116
+ #listeners = new ListenerSet();
117
+ #pass;
118
+ #element = null;
119
+ #onMorph = () => this.#pass.schedule();
120
+ constructor(run) {
121
+ this.#pass = new MicrotaskCoalescer(run);
146
122
  }
147
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
148
- activate() {
149
- const first = _BeforeCacheReset.#subscribers.size === 0;
150
- _BeforeCacheReset.#subscribers.add(this);
151
- if (first) {
152
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
153
- }
123
+ observe(element) {
124
+ if (this.#element === element) return;
125
+ this.disconnect();
126
+ this.#element = element;
127
+ this.#pass.activate();
128
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
154
129
  }
155
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
156
- deactivate() {
157
- _BeforeCacheReset.#subscribers.delete(this);
158
- if (_BeforeCacheReset.#subscribers.size > 0) return;
159
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
130
+ schedule() {
131
+ this.#pass.schedule();
132
+ }
133
+ disconnect() {
134
+ this.#listeners.dispose();
135
+ this.#pass.cancel();
136
+ this.#element = null;
160
137
  }
161
138
  };
162
139
 
163
- // src/utils/tabindex_loan.ts
164
- var TabindexLoan = class {
140
+ function queryOpenDescendants(root, selector) {
141
+ const matches = [];
142
+ const roots = [root];
143
+ for (const scope of roots) {
144
+ matches.push(...scope.querySelectorAll(selector));
145
+ if (scope.nodeType === Node.ELEMENT_NODE) {
146
+ const shadow = scope.shadowRoot;
147
+ if (shadow) roots.push(shadow);
148
+ }
149
+ for (const element of scope.querySelectorAll("*")) {
150
+ if (element.shadowRoot) roots.push(element.shadowRoot);
151
+ }
152
+ }
153
+ return matches;
154
+ }
155
+
156
+ function sharedRegistry(key, create) {
157
+ const symbol = Symbol.for(key);
158
+ const scope = globalThis;
159
+ const existing = scope[symbol];
160
+ if (existing !== void 0) return existing;
161
+ const registry = create();
162
+ Object.defineProperty(globalThis, symbol, { value: registry });
163
+ return registry;
164
+ }
165
+
166
+ var TabindexLoan = class _TabindexLoan {
167
+ static #registry = sharedRegistry(
168
+ "stimeo-ui.tabindex-loan.registry.v1",
169
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
170
+ );
165
171
  #value;
172
+ #record;
166
173
  #lent = /* @__PURE__ */ new Set();
167
- /** Returns live loans before Turbo can copy them into its page snapshot. */
168
- #beforeCache = new BeforeCacheReset(() => this.returnAll());
169
- /**
170
- * @param value - the `tabindex` to lend. `"-1"` (the default) is
171
- * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
172
- * which a scroll region with no focusable content of its own needs.
173
- */
174
- constructor(value = "-1") {
174
+ constructor(value, owner) {
175
175
  this.#value = value;
176
+ this.#record = `data-${owner}-tabindex-loan`;
177
+ _TabindexLoan.#registry.names.add(this.#record);
176
178
  }
177
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
178
179
  lend(element) {
179
- if (element.hasAttribute("tabindex")) return;
180
- element.setAttribute("tabindex", this.#value);
180
+ const stack = this.#stack(element);
181
+ const current = element.getAttribute("tabindex");
182
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
183
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
184
+ );
185
+ const linked = stack.at(-1)?.value === current;
186
+ if (!held && current !== null && !linked) return;
181
187
  this.#lent.add(element);
182
- this.#beforeCache.activate();
188
+ if (held) held.loan = this;
189
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
190
+ this.#mark(element, stack);
191
+ if (current === null || !held && current !== this.#value) {
192
+ element.setAttribute("tabindex", this.#value);
193
+ }
183
194
  }
184
- /** Takes back every loan whose value is still the one that was lent. */
185
195
  returnAll() {
186
196
  for (const element of this.#lent) {
187
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
197
+ const stack = this.#stack(element);
198
+ const hold = stack.find((held) => held.loan === this);
199
+ if (hold) this.#release(element, stack, hold);
188
200
  }
189
201
  this.#lent.clear();
190
- this.#beforeCache.deactivate();
202
+ }
203
+ reclaim(element) {
204
+ if (element.getAttribute(this.#record) !== this.#value) return;
205
+ const stack = this.#stack(element);
206
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
207
+ if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
208
+ }
209
+ reclaimWithin(root) {
210
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
211
+ this.reclaim(element);
212
+ }
213
+ }
214
+ #stack(element) {
215
+ const stacks = _TabindexLoan.#registry.stacks;
216
+ const stack = stacks.get(element) ?? this.#seed(element);
217
+ stacks.set(element, stack);
218
+ return stack;
219
+ }
220
+ #seed(element) {
221
+ const current = element.getAttribute("tabindex");
222
+ const holds = [];
223
+ for (const record of _TabindexLoan.#registry.names) {
224
+ const value = element.getAttribute(record);
225
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
226
+ }
227
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
228
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
229
+ return holds;
230
+ }
231
+ #release(element, stack, hold) {
232
+ const current = element.getAttribute("tabindex");
233
+ const index = stack.indexOf(hold);
234
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
235
+ stack.splice(index, 1);
236
+ const above = stack[index];
237
+ if (above) above.linked &&= hold.linked;
238
+ this.#mark(element, stack);
239
+ if (!owns) return;
240
+ const below = hold.linked ? stack[index - 1] : void 0;
241
+ if (!below) element.removeAttribute("tabindex");
242
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
243
+ }
244
+ #mark(element, stack) {
245
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
246
+ for (const hold of stack) records.set(hold.record, hold.value);
247
+ for (const [name, recorded] of records) {
248
+ if (element.getAttribute(name) === recorded) continue;
249
+ if (recorded === null) element.removeAttribute(name);
250
+ else element.setAttribute(name, recorded);
251
+ }
191
252
  }
192
253
  };
193
254
 
194
- // src/controllers/conditional_fields_controller.ts
195
255
  var DISABLED_MARKER = "data-conditional-disabled";
256
+ var CONTROLS = "input, textarea, select, button";
196
257
  var OBSERVED_ATTRIBUTES = [
197
258
  "hidden",
198
259
  "aria-hidden",
@@ -215,23 +276,19 @@ var ConditionalFieldsController = class extends Controller {
215
276
  };
216
277
  static actions = ["evaluate"];
217
278
  static events = ["change", "reconcile"];
218
- /** Logical state last committed for each current region; rebuilt on every connect. */
219
279
  #lastVisible = /* @__PURE__ */ new WeakMap();
220
- /** Controller writes awaiting an observer callback, separated from authored morphs. */
221
280
  #internalWrites = /* @__PURE__ */ new WeakMap();
222
281
  #observing = false;
223
- /** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
224
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
282
+ #departed = /* @__PURE__ */ new Set();
283
+ #concealed = /* @__PURE__ */ new WeakSet();
284
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileDom());
225
285
  #listeners = new ListenerSet();
226
286
  #formReset = new FormResetWatcher(
227
287
  (form) => this.#hasTriggerOwnedBy(form),
228
288
  () => this.#reconcile.schedule()
229
289
  );
230
- /** Provides the last-resort focus landmark without claiming an authored tabindex. */
231
- #rootTabindex = new TabindexLoan();
232
- /** Watches retained targets and controls whose declarative or reflected state changed. */
290
+ #rootTabindex = new TabindexLoan("-1", this.identifier);
233
291
  #observer = new MutationObserver((records) => this.#onMutations(records));
234
- /** Evaluates only events whose source is one of this instance's live trigger targets. */
235
292
  #onTriggerInput = (event) => {
236
293
  const target = event.target;
237
294
  const triggers = this.triggerTargets;
@@ -239,16 +296,12 @@ var ConditionalFieldsController = class extends Controller {
239
296
  this.#commitChange(triggers);
240
297
  }
241
298
  };
242
- /** Reconciles property-only retained-element changes after Turbo morphs. */
243
- #onMorph = () => {
244
- this.#reconcile.schedule();
245
- };
246
- /** Reflects initial live state and opens every retained-DOM reconciliation path. */
247
299
  connect() {
300
+ this.#rootTabindex.reclaim(this.element);
248
301
  this.#lastVisible = /* @__PURE__ */ new WeakMap();
249
302
  this.#internalWrites = /* @__PURE__ */ new WeakMap();
250
- this.#reconcile.activate();
251
303
  this.#settle();
304
+ this.#reconcile.observe(this.element);
252
305
  this.#observer.observe(this.element, {
253
306
  attributes: true,
254
307
  attributeFilter: OBSERVED_ATTRIBUTES,
@@ -258,56 +311,46 @@ var ConditionalFieldsController = class extends Controller {
258
311
  this.#observing = true;
259
312
  this.#listeners.add(this.element, "change", this.#onTriggerInput);
260
313
  this.#listeners.add(this.element, "input", this.#onTriggerInput);
261
- this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
262
314
  this.#formReset.observe();
263
315
  }
264
- /** Releases all external resources and returns the temporary focus landmark loan. */
265
316
  disconnect() {
266
- this.#reconcile.cancel();
317
+ this.#reconcile.disconnect();
267
318
  this.#observing = false;
268
319
  this.#observer.disconnect();
269
320
  this.#listeners.dispose();
270
321
  this.#formReset.disconnect();
271
322
  this.#rootTabindex.returnAll();
272
323
  }
273
- /** Reconciles after a trigger target is inserted or restored. */
274
324
  triggerTargetConnected() {
275
325
  this.#reconcile.schedule();
276
326
  }
277
- /** Reconciles after a trigger target is removed or replaced. */
278
327
  triggerTargetDisconnected() {
279
328
  this.#reconcile.schedule();
280
329
  }
281
- /** Initializes a region target inserted or restored at runtime. */
282
330
  regionTargetConnected() {
283
331
  this.#reconcile.schedule();
284
332
  }
285
- /** Reconciles the remaining region set after a target leaves. */
286
- regionTargetDisconnected() {
333
+ regionTargetDisconnected(region) {
334
+ this.#departed.add(region);
287
335
  this.#reconcile.schedule();
288
336
  }
289
- /** Reconciles when application code or a morph changes the match policy. */
290
337
  matchValueChanged() {
291
338
  this.#reconcile.schedule();
292
339
  }
293
- /** Reconciles control ownership when hidden controls become enabled or excluded. */
294
340
  disableHiddenValueChanged() {
295
341
  this.#reconcile.schedule();
296
342
  }
297
- /** Re-evaluates every region and repairs all reflected DOM state. */
298
343
  evaluate() {
299
344
  this.#commitChange();
300
345
  }
301
- /**
302
- * Computes one settled plan, shows destinations before hiding sources, and then
303
- * returns logical transitions in DOM order after every region is internally coherent.
304
- *
305
- * @stimeoRenderRoot
306
- */
307
346
  #settle(triggers = this.triggerTargets) {
308
347
  const regions = this.regionTargets;
309
348
  const plan = /* @__PURE__ */ new Map();
310
349
  for (const region of regions) plan.set(region, this.#isVisible(region, triggers));
350
+ for (const departed of this.#departed) {
351
+ if (!plan.has(departed)) this.#release(departed, plan);
352
+ }
353
+ this.#departed.clear();
311
354
  const changed = /* @__PURE__ */ new Set();
312
355
  const applicationOrder = [
313
356
  ...regions.filter((region) => plan.get(region) === true),
@@ -321,19 +364,16 @@ var ConditionalFieldsController = class extends Controller {
321
364
  }
322
365
  return regions.filter((region) => changed.has(region)).map((region) => ({ region, visible: plan.get(region) === true }));
323
366
  }
324
- /** Reports logical transitions caused by user input or the public action. */
325
367
  #commitChange(triggers = this.triggerTargets) {
326
368
  for (const detail of this.#settle(triggers)) {
327
369
  this.dispatch("change", { detail });
328
370
  }
329
371
  }
330
- /** Reports logical transitions derived from DOM, lifecycle, reset, or Value state. */
331
372
  #reconcileDom() {
332
373
  for (const detail of this.#settle()) {
333
374
  this.dispatch("reconcile", { detail });
334
375
  }
335
376
  }
336
- /** Applies one planned state while keeping event history separate from DOM repair. */
337
377
  #applyRegion(region, visible, plan) {
338
378
  const previous = this.#lastVisible.get(region) ?? !region.hidden;
339
379
  const changed = visible !== previous;
@@ -343,17 +383,16 @@ var ConditionalFieldsController = class extends Controller {
343
383
  this.#syncRegionControls(region, visible);
344
384
  return changed;
345
385
  }
346
- /** Reflects visibility idempotently, recording writes so the observer ignores its own work. */
347
386
  #syncRegionState(region, visible) {
387
+ if (!visible && this.#predicateFor(region) !== null) this.#concealed.add(region);
388
+ else this.#concealed.delete(region);
348
389
  this.#writeAttribute(region, "hidden", visible ? null : "");
349
390
  this.#writeAttribute(region, "aria-hidden", visible ? null : "true");
350
391
  this.#writeAttribute(region, "data-visible", visible ? "true" : null);
351
392
  }
352
- /** Enables/disables descendant controls, touching only state carrying our marker. */
353
393
  #syncRegionControls(region, visible) {
354
394
  const shouldDisable = !visible && this.disableHiddenValue;
355
- const controls = region.querySelectorAll("input, textarea, select, button");
356
- for (const control of controls) {
395
+ for (const control of region.querySelectorAll(CONTROLS)) {
357
396
  const marked = control.hasAttribute(DISABLED_MARKER);
358
397
  if (shouldDisable) {
359
398
  if (!control.disabled) {
@@ -363,18 +402,30 @@ var ConditionalFieldsController = class extends Controller {
363
402
  this.#writeAttribute(control, DISABLED_MARKER, "true");
364
403
  }
365
404
  } else if (marked) {
366
- this.#writeAttribute(control, "disabled", null);
367
- this.#writeAttribute(control, DISABLED_MARKER, null);
405
+ this.#enable(control);
406
+ }
407
+ }
408
+ }
409
+ #enable(control) {
410
+ this.#writeAttribute(control, "disabled", null);
411
+ this.#writeAttribute(control, DISABLED_MARKER, null);
412
+ }
413
+ #release(element, plan) {
414
+ this.#lastVisible.delete(element);
415
+ if (this.#concealed.delete(element)) this.#writeAttribute(element, "hidden", null);
416
+ this.#writeAttribute(element, "aria-hidden", null);
417
+ this.#writeAttribute(element, "data-visible", null);
418
+ for (const control of element.querySelectorAll(CONTROLS)) {
419
+ if (control.hasAttribute(DISABLED_MARKER) && this.#survivesPlan(control, plan)) {
420
+ this.#enable(control);
368
421
  }
369
422
  }
370
423
  }
371
- /** Whether a region's declared condition holds across `triggers` per `match`. */
372
424
  #isVisible(region, triggers) {
373
425
  const predicate = this.#predicateFor(region);
374
426
  if (predicate === null) return !region.hidden;
375
427
  return triggers.length > 0 && (this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate));
376
428
  }
377
- /** Builds the deterministic per-trigger predicate from the region's condition. */
378
429
  #predicateFor(region) {
379
430
  if (region.hasAttribute("data-when-checked")) {
380
431
  return (trigger) => this.#isChecked(trigger);
@@ -391,19 +442,16 @@ var ConditionalFieldsController = class extends Controller {
391
442
  #isChecked(trigger) {
392
443
  return trigger instanceof HTMLInputElement && trigger.checked;
393
444
  }
394
- /** A trigger has a value when a radio/checkbox is selected, or its control value matches. */
395
445
  #matchesValue(trigger, wanted) {
396
446
  if (trigger instanceof HTMLInputElement && (trigger.type === "checkbox" || trigger.type === "radio")) {
397
447
  return trigger.checked && trigger.value === wanted;
398
448
  }
399
449
  return trigger.value === wanted;
400
450
  }
401
- /** Moves focus to the first safe trigger, or to the controller landmark as a fallback. */
402
451
  #retreatFocus(region, plan) {
403
452
  const active = document.activeElement;
404
453
  if (active && region.contains(active)) this.#moveFocus(plan);
405
454
  }
406
- /** Moves focus after the caller establishes that the active node is being hidden. */
407
455
  #moveFocus(plan) {
408
456
  const destination = this.triggerTargets.find(
409
457
  (trigger) => canTakeFocus(trigger) && this.#survivesPlan(trigger, plan)
@@ -416,11 +464,9 @@ var ConditionalFieldsController = class extends Controller {
416
464
  this.#rootTabindex.lend(this.element);
417
465
  this.element.focus();
418
466
  }
419
- /** Whether a focus candidate sits outside every region this pass will hide. */
420
467
  #survivesPlan(candidate, plan) {
421
468
  return [...plan].every(([region, visible]) => visible || !region.contains(candidate));
422
469
  }
423
- /** Records one idempotent attribute write before MutationObserver sees it. */
424
470
  #writeAttribute(element, name, value) {
425
471
  if (element.getAttribute(name) !== value) {
426
472
  if (this.#observing) {
@@ -435,7 +481,6 @@ var ConditionalFieldsController = class extends Controller {
435
481
  else element.setAttribute(name, value);
436
482
  }
437
483
  }
438
- /** Separates authored records from controller writes and queues one relevant repair. */
439
484
  #onMutations(records) {
440
485
  const triggers = this.triggerTargets;
441
486
  const regions = this.regionTargets;
@@ -465,16 +510,13 @@ var ConditionalFieldsController = class extends Controller {
465
510
  }
466
511
  if (needsReconcile) this.#reconcile.schedule();
467
512
  }
468
- /** Whether an authored mutation target belongs to a trigger, region, or managed control. */
469
513
  #elementAffectsState(target, triggers, regions) {
470
514
  return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
471
515
  }
472
- /** Whether a form owns at least one current trigger, including `form=` associations. */
473
516
  #hasTriggerOwnedBy(form) {
474
517
  return this.triggerTargets.some((trigger) => trigger.form === form);
475
518
  }
476
519
  };
477
520
 
478
521
  export { ConditionalFieldsController };
479
- //# sourceMappingURL=conditional_fields_controller.js.map
480
522
  //# sourceMappingURL=conditional_fields_controller.js.map