stimeo-ui 0.6.0 → 0.7.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 (199) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  6. package/dist/controllers/auto_submit_controller.js.map +1 -1
  7. package/dist/controllers/avatar_controller.js +47 -5
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  12. package/dist/controllers/bulk_select_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +2 -0
  14. package/dist/controllers/calendar_controller.js.map +1 -1
  15. package/dist/controllers/character_counter_controller.d.ts +52 -20
  16. package/dist/controllers/character_counter_controller.js +338 -63
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/checkbox_controller.d.ts +3 -0
  19. package/dist/controllers/checkbox_controller.js.map +1 -1
  20. package/dist/controllers/color_picker_controller.d.ts +3 -0
  21. package/dist/controllers/color_picker_controller.js.map +1 -1
  22. package/dist/controllers/combobox_controller.d.ts +2 -0
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +75 -4
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  28. package/dist/controllers/conditional_fields_controller.js +345 -51
  29. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  30. package/dist/controllers/confirm_controller.d.ts +2 -0
  31. package/dist/controllers/confirm_controller.js +75 -4
  32. package/dist/controllers/confirm_controller.js.map +1 -1
  33. package/dist/controllers/count_up_controller.d.ts +2 -0
  34. package/dist/controllers/count_up_controller.js.map +1 -1
  35. package/dist/controllers/countdown_controller.d.ts +4 -0
  36. package/dist/controllers/countdown_controller.js.map +1 -1
  37. package/dist/controllers/currency_input_controller.d.ts +2 -0
  38. package/dist/controllers/currency_input_controller.js.map +1 -1
  39. package/dist/controllers/data_grid_controller.d.ts +3 -0
  40. package/dist/controllers/data_grid_controller.js.map +1 -1
  41. package/dist/controllers/dialog_controller.js +75 -4
  42. package/dist/controllers/dialog_controller.js.map +1 -1
  43. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  44. package/dist/controllers/direct_upload_controller.js +201 -45
  45. package/dist/controllers/direct_upload_controller.js.map +1 -1
  46. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  47. package/dist/controllers/dirty_form_controller.js +192 -29
  48. package/dist/controllers/dirty_form_controller.js.map +1 -1
  49. package/dist/controllers/dismissible_controller.d.ts +2 -0
  50. package/dist/controllers/dismissible_controller.js +83 -18
  51. package/dist/controllers/dismissible_controller.js.map +1 -1
  52. package/dist/controllers/drawer_controller.js +75 -4
  53. package/dist/controllers/drawer_controller.js.map +1 -1
  54. package/dist/controllers/empty_state_controller.d.ts +2 -0
  55. package/dist/controllers/empty_state_controller.js.map +1 -1
  56. package/dist/controllers/file_dropzone_controller.d.ts +2 -0
  57. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  58. package/dist/controllers/filter_controller.d.ts +2 -0
  59. package/dist/controllers/filter_controller.js.map +1 -1
  60. package/dist/controllers/flash_controller.d.ts +4 -0
  61. package/dist/controllers/flash_controller.js.map +1 -1
  62. package/dist/controllers/focus_controller.d.ts +2 -0
  63. package/dist/controllers/focus_controller.js +75 -4
  64. package/dist/controllers/focus_controller.js.map +1 -1
  65. package/dist/controllers/form_field_controller.d.ts +50 -10
  66. package/dist/controllers/form_field_controller.js +280 -62
  67. package/dist/controllers/form_field_controller.js.map +1 -1
  68. package/dist/controllers/form_validation_controller.d.ts +10 -8
  69. package/dist/controllers/form_validation_controller.js +208 -83
  70. package/dist/controllers/form_validation_controller.js.map +1 -1
  71. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  72. package/dist/controllers/frame_loading_controller.js.map +1 -1
  73. package/dist/controllers/highlight_controller.d.ts +2 -0
  74. package/dist/controllers/highlight_controller.js.map +1 -1
  75. package/dist/controllers/idle_controller.d.ts +2 -0
  76. package/dist/controllers/idle_controller.js.map +1 -1
  77. package/dist/controllers/input_mask_controller.d.ts +2 -0
  78. package/dist/controllers/input_mask_controller.js.map +1 -1
  79. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  80. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  81. package/dist/controllers/listbox_controller.d.ts +2 -0
  82. package/dist/controllers/listbox_controller.js.map +1 -1
  83. package/dist/controllers/local_time_controller.d.ts +2 -0
  84. package/dist/controllers/local_time_controller.js.map +1 -1
  85. package/dist/controllers/masonry_controller.d.ts +2 -0
  86. package/dist/controllers/masonry_controller.js.map +1 -1
  87. package/dist/controllers/meter_controller.d.ts +2 -0
  88. package/dist/controllers/meter_controller.js.map +1 -1
  89. package/dist/controllers/multi_select_controller.d.ts +2 -0
  90. package/dist/controllers/multi_select_controller.js.map +1 -1
  91. package/dist/controllers/nested_form_controller.d.ts +2 -0
  92. package/dist/controllers/nested_form_controller.js.map +1 -1
  93. package/dist/controllers/network_status_controller.d.ts +2 -0
  94. package/dist/controllers/network_status_controller.js.map +1 -1
  95. package/dist/controllers/number_input_controller.d.ts +2 -0
  96. package/dist/controllers/number_input_controller.js +47 -5
  97. package/dist/controllers/number_input_controller.js.map +1 -1
  98. package/dist/controllers/otp_controller.d.ts +3 -0
  99. package/dist/controllers/otp_controller.js.map +1 -1
  100. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  101. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  102. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  103. package/dist/controllers/overflow_menu_controller.js +2 -1
  104. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  105. package/dist/controllers/pagination_controller.d.ts +2 -0
  106. package/dist/controllers/pagination_controller.js +2 -1
  107. package/dist/controllers/pagination_controller.js.map +1 -1
  108. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  109. package/dist/controllers/password_reveal_controller.js.map +1 -1
  110. package/dist/controllers/password_strength_controller.d.ts +2 -0
  111. package/dist/controllers/password_strength_controller.js.map +1 -1
  112. package/dist/controllers/persist_controller.d.ts +30 -16
  113. package/dist/controllers/persist_controller.js +432 -122
  114. package/dist/controllers/persist_controller.js.map +1 -1
  115. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  116. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  117. package/dist/controllers/popover_controller.js +77 -4
  118. package/dist/controllers/popover_controller.js.map +1 -1
  119. package/dist/controllers/portal_controller.d.ts +2 -0
  120. package/dist/controllers/portal_controller.js.map +1 -1
  121. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  122. package/dist/controllers/preview_guard_controller.js.map +1 -1
  123. package/dist/controllers/progress_controller.d.ts +3 -0
  124. package/dist/controllers/progress_controller.js.map +1 -1
  125. package/dist/controllers/radio_group_controller.d.ts +3 -0
  126. package/dist/controllers/radio_group_controller.js.map +1 -1
  127. package/dist/controllers/rating_controller.d.ts +2 -0
  128. package/dist/controllers/rating_controller.js +9 -5
  129. package/dist/controllers/rating_controller.js.map +1 -1
  130. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  131. package/dist/controllers/reading_progress_controller.js.map +1 -1
  132. package/dist/controllers/resizable_controller.d.ts +2 -0
  133. package/dist/controllers/resizable_controller.js.map +1 -1
  134. package/dist/controllers/roving_controller.d.ts +2 -0
  135. package/dist/controllers/roving_controller.js.map +1 -1
  136. package/dist/controllers/scroll_area_controller.d.ts +23 -15
  137. package/dist/controllers/scroll_area_controller.js +462 -162
  138. package/dist/controllers/scroll_area_controller.js.map +1 -1
  139. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  140. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  141. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  142. package/dist/controllers/scrollspy_controller.js.map +1 -1
  143. package/dist/controllers/separator_controller.d.ts +54 -11
  144. package/dist/controllers/separator_controller.js +354 -38
  145. package/dist/controllers/separator_controller.js.map +1 -1
  146. package/dist/controllers/sidebar_controller.js +83 -10
  147. package/dist/controllers/sidebar_controller.js.map +1 -1
  148. package/dist/controllers/skeleton_controller.d.ts +2 -0
  149. package/dist/controllers/skeleton_controller.js.map +1 -1
  150. package/dist/controllers/slider_controller.d.ts +2 -0
  151. package/dist/controllers/slider_controller.js.map +1 -1
  152. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  153. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  154. package/dist/controllers/spinner_controller.d.ts +2 -0
  155. package/dist/controllers/spinner_controller.js.map +1 -1
  156. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  157. package/dist/controllers/step_indicator_controller.js.map +1 -1
  158. package/dist/controllers/stepper_controller.d.ts +2 -0
  159. package/dist/controllers/stepper_controller.js.map +1 -1
  160. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  161. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  162. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  163. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  164. package/dist/controllers/submit_once_controller.d.ts +94 -38
  165. package/dist/controllers/submit_once_controller.js +399 -121
  166. package/dist/controllers/submit_once_controller.js.map +1 -1
  167. package/dist/controllers/switch_controller.d.ts +2 -0
  168. package/dist/controllers/switch_controller.js.map +1 -1
  169. package/dist/controllers/tags_input_controller.d.ts +2 -0
  170. package/dist/controllers/tags_input_controller.js.map +1 -1
  171. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  172. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  173. package/dist/controllers/theme_controller.d.ts +2 -0
  174. package/dist/controllers/theme_controller.js +8 -6
  175. package/dist/controllers/theme_controller.js.map +1 -1
  176. package/dist/controllers/time_picker_controller.d.ts +2 -0
  177. package/dist/controllers/time_picker_controller.js.map +1 -1
  178. package/dist/controllers/toast_controller.d.ts +4 -0
  179. package/dist/controllers/toast_controller.js.map +1 -1
  180. package/dist/controllers/toggle_group_controller.d.ts +2 -0
  181. package/dist/controllers/toggle_group_controller.js.map +1 -1
  182. package/dist/controllers/toolbar_controller.js.map +1 -1
  183. package/dist/controllers/transition_controller.d.ts +2 -0
  184. package/dist/controllers/transition_controller.js.map +1 -1
  185. package/dist/controllers/tree_view_controller.d.ts +2 -0
  186. package/dist/controllers/tree_view_controller.js +2 -1
  187. package/dist/controllers/tree_view_controller.js.map +1 -1
  188. package/dist/index.js +2387 -861
  189. package/dist/index.js.map +1 -1
  190. package/dist/inspector/cli.d.ts +107 -3
  191. package/dist/inspector/cli.js +192 -14
  192. package/dist/inspector/cli.js.map +1 -1
  193. package/dist/inspector/cli_bin.js +254 -46
  194. package/dist/inspector/cli_bin.js.map +1 -1
  195. package/dist/inspector/examples.json +7 -7
  196. package/dist/inspector/manifest.json +434 -44
  197. package/dist/positioning/index.d.ts +2 -0
  198. package/dist/positioning/index.js.map +1 -1
  199. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- /** Trigger controls whose state drives region visibility. */
