stimeo-ui 0.15.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 (332) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +128 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +392 -417
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +34 -5
  7. package/dist/controllers/accordion_controller.js +228 -28
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +20 -9
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +38 -7
  13. package/dist/controllers/announcer_controller.js +135 -191
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +295 -110
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +172 -106
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  23. package/dist/controllers/breadcrumb_controller.js +0 -129
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +22 -4
  26. package/dist/controllers/bulk_select_controller.js +264 -53
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +109 -28
  29. package/dist/controllers/calendar_controller.js +346 -180
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +99 -27
  32. package/dist/controllers/carousel_controller.js +587 -300
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +18 -2
  35. package/dist/controllers/character_counter_controller.js +271 -143
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +77 -75
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +44 -7
  41. package/dist/controllers/clipboard_controller.js +187 -108
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +41 -5
  44. package/dist/controllers/collapsible_controller.js +374 -108
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +47 -14
  47. package/dist/controllers/color_picker_controller.js +183 -127
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +33 -6
  50. package/dist/controllers/combobox_controller.js +426 -109
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +45 -14
  53. package/dist/controllers/command_palette_controller.js +994 -350
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +212 -125
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +939 -245
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +36 -3
  62. package/dist/controllers/context_menu_controller.js +275 -127
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +75 -2
  68. package/dist/controllers/countdown_controller.js +235 -97
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -15
  71. package/dist/controllers/currency_input_controller.js +241 -118
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +78 -22
  74. package/dist/controllers/data_grid_controller.js +229 -135
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +64 -11
  77. package/dist/controllers/date_range_picker_controller.js +359 -165
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +22 -3
  80. package/dist/controllers/dialog_controller.js +887 -225
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  83. package/dist/controllers/direct_upload_controller.js +80 -142
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  86. package/dist/controllers/dirty_form_controller.js +26 -47
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +175 -20
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +52 -16
  92. package/dist/controllers/drawer_controller.js +963 -336
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +39 -3
  95. package/dist/controllers/dropdown_controller.js +342 -85
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +182 -58
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +67 -53
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  103. package/dist/controllers/file_dropzone_controller.js +426 -229
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.d.ts +10 -4
  106. package/dist/controllers/filter_controller.js +78 -36
  107. package/dist/controllers/filter_controller.js.map +1 -1
  108. package/dist/controllers/flash_controller.d.ts +57 -14
  109. package/dist/controllers/flash_controller.js +328 -211
  110. package/dist/controllers/flash_controller.js.map +1 -1
  111. package/dist/controllers/focus_controller.d.ts +12 -3
  112. package/dist/controllers/focus_controller.js +654 -239
  113. package/dist/controllers/focus_controller.js.map +1 -1
  114. package/dist/controllers/form_field_controller.js +153 -136
  115. package/dist/controllers/form_field_controller.js.map +1 -1
  116. package/dist/controllers/form_validation_controller.js +29 -96
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  119. package/dist/controllers/frame_loading_controller.js +263 -198
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +22 -2
  122. package/dist/controllers/highlight_controller.js +119 -70
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +65 -4
  125. package/dist/controllers/hover_card_controller.js +335 -118
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +58 -4
  128. package/dist/controllers/idle_controller.js +316 -65
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +5 -2
  131. package/dist/controllers/input_mask_controller.js +116 -61
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +147 -103
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  137. package/dist/controllers/lazy_frame_controller.js +9 -56
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +84 -10
  140. package/dist/controllers/listbox_controller.js +413 -153
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +1 -1
  143. package/dist/controllers/local_time_controller.js +54 -64
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +70 -95
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +40 -9
  149. package/dist/controllers/menu_controller.js +254 -167
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +15 -4
  152. package/dist/controllers/menubar_controller.js +206 -331
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +20 -0
  155. package/dist/controllers/meter_controller.js +159 -71
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +50 -7
  158. package/dist/controllers/multi_select_controller.js +797 -388
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +19 -1
  161. package/dist/controllers/navigation_menu_controller.js +156 -231
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +373 -174
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +241 -45
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +62 -10
  170. package/dist/controllers/number_input_controller.js +387 -227
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.js +28 -75
  173. package/dist/controllers/optimistic_controller.js.map +1 -1
  174. package/dist/controllers/otp_controller.d.ts +38 -7
  175. package/dist/controllers/otp_controller.js +344 -196
  176. package/dist/controllers/otp_controller.js.map +1 -1
  177. package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
  178. package/dist/controllers/overflow_indicator_controller.js +167 -93
  179. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  180. package/dist/controllers/overflow_menu_controller.d.ts +66 -9
  181. package/dist/controllers/overflow_menu_controller.js +408 -277
  182. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  183. package/dist/controllers/pagination_controller.d.ts +52 -15
  184. package/dist/controllers/pagination_controller.js +342 -120
  185. package/dist/controllers/pagination_controller.js.map +1 -1
  186. package/dist/controllers/password_reveal_controller.d.ts +60 -9
  187. package/dist/controllers/password_reveal_controller.js +424 -99
  188. package/dist/controllers/password_reveal_controller.js.map +1 -1
  189. package/dist/controllers/password_strength_controller.d.ts +14 -5
  190. package/dist/controllers/password_strength_controller.js +99 -153
  191. package/dist/controllers/password_strength_controller.js.map +1 -1
  192. package/dist/controllers/persist_controller.d.ts +11 -4
  193. package/dist/controllers/persist_controller.js +252 -96
  194. package/dist/controllers/persist_controller.js.map +1 -1
  195. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  196. package/dist/controllers/pointer_drag_controller.js +263 -164
  197. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  198. package/dist/controllers/popover_controller.d.ts +46 -4
  199. package/dist/controllers/popover_controller.js +267 -94
  200. package/dist/controllers/popover_controller.js.map +1 -1
  201. package/dist/controllers/portal_controller.d.ts +44 -4
  202. package/dist/controllers/portal_controller.js +209 -72
  203. package/dist/controllers/portal_controller.js.map +1 -1
  204. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  205. package/dist/controllers/preview_guard_controller.js +243 -112
  206. package/dist/controllers/preview_guard_controller.js.map +1 -1
  207. package/dist/controllers/progress_controller.d.ts +11 -0
  208. package/dist/controllers/progress_controller.js +132 -59
  209. package/dist/controllers/progress_controller.js.map +1 -1
  210. package/dist/controllers/radio_group_controller.d.ts +19 -8
  211. package/dist/controllers/radio_group_controller.js +138 -133
  212. package/dist/controllers/radio_group_controller.js.map +1 -1
  213. package/dist/controllers/range_slider_controller.d.ts +92 -4
  214. package/dist/controllers/range_slider_controller.js +245 -112
  215. package/dist/controllers/range_slider_controller.js.map +1 -1
  216. package/dist/controllers/rating_controller.d.ts +33 -6
  217. package/dist/controllers/rating_controller.js +429 -194
  218. package/dist/controllers/rating_controller.js.map +1 -1
  219. package/dist/controllers/read_more_controller.d.ts +26 -2
  220. package/dist/controllers/read_more_controller.js +185 -47
  221. package/dist/controllers/read_more_controller.js.map +1 -1
  222. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  223. package/dist/controllers/reading_progress_controller.js +147 -135
  224. package/dist/controllers/reading_progress_controller.js.map +1 -1
  225. package/dist/controllers/relative_time_controller.d.ts +18 -4
  226. package/dist/controllers/relative_time_controller.js +145 -110
  227. package/dist/controllers/relative_time_controller.js.map +1 -1
  228. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  229. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  230. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  231. package/dist/controllers/resizable_controller.d.ts +44 -4
  232. package/dist/controllers/resizable_controller.js +294 -118
  233. package/dist/controllers/resizable_controller.js.map +1 -1
  234. package/dist/controllers/roving_controller.js +39 -84
  235. package/dist/controllers/roving_controller.js.map +1 -1
  236. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  237. package/dist/controllers/scroll_area_controller.js +428 -221
  238. package/dist/controllers/scroll_area_controller.js.map +1 -1
  239. package/dist/controllers/scroll_restore_controller.js +0 -53
  240. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  241. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  242. package/dist/controllers/scroll_visibility_controller.js +395 -146
  243. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  244. package/dist/controllers/scrollspy_controller.d.ts +9 -3
  245. package/dist/controllers/scrollspy_controller.js +181 -236
  246. package/dist/controllers/scrollspy_controller.js.map +1 -1
  247. package/dist/controllers/separator_controller.d.ts +59 -17
  248. package/dist/controllers/separator_controller.js +288 -132
  249. package/dist/controllers/separator_controller.js.map +1 -1
  250. package/dist/controllers/sidebar_controller.d.ts +81 -7
  251. package/dist/controllers/sidebar_controller.js +1071 -375
  252. package/dist/controllers/sidebar_controller.js.map +1 -1
  253. package/dist/controllers/skeleton_controller.d.ts +23 -0
  254. package/dist/controllers/skeleton_controller.js +248 -92
  255. package/dist/controllers/skeleton_controller.js.map +1 -1
  256. package/dist/controllers/slider_controller.d.ts +72 -8
  257. package/dist/controllers/slider_controller.js +211 -96
  258. package/dist/controllers/slider_controller.js.map +1 -1
  259. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  260. package/dist/controllers/smart_sticky_header_controller.js +163 -54
  261. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  262. package/dist/controllers/sortable_controller.js +13 -110
  263. package/dist/controllers/sortable_controller.js.map +1 -1
  264. package/dist/controllers/spinner_controller.d.ts +39 -4
  265. package/dist/controllers/spinner_controller.js +277 -161
  266. package/dist/controllers/spinner_controller.js.map +1 -1
  267. package/dist/controllers/step_indicator_controller.d.ts +24 -17
  268. package/dist/controllers/step_indicator_controller.js +107 -53
  269. package/dist/controllers/step_indicator_controller.js.map +1 -1
  270. package/dist/controllers/stepper_controller.d.ts +43 -9
  271. package/dist/controllers/stepper_controller.js +171 -48
  272. package/dist/controllers/stepper_controller.js.map +1 -1
  273. package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
  274. package/dist/controllers/stick_to_bottom_controller.js +310 -116
  275. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  276. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  277. package/dist/controllers/sticky_observer_controller.js +214 -49
  278. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  279. package/dist/controllers/submit_once_controller.d.ts +22 -6
  280. package/dist/controllers/submit_once_controller.js +294 -192
  281. package/dist/controllers/submit_once_controller.js.map +1 -1
  282. package/dist/controllers/switch_controller.d.ts +40 -7
  283. package/dist/controllers/switch_controller.js +107 -43
  284. package/dist/controllers/switch_controller.js.map +1 -1
  285. package/dist/controllers/tabs_controller.d.ts +13 -1
  286. package/dist/controllers/tabs_controller.js +50 -25
  287. package/dist/controllers/tabs_controller.js.map +1 -1
  288. package/dist/controllers/tags_input_controller.d.ts +27 -3
  289. package/dist/controllers/tags_input_controller.js +290 -149
  290. package/dist/controllers/tags_input_controller.js.map +1 -1
  291. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  292. package/dist/controllers/textarea_autosize_controller.js +71 -58
  293. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  294. package/dist/controllers/theme_controller.d.ts +37 -8
  295. package/dist/controllers/theme_controller.js +148 -80
  296. package/dist/controllers/theme_controller.js.map +1 -1
  297. package/dist/controllers/time_picker_controller.d.ts +18 -1
  298. package/dist/controllers/time_picker_controller.js +162 -81
  299. package/dist/controllers/time_picker_controller.js.map +1 -1
  300. package/dist/controllers/toast_controller.d.ts +85 -23
  301. package/dist/controllers/toast_controller.js +365 -193
  302. package/dist/controllers/toast_controller.js.map +1 -1
  303. package/dist/controllers/toggle_group_controller.d.ts +60 -9
  304. package/dist/controllers/toggle_group_controller.js +194 -99
  305. package/dist/controllers/toggle_group_controller.js.map +1 -1
  306. package/dist/controllers/toolbar_controller.js +92 -90
  307. package/dist/controllers/toolbar_controller.js.map +1 -1
  308. package/dist/controllers/tooltip_controller.d.ts +52 -2
  309. package/dist/controllers/tooltip_controller.js +185 -125
  310. package/dist/controllers/tooltip_controller.js.map +1 -1
  311. package/dist/controllers/transition_controller.d.ts +22 -4
  312. package/dist/controllers/transition_controller.js +123 -115
  313. package/dist/controllers/transition_controller.js.map +1 -1
  314. package/dist/controllers/tree_view_controller.d.ts +59 -18
  315. package/dist/controllers/tree_view_controller.js +248 -290
  316. package/dist/controllers/tree_view_controller.js.map +1 -1
  317. package/dist/index.d.ts +33 -3
  318. package/dist/index.js +8649 -8572
  319. package/dist/index.js.map +1 -1
  320. package/dist/inspector/cli.d.ts +95 -9
  321. package/dist/inspector/cli.js +343 -77
  322. package/dist/inspector/cli.js.map +1 -1
  323. package/dist/inspector/cli_bin.js +390 -96
  324. package/dist/inspector/cli_bin.js.map +1 -1
  325. package/dist/inspector/examples.json +45 -45
  326. package/dist/inspector/manifest.json +1308 -155
  327. package/dist/positioning/index.d.ts +8 -0
  328. package/dist/positioning/index.js +148 -45
  329. package/dist/positioning/index.js.map +1 -1
  330. package/package.json +7 -7
  331. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  332. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,34 +1,215 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/listbox_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/arrow_step.ts
