stimeo-ui 0.16.0 → 0.18.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 (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  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 +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  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 +300 -171
  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 +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  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 +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  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 +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  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 +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  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 +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  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 +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  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 +396 -192
  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 +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,14 +1,279 @@
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
  }
29
+ function hasModifierChord(event) {
30
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
31
+ }
32
+
33
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
34
+ if (element.getAttribute(name) === value) return false;
35
+ beforeWrite?.();
36
+ if (value === null) element.removeAttribute(name);
37
+ else element.setAttribute(name, value);
38
+ return true;
39
+ }
40
+
41
+ function sharedRegistry(key, create) {
42
+ const symbol = Symbol.for(key);
43
+ const scope = globalThis;
44
+ const existing = scope[symbol];
45
+ if (existing !== void 0) return existing;
46
+ const registry2 = create();
47
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
48
+ return registry2;
49
+ }
50
+
51
+ var registry = sharedRegistry(
52
+ "stimeo-ui.attribute-lease.registry.v1",
53
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
54
+ );
55
+ function isAttributeValue(value) {
56
+ return value === null || typeof value === "string";
57
+ }
58
+ function readLeaseRecord(element, record) {
59
+ try {
60
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
61
+ if (!Array.isArray(parsed)) return void 0;
62
+ const [original, written, ...rest] = parsed;
63
+ const linked = rest[0] !== false;
64
+ const beneath = linked ? rest : rest.slice(1);
65
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
66
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+ function attributeStack(element, attribute) {
72
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
73
+ registry.stacks.set(element, byAttribute);
74
+ const known = byAttribute.get(attribute);
75
+ if (known) return known;
76
+ const found = [];
77
+ for (const record of registry.names.get(attribute) ?? []) {
78
+ const read = readLeaseRecord(element, record);
79
+ if (read) found.push({ ...read, record });
80
+ }
81
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
82
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
83
+ lease: null,
84
+ record,
85
+ written,
86
+ original,
87
+ linked: linked && index > 0
88
+ }));
89
+ byAttribute.set(attribute, stack);
90
+ return stack;
91
+ }
92
+ function findHold(stack, lease, record) {
93
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
94
+ }
95
+ var AttributeLease = class {
96
+ #attribute;
97
+ #record;
98
+ #base;
99
+ #preserveAuthor;
100
+ #held = /* @__PURE__ */ new Set();
101
+ constructor(attribute, owner, options = {}) {
102
+ this.#attribute = attribute;
103
+ this.#record = `data-${owner}-${attribute}-lease`;
104
+ this.#base = options.base === true;
105
+ this.#preserveAuthor = options.preserveAuthor === true;
106
+ const names = registry.names;
107
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
108
+ }
109
+ write(element, value) {
110
+ const stack = attributeStack(element, this.#attribute);
111
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
112
+ this.#held.add(element);
113
+ const shows = !this.#covered(element, stack, hold);
114
+ hold.written = value;
115
+ this.#mark(element, stack);
116
+ if (shows) this.#reflect(element, value);
117
+ }
118
+ return(element) {
119
+ const held = this.#held.delete(element);
120
+ const known = registry.stacks.get(element)?.has(this.#attribute);
121
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
122
+ const hold = this.#take(stack);
123
+ if (!hold) return false;
124
+ const carries = element.getAttribute(this.#attribute) === hold.written;
125
+ const owns = carries && !this.#covered(element, stack, hold);
126
+ const index = stack.indexOf(hold);
127
+ stack.splice(index, 1);
128
+ const above = stack[index];
129
+ if (above) above.linked &&= hold.linked;
130
+ this.#mark(element, stack);
131
+ if (!owns) return true;
132
+ const below = hold.linked ? stack[index - 1] : void 0;
133
+ this.#reflect(element, below ? below.written : hold.original);
134
+ return true;
135
+ }
136
+ returnAll() {
137
+ for (const element of Array.from(this.#held)) this.return(element);
138
+ }
139
+ #take(stack) {
140
+ const hold = findHold(stack, this, this.#record);
141
+ if (hold) hold.lease = this;
142
+ return hold;
143
+ }
144
+ #acquire(element, stack) {
145
+ const current = element.getAttribute(this.#attribute);
146
+ const top = stack.at(-1);
147
+ const bottom = stack[0];
148
+ const over = top?.written === current ? top : void 0;
149
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
150
+ const hold = {
151
+ lease: this,
152
+ record: this.#record,
153
+ written: current,
154
+ original: from ? from.original : current,
155
+ linked: !this.#base && over !== void 0
156
+ };
157
+ if (this.#base && bottom) {
158
+ bottom.linked = true;
159
+ stack.unshift(hold);
160
+ } else stack.push(hold);
161
+ return hold;
162
+ }
163
+ #covered(element, stack, hold) {
164
+ const current = element.getAttribute(this.#attribute);
165
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
166
+ }
167
+ #mark(element, stack) {
168
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
169
+ stack.forEach((hold, index) => {
170
+ const names = stack.slice(0, index).map((under) => under.record);
171
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
172
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
173
+ records.set(
174
+ hold.record,
175
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
176
+ );
177
+ });
178
+ for (const [name, recorded] of records) {
179
+ setAttributeIfChanged(element, name, recorded);
180
+ }
181
+ }
182
+ #reflect(element, value) {
183
+ setAttributeIfChanged(element, this.#attribute, value);
184
+ }
185
+ };
186
+
187
+ var AuthoredMutationWatcher = class {
188
+ #onAuthored;
189
+ #claims = null;
190
+ #observer = null;
191
+ constructor(onAuthored) {
192
+ this.#onAuthored = onAuthored;
193
+ }
194
+ observe(root, options) {
195
+ this.disconnect();
196
+ const observer = new MutationObserver((records) => {
197
+ if (this.#observer !== observer) return;
198
+ const authored = this.#filter(records);
199
+ if (authored.length > 0) this.#onAuthored(authored);
200
+ });
201
+ this.#observer = observer;
202
+ this.#claims = { attributes: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
203
+ observer.observe(root, {
204
+ attributes: true,
205
+ attributeFilter: [...options.attributes],
206
+ subtree: options.subtree ?? false,
207
+ childList: options.childList ?? false
208
+ });
209
+ if (options.ancestorAttributes) {
210
+ let ancestor = root.parentElement;
211
+ while (ancestor) {
212
+ observer.observe(ancestor, {
213
+ attributes: true,
214
+ attributeFilter: [...options.ancestorAttributes]
215
+ });
216
+ ancestor = ancestor.parentElement;
217
+ }
218
+ }
219
+ }
220
+ claim(target, name, value) {
221
+ const claims = this.#claims?.attributes;
222
+ let attributes = claims?.get(target);
223
+ if (!attributes) {
224
+ attributes = /* @__PURE__ */ new Map();
225
+ claims?.set(target, attributes);
226
+ }
227
+ const pending = attributes.get(name);
228
+ attributes.set(name, { value, count: (pending?.count ?? 0) + 1 });
229
+ }
230
+ claimCurrent(target, name) {
231
+ this.claim(target, name, target.getAttribute(name));
232
+ }
233
+ claimChildList(target, added, removed) {
234
+ const claims = this.#claims?.children;
235
+ const pending = claims?.get(target) ?? { added: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Map() };
236
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) + 1);
237
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) + 1);
238
+ claims?.set(target, pending);
239
+ }
240
+ take() {
241
+ return this.#filter(this.#observer?.takeRecords() ?? []);
242
+ }
243
+ disconnect() {
244
+ this.#observer?.disconnect();
245
+ this.#observer = null;
246
+ this.#claims = null;
247
+ }
248
+ #filter(records) {
249
+ const authored = records.filter((record) => !this.#consume(record));
250
+ this.#claims?.attributes.clear();
251
+ this.#claims?.children.clear();
252
+ return authored;
253
+ }
254
+ #consume(record) {
255
+ if (record.type === "attributes") {
256
+ const target = record.target;
257
+ const pending2 = this.#claims?.attributes.get(target)?.get(record.attributeName);
258
+ if (!pending2 || pending2.count === 0 || target.getAttribute(record.attributeName) !== pending2.value) {
259
+ return false;
260
+ }
261
+ pending2.count -= 1;
262
+ return true;
263
+ }
264
+ const pending = this.#claims?.children.get(record.target);
265
+ if (!pending) return false;
266
+ const added = [...record.addedNodes];
267
+ const removed = [...record.removedNodes];
268
+ if (added.some((node) => !pending.added.get(node)) || removed.some((node) => !pending.removed.get(node))) {
269
+ return false;
270
+ }
271
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) - 1);
272
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) - 1);
273
+ return true;
274
+ }
275
+ };
10
276
 