3
+ /** Trigger controls whose live state drives region visibility. */
4
4
  type Trigger = HTMLInputElement | HTMLSelectElement;
5
5
  /**
6
6
  * Headless dependent-fields behavior: shows/hides (and enables/disables) regions
7
- * based on trigger control values, keeping `hidden` / `aria-hidden` / `disabled` in
7
+ * from trigger control values while keeping submission and accessibility state in
8
8
  * sync (no dedicated APG pattern).
9
9
  *
10
10
  * Markup contract (identifier: `stimeo--conditional-fields`):
@@ -15,17 +15,32 @@ type Trigger = HTMLInputElement | HTMLSelectElement;
15
15
  * </fieldset>
16
16
  * </form>
17
17
  *
18
- * Each region declares its condition with a data attribute: `data-when-checked`,
19
- * `data-when-unchecked`, or `data-when-value="x"`. When the condition (combined
20
- * across triggers per `match`) holds the region is shown and its inputs enabled;
21
- * otherwise it is hidden, `aria-hidden`, and — with `disableHidden` — its inputs are
22
- * disabled so they drop out of the submission. Hiding a region that holds focus
23
- * first retreats focus to a trigger.
18
+ * A region declares one condition: `data-when-checked`, `data-when-unchecked`,
19
+ * or `data-when-value="x"`. The condition is combined across all triggers using
20
+ * `match` (`"any"` by default, or `"all"`). A region with no condition preserves
21
+ * its authored `hidden` state; a declared condition with no triggers is false.
22
+ *
23
+ * Hidden regions receive `hidden` and `aria-hidden="true"`; visible regions expose
24
+ * `data-visible="true"`. With `disableHidden`, descendant controls are disabled and
25
+ * marked with `data-conditional-disabled="true"`, and only marked controls are
26
+ * re-enabled. Before a focused region is hidden, focus moves to the first trigger
27
+ * that can actually receive it. If every trigger is unsafe — including one inside
28
+ * another region being hidden in the same pass — the controller root temporarily
29
+ * receives `tabindex="-1"` and focus.
30
+ *
31
+ * `change` and `reconcile` dispatch `{ region, visible }`.
24
32
  *
25
33
  * @remarks
26
- * Behavior only. Visibility/enabled state lives entirely on the elements' `hidden` /
27
- * `disabled` / `aria-hidden`, re-derived from triggers on `connect()` (Morph-safe).
28
- * The change/input listener is removed on `disconnect()` (Turbo navigation included).
34
+ * Behavior only. Trigger `input` / `change`, dynamic targets and descendants,
35
+ * condition attributes, Values, native form reset, and Turbo retained-element
36
+ * morphs all reconcile against the live DOM. Direct writes to an input's live
37
+ * `checked` / `value` property remain observable only when the caller also emits
38
+ * the corresponding native event. DOM repair is silent. User input or the public
39
+ * action dispatches `stimeo--conditional-fields:change`; a lifecycle, Value,
40
+ * reset, mutation, or morph reconciliation dispatches
41
+ * `stimeo--conditional-fields:reconcile` instead. Initial connection is silent.
42
+ * Every listener, observer, queued pass, and borrowed root `tabindex` is released
43
+ * on `disconnect()`.
29
44
  */