6
25
  function isReservedArrowChord(event, allow = []) {
7
26
  if (!event.key.startsWith("Arrow")) return false;
8
27
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
28
  }
10
29
 
11
- // src/utils/microtask_coalescer.ts
30
+ function sharedRegistry(key, create) {
31
+ const symbol = Symbol.for(key);
32
+ const scope = globalThis;
33
+ const existing = scope[symbol];
34
+ if (existing !== void 0) return existing;
35
+ const registry = create();
36
+ Object.defineProperty(globalThis, symbol, { value: registry });
37
+ return registry;
38
+ }
39
+
40
+ function isAttributeValue(value) {
41
+ return value === null || typeof value === "string";
42
+ }
43
+ function readLeaseRecord(element, record) {
44
+ try {
45
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
46
+ if (!Array.isArray(parsed)) return void 0;
47
+ const [original, written, ...rest] = parsed;
48
+ const linked = rest[0] !== false;
49
+ const beneath = linked ? rest : rest.slice(1);
50
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
51
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
52
+ } catch {
53
+ return void 0;
54
+ }
55
+ }
56
+ var AttributeLease = class _AttributeLease {
57
+ static #registry = sharedRegistry(
58
+ "stimeo-ui.attribute-lease.registry.v1",
59
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
60
+ );
61
+ #attribute;
62
+ #record;
63
+ #base;
64
+ #held = /* @__PURE__ */ new Set();
65
+ constructor(attribute, owner, options = {}) {
66
+ this.#attribute = attribute;
67
+ this.#record = `data-${owner}-${attribute}-lease`;
68
+ this.#base = options.base === true;
69
+ const names = _AttributeLease.#registry.names;
70
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
71
+ }
72
+ write(element, value) {
73
+ const stack = this.#stack(element);
74
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
75
+ this.#held.add(element);
76
+ const shows = !this.#covered(element, stack, hold);
77
+ hold.written = value;
78
+ this.#mark(element, stack);
79
+ if (shows) this.#reflect(element, value);
80
+ }
81
+ return(element) {
82
+ const held = this.#held.delete(element);
83
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
84
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
85
+ const hold = this.#take(stack);
86
+ if (!hold) return false;
87
+ const carries = element.getAttribute(this.#attribute) === hold.written;
88
+ const owns = carries && !this.#covered(element, stack, hold);
89
+ const index = stack.indexOf(hold);
90
+ stack.splice(index, 1);
91
+ const above = stack[index];
92
+ if (above) above.linked &&= hold.linked;
93
+ this.#mark(element, stack);
94
+ if (!owns) return true;
95
+ const below = hold.linked ? stack[index - 1] : void 0;
96
+ this.#reflect(element, below ? below.written : hold.original);
97
+ return true;
98
+ }
99
+ returnAll() {
100
+ for (const element of Array.from(this.#held)) this.return(element);
101
+ }
102
+ #stack(element) {
103
+ const stacks = _AttributeLease.#registry.stacks;
104
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
105
+ stacks.set(element, byAttribute);
106
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
107
+ byAttribute.set(this.#attribute, stack);
108
+ return stack;
109
+ }
110
+ #seed(element) {
111
+ const found = [];
112
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
113
+ const read = readLeaseRecord(element, record);
114
+ if (read) found.push({ ...read, record });
115
+ }
116
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
117
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
118
+ lease: null,
119
+ record,
120
+ written,
121
+ original,
122
+ linked: linked && index > 0
123
+ }));
124
+ }
125
+ #take(stack) {
126
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
127
+ if (hold) hold.lease = this;
128
+ return hold;
129
+ }
130
+ #acquire(element, stack) {
131
+ const current = element.getAttribute(this.#attribute);
132
+ const top = stack.at(-1);
133
+ const bottom = stack[0];
134
+ const over = top?.written === current ? top : void 0;
135
+ const from = this.#base ? bottom : over;
136
+ const hold = {
137
+ lease: this,
138
+ record: this.#record,
139
+ written: current,
140
+ original: from ? from.original : current,
141
+ linked: !this.#base && over !== void 0
142
+ };
143
+ if (this.#base && bottom) {
144
+ bottom.linked = true;
145
+ stack.unshift(hold);
146
+ } else stack.push(hold);
147
+ return hold;
148
+ }
149
+ #covered(element, stack, hold) {
150
+ const current = element.getAttribute(this.#attribute);
151
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
152
+ }
153
+ #mark(element, stack) {
154
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
155
+ stack.forEach((hold, index) => {
156
+ const names = stack.slice(0, index).map((under) => under.record);
157
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
158
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
159
+ records.set(
160
+ hold.record,
161
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
162
+ );
163
+ });
164
+ for (const [name, recorded] of records) {
165
+ if (element.getAttribute(name) === recorded) continue;
166
+ if (recorded === null) element.removeAttribute(name);
167
+ else element.setAttribute(name, recorded);
168
+ }
169
+ }
170
+ #reflect(element, value) {
171
+ if (element.getAttribute(this.#attribute) === value) return;
172
+ if (value === null) element.removeAttribute(this.#attribute);
173
+ else element.setAttribute(this.#attribute, value);
174
+ }
175
+ };
176
+
177
+ function writeField(field, value) {
178
+ if (field.value === value) return false;
179
+ field.value = value;
180
+ return true;
181
+ }
182
+ function commitField(target) {
183
+ target.dispatchEvent(new Event("change", { bubbles: true }));
184
+ }
185
+
186
+ var ListenerSet = class {
187
+ #abort = new AbortController();
188
+ add(target, type, handler, options) {
189
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
190
+ }
191
+ dispose() {
192
+ this.#abort.abort();
193
+ this.#abort = new AbortController();
194
+ }
195
+ };
196
+
12
197
  var MicrotaskCoalescer = class {
13
198
  #run;
14
199
  #queued = false;
15
200
  #active = false;
16
201
  #generation = 0;
17
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
18
202
  constructor(run) {
19
203
  this.#run = run;
20
204
  }
21
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
22
205
  activate() {
23
206
  this.#active = true;
24
207
  }
25
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
26
208
  cancel() {
27
209
  this.#active = false;
28
210
  this.#queued = false;
29
211
  this.#generation += 1;
30
212
  }
31
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
32
213
  schedule() {
33
214
  if (!this.#active || this.#queued) return;
34
215
  this.#queued = true;
@@ -41,7 +222,42 @@ var MicrotaskCoalescer = class {
41
222
  }
42
223
  };
43
224
 
44
- // src/utils/option_scroll.ts
225
+ var MorphRenderWatcher = class {
226
+ #listeners = new ListenerSet();
227
+ #pass;
228
+ #element = null;
229
+ #onMorph = () => this.#pass.schedule();
230
+ constructor(run) {
231
+ this.#pass = new MicrotaskCoalescer(run);
232
+ }
233
+ observe(element) {
234
+ if (this.#element === element) return;
235
+ this.disconnect();
236
+ this.#element = element;
237
+ this.#pass.activate();
238
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
239
+ }
240
+ schedule() {
241
+ this.#pass.schedule();
242
+ }
243
+ disconnect() {
244
+ this.#listeners.dispose();
245
+ this.#pass.cancel();
246
+ this.#element = null;
247
+ }
248
+ };
249
+
250
+ var MoveCounter = class {
251
+ #sequence = 0;
252
+ record() {
253
+ this.#sequence += 1;
254
+ return this.#sequence;
255
+ }
256
+ isLatest(token) {
257
+ return token === this.#sequence;
258
+ }
259
+ };
260
+
45
261
  function scrollOptionIntoView(list, option) {
46
262
  if (list.scrollHeight <= list.clientHeight) return;
47
263
  const listRect = list.getBoundingClientRect();
@@ -53,43 +269,28 @@ function scrollOptionIntoView(list, option) {
53
269
  }
54
270
  }
55
271
 
56
- // src/utils/safe_timeout.ts
272
+ function targetSelector(identifier, name) {
273
+ return `[data-${identifier}-target~="${name}"]`;
274
+ }
275
+
57
276
  var TimerRegistry = class {
58
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
59
277
  ids = /* @__PURE__ */ new Set();
60
- /**
61
- * Cancels a single tracked timer.
62
- *
63
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
64
- * registry), so callers can clear defensively without guarding.
65
- */
66
278
  clear(id) {
67
279
  if (this.ids.delete(id)) {
68
280
  this.cancel(id);
69
281
  }
70
282
  }
71
- /**
72
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
73
- * to guarantee no timer outlives the element.
74
- */
75
283
  clearAll() {
76
284
  for (const id of this.ids) {
77
285
  this.cancel(id);
78
286
  }
79
287
  this.ids.clear();
80
288
  }
81
- /** Number of timers currently tracked (pending). */
82
289
  get size() {
83
290
  return this.ids.size;
84
291
  }
85
292
  };
86
293
  var SafeTimeout = class extends TimerRegistry {
87
- /**
88
- * Schedules `callback` after `delay` ms and returns the timer id.
89
- *
90
- * The id is removed from the registry automatically when the timeout fires,
91
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
92
- */
93
294
  set(callback, delay) {
94
295
  const id = this.schedule(() => {
95
296
  this.ids.delete(id);
@@ -106,29 +307,18 @@ var SafeTimeout = class extends TimerRegistry {
106
307
  }
107
308
  };
108
309
 
109
- // src/utils/typeahead.ts
110
310
  var TYPEAHEAD_RESET_MS = 500;
111
311
  var Typeahead = class {
112
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
113
312
  #timers = new SafeTimeout();
114
- /** Idle window before the query resets, in milliseconds. */
115
313
  #resetMs;
116
- /** The accumulated lowercase query, empty when idle. */
117
314
  #query = "";
118
- /** Id of the pending reset timer, `0` when none is scheduled. */
119
315
  #timerId = 0;
120
- /** @param options - Overrides for the idle window. */
121
316
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
122
317
  this.#resetMs = resetMs;
123
318
  }
124
- /** The query a search would currently run with; empty while idle. */
125
319
  get query() {
126
320
  return this.#query;
127
321
  }
128
- /**
129
- * Folds `key` into the query, restarts the idle window, and returns the query to
130
- * search with. A repeated character collapses the query to that one character.
131
- */
132
322
  push(key) {
133
323
  const char = key.toLowerCase();
134
324
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -137,7 +327,6 @@ var Typeahead = class {
137
327
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
138
328
  return this.#query;
139
329
  }
140
- /** Clears the query and cancels the pending idle reset. */
141
330
  reset() {
142
331
  this.#query = "";
143
332
  this.#timers.clear(this.#timerId);
@@ -164,83 +353,129 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
164
353
  return -1;
165
354
  }
166
355
 
167
- // src/controllers/listbox_controller.ts
356
+ var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
357
+ function setAttributeIfChanged(element, name, value) {
358
+ if (element.getAttribute(name) !== value) element.setAttribute(name, value);
359
+ }
168
360
  var ListboxController = class extends Controller {
361
+ #moves = new MoveCounter();
169
362
  static targets = ["trigger", "value", "list", "option", "field"];
363
+ static values = {
364
+ placeholder: { type: String, default: "" }
365
+ };
170
366
  static actions = ["close", "onTriggerKeydown", "open", "select", "toggle"];
171
- static events = ["change"];
172
- /** Stable ID of the active option; DOM targets are resolved afresh before use. */
367
+ static events = ["change", "reconcile"];
173
368
  #activeId = null;
174
- /** Target ID order captured while an option is active, used only for removal fallback. */
175
369
  #activeOrder = [];
176
- #connected = false;
177
- /** Collapses one mutation batch of target callbacks into a single pass. */
178
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileActive());
179
- /** Accumulated typeahead query and its idle-reset timer. */
370
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileOptions());
371
+ #observer = null;
372
+ #settled = { option: null, value: "" };
373
+ #shownList = null;
374
+ #listHidden = new AttributeLease("hidden", this.identifier);
375
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
376
+ #activeDescendant = new AttributeLease(
377
+ "aria-activedescendant",
378
+ this.identifier
379
+ );
180
380
  #typeahead = new Typeahead();
181
- /** Establishes the ARIA baseline, starts closed, and listens for outside clicks. */
182
381
  connect() {
183
382
  this.#normalizeSelection();
184
383
  this.close();
185
384
  document.addEventListener("click", this.#onOutsideClick, true);
186
- this.#connected = true;
187
- this.#reconcile.activate();
385
+ this.#settled = this.#selection();
386
+ this.#reconcile.observe(this.element);
387
+ this.#observeOptions();
188
388
  }
189
- /**
190
- * Establishes an inactive baseline for a late option, re-resolves active
191
- * identity, and re-applies the selection baseline.
192
- */
193
389
  optionTargetConnected(option) {
194
390
  option.removeAttribute("data-active");
195
- if (!this.#connected) return;
196
- this.#normalizeSelection();
197
- this.#queueOptionReconciliation();
391
+ this.#reconcile.schedule();
198
392
  }
199
- /** Removes controller-owned active state and reconciles the surviving targets. */
200
393
  optionTargetDisconnected(option) {
201
394
  option.removeAttribute("data-active");
202
- if (this.#connected) this.#queueOptionReconciliation();
203
- }
204
- /**
205
- * Brings the authored DOM to the shape the APG requires, and derives the state
206
- * that follows from the initial selection.
207
- *
208
- * Three things happen, and only these three — which option is chosen is the
209
- * author's, and is never changed:
210
- *
211
- * 1. Every option gets an explicit value. An absent `aria-selected` means "not
212
- * selectable" in ARIA, so a forgotten attribute hides a selectable option
213
- * from assistive technology.
214
- * 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 "".
219
- *
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.
223
- */
395
+ this.#reconcile.schedule();
396
+ }
397
+ triggerTargetConnected() {
398
+ this.#reconcile.schedule();
399
+ }
400
+ triggerTargetDisconnected(trigger) {
401
+ if (!this.triggerTargets.includes(trigger)) {
402
+ this.#expanded.return(trigger);
403
+ this.#activeDescendant.return(trigger);
404
+ }
405
+ this.#reconcile.schedule();
406
+ }
407
+ listTargetConnected() {
408
+ this.#reconcile.schedule();
409
+ }
410
+ listTargetDisconnected(list) {
411
+ if (!this.listTargets.includes(list)) {
412
+ if (list === this.#shownList) this.#adoptPageClose();
413
+ this.#listHidden.return(list);
414
+ }
415
+ this.#reconcile.schedule();
416
+ }
417
+ fieldTargetConnected() {
418
+ this.#reconcile.schedule();
419
+ }
420
+ fieldTargetDisconnected() {
421
+ this.#reconcile.schedule();
422
+ }
423
+ valueTargetConnected() {
424
+ this.#reconcile.schedule();
425
+ }
426
+ valueTargetDisconnected() {
427
+ this.#reconcile.schedule();
428
+ }
429
+ placeholderValueChanged() {
430
+ this.#reconcile.schedule();
431
+ }
224
432
  #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");
433
+ const selected = this.optionTargets.find(
434
+ (option) => option.getAttribute("aria-selected") === "true"
435
+ );
436
+ this.#applySelection(selected);
437
+ }
438
+ #reconcileOptions() {
439
+ this.#normalizeSelection();
440
+ this.#reconcileOpenState();
441
+ this.#reconcileActive();
442
+ this.#reportMove();
443
+ }
444
+ #reconcileOpenState() {
445
+ if (!this.hasListTarget) {
446
+ this.#shownList = null;
447
+ return;
229
448
  }
230
- if (selected) this.#applySelection(selected);
449
+ if (this.#shownList && this.listTargets.includes(this.#shownList)) this.#adoptPageClose();
450
+ const list = this.listTarget;
451
+ if (this.#shownList !== null && list !== this.#shownList) {
452
+ this.#listHidden.write(list, null);
453
+ this.#shownList = list;
454
+ }
455
+ if (this.hasTriggerTarget) {
456
+ this.#expanded.write(this.triggerTarget, list.hidden ? "false" : "true");
457
+ }
458
+ }
459
+ #adoptPageClose() {
460
+ if (this.#shownList?.hidden) this.#shownList = null;
461
+ }
462
+ #reportMove() {
463
+ const { option, value } = this.#selection();
464
+ if (option === this.#settled.option && value === this.#settled.value) return;
465
+ this.#settled = { option, value };
466
+ this.dispatch("reconcile", { detail: { value, option } });
467
+ }
468
+ #selection() {
469
+ const option = this.optionTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
470
+ return { option, value: option ? this.#optionValue(option) : "" };
231
471
  }
232
- /** Removes the document listener and clears the typeahead timer. */
233
472
  disconnect() {
234
- this.#connected = false;
235
- this.#reconcile.cancel();
473
+ this.#reconcile.disconnect();
474
+ this.#observer?.disconnect();
475
+ this.#observer = null;
236
476
  document.removeEventListener("click", this.#onOutsideClick, true);
237
477
  this.#typeahead.reset();
238
478
  }
239
- /**
240
- * Toggles the list on a real mouse click. Keyboard activation of the
241
- * `<button>` also fires a click (`detail === 0`); the keydown handler already
242
- * drives that, so the synthetic click is ignored to avoid double-toggling.
243
- */
244
479
  toggle(event) {
245
480
  if (event.detail === 0) return;
246
481
  if (this.#isClosed) {
@@ -249,7 +484,6 @@ var ListboxController = class extends Controller {
249
484
  this.close();
250
485
  }
251
486
  }
252
- /** Yields claimed keys; otherwise routes the APG select-only keyboard model. */
253
487
  onTriggerKeydown(event) {
254
488
  if (event.defaultPrevented) return;
255
489
  if (isReservedArrowChord(event)) return;
@@ -275,7 +509,7 @@ var ListboxController = class extends Controller {
275
509
  switch (event.key) {
276
510
  case "ArrowDown":
277
511
  event.preventDefault();
278
- this.#setActive(activeIndex < 0 ? 0 : (activeIndex + 1) % length);
512
+ this.#setActive((activeIndex + 1) % length);
279
513
  break;
280
514
  case "ArrowUp":
281
515
  event.preventDefault();
@@ -311,19 +545,24 @@ var ListboxController = class extends Controller {
311
545
  break;
312
546
  }
313
547
  }
314
- /** Selects the clicked option and closes, returning focus to the trigger. */
315
- select(event) {
316
- const option = event.currentTarget.closest('[role="option"]');
548
+ select(source) {
549
+ const { event, host, origin, reason } = actionSource(source);
550
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
551
+ return;
552
+ const option = host?.closest(targetSelector(this.identifier, "option"));
317
553
  if (!option || !this.optionTargets.includes(option)) return;
318
- this.#selectOption(option);
554
+ const focus = event !== null || this.element.contains(document.activeElement);
555
+ if (!this.#selectOption(option, reason)) return;
319
556
  this.close();
320
- this.triggerTarget.focus();
557
+ if (focus && (event !== null || this.element.contains(document.activeElement))) {
558
+ this.triggerTarget.focus();
559
+ }
321
560
  }
322
- /** Opens the list and activates the selected option (else the first). */
323
561
  open() {
324
562
  if (!this.hasListTarget) return;
325
- this.listTarget.hidden = false;
326
- this.triggerTarget.setAttribute("aria-expanded", "true");
563
+ this.#listHidden.write(this.listTarget, null);
564
+ this.#shownList = this.listTarget;
565
+ this.#expanded.write(this.triggerTarget, "true");
327
566
  if (this.optionTargets.length === 0) {
328
567
  this.#setActive(-1);
329
568
  return;
@@ -333,56 +572,94 @@ var ListboxController = class extends Controller {
333
572
  );
334
573
  this.#setActive(selected === -1 ? 0 : selected);
335
574
  }
336
- /** Closes the list, clears the active option, and resets the typeahead buffer. */
337
575
  close() {
338
576
  if (!this.hasListTarget) return;
339
- this.listTarget.hidden = true;
340
- this.triggerTarget.setAttribute("aria-expanded", "false");
577
+ this.#listHidden.write(this.listTarget, "");
578
+ this.#shownList = null;
579
+ this.#expanded.write(this.triggerTarget, "false");
341
580
  this.#setActive(-1);
342
581
  this.#typeahead.reset();
343
582
  }
344
- /** Commits the active option (keyboard) and closes, returning focus. */
345
583
  #commitActive() {
346
- this.#reconcileActive();
347
584
  const options = this.optionTargets;
348
585
  const activeIndex = this.#findActiveIndex(options);
349
- const option = activeIndex < 0 ? void 0 : options[activeIndex];
586
+ const option = options[activeIndex];
350
587
  if (option) this.#selectOption(option);
351
588
  this.close();
352
589
  this.triggerTarget.focus();
353
590
  }
354
- /** Applies selection: `aria-selected`, trigger label, hidden field, `change`. */
355
- #selectOption(option) {
591
+ #selectOption(option, reason = "user") {
356
592
  const { value, fieldChanged } = this.#applySelection(option);
357
- if (fieldChanged) {
358
- this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
359
- }
360
- this.dispatch("change", { detail: { value, option } });
593
+ const changed = option !== this.#settled.option || value !== this.#settled.value;
594
+ this.#settled = { option, value };
595
+ if (!changed) return true;
596
+ const token = this.#moves.record();
597
+ if (fieldChanged) commitField(this.fieldTarget);
598
+ if (!this.#moves.isLatest(token)) return false;
599
+ this.dispatch("change", { detail: { value, option, reason } });
600
+ return this.#moves.isLatest(token);
361
601
  }
362
- /**
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.
366
- */
367
602
  #applySelection(option) {
368
603
  for (const candidate of this.optionTargets) {
369
- candidate.setAttribute("aria-selected", candidate === option ? "true" : "false");
604
+ setAttributeIfChanged(candidate, "aria-selected", candidate === option ? "true" : "false");
370
605
  }
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;
606
+ this.#dropOwnRecords();
607
+ const value = option ? this.#optionValue(option) : "";
608
+ this.#renderLabel(option);
609
+ const fieldChanged = this.hasFieldTarget && writeField(this.fieldTarget, value);
376
610
  return { value, fieldChanged };
377
611
  }
378
- /**
379
- * Marks the option at `index` active via `data-active` and the trigger's
380
- * `aria-activedescendant`. Pass `-1` to clear it (the attribute is removed, not
381
- * set to empty, per the APG).
382
- */
612
+ #renderLabel(option) {
613
+ if (!this.hasValueTarget) return;
614
+ const label = this.valueTarget;
615
+ const ownsLabel = `data-${this.identifier}-owns-label`;
616
+ const originalLabel = `data-${this.identifier}-original-label`;
617
+ const owned = label.getAttribute(ownsLabel) === label.textContent && label.childElementCount === 0;
618
+ if (option === void 0 && !owned && label.hasAttribute(ownsLabel)) return;
619
+ const text = option ? this.#optionLabel(option) : this.placeholderValue || null;
620
+ if (text === null) {
621
+ if (!owned) return;
622
+ label.textContent = label.getAttribute(originalLabel) ?? "";
623
+ label.removeAttribute(ownsLabel);
624
+ label.removeAttribute(originalLabel);
625
+ return;
626
+ }
627
+ if (!owned) {
628
+ const current = (label.textContent ?? "").trim();
629
+ if (option === void 0 || current !== text) {
630
+ setAttributeIfChanged(label, originalLabel, current);
631
+ }
632
+ }
633
+ if (label.textContent !== text) label.textContent = text;
634
+ setAttributeIfChanged(label, ownsLabel, text);
635
+ }
636
+ #optionLabel(option) {
637
+ return (option.textContent ?? "").trim();
638
+ }
639
+ #optionValue(option) {
640
+ return option.dataset.value ?? this.#optionLabel(option);
641
+ }
642
+ #observeOptions() {
643
+ const observer = new MutationObserver((records) => {
644
+ if (this.#concernsOptions(records)) this.#reconcile.schedule();
645
+ });
646
+ observer.observe(this.element, {
647
+ subtree: true,
648
+ attributes: true,
649
+ attributeFilter: OBSERVED_ATTRIBUTES
650
+ });
651
+ this.#observer = observer;
652
+ }
653
+ #concernsOptions(records) {
654
+ const options = new Set(this.optionTargets);
655
+ return records.some((record) => options.has(record.target));
656
+ }
657
+ #dropOwnRecords() {
658
+ this.#observer?.takeRecords();
659
+ }
383
660
  #setActive(index) {
384
661
  const options = this.optionTargets;
385
- const active = index < 0 ? null : options[index] ?? null;
662
+ const active = options[index] ?? null;
386
663
  for (const option of options) {
387
664
  const marked = option.hasAttribute("data-active");
388
665
  if (option === active) {
@@ -393,15 +670,14 @@ var ListboxController = class extends Controller {
393
670
  }
394
671
  if (active?.id) {
395
672
  this.#activeId = active.id;
396
- this.triggerTarget.setAttribute("aria-activedescendant", active.id);
673
+ this.#activeDescendant.write(this.triggerTarget, active.id);
397
674
  } else {
398
675
  this.#activeId = null;
399
- this.triggerTarget.removeAttribute("aria-activedescendant");
676
+ this.#activeDescendant.write(this.triggerTarget, null);
400
677
  }
401
678
  this.#activeOrder = active ? options.map((option) => option.id).filter(Boolean) : [];
402
679
  if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);
403
680
  }
404
- /** Resolves active state against the current target collection. */
405
681
  #reconcileActive() {
406
682
  if (!this.hasTriggerTarget || this.#isClosed) {
407
683
  if (this.hasTriggerTarget) this.#setActive(-1);
@@ -421,17 +697,15 @@ var ListboxController = class extends Controller {
421
697
  return;
422
698
  }
423
699
  const activeId = this.triggerTarget.getAttribute("aria-activedescendant") ?? this.#activeId;
424
- this.#setActive(activeId ? this.#findFallbackIndex(options, activeId) : -1);
700
+ this.#setActive(this.#findFallbackIndex(options, activeId));
425
701
  }
426
- /** Finds the live target carrying the stable ID, or the active marker for an ID-less option. */
427
702
  #findActiveIndex(options) {
428
703
  const activeId = (this.hasTriggerTarget ? this.triggerTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
429
704
  if (activeId) return options.findIndex((option) => option.id === activeId);
430
705
  return options.findIndex((option) => option.hasAttribute("data-active"));
431
706
  }
432
- /** Chooses a surviving former successor, then a former predecessor. */
433
707
  #findFallbackIndex(options, activeId) {
434
- const oldIndex = this.#activeOrder.indexOf(activeId);
708
+ const oldIndex = this.#activeOrder.indexOf(activeId ?? "");
435
709
  if (oldIndex < 0) return -1;
436
710
  const indexesById = new Map(options.map((option, index) => [option.id, index]));
437
711
  for (let index = oldIndex + 1; index < this.#activeOrder.length; index += 1) {
@@ -444,31 +718,17 @@ var ListboxController = class extends Controller {
444
718
  }
445
719
  return -1;
446
720
  }
447
- /** Coalesces all target callbacks from one MutationObserver batch. */
448
- #queueOptionReconciliation() {
449
- this.#reconcile.schedule();
450
- }
451
- /**
452
- * Advances the typeahead query and activates the next matching option.
453
- *
454
- * The search resumes just after the active option so repeating a character
455
- * cycles through the options starting with it, rather than re-activating the
456
- * same first match on every press.
457
- */
458
721
  #typeaheadTo(options, activeIndex, char) {
459
722
  const index = findTypeaheadMatch(options, activeIndex, this.#typeahead.push(char));
460
723
  if (index !== -1) this.#setActive(index);
461
724
  }
462
- /** Closes on an outside click before an inside handler can detach its target. */
463
725
  #onOutsideClick = (event) => {
464
726
  if (!this.#isClosed && !this.element.contains(event.target)) this.close();
465
727
  };
466
- /** Whether the list is currently hidden. */
467
728
  get #isClosed() {
468
729
  return !this.hasListTarget || this.listTarget.hidden !== false;
469
730
  }
470
731
  };
471
732
 
472
733
  export { ListboxController };
473
- //# sourceMappingURL=listbox_controller.js.map
474
734
  //# sourceMappingURL=listbox_controller.js.map