11
- // src/utils/field_mirror.ts
12
277
  function writeField(field, value) {
13
278
  if (field.value === value) return false;
14
279
  field.value = value;
@@ -18,27 +283,36 @@ function commitField(target) {
18
283
  target.dispatchEvent(new Event("change", { bubbles: true }));
19
284
  }
20
285
 
21
- // src/utils/microtask_coalescer.ts
286
+ var ListenerSet = class {
287
+ #abort = new AbortController();
288
+ add(target, type, handler, options) {
289
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
290
+ }
291
+ dispose() {
292
+ this.#abort.abort();
293
+ this.#abort = new AbortController();
294
+ }
295
+ };
296
+
22
297
  var MicrotaskCoalescer = class {
23
298
  #run;
24
299
  #queued = false;
25
300
  #active = false;
26
301
  #generation = 0;
27
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
28
302
  constructor(run) {
29
303
  this.#run = run;
30
304
  }
31
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
32
305
  activate() {
33
306
  this.#active = true;
34
307
  }
35
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
36
308
  cancel() {
37
309
  this.#active = false;
38
310
  this.#queued = false;
39
311
  this.#generation += 1;
40
312
  }
41
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
313
+ get pending() {
314
+ return this.#queued;
315
+ }
42
316
  schedule() {
43
317
  if (!this.#active || this.#queued) return;
44
318
  this.#queued = true;
@@ -51,7 +325,45 @@ var MicrotaskCoalescer = class {
51
325
  }
52
326
  };
53
327
 
54
- // src/utils/option_scroll.ts
328
+ var MorphRenderWatcher = class {
329
+ #listeners = new ListenerSet();
330
+ #pass;
331
+ #element = null;
332
+ #onMorph = () => this.#pass.schedule();
333
+ constructor(run) {
334
+ this.#pass = new MicrotaskCoalescer(run);
335
+ }
336
+ observe(element) {
337
+ if (this.#element === element) return;
338
+ this.disconnect();
339
+ this.#element = element;
340
+ this.#pass.activate();
341
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
342
+ }
343
+ schedule() {
344
+ this.#pass.schedule();
345
+ }
346
+ get pending() {
347
+ return this.#pass.pending;
348
+ }
349
+ disconnect() {
350
+ this.#listeners.dispose();
351
+ this.#pass.cancel();
352
+ this.#element = null;
353
+ }
354
+ };
355
+
356
+ var MoveCounter = class {
357
+ #sequence = 0;
358
+ record() {
359
+ this.#sequence += 1;
360
+ return this.#sequence;
361
+ }
362
+ isLatest(token) {
363
+ return token === this.#sequence;
364
+ }
365
+ };
366
+
55
367
  function scrollOptionIntoView(list, option) {
56
368
  if (list.scrollHeight <= list.clientHeight) return;
57
369
  const listRect = list.getBoundingClientRect();
@@ -63,43 +375,109 @@ function scrollOptionIntoView(list, option) {
63
375
  }
64
376
  }
65
377
 
66
- // src/utils/safe_timeout.ts
378
+ var OwnedText = class {
379
+ #attribute;
380
+ #channel;
381
+ #guard;
382
+ #writes = /* @__PURE__ */ new WeakMap();
383
+ constructor(attribute, channel = "text", guard = () => true) {
384
+ this.#attribute = attribute;
385
+ this.#channel = channel;
386
+ this.#guard = guard;
387
+ }
388
+ hasRecord(element) {
389
+ return element.hasAttribute(this.#attribute);
390
+ }
391
+ read(element, evidence = {}) {
392
+ if (evidence.source === "live") return this.#writes.get(element) ?? null;
393
+ const raw = element.getAttribute(this.#attribute);
394
+ if (raw === null) return null;
395
+ try {
396
+ const record = JSON.parse(raw);
397
+ if (!Array.isArray(record) || record.length !== 3 || record[0] !== this.#channel || typeof record[1] !== "string" || typeof record[2] !== "string")
398
+ return null;
399
+ return { original: record[1], written: record[2] };
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+ owns(element, evidence = {}) {
405
+ return this.#ownedRecord(element, evidence) !== null;
406
+ }
407
+ #ownedRecord(element, evidence) {
408
+ const record = this.read(element, evidence);
409
+ if (record === null || !this.#guard(element)) return null;
410
+ if (this.#channel === "text" && element.firstElementChild !== null) return null;
411
+ const current = this.#current(element);
412
+ return current === record.written || evidence.source === "live" && evidence.allowOriginal === true && current === record.original ? record : null;
413
+ }
414
+ write(element, written, original, force = false) {
415
+ const current = this.#current(element);
416
+ if (current === null) return;
417
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
418
+ if (force || current !== written) this.#put(element, written);
419
+ this.remember(element, written, before);
420
+ }
421
+ remember(element, written, original) {
422
+ const current = this.#current(element);
423
+ if (current === null) return;
424
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
425
+ this.#writes.set(element, { original: before, written });
426
+ setAttributeIfChanged(
427
+ element,
428
+ this.#attribute,
429
+ JSON.stringify([this.#channel, before, written])
430
+ );
431
+ }
432
+ clear(element, evidence = {}) {
433
+ if (!this.owns(element, evidence)) return false;
434
+ if (this.#current(element) !== "") this.#put(element, "");
435
+ this.forget(element);
436
+ return true;
437
+ }
438
+ restore(element, evidence = {}) {
439
+ const record = this.#ownedRecord(element, evidence);
440
+ if (record === null) return false;
441
+ if (this.#current(element) !== record.original) this.#put(element, record.original);
442
+ this.forget(element);
443
+ return true;
444
+ }
445
+ forget(element) {
446
+ setAttributeIfChanged(element, this.#attribute, null);
447
+ this.#writes.delete(element);
448
+ }
449
+ #current(element) {
450
+ if (this.#channel === "text") return element.textContent ?? "";
451
+ return element instanceof HTMLInputElement ? element.value : null;
452
+ }
453
+ #put(element, text) {
454
+ if (this.#channel === "text") element.textContent = text;
455
+ else if (element instanceof HTMLInputElement) element.value = text;
456
+ }
457
+ };
458
+
459
+ function targetSelector(identifier, name) {
460
+ return `[data-${identifier}-target~="${name}"]`;
461
+ }
462
+
67
463
  var TimerRegistry = class {
68
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
69
464
  ids = /* @__PURE__ */ new Set();
70
- /**
71
- * Cancels a single tracked timer.
72
- *
73
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
74
- * registry), so callers can clear defensively without guarding.
75
- */
76
465
  clear(id) {
77
466
  if (this.ids.delete(id)) {
78
467
  this.cancel(id);
79
468
  }
80
469
  }
81
- /**
82
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
83
- * to guarantee no timer outlives the element.
84
- */
85
470
  clearAll() {
86
471
  for (const id of this.ids) {
87
472
  this.cancel(id);
88
473
  }
89
474
  this.ids.clear();
90
475
  }
91
- /** Number of timers currently tracked (pending). */
92
476
  get size() {
93
477
  return this.ids.size;
94
478
  }
95
479
  };
96
480
  var SafeTimeout = class extends TimerRegistry {
97
- /**
98
- * Schedules `callback` after `delay` ms and returns the timer id.
99
- *
100
- * The id is removed from the registry automatically when the timeout fires,
101
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
102
- */
103
481
  set(callback, delay) {
104
482
  const id = this.schedule(() => {
105
483
  this.ids.delete(id);
@@ -116,29 +494,18 @@ var SafeTimeout = class extends TimerRegistry {
116
494
  }
117
495
  };
118
496
 
119
- // src/utils/typeahead.ts
120
497
  var TYPEAHEAD_RESET_MS = 500;
121
498
  var Typeahead = class {
122
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
123
499
  #timers = new SafeTimeout();
124
- /** Idle window before the query resets, in milliseconds. */
125
500
  #resetMs;
126
- /** The accumulated lowercase query, empty when idle. */
127
501
  #query = "";
128
- /** Id of the pending reset timer, `0` when none is scheduled. */
129
502
  #timerId = 0;
130
- /** @param options - Overrides for the idle window. */
131
503
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
132
504
  this.#resetMs = resetMs;
133
505
  }
134
- /** The query a search would currently run with; empty while idle. */
135
506
  get query() {
136
507
  return this.#query;
137
508
  }
138
- /**
139
- * Folds `key` into the query, restarts the idle window, and returns the query to
140
- * search with. A repeated character collapses the query to that one character.
141
- */
142
509
  push(key) {
143
510
  const char = key.toLowerCase();
144
511
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -147,7 +514,6 @@ var Typeahead = class {
147
514
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
148
515
  return this.#query;
149
516
  }
150
- /** Clears the query and cancels the pending idle reset. */
151
517
  reset() {
152
518
  this.#query = "";
153
519
  this.#timers.clear(this.#timerId);
@@ -174,149 +540,131 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
174
540
  return -1;
175
541
  }
176
542
 
177
- // src/controllers/listbox_controller.ts
178
543
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
179
- function setAttributeIfChanged(element, name, value) {
180
- if (element.getAttribute(name) !== value) element.setAttribute(name, value);
181
- }
182
544
  var ListboxController = class extends Controller {
545
+ #labelText = new OwnedText(`data-${this.identifier}-owns-label`);
546
+ #moves = new MoveCounter();
547
+ #connectionGeneration = 0;
183
548
  static targets = ["trigger", "value", "list", "option", "field"];
184
549
  static values = {
185
550
  placeholder: { type: String, default: "" }
186
551
  };
187
552
  static actions = ["close", "onTriggerKeydown", "open", "select", "toggle"];
188
553
  static events = ["change", "reconcile"];
189
- /** Stable ID of the active option; DOM targets are resolved afresh before use. */
190
554
  #activeId = null;
191
- /** Target ID order captured while an option is active, used only for removal fallback. */
192
555
  #activeOrder = [];
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. */
556
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileOptions());
557
+ #observer = new AuthoredMutationWatcher((records) => {
558
+ if (this.#concernsOptions(records)) this.#reconcile.schedule();
559
+ });
202
560
  #settled = { option: null, value: "" };
203
- /** Accumulated typeahead query and its idle-reset timer. */
561
+ #shownList = null;
562
+ #listHidden = new AttributeLease("hidden", this.identifier);
563
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
564
+ #activeDescendant = new AttributeLease(
565
+ "aria-activedescendant",
566
+ this.identifier
567
+ );
204
568
  #typeahead = new Typeahead();
205
- /**
206
- * Establishes the ARIA baseline, settles the selection without reporting it,
207
- * starts closed, and listens for outside clicks.
208
- */
569
+ #listeners = new ListenerSet();
209
570
  connect() {
210
571
  this.#normalizeSelection();
211
572
  this.close();
212
- document.addEventListener("click", this.#onOutsideClick, true);
573
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
213
574
  this.#settled = this.#selection();
214
- this.#reconcile.activate();
575
+ this.#reconcile.observe(this.element);
215
576
  this.#observeOptions();
216
577
  }
217
- /**
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.
220
- */
221
578
  optionTargetConnected(option) {
222
579
  option.removeAttribute("data-active");
223
580
  this.#reconcile.schedule();
224
581
  }
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
- */
229
582
  optionTargetDisconnected(option) {
230
583
  option.removeAttribute("data-active");
231
584
  this.#reconcile.schedule();
232
585
  }
233
- /** Fills a form field inserted or replaced at runtime with the current selection. */
586
+ triggerTargetConnected() {
587
+ this.#reconcile.schedule();
588
+ }
589
+ triggerTargetDisconnected(trigger) {
590
+ if (!this.triggerTargets.includes(trigger)) {
591
+ this.#expanded.return(trigger);
592
+ this.#activeDescendant.return(trigger);
593
+ }
594
+ this.#reconcile.schedule();
595
+ }
596
+ listTargetConnected() {
597
+ this.#reconcile.schedule();
598
+ }
599
+ listTargetDisconnected(list) {
600
+ if (!this.listTargets.includes(list)) {
601
+ if (list === this.#shownList) this.#adoptPageClose();
602
+ this.#listHidden.return(list);
603
+ }
604
+ this.#reconcile.schedule();
605
+ }
234
606
  fieldTargetConnected() {
235
607
  this.#reconcile.schedule();
236
608
  }
237
- /** Writes the current selection into a trigger value inserted or replaced at runtime. */
609
+ fieldTargetDisconnected() {
610
+ this.#reconcile.schedule();
611
+ }
238
612
  valueTargetConnected() {
239
613
  this.#reconcile.schedule();
240
614
  }
241
- /**
242
- * Re-renders the empty-selection label when application code (or a Turbo morph)
243
- * changes `placeholder` at runtime.
244
- */
615
+ valueTargetDisconnected() {
616
+ this.#reconcile.schedule();
617
+ }
245
618
  placeholderValueChanged() {
246
619
  this.#reconcile.schedule();
247
620
  }
248
- /**
249
- * Brings the authored DOM to the shape the APG requires, and derives the state
250
- * that follows from the selection.
251
- *
252
- * Three things happen, and only these three — which option is chosen is the
253
- * author's, and is never changed:
254
- *
255
- * 1. Every option gets an explicit value. An absent `aria-selected` means "not
256
- * selectable" in ARIA, so a forgotten attribute hides a selectable option
257
- * from assistive technology.
258
- * 2. At most one stays `true`. The first in DOM order wins, since that is the
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 "".
265
- *
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.
270
- */
271
621
  #normalizeSelection() {
272
622
  const selected = this.optionTargets.find(
273
623
  (option) => option.getAttribute("aria-selected") === "true"
274
624
  );
275
625
  this.#applySelection(selected);
276
626
  }
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
627
  #reconcileOptions() {
284
628
  this.#normalizeSelection();
629
+ this.#reconcileOpenState();
285
630
  this.#reconcileActive();
286
631
  this.#reportMove();
287
632
  }
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
- */
633
+ #reconcileOpenState() {
634
+ if (!this.hasListTarget) {
635
+ this.#shownList = null;
636
+ return;
637
+ }
638
+ if (this.#shownList && this.listTargets.includes(this.#shownList)) this.#adoptPageClose();
639
+ const list = this.listTarget;
640
+ if (this.#shownList !== null && list !== this.#shownList) {
641
+ this.#listHidden.write(list, null);
642
+ this.#shownList = list;
643
+ }
644
+ if (this.hasTriggerTarget) {
645
+ this.#expanded.write(this.triggerTarget, list.hidden ? "false" : "true");
646
+ }
647
+ }
648
+ #adoptPageClose() {
649
+ if (this.#shownList?.hidden) this.#shownList = null;
650
+ }
293
651
  #reportMove() {
294
652
  const { option, value } = this.#selection();
295
653
  if (option === this.#settled.option && value === this.#settled.value) return;
296
654
  this.#settled = { option, value };
297
655
  this.dispatch("reconcile", { detail: { value, option } });
298
656
  }
299
- /** The selected option target, if any, and the value its field submits. */
300
657
  #selection() {
301
658
  const option = this.optionTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
302
659
  return { option, value: option ? this.#optionValue(option) : "" };
303
660
  }
304
- /**
305
- * Releases the outside-click listener and the option observer, drops a queued
306
- * pass, and clears the typeahead timer.
307
- */
308
661
  disconnect() {
309
- this.#reconcile.cancel();
310
- this.#observer?.disconnect();
311
- this.#observer = null;
312
- document.removeEventListener("click", this.#onOutsideClick, true);
662
+ this.#connectionGeneration += 1;
663
+ this.#reconcile.disconnect();
664
+ this.#observer.disconnect();
665
+ this.#listeners.dispose();
313
666
  this.#typeahead.reset();
314
667
  }
315
- /**
316
- * Toggles the list on a real mouse click. Keyboard activation of the
317
- * `<button>` also fires a click (`detail === 0`); the keydown handler already
318
- * drives that, so the synthetic click is ignored to avoid double-toggling.
319
- */
320
668
  toggle(event) {
321
669
  if (event.detail === 0) return;
322
670
  if (this.#isClosed) {
@@ -325,10 +673,10 @@ var ListboxController = class extends Controller {
325
673
  this.close();
326
674
  }
327
675
  }
328
- /** Yields claimed keys; otherwise routes the APG select-only keyboard model. */
329
676
  onTriggerKeydown(event) {
330
677
  if (event.defaultPrevented) return;
331
678
  if (isReservedArrowChord(event)) return;
679
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
332
680
  if (!this.#isClosed) this.#reconcileActive();
333
681
  const options = this.optionTargets;
334
682
  const length = options.length;
@@ -351,7 +699,7 @@ var ListboxController = class extends Controller {
351
699
  switch (event.key) {
352
700
  case "ArrowDown":
353
701
  event.preventDefault();
354
- this.#setActive(activeIndex < 0 ? 0 : (activeIndex + 1) % length);
702
+ this.#setActive((activeIndex + 1) % length);
355
703
  break;
356
704
  case "ArrowUp":
357
705
  event.preventDefault();
@@ -387,19 +735,24 @@ var ListboxController = class extends Controller {
387
735
  break;
388
736
  }
389
737
  }
390
- /** Selects the clicked option and closes, returning focus to the trigger. */
391
- select(event) {
392
- const option = event.currentTarget.closest('[role="option"]');
738
+ select(source) {
739
+ const { event, host, origin, reason } = actionSource(source);
740
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
741
+ return;
742
+ const option = host?.closest(targetSelector(this.identifier, "option"));
393
743
  if (!option || !this.optionTargets.includes(option)) return;
394
- this.#selectOption(option);
744
+ const focus = event !== null || this.element.contains(document.activeElement);
745
+ if (!this.#selectOption(option, reason)) return;
395
746
  this.close();
396
- this.triggerTarget.focus();
747
+ if (focus && (event !== null || this.element.contains(document.activeElement))) {
748
+ this.triggerTarget.focus();
749
+ }
397
750
  }
398
- /** Opens the list and activates the selected option (else the first). */
399
751
  open() {
400
752
  if (!this.hasListTarget) return;
401
- this.listTarget.hidden = false;
402
- this.triggerTarget.setAttribute("aria-expanded", "true");
753
+ this.#listHidden.write(this.listTarget, null);
754
+ this.#shownList = this.listTarget;
755
+ this.#expanded.write(this.triggerTarget, "true");
403
756
  if (this.optionTargets.length === 0) {
404
757
  this.#setActive(-1);
405
758
  return;
@@ -409,142 +762,80 @@ var ListboxController = class extends Controller {
409
762
  );
410
763
  this.#setActive(selected === -1 ? 0 : selected);
411
764
  }
412
- /** Closes the list, clears the active option, and resets the typeahead buffer. */
413
765
  close() {
414
766
  if (!this.hasListTarget) return;
415
- this.listTarget.hidden = true;
416
- this.triggerTarget.setAttribute("aria-expanded", "false");
767
+ this.#listHidden.write(this.listTarget, "");
768
+ this.#shownList = null;
769
+ this.#expanded.write(this.triggerTarget, "false");
417
770
  this.#setActive(-1);
418
771
  this.#typeahead.reset();
419
772
  }
420
- /** Commits the active option (keyboard) and closes, returning focus. */
421
773
  #commitActive() {
422
- this.#reconcileActive();
423
774
  const options = this.optionTargets;
424
775
  const activeIndex = this.#findActiveIndex(options);
425
- const option = activeIndex < 0 ? void 0 : options[activeIndex];
776
+ const option = options[activeIndex];
426
777
  if (option) this.#selectOption(option);
427
778
  this.close();
428
779
  this.triggerTarget.focus();
429
780
  }
430
- /** Applies selection: `aria-selected`, trigger label, hidden field, `change`. */
431
- #selectOption(option) {
781
+ #selectOption(option, reason = "user") {
782
+ const generation = this.#connectionGeneration;
432
783
  const { value, fieldChanged } = this.#applySelection(option);
784
+ const changed = option !== this.#settled.option || value !== this.#settled.value;
433
785
  this.#settled = { option, value };
786
+ if (!changed) return true;
787
+ const token = this.#moves.record();
434
788
  if (fieldChanged) commitField(this.fieldTarget);
435
- this.dispatch("change", { detail: { value, option } });
436
- }
437
- /**
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.
442
- */
789
+ if (generation !== this.#connectionGeneration || !this.#moves.isLatest(token)) return false;
790
+ this.dispatch("change", { detail: { value, option, reason } });
791
+ return this.#moves.isLatest(token);
792
+ }
443
793
  #applySelection(option) {
444
794
  for (const candidate of this.optionTargets) {
445
- setAttributeIfChanged(candidate, "aria-selected", candidate === option ? "true" : "false");
795
+ this.#writeSelection(candidate, candidate === option ? "true" : "false");
446
796
  }
447
- this.#dropOwnRecords();
797
+ this.#flushMutations();
448
798
  const value = option ? this.#optionValue(option) : "";
449
799
  this.#renderLabel(option);
450
800
  const fieldChanged = this.hasFieldTarget && writeField(this.fieldTarget, value);
451
801
  return { value, fieldChanged };
452
802
  }
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
803
  #renderLabel(option) {
473
804
  if (!this.hasValueTarget) return;
474
805
  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;
806
+ const owned = this.#labelText.owns(label);
807
+ if (option === void 0 && !owned && this.#labelText.hasRecord(label)) return;
479
808
  const text = option ? this.#optionLabel(option) : this.placeholderValue || null;
480
809
  if (text === null) {
481
- if (!owned) return;
482
- label.textContent = label.getAttribute(originalLabel) ?? "";
483
- label.removeAttribute(ownsLabel);
484
- label.removeAttribute(originalLabel);
810
+ this.#labelText.restore(label);
485
811
  return;
486
812
  }
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);
813
+ const current = (label.textContent ?? "").trim();
814
+ const original = owned ? void 0 : option === void 0 || current !== text ? current : this.#labelText.read(label)?.original ?? "";
815
+ this.#labelText.write(label, text, original);
495
816
  }
496
- /** An option's visible label: its own text, trimmed. */
497
817
  #optionLabel(option) {
498
818
  return (option.textContent ?? "").trim();
499
819
  }
500
- /** The value an option submits: its `data-value`, else its label. */
501
820
  #optionValue(option) {
502
821
  return option.dataset.value ?? this.#optionLabel(option);
503
822
  }
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
823
  #observeOptions() {
511
- const observer = new MutationObserver((records) => {
512
- if (this.#concernsOptions(records)) this.#reconcile.schedule();
513
- });
514
- observer.observe(this.element, {
824
+ this.#observer.observe(this.element, {
515
825
  subtree: true,
516
- attributes: true,
517
- attributeFilter: OBSERVED_ATTRIBUTES
826
+ attributes: OBSERVED_ATTRIBUTES
518
827
  });
519
- this.#observer = observer;
520
828
  }
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
829
  #concernsOptions(records) {
527
830
  const options = new Set(this.optionTargets);
528
831
  return records.some((record) => options.has(record.target));
529
832
  }
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
- }
540
- /**
541
- * Marks the option at `index` active via `data-active` and the trigger's
542
- * `aria-activedescendant`. Pass `-1` to clear it (the attribute is removed, not
543
- * set to empty, per the APG).
544
- */
833
+ #flushMutations() {
834
+ if (this.#concernsOptions(this.#observer.take())) this.#reconcile.schedule();
835
+ }
545
836
  #setActive(index) {
546
837
  const options = this.optionTargets;
547
- const active = index < 0 ? null : options[index] ?? null;
838
+ const active = options[index] ?? null;
548
839
  for (const option of options) {
549
840
  const marked = option.hasAttribute("data-active");
550
841
  if (option === active) {
@@ -555,15 +846,14 @@ var ListboxController = class extends Controller {
555
846
  }
556
847
  if (active?.id) {
557
848
  this.#activeId = active.id;
558
- this.triggerTarget.setAttribute("aria-activedescendant", active.id);
849
+ this.#activeDescendant.write(this.triggerTarget, active.id);
559
850
  } else {
560
851
  this.#activeId = null;
561
- this.triggerTarget.removeAttribute("aria-activedescendant");
852
+ this.#activeDescendant.write(this.triggerTarget, null);
562
853
  }
563
854
  this.#activeOrder = active ? options.map((option) => option.id).filter(Boolean) : [];
564
855
  if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);
565
856
  }
566
- /** Resolves active state against the current target collection. */
567
857
  #reconcileActive() {
568
858
  if (!this.hasTriggerTarget || this.#isClosed) {
569
859
  if (this.hasTriggerTarget) this.#setActive(-1);
@@ -583,17 +873,15 @@ var ListboxController = class extends Controller {
583
873
  return;
584
874
  }
585
875
  const activeId = this.triggerTarget.getAttribute("aria-activedescendant") ?? this.#activeId;
586
- this.#setActive(activeId ? this.#findFallbackIndex(options, activeId) : -1);
876
+ this.#setActive(this.#findFallbackIndex(options, activeId));
587
877
  }
588
- /** Finds the live target carrying the stable ID, or the active marker for an ID-less option. */
589
878
  #findActiveIndex(options) {
590
879
  const activeId = (this.hasTriggerTarget ? this.triggerTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
591
880
  if (activeId) return options.findIndex((option) => option.id === activeId);
592
881
  return options.findIndex((option) => option.hasAttribute("data-active"));
593
882
  }
594
- /** Chooses a surviving former successor, then a former predecessor. */
595
883
  #findFallbackIndex(options, activeId) {
596
- const oldIndex = this.#activeOrder.indexOf(activeId);
884
+ const oldIndex = this.#activeOrder.indexOf(activeId ?? "");
597
885
  if (oldIndex < 0) return -1;
598
886
  const indexesById = new Map(options.map((option, index) => [option.id, index]));
599
887
  for (let index = oldIndex + 1; index < this.#activeOrder.length; index += 1) {
@@ -606,27 +894,22 @@ var ListboxController = class extends Controller {
606
894
  }
607
895
  return -1;
608
896
  }
609
- /**
610
- * Advances the typeahead query and activates the next matching option.
611
- *
612
- * The search resumes just after the active option so repeating a character
613
- * cycles through the options starting with it, rather than re-activating the
614
- * same first match on every press.
615
- */
616
897
  #typeaheadTo(options, activeIndex, char) {
617
898
  const index = findTypeaheadMatch(options, activeIndex, this.#typeahead.push(char));
618
899
  if (index !== -1) this.#setActive(index);
619
900
  }
620
- /** Closes on an outside click before an inside handler can detach its target. */
621
901
  #onOutsideClick = (event) => {
622
902
  if (!this.#isClosed && !this.element.contains(event.target)) this.close();
623
903
  };
624
- /** Whether the list is currently hidden. */
625
904
  get #isClosed() {
626
905
  return !this.hasListTarget || this.listTarget.hidden !== false;
627
906
  }
907
+ #writeSelection(target, value) {
908
+ setAttributeIfChanged(target, "aria-selected", value, () => {
909
+ this.#observer.claim(target, "aria-selected", value);
910
+ });
911
+ }
628
912
  };
629
913
 
630
914
  export { ListboxController };
631
- //# sourceMappingURL=listbox_controller.js.map
632
915
  //# sourceMappingURL=listbox_controller.js.map