30
45
  declare class ConditionalFieldsController extends Controller<HTMLElement> {
31
46
  #private;
@@ -41,15 +56,28 @@ declare class ConditionalFieldsController extends Controller<HTMLElement> {
41
56
  };
42
57
  };
43
58
  static actions: readonly ["evaluate"];
44
- static events: readonly ["change"];
59
+ static events: readonly ["change", "reconcile"];
45
60
  readonly triggerTargets: Trigger[];
46
61
  readonly regionTargets: HTMLElement[];
47
- readonly hasTriggerTarget: boolean;
48
62
  disableHiddenValue: boolean;
49
63
  matchValue: string;
64
+ /** Reflects initial live state and opens every retained-DOM reconciliation path. */
50
65
  connect(): void;
66
+ /** Releases all external resources and returns the temporary focus landmark loan. */
51
67
  disconnect(): void;
52
- /** Re-evaluates every region against the current trigger state. */
68
+ /** Reconciles after a trigger target is inserted or restored. */
69
+ triggerTargetConnected(): void;
70
+ /** Reconciles after a trigger target is removed or replaced. */
71
+ triggerTargetDisconnected(): void;
72
+ /** Initializes a region target inserted or restored at runtime. */
73
+ regionTargetConnected(): void;
74
+ /** Reconciles the remaining region set after a target leaves. */
75
+ regionTargetDisconnected(): void;
76
+ /** Reconciles when application code or a morph changes the match policy. */
77
+ matchValueChanged(): void;
78
+ /** Reconciles control ownership when hidden controls become enabled or excluded. */
79
+ disableHiddenValueChanged(): void;
80
+ /** Re-evaluates every region and repairs all reflected DOM state. */
53
81
  evaluate(): void;
