stimeo-ui 0.15.0 → 0.16.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 (267) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +63 -15
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +8 -1
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +43 -2
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +9 -8
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +85 -23
  24. package/dist/controllers/calendar_controller.js +323 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +54 -10
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +56 -11
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/countdown_controller.d.ts +30 -1
  57. package/dist/controllers/countdown_controller.js +129 -26
  58. package/dist/controllers/countdown_controller.js.map +1 -1
  59. package/dist/controllers/currency_input_controller.d.ts +74 -15
  60. package/dist/controllers/currency_input_controller.js +205 -58
  61. package/dist/controllers/currency_input_controller.js.map +1 -1
  62. package/dist/controllers/data_grid_controller.d.ts +63 -18
  63. package/dist/controllers/data_grid_controller.js +195 -29
  64. package/dist/controllers/data_grid_controller.js.map +1 -1
  65. package/dist/controllers/date_range_picker_controller.d.ts +34 -6
  66. package/dist/controllers/date_range_picker_controller.js +139 -27
  67. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  68. package/dist/controllers/dialog_controller.d.ts +10 -3
  69. package/dist/controllers/dialog_controller.js +35 -8
  70. package/dist/controllers/dialog_controller.js.map +1 -1
  71. package/dist/controllers/direct_upload_controller.js +22 -4
  72. package/dist/controllers/direct_upload_controller.js.map +1 -1
  73. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  74. package/dist/controllers/dirty_form_controller.js +14 -1
  75. package/dist/controllers/dirty_form_controller.js.map +1 -1
  76. package/dist/controllers/dismissible_controller.js +1 -0
  77. package/dist/controllers/dismissible_controller.js.map +1 -1
  78. package/dist/controllers/drawer_controller.d.ts +18 -10
  79. package/dist/controllers/drawer_controller.js +54 -19
  80. package/dist/controllers/drawer_controller.js.map +1 -1
  81. package/dist/controllers/dropdown_controller.d.ts +9 -3
  82. package/dist/controllers/dropdown_controller.js +36 -9
  83. package/dist/controllers/dropdown_controller.js.map +1 -1
  84. package/dist/controllers/editable_controller.js +34 -0
  85. package/dist/controllers/editable_controller.js.map +1 -1
  86. package/dist/controllers/file_dropzone_controller.js +144 -51
  87. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  88. package/dist/controllers/filter_controller.d.ts +10 -4
  89. package/dist/controllers/filter_controller.js +20 -6
  90. package/dist/controllers/filter_controller.js.map +1 -1
  91. package/dist/controllers/flash_controller.d.ts +26 -6
  92. package/dist/controllers/flash_controller.js +305 -46
  93. package/dist/controllers/flash_controller.js.map +1 -1
  94. package/dist/controllers/focus_controller.js +1 -0
  95. package/dist/controllers/focus_controller.js.map +1 -1
  96. package/dist/controllers/form_field_controller.js +7 -5
  97. package/dist/controllers/form_field_controller.js.map +1 -1
  98. package/dist/controllers/form_validation_controller.js +19 -13
  99. package/dist/controllers/form_validation_controller.js.map +1 -1
  100. package/dist/controllers/frame_loading_controller.js +16 -2
  101. package/dist/controllers/frame_loading_controller.js.map +1 -1
  102. package/dist/controllers/highlight_controller.js +19 -2
  103. package/dist/controllers/highlight_controller.js.map +1 -1
  104. package/dist/controllers/hover_card_controller.d.ts +10 -2
  105. package/dist/controllers/hover_card_controller.js +40 -14
  106. package/dist/controllers/hover_card_controller.js.map +1 -1
  107. package/dist/controllers/idle_controller.d.ts +16 -3
  108. package/dist/controllers/idle_controller.js +90 -5
  109. package/dist/controllers/idle_controller.js.map +1 -1
  110. package/dist/controllers/input_mask_controller.d.ts +5 -2
  111. package/dist/controllers/input_mask_controller.js +65 -9
  112. package/dist/controllers/input_mask_controller.js.map +1 -1
  113. package/dist/controllers/intersection_controller.js +3 -0
  114. package/dist/controllers/intersection_controller.js.map +1 -1
  115. package/dist/controllers/lazy_frame_controller.js +11 -2
  116. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  117. package/dist/controllers/listbox_controller.d.ts +50 -8
  118. package/dist/controllers/listbox_controller.js +203 -45
  119. package/dist/controllers/listbox_controller.js.map +1 -1
  120. package/dist/controllers/masonry_controller.js +0 -2
  121. package/dist/controllers/masonry_controller.js.map +1 -1
  122. package/dist/controllers/menu_controller.d.ts +9 -3
  123. package/dist/controllers/menu_controller.js +45 -16
  124. package/dist/controllers/menu_controller.js.map +1 -1
  125. package/dist/controllers/menubar_controller.d.ts +11 -0
  126. package/dist/controllers/menubar_controller.js +58 -24
  127. package/dist/controllers/menubar_controller.js.map +1 -1
  128. package/dist/controllers/meter_controller.js +9 -5
  129. package/dist/controllers/meter_controller.js.map +1 -1
  130. package/dist/controllers/multi_select_controller.d.ts +18 -3
  131. package/dist/controllers/multi_select_controller.js +278 -104
  132. package/dist/controllers/multi_select_controller.js.map +1 -1
  133. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  134. package/dist/controllers/navigation_menu_controller.js +48 -15
  135. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  136. package/dist/controllers/nested_form_controller.js +37 -8
  137. package/dist/controllers/nested_form_controller.js.map +1 -1
  138. package/dist/controllers/network_status_controller.js +9 -1
  139. package/dist/controllers/network_status_controller.js.map +1 -1
  140. package/dist/controllers/number_input_controller.d.ts +36 -8
  141. package/dist/controllers/number_input_controller.js +124 -21
  142. package/dist/controllers/number_input_controller.js.map +1 -1
  143. package/dist/controllers/optimistic_controller.js +14 -1
  144. package/dist/controllers/optimistic_controller.js.map +1 -1
  145. package/dist/controllers/otp_controller.d.ts +22 -7
  146. package/dist/controllers/otp_controller.js +198 -55
  147. package/dist/controllers/otp_controller.js.map +1 -1
  148. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  149. package/dist/controllers/overflow_indicator_controller.js +84 -13
  150. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  151. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  152. package/dist/controllers/overflow_menu_controller.js +112 -41
  153. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  154. package/dist/controllers/pagination_controller.d.ts +19 -11
  155. package/dist/controllers/pagination_controller.js +74 -28
  156. package/dist/controllers/pagination_controller.js.map +1 -1
  157. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  158. package/dist/controllers/password_reveal_controller.js +59 -2
  159. package/dist/controllers/password_reveal_controller.js.map +1 -1
  160. package/dist/controllers/persist_controller.js +30 -8
  161. package/dist/controllers/persist_controller.js.map +1 -1
  162. package/dist/controllers/pointer_drag_controller.js +131 -52
  163. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  164. package/dist/controllers/popover_controller.d.ts +9 -3
  165. package/dist/controllers/popover_controller.js +45 -11
  166. package/dist/controllers/popover_controller.js.map +1 -1
  167. package/dist/controllers/portal_controller.d.ts +1 -1
  168. package/dist/controllers/portal_controller.js +6 -2
  169. package/dist/controllers/portal_controller.js.map +1 -1
  170. package/dist/controllers/preview_guard_controller.js +16 -1
  171. package/dist/controllers/preview_guard_controller.js.map +1 -1
  172. package/dist/controllers/progress_controller.js +8 -4
  173. package/dist/controllers/progress_controller.js.map +1 -1
  174. package/dist/controllers/radio_group_controller.d.ts +6 -4
  175. package/dist/controllers/radio_group_controller.js +42 -17
  176. package/dist/controllers/radio_group_controller.js.map +1 -1
  177. package/dist/controllers/range_slider_controller.d.ts +49 -1
  178. package/dist/controllers/range_slider_controller.js +88 -42
  179. package/dist/controllers/range_slider_controller.js.map +1 -1
  180. package/dist/controllers/rating_controller.d.ts +14 -3
  181. package/dist/controllers/rating_controller.js +39 -15
  182. package/dist/controllers/rating_controller.js.map +1 -1
  183. package/dist/controllers/read_more_controller.d.ts +24 -2
  184. package/dist/controllers/read_more_controller.js +69 -2
  185. package/dist/controllers/read_more_controller.js.map +1 -1
  186. package/dist/controllers/resizable_controller.d.ts +16 -2
  187. package/dist/controllers/resizable_controller.js +82 -22
  188. package/dist/controllers/resizable_controller.js.map +1 -1
  189. package/dist/controllers/scroll_area_controller.js +5 -1
  190. package/dist/controllers/scroll_area_controller.js.map +1 -1
  191. package/dist/controllers/scroll_visibility_controller.js +0 -1
  192. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  193. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  194. package/dist/controllers/scrollspy_controller.js +6 -0
  195. package/dist/controllers/scrollspy_controller.js.map +1 -1
  196. package/dist/controllers/separator_controller.d.ts +41 -14
  197. package/dist/controllers/separator_controller.js +66 -37
  198. package/dist/controllers/separator_controller.js.map +1 -1
  199. package/dist/controllers/sidebar_controller.d.ts +20 -3
  200. package/dist/controllers/sidebar_controller.js +77 -18
  201. package/dist/controllers/sidebar_controller.js.map +1 -1
  202. package/dist/controllers/skeleton_controller.js +6 -1
  203. package/dist/controllers/skeleton_controller.js.map +1 -1
  204. package/dist/controllers/slider_controller.d.ts +45 -7
  205. package/dist/controllers/slider_controller.js +82 -47
  206. package/dist/controllers/slider_controller.js.map +1 -1
  207. package/dist/controllers/smart_sticky_header_controller.js +11 -1
  208. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  209. package/dist/controllers/sortable_controller.js +17 -2
  210. package/dist/controllers/sortable_controller.js.map +1 -1
  211. package/dist/controllers/spinner_controller.js +10 -2
  212. package/dist/controllers/spinner_controller.js.map +1 -1
  213. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  214. package/dist/controllers/step_indicator_controller.js +18 -17
  215. package/dist/controllers/step_indicator_controller.js.map +1 -1
  216. package/dist/controllers/stepper_controller.d.ts +34 -9
  217. package/dist/controllers/stepper_controller.js +101 -19
  218. package/dist/controllers/stepper_controller.js.map +1 -1
  219. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  220. package/dist/controllers/stick_to_bottom_controller.js +75 -5
  221. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  222. package/dist/controllers/submit_once_controller.d.ts +3 -2
  223. package/dist/controllers/submit_once_controller.js +16 -3
  224. package/dist/controllers/submit_once_controller.js.map +1 -1
  225. package/dist/controllers/switch_controller.d.ts +29 -7
  226. package/dist/controllers/switch_controller.js +101 -10
  227. package/dist/controllers/switch_controller.js.map +1 -1
  228. package/dist/controllers/tabs_controller.d.ts +12 -0
  229. package/dist/controllers/tabs_controller.js +21 -2
  230. package/dist/controllers/tabs_controller.js.map +1 -1
  231. package/dist/controllers/tags_input_controller.d.ts +15 -3
  232. package/dist/controllers/tags_input_controller.js +209 -59
  233. package/dist/controllers/tags_input_controller.js.map +1 -1
  234. package/dist/controllers/theme_controller.d.ts +20 -3
  235. package/dist/controllers/theme_controller.js +64 -14
  236. package/dist/controllers/theme_controller.js.map +1 -1
  237. package/dist/controllers/time_picker_controller.js +23 -8
  238. package/dist/controllers/time_picker_controller.js.map +1 -1
  239. package/dist/controllers/toast_controller.d.ts +52 -15
  240. package/dist/controllers/toast_controller.js +337 -54
  241. package/dist/controllers/toast_controller.js.map +1 -1
  242. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  243. package/dist/controllers/toggle_group_controller.js +159 -23
  244. package/dist/controllers/toggle_group_controller.js.map +1 -1
  245. package/dist/controllers/toolbar_controller.js +32 -0
  246. package/dist/controllers/toolbar_controller.js.map +1 -1
  247. package/dist/controllers/tooltip_controller.d.ts +8 -0
  248. package/dist/controllers/tooltip_controller.js +39 -13
  249. package/dist/controllers/tooltip_controller.js.map +1 -1
  250. package/dist/controllers/transition_controller.js +4 -0
  251. package/dist/controllers/transition_controller.js.map +1 -1
  252. package/dist/controllers/tree_view_controller.d.ts +39 -8
  253. package/dist/controllers/tree_view_controller.js +169 -16
  254. package/dist/controllers/tree_view_controller.js.map +1 -1
  255. package/dist/index.d.ts +28 -1
  256. package/dist/index.js +4588 -1535
  257. package/dist/index.js.map +1 -1
  258. package/dist/inspector/cli.d.ts +72 -6
  259. package/dist/inspector/cli.js +262 -51
  260. package/dist/inspector/cli.js.map +1 -1
  261. package/dist/inspector/cli_bin.js +309 -51
  262. package/dist/inspector/cli_bin.js.map +1 -1
  263. package/dist/inspector/examples.json +40 -40
  264. package/dist/inspector/manifest.json +525 -59
  265. package/dist/positioning/index.js +2 -0
  266. package/dist/positioning/index.js.map +1 -1
  267. package/package.json +2 -2