54
82
  }
55
83
 
@@ -1,7 +1,134 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/conditional_fields_controller.ts
4
+
5
+ // src/utils/focus_candidate.ts
6
+ function inheritsFieldsetDisabled(control) {
7
+ let fieldset = control.closest("fieldset[disabled]");
8
+ while (fieldset) {
9
+ const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
10
+ if (!legend?.contains(control)) return true;
11
+ fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
12
+ }
13
+ return false;
14
+ }
15
+ function canTakeFocus(element) {
16
+ if (element.closest("[hidden], [inert]")) return false;
17
+ if (element instanceof HTMLInputElement && element.type === "hidden") return false;
18
+ if (!("disabled" in element)) return true;
19
+ if (element.disabled) return false;
20
+ return !inheritsFieldsetDisabled(element);
21
+ }
22
+
23
+ // src/utils/microtask_coalescer.ts
24
+ var MicrotaskCoalescer = class {
25
+ #run;
26
+ #queued = false;
27
+ #active = false;
28
+ #generation = 0;
29
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
30
+ constructor(run) {
31
+ this.#run = run;
32
+ }
33
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
34
+ activate() {
35
+ this.#active = true;
36
+ }
37
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
38
+ cancel() {
39
+ this.#active = false;
40
+ this.#queued = false;
41
+ this.#generation += 1;
42
+ }
43
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
44
+ schedule() {
45
+ if (!this.#active || this.#queued) return;
46
+ this.#queued = true;
47
+ const generation = this.#generation;
48
+ queueMicrotask(() => {
49
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
50
+ this.#queued = false;
51
+ this.#run();
52
+ });
53
+ }
54
+ };
55
+
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;
68
+ }
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
+ }
76
+ }
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);
82
+ }
83
+ };
84
+
85
+ // src/utils/tabindex_loan.ts
86
+ var TabindexLoan = class {
87
+ #value;
88
+ #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") {
97
+ this.#value = value;
98
+ }
99
+ /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
100
+ lend(element) {
101
+ if (element.hasAttribute("tabindex")) return;
102
+ element.setAttribute("tabindex", this.#value);
103
+ this.#lent.add(element);
104
+ this.#beforeCache.activate();
105
+ }
106
+ /** Takes back every loan whose value is still the one that was lent. */
107
+ returnAll() {
108
+ for (const element of this.#lent) {
109
+ if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
110
+ }
111
+ this.#lent.clear();
112
+ this.#beforeCache.deactivate();
113
+ }
114
+ };
115
+
3
116
  // src/controllers/conditional_fields_controller.ts
4
117
  var DISABLED_MARKER = "data-conditional-disabled";
118
+ var OBSERVED_ATTRIBUTES = [
119
+ "hidden",
120
+ "aria-hidden",
121
+ "data-visible",
122
+ DISABLED_MARKER,
123
+ "disabled",
124
+ "data-when-checked",
125
+ "data-when-unchecked",
126
+ "data-when-value",
127
+ "checked",
128
+ "selected",
129
+ "value",
130
+ "type"
131
+ ];
5
132
  var ConditionalFieldsController = class extends Controller {
6
133
  static targets = ["trigger", "region"];
7
134
  static values = {
@@ -9,56 +136,171 @@ var ConditionalFieldsController = class extends Controller {
9
136
  match: { type: String, default: "any" }
10
137
  };
11
138
  static actions = ["evaluate"];
12
- static events = ["change"];
139
+ static events = ["change", "reconcile"];
140
+ /** Logical state last committed for each current region; rebuilt on every connect. */
13
141
  #lastVisible = /* @__PURE__ */ new WeakMap();
14
- #onChange = () => {
15
- this.evaluate();
142
+ /** Controller writes awaiting an observer callback, separated from authored morphs. */
143
+ #internalWrites = /* @__PURE__ */ new WeakMap();
144
+ #observing = false;
145
+ /** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
146
+ #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
147
+ /** Provides the last-resort focus landmark without claiming an authored tabindex. */
148
+ #rootTabindex = new TabindexLoan();
149
+ /** Watches retained targets and controls whose declarative or reflected state changed. */
150
+ #observer = new MutationObserver((records) => this.#onMutations(records));
151
+ /** Evaluates only events whose source is one of this instance's live trigger targets. */
152
+ #onTriggerInput = (event) => {
153
+ const target = event.target;
154
+ const triggers = this.triggerTargets;
155
+ if (target instanceof Element && triggers.some((trigger) => trigger === target)) {
156
+ this.#commitChange(triggers);
157
+ }
158
+ };
159
+ /** Reconciles property-only retained-element changes after Turbo morphs. */
160
+ #onMorph = () => {
161
+ this.#reconcile.schedule();
162
+ };
163
+ /** Reconciles the settled defaults after a non-cancelled owning form reset. */
164
+ #onReset = (event) => {
165
+ const form = event.target;
166
+ if (form instanceof HTMLFormElement && this.#hasTriggerOwnedBy(form)) {
167
+ queueMicrotask(() => {
168
+ if (!event.defaultPrevented) this.#reconcile.schedule();
169
+ });
170
+ }
16
171
  };