@@ -8,6 +8,16 @@ function isReservedArrowChord(event, allow = []) {
8
8
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
9
  }
10
10
 
11
+ // src/utils/field_mirror.ts
12
+ function writeField(field, value) {
13
+ if (field.value === value) return false;
14
+ field.value = value;
15
+ return true;
16
+ }
17
+ function commitField(target) {
18
+ target.dispatchEvent(new Event("change", { bubbles: true }));
19
+ }
20
+
11
21
  // src/utils/microtask_coalescer.ts
12
22
  var MicrotaskCoalescer = class {
13
23
  #run;
@@ -165,45 +175,79 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
165
175
  }
166
176
 
167
177
  // src/controllers/listbox_controller.ts
178
+ var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
179
+ function setAttributeIfChanged(element, name, value) {
180
+ if (element.getAttribute(name) !== value) element.setAttribute(name, value);
181
+ }
168
182
  var ListboxController = class extends Controller {
169
183
  static targets = ["trigger", "value", "list", "option", "field"];
184
+ static values = {
185
+ placeholder: { type: String, default: "" }
186
+ };
170
187
  static actions = ["close", "onTriggerKeydown", "open", "select", "toggle"];
171
- static events = ["change"];
188
+ static events = ["change", "reconcile"];
172
189
  /** Stable ID of the active option; DOM targets are resolved afresh before use. */
173
190
  #activeId = null;
174
191
  /** Target ID order captured while an option is active, used only for removal fallback. */
175
192
  #activeOrder = [];
176
- #connected = false;
177
- /** Collapses one mutation batch of target callbacks into a single pass. */
178
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileActive());
193
+ /**
194
+ * Collapses the target callbacks, Value changes and observed option writes of
195
+ * one DOM mutation into a single pass; refused before `connect()` and after
196
+ * `disconnect()`.
197
+ */
198
+ #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());
199
+ /** Watches the option attributes a page can rewrite in place; set while connected. */
200
+ #observer = null;
201
+ /** The selection last settled: on connect, by the user, or by a reported pass. */
202
+ #settled = { option: null, value: "" };
179
203
  /** Accumulated typeahead query and its idle-reset timer. */