172
+ /** Reflects initial live state and opens every retained-DOM reconciliation path. */
17
173
  connect() {
18
- this.evaluate();
19
- this.element.addEventListener("change", this.#onChange);
20
- this.element.addEventListener("input", this.#onChange);
174
+ this.#lastVisible = /* @__PURE__ */ new WeakMap();
175
+ this.#internalWrites = /* @__PURE__ */ new WeakMap();
176
+ this.#reconcile.activate();
177
+ this.#settle();
178
+ this.#observer.observe(this.element, {
179
+ attributes: true,
180
+ attributeFilter: OBSERVED_ATTRIBUTES,
181
+ childList: true,
182
+ subtree: true
183
+ });
184
+ this.#observing = true;
185
+ this.element.addEventListener("change", this.#onTriggerInput);
186
+ this.element.addEventListener("input", this.#onTriggerInput);
187
+ this.element.addEventListener("turbo:morph-element", this.#onMorph);
188
+ document.addEventListener("reset", this.#onReset, true);
21
189
  }
190
+ /** Releases all external resources and returns the temporary focus landmark loan. */
22
191
  disconnect() {
23
- this.element.removeEventListener("change", this.#onChange);
24
- this.element.removeEventListener("input", this.#onChange);
192
+ this.#reconcile.cancel();
193
+ this.#observing = false;
194
+ this.#observer.disconnect();
195
+ this.element.removeEventListener("change", this.#onTriggerInput);
196
+ this.element.removeEventListener("input", this.#onTriggerInput);
197
+ this.element.removeEventListener("turbo:morph-element", this.#onMorph);
198
+ document.removeEventListener("reset", this.#onReset, true);
199
+ this.#rootTabindex.returnAll();
200
+ }
201
+ /** Reconciles after a trigger target is inserted or restored. */
202
+ triggerTargetConnected() {
203
+ this.#reconcile.schedule();
204
+ }
205
+ /** Reconciles after a trigger target is removed or replaced. */
206
+ triggerTargetDisconnected() {
207
+ this.#reconcile.schedule();
208
+ }
209
+ /** Initializes a region target inserted or restored at runtime. */
210
+ regionTargetConnected() {
211
+ this.#reconcile.schedule();
212
+ }
213
+ /** Reconciles the remaining region set after a target leaves. */
214
+ regionTargetDisconnected() {
215
+ this.#reconcile.schedule();
25
216
  }
26
- /** Re-evaluates every region against the current trigger state. */
217
+ /** Reconciles when application code or a morph changes the match policy. */
218
+ matchValueChanged() {
219
+ this.#reconcile.schedule();
220
+ }
221
+ /** Reconciles control ownership when hidden controls become enabled or excluded. */
222
+ disableHiddenValueChanged() {
223
+ this.#reconcile.schedule();
224
+ }
225
+ /** Re-evaluates every region and repairs all reflected DOM state. */
27
226
  evaluate() {
28
- const triggers = this.triggerTargets;
29
- for (const region of this.regionTargets) {
30
- this.#applyRegion(region, this.#isVisible(region, triggers));
227
+ this.#commitChange();
228
+ }
229
+ /**
230
+ * Computes one settled plan, shows destinations before hiding sources, and then
231
+ * returns logical transitions in DOM order after every region is internally coherent.
232
+ */
233
+ #settle(triggers = this.triggerTargets) {
234
+ const regions = this.regionTargets;
235
+ const plan = /* @__PURE__ */ new Map();
236
+ for (const region of regions) plan.set(region, this.#isVisible(region, triggers));
237
+ const changed = /* @__PURE__ */ new Set();
238
+ const applicationOrder = [
239
+ ...regions.filter((region) => plan.get(region) === true),
240
+ ...regions.filter((region) => plan.get(region) === false)
241
+ ];
242
+ for (const region of applicationOrder) {
243
+ const visible = plan.get(region);
244
+ if (visible !== void 0 && this.#applyRegion(region, visible, plan)) {
245
+ changed.add(region);
246
+ }
247
+ }
248
+ return regions.filter((region) => changed.has(region)).map((region) => ({ region, visible: plan.get(region) === true }));
249
+ }
250
+ /** Reports logical transitions caused by user input or the public action. */
251
+ #commitChange(triggers = this.triggerTargets) {
252
+ for (const detail of this.#settle(triggers)) {
253
+ this.dispatch("change", { detail });
254
+ }
255
+ }
256
+ /** Reports logical transitions derived from DOM, lifecycle, reset, or Value state. */
257
+ #reconcileDom() {
258
+ for (const detail of this.#settle()) {
259
+ this.dispatch("reconcile", { detail });
31
260
  }
32
261
  }
33
- /** Applies a region's visibility, syncing hidden/aria/disabled and emitting change. */
34
- #applyRegion(region, visible) {
262
+ /** Applies one planned state while keeping event history separate from DOM repair. */
263
+ #applyRegion(region, visible, plan) {
35
264
  const previous = this.#lastVisible.get(region) ?? !region.hidden;
36
- if (visible === previous && this.#lastVisible.has(region)) return;
265
+ const changed = visible !== previous;
37
266
  this.#lastVisible.set(region, visible);
38
- if (visible) {
39
- region.hidden = false;
40
- region.removeAttribute("aria-hidden");
41
- region.setAttribute("data-visible", "true");
42
- this.#setRegionDisabled(region, false);
43
- } else {
44
- this.#retreatFocus(region);
45
- region.hidden = true;
46
- region.setAttribute("aria-hidden", "true");
47
- region.removeAttribute("data-visible");
48
- this.#setRegionDisabled(region, true);
49
- }
50
- if (visible !== previous) {
51
- this.dispatch("change", { detail: { region, visible } });
267
+ if (!visible) this.#retreatFocus(region, plan);
268
+ this.#syncRegionState(region, visible);
269
+ this.#syncRegionControls(region, visible);
270
+ return changed;
271
+ }
272
+ /** Reflects visibility idempotently, recording writes so the observer ignores its own work. */
273
+ #syncRegionState(region, visible) {
274
+ this.#writeAttribute(region, "hidden", visible ? null : "");
275
+ this.#writeAttribute(region, "aria-hidden", visible ? null : "true");
276
+ this.#writeAttribute(region, "data-visible", visible ? "true" : null);
277
+ }
278
+ /** Enables/disables descendant controls, touching only state carrying our marker. */
279
+ #syncRegionControls(region, visible) {
280
+ const shouldDisable = !visible && this.disableHiddenValue;
281
+ const controls = region.querySelectorAll("input, textarea, select, button");
282
+ for (const control of controls) {
283
+ const marked = control.hasAttribute(DISABLED_MARKER);
284
+ if (shouldDisable) {
285
+ if (!control.disabled) {
286
+ this.#writeAttribute(control, "disabled", "");
287
+ this.#writeAttribute(control, DISABLED_MARKER, "true");
288
+ } else if (marked && control.getAttribute(DISABLED_MARKER) !== "true") {
289
+ this.#writeAttribute(control, DISABLED_MARKER, "true");
290
+ }
291
+ } else if (marked) {
292
+ this.#writeAttribute(control, "disabled", null);
293
+ this.#writeAttribute(control, DISABLED_MARKER, null);
294
+ }
52
295
  }
53
296
  }
54
297
  /** Whether a region's declared condition holds across `triggers` per `match`. */
55
298
  #isVisible(region, triggers) {
56
299
  const predicate = this.#predicateFor(region);
57
300
  if (predicate === null) return !region.hidden;
58
- if (triggers.length === 0) return false;
59
- return this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate);
301
+ return triggers.length > 0 && (this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate));
60
302
  }
61
- /** Builds the per-trigger predicate from the region's `data-when-*` attribute. */
303
+ /** Builds the deterministic per-trigger predicate from the region's condition. */
62
304
  #predicateFor(region) {
63
305
  if (region.hasAttribute("data-when-checked")) {
64
306
  return (trigger) => this.#isChecked(trigger);
@@ -75,35 +317,87 @@ var ConditionalFieldsController = class extends Controller {
75
317
  #isChecked(trigger) {
76
318
  return trigger instanceof HTMLInputElement && trigger.checked;
77
319
  }
78
- /** A trigger "has" a value: selected radio/checkbox value, or the control value. */
320
+ /** A trigger has a value when a radio/checkbox is selected, or its control value matches. */
79
321
  #matchesValue(trigger, wanted) {
80
322
  if (trigger instanceof HTMLInputElement && (trigger.type === "checkbox" || trigger.type === "radio")) {
81
323
  return trigger.checked && trigger.value === wanted;
82
324
  }
83
325
  return trigger.value === wanted;
84
326
  }
85
- /** Enables/disables a region's inputs, tracking only the ones we disabled. */
86
- #setRegionDisabled(region, disabled) {
87
- if (!this.disableHiddenValue) return;
88
- const controls = region.querySelectorAll("input, textarea, select, button");
89
- for (const control of Array.from(controls)) {
90
- if (disabled) {
91
- if (!control.disabled) {
92
- control.disabled = true;
93
- control.setAttribute(DISABLED_MARKER, "true");
327
+ /** Moves focus to the first safe trigger, or to the controller landmark as a fallback. */
328
+ #retreatFocus(region, plan) {
329
+ const active = document.activeElement;
330
+ if (active && region.contains(active)) this.#moveFocus(plan);
331
+ }
332
+ /** Moves focus after the caller establishes that the active node is being hidden. */
333
+ #moveFocus(plan) {
334
+ const destination = this.triggerTargets.find(
335
+ (trigger) => canTakeFocus(trigger) && this.#survivesPlan(trigger, plan)
336
+ );
337
+ if (destination) {
338
+ this.#rootTabindex.returnAll();
339
+ destination.focus();
340
+ return;
341
+ }
342
+ this.#rootTabindex.lend(this.element);
343
+ this.element.focus();
344
+ }
345
+ /** Whether a focus candidate sits outside every region this pass will hide. */
346
+ #survivesPlan(candidate, plan) {
347
+ return [...plan].every(([region, visible]) => visible || !region.contains(candidate));
348
+ }
349
+ /** Records one idempotent attribute write before MutationObserver sees it. */
350
+ #writeAttribute(element, name, value) {
351
+ if (element.getAttribute(name) !== value) {
352
+ if (this.#observing) {
353
+ let writes = this.#internalWrites.get(element);
354
+ if (!writes) {
355
+ writes = /* @__PURE__ */ new Map();
356
+ this.#internalWrites.set(element, writes);
94
357
  }
95
- } else if (control.hasAttribute(DISABLED_MARKER)) {
96
- control.disabled = false;
97
- control.removeAttribute(DISABLED_MARKER);
358
+ writes.set(name, value);
98
359
  }
360
+ if (value === null) element.removeAttribute(name);
361
+ else element.setAttribute(name, value);
99
362
  }
100
363
  }
101
- /** Moves focus out of a region about to be hidden, to a trigger when possible. */
102
- #retreatFocus(region) {
103
- const active = document.activeElement;
104
- if (!(active instanceof HTMLElement) || !region.contains(active)) return;
105
- active.blur();
106
- if (this.hasTriggerTarget) this.triggerTargets[0]?.focus();
364
+ /** Separates authored records from controller writes and queues one relevant repair. */
365
+ #onMutations(records) {
366
+ const triggers = this.triggerTargets;
367
+ const regions = this.regionTargets;
368
+ const touchedWrites = /* @__PURE__ */ new Map();
369
+ let needsReconcile = false;
370
+ for (const record of records) {
371
+ if (record.type === "attributes" && record.target instanceof Element) {
372
+ const name = record.attributeName;
373
+ const writes = this.#internalWrites.get(record.target);
374
+ if (name && writes?.has(name)) {
375
+ let names = touchedWrites.get(record.target);
376
+ if (!names) {
377
+ names = /* @__PURE__ */ new Set();
378
+ touchedWrites.set(record.target, names);
379
+ }
380
+ names.add(name);
381
+ if (record.target.getAttribute(name) === writes.get(name)) continue;
382
+ }
383
+ }
384
+ if (record.target instanceof Element && this.#elementAffectsState(record.target, triggers, regions)) {
385
+ needsReconcile = true;
386
+ }
387
+ }
388
+ for (const [element, names] of touchedWrites) {
389
+ const writes = this.#internalWrites.get(element);
390
+ for (const name of names) writes?.delete(name);
391
+ }
392
+ if (needsReconcile) this.#reconcile.schedule();
393
+ }
394
+ /** Whether an authored mutation target belongs to a trigger, region, or managed control. */
395
+ #elementAffectsState(target, triggers, regions) {
396
+ return triggers.some((trigger) => trigger === target || trigger.contains(target)) || regions.some((region) => region === target || region.contains(target));
397
+ }
398
+ /** Whether a form owns at least one current trigger, including `form=` associations. */
399
+ #hasTriggerOwnedBy(form) {
400
+ return this.triggerTargets.some((trigger) => trigger.form === form);
107
401
  }
108
402
  };
109
403