180
204
  #typeahead = new Typeahead();
181
- /** Establishes the ARIA baseline, starts closed, and listens for outside clicks. */
205
+ /**
206
+ * Establishes the ARIA baseline, settles the selection without reporting it,
207
+ * starts closed, and listens for outside clicks.
208
+ */
182
209
  connect() {
183
210
  this.#normalizeSelection();
184
211
  this.close();
185
212
  document.addEventListener("click", this.#onOutsideClick, true);
186
- this.#connected = true;
213
+ this.#settled = this.#selection();
187
214
  this.#reconcile.activate();
215
+ this.#observeOptions();
188
216
  }
189
217
  /**
190
- * Establishes an inactive baseline for a late option, re-resolves active
191
- * identity, and re-applies the selection baseline.
218
+ * Establishes an inactive baseline for a late option, then re-applies the
219
+ * selection baseline and re-resolves active identity once the batch is in.
192
220
  */
193
221
  optionTargetConnected(option) {
194
222
  option.removeAttribute("data-active");
195
- if (!this.#connected) return;
196
- this.#normalizeSelection();
197
- this.#queueOptionReconciliation();
223
+ this.#reconcile.schedule();
198
224
  }
199
- /** Removes controller-owned active state and reconciles the surviving targets. */
225
+ /**
226
+ * Removes controller-owned active state, then settles the selection and the
227
+ * active option among the surviving targets once the batch is in.
228
+ */
200
229
  optionTargetDisconnected(option) {
201
230
  option.removeAttribute("data-active");
202
- if (this.#connected) this.#queueOptionReconciliation();
231
+ this.#reconcile.schedule();
232
+ }
233
+ /** Fills a form field inserted or replaced at runtime with the current selection. */
234
+ fieldTargetConnected() {
235
+ this.#reconcile.schedule();
236
+ }
237
+ /** Writes the current selection into a trigger value inserted or replaced at runtime. */
238
+ valueTargetConnected() {
239
+ this.#reconcile.schedule();
240
+ }
241
+ /**
242
+ * Re-renders the empty-selection label when application code (or a Turbo morph)
243
+ * changes `placeholder` at runtime.
244
+ */
245
+ placeholderValueChanged() {
246
+ this.#reconcile.schedule();
203
247
  }
204
248
  /**
205
249
  * Brings the authored DOM to the shape the APG requires, and derives the state
206
- * that follows from the initial selection.
250
+ * that follows from the selection.
207
251
  *
208
252
  * Three things happen, and only these three — which option is chosen is the
209
253
  * author's, and is never changed:
@@ -212,27 +256,59 @@ var ListboxController = class extends Controller {
212
256
  * selectable" in ARIA, so a forgotten attribute hides a selectable option
213
257
  * from assistive technology.
214
258
  * 2. At most one stays `true`. The first in DOM order wins, since that is the
215
- * only deterministic reading of "which one did the author mean".
216
- * 3. The trigger label and the hidden field are derived from that selection.
217
- * Without this the widget announces a choice it does not submit: the popup
218
- * says "Banana", the trigger still says "Choose…", and the form posts "".
259
+ * only deterministic reading of "which one did the author mean"; an option
260
+ * the page inserts ahead of the selection, already selected, takes it over.
261
+ * 3. The trigger label and the hidden field are derived from that selection,
262
+ * an empty one included. Without this the widget announces a choice it does
263
+ * not submit: the popup says "Banana", the trigger still says "Choose…", and
264
+ * the form posts "".
219
265
  *
220
- * No `change` fires — nothing changed, this is the initial state being told
221
- * properly. The scan is the `option` target set: a `role="option"` without the
222
- * target is outside the contract and is neither counted nor written.
266
+ * Nothing is reported here: `connect()` describes the initial state, and a
267
+ * page-driven pass reports a moved selection itself. The scan is the `option`
268
+ * target set: a `role="option"` without the target is outside the contract and
269
+ * is neither counted nor written.
223
270
  */
224
271
  #normalizeSelection() {
225
- const options = this.optionTargets;
226
- const selected = options.find((option) => option.getAttribute("aria-selected") === "true");
227
- for (const option of options) {
228
- option.setAttribute("aria-selected", option === selected ? "true" : "false");
229
- }
230
- if (selected) this.#applySelection(selected);
272
+ const selected = this.optionTargets.find(
273
+ (option) => option.getAttribute("aria-selected") === "true"
274
+ );
275
+ this.#applySelection(selected);
276
+ }
277
+ /**
278
+ * The pass the page's changes run: the selection is normalized and mirrored,
279
+ * the active option re-resolved, then a selection that moved is reported. The
280
+ * report comes last, so options a subscriber rewrites are the next pass's to
281
+ * settle.
282
+ */
283
+ #reconcileOptions() {
284
+ this.#normalizeSelection();
285
+ this.#reconcileActive();
286
+ this.#reportMove();
231
287
  }
232
- /** Removes the document listener and clears the typeahead timer. */
288
+ /**
289
+ * Reports the selection as `reconcile` when its option or value differs from the
290
+ * selection last settled. The settled selection is replaced before dispatching,
291
+ * so a move a subscriber makes is measured against what it was told.
292
+ */
293
+ #reportMove() {
294
+ const { option, value } = this.#selection();
295
+ if (option === this.#settled.option && value === this.#settled.value) return;
296
+ this.#settled = { option, value };
297
+ this.dispatch("reconcile", { detail: { value, option } });
298
+ }
299
+ /** The selected option target, if any, and the value its field submits. */
300
+ #selection() {
301
+ const option = this.optionTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
302
+ return { option, value: option ? this.#optionValue(option) : "" };
303
+ }
304
+ /**
305
+ * Releases the outside-click listener and the option observer, drops a queued
306
+ * pass, and clears the typeahead timer.
307
+ */
233
308
  disconnect() {
234
- this.#connected = false;
235
309
  this.#reconcile.cancel();
310
+ this.#observer?.disconnect();
311
+ this.#observer = null;
236
312
  document.removeEventListener("click", this.#onOutsideClick, true);
237
313
  this.#typeahead.reset();
238
314
  }
@@ -354,27 +430,113 @@ var ListboxController = class extends Controller {
354
430
  /** Applies selection: `aria-selected`, trigger label, hidden field, `change`. */
355
431
  #selectOption(option) {
356
432
  const { value, fieldChanged } = this.#applySelection(option);
357
- if (fieldChanged) {
358
- this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
359
- }
433
+ this.#settled = { option, value };
434
+ if (fieldChanged) commitField(this.fieldTarget);
360
435
  this.dispatch("change", { detail: { value, option } });
361
436
  }
362
437
  /**
363
- * Writes `option` into `aria-selected`, the trigger label and the hidden field.
364
- * Emits nothing — {@link #normalizeSelection} reuses this at connect, where the
365
- * state is being described rather than changed.
438
+ * Writes `option` — or, when it is `undefined`, no selection — into
439
+ * `aria-selected`, the trigger label and the hidden field. Emits nothing: the
440
+ * user's commit and a page-driven pass each report it their own way, and
441
+ * `connect()` only describes the state it finds.
366
442
  */
367
443
  #applySelection(option) {
368
444
  for (const candidate of this.optionTargets) {
369
- candidate.setAttribute("aria-selected", candidate === option ? "true" : "false");
445
+ setAttributeIfChanged(candidate, "aria-selected", candidate === option ? "true" : "false");
370
446
  }
371
- const label = (option.textContent ?? "").trim();
372
- const value = option.dataset.value ?? label;
373
- if (this.hasValueTarget) this.valueTarget.textContent = label;
374
- const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;
375
- if (fieldChanged) this.fieldTarget.value = value;
447
+ this.#dropOwnRecords();
448
+ const value = option ? this.#optionValue(option) : "";
449
+ this.#renderLabel(option);
450
+ const fieldChanged = this.hasFieldTarget && writeField(this.fieldTarget, value);
376
451
  return { value, fieldChanged };
377
452
  }
453
+ /**
454
+ * Keeps the trigger value on the selection: the selected option's text, or, with
455
+ * nothing selected, `placeholder` when it is set, else the text the value held
456
+ * before this controller first wrote into it.
457
+ *
458
+ * The value is this controller's only while it holds the text last written into
459
+ * it and nothing else — the `owns-label` marker carries that text — and the text
460
+ * it held before is kept in the `original-label` marker. A value the page
461
+ * rewrote or added an element to after that — the marker is still there but no
462
+ * longer matches — is the page's: with nothing selected it is left exactly as it
463
+ * is, `placeholder` or not, and the next selection remembers its text before
464
+ * replacing it. A value with no marker at all — the author's, or one a morph put
465
+ * back without the markers — is taken over the same way by `placeholder` or by a
466
+ * selection. A value that only ever showed a selection has no text of its own to
467
+ * return to, so it is emptied rather than left naming an option that is not
468
+ * selected.
469
+ *
470
+ * @stimeoRenderRoot
471
+ */
472
+ #renderLabel(option) {
473
+ if (!this.hasValueTarget) return;
474
+ const label = this.valueTarget;
475
+ const ownsLabel = `data-${this.identifier}-owns-label`;
476
+ const originalLabel = `data-${this.identifier}-original-label`;
477
+ const owned = label.getAttribute(ownsLabel) === label.textContent && label.childElementCount === 0;
478
+ if (option === void 0 && !owned && label.hasAttribute(ownsLabel)) return;
479
+ const text = option ? this.#optionLabel(option) : this.placeholderValue || null;
480
+ if (text === null) {
481
+ if (!owned) return;
482
+ label.textContent = label.getAttribute(originalLabel) ?? "";
483
+ label.removeAttribute(ownsLabel);
484
+ label.removeAttribute(originalLabel);
485
+ return;
486
+ }
487
+ if (!owned) {
488
+ const current = (label.textContent ?? "").trim();
489
+ if (option === void 0 || current !== text) {
490
+ setAttributeIfChanged(label, originalLabel, current);
491
+ }
492
+ }
493
+ if (label.textContent !== text) label.textContent = text;
494
+ setAttributeIfChanged(label, ownsLabel, text);
495
+ }
496
+ /** An option's visible label: its own text, trimmed. */
497
+ #optionLabel(option) {
498
+ return (option.textContent ?? "").trim();
499
+ }
500
+ /** The value an option submits: its `data-value`, else its label. */
501
+ #optionValue(option) {
502
+ return option.dataset.value ?? this.#optionLabel(option);
503
+ }
504
+ /**
505
+ * Watches the options' `aria-selected` and `data-value`. The listbox's own writes
506
+ * to them are dropped from the queue as they happen, so each record the callback
507
+ * receives is the page's, and only a record on one of the listbox's own options
508
+ * schedules a pass.
509
+ */
510
+ #observeOptions() {
511
+ const observer = new MutationObserver((records) => {
512
+ if (this.#concernsOptions(records)) this.#reconcile.schedule();
513
+ });
514
+ observer.observe(this.element, {
515
+ subtree: true,
516
+ attributes: true,
517
+ attributeFilter: OBSERVED_ATTRIBUTES
518
+ });
519
+ this.#observer = observer;
520
+ }
521
+ /**
522
+ * Whether any record is about one of this listbox's own options. A widget nested
523
+ * inside it writes the same attributes on its own elements, and those are no
524
+ * reason to reconcile the listbox.
525
+ */
526
+ #concernsOptions(records) {
527
+ const options = new Set(this.optionTargets);
528
+ return records.some((record) => options.has(record.target));
529
+ }
530
+ /**
531
+ * Drops the records the listbox's own writes just queued, so the observer never
532
+ * takes them for the page's. A page write queued just before goes with them and
533
+ * owes nothing: every such write leaves every option's `aria-selected` at the
534
+ * value the selection gives it, and the selection is then read back from the DOM,
535
+ * so it already holds what the page wrote.
536
+ */
537
+ #dropOwnRecords() {
538
+ this.#observer?.takeRecords();
539
+ }
378
540
  /**
379
541
  * Marks the option at `index` active via `data-active` and the trigger's
380
542
  * `aria-activedescendant`. Pass `-1` to clear it (the attribute is removed, not
@@ -444,10 +606,6 @@ var ListboxController = class extends Controller {
444
606
  }
445
607
  return -1;
446
608
  }
447
- /** Coalesces all target callbacks from one MutationObserver batch. */
448
- #queueOptionReconciliation() {
449
- this.#reconcile.schedule();
450
- }
451
609
  /**
452
610
  * Advances the typeahead query and activates the next matching option.
453
611
  *