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,19 +1,137 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/tree_view_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/logical_scroll.ts
6
25
  function isRtl(element) {
7
26
  return window.getComputedStyle(element).direction === "rtl";
8
27
  }
9
28
 
10
- // src/utils/arrow_step.ts
11
29
  function isReservedArrowChord(event, allow = []) {
12
30
  if (!event.key.startsWith("Arrow")) return false;
13
31
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
14
32
  }
33
+ function hasModifierChord(event) {
34
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
35
+ }
36
+
37
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
38
+ if (element.getAttribute(name) === value) return false;
39
+ beforeWrite?.();
40
+ if (value === null) element.removeAttribute(name);
41
+ else element.setAttribute(name, value);
42
+ return true;
43
+ }
44
+
45
+ var AuthoredMutationWatcher = class {
46
+ #onAuthored;
47
+ #claims = null;
48
+ #observer = null;
49
+ constructor(onAuthored) {
50
+ this.#onAuthored = onAuthored;
51
+ }
52
+ observe(root, options) {
53
+ this.disconnect();
54
+ const observer = new MutationObserver((records) => {
55
+ if (this.#observer !== observer) return;
56
+ const authored = this.#filter(records);
57
+ if (authored.length > 0) this.#onAuthored(authored);
58
+ });
59
+ this.#observer = observer;
60
+ this.#claims = { attributes: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map() };
61
+ observer.observe(root, {
62
+ attributes: true,
63
+ attributeFilter: [...options.attributes],
64
+ subtree: options.subtree ?? false,
65
+ childList: options.childList ?? false
66
+ });
67
+ if (options.ancestorAttributes) {
68
+ let ancestor = root.parentElement;
69
+ while (ancestor) {
70
+ observer.observe(ancestor, {
71
+ attributes: true,
72
+ attributeFilter: [...options.ancestorAttributes]
73
+ });
74
+ ancestor = ancestor.parentElement;
75
+ }
76
+ }
77
+ }
78
+ claim(target, name, value) {
79
+ const claims = this.#claims?.attributes;
80
+ let attributes = claims?.get(target);
81
+ if (!attributes) {
82
+ attributes = /* @__PURE__ */ new Map();
83
+ claims?.set(target, attributes);
84
+ }
85
+ const pending = attributes.get(name);
86
+ attributes.set(name, { value, count: (pending?.count ?? 0) + 1 });
87
+ }
88
+ claimCurrent(target, name) {
89
+ this.claim(target, name, target.getAttribute(name));
90
+ }
91
+ claimChildList(target, added, removed) {
92
+ const claims = this.#claims?.children;
93
+ const pending = claims?.get(target) ?? { added: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Map() };
94
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) + 1);
95
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) + 1);
96
+ claims?.set(target, pending);
97
+ }
98
+ take() {
99
+ return this.#filter(this.#observer?.takeRecords() ?? []);
100
+ }
101
+ disconnect() {
102
+ this.#observer?.disconnect();
103
+ this.#observer = null;
104
+ this.#claims = null;
105
+ }
106
+ #filter(records) {
107
+ const authored = records.filter((record) => !this.#consume(record));
108
+ this.#claims?.attributes.clear();
109
+ this.#claims?.children.clear();
110
+ return authored;
111
+ }
112
+ #consume(record) {
113
+ if (record.type === "attributes") {
114
+ const target = record.target;
115
+ const pending2 = this.#claims?.attributes.get(target)?.get(record.attributeName);
116
+ if (!pending2 || pending2.count === 0 || target.getAttribute(record.attributeName) !== pending2.value) {
117
+ return false;
118
+ }
119
+ pending2.count -= 1;
120
+ return true;
121
+ }
122
+ const pending = this.#claims?.children.get(record.target);
123
+ if (!pending) return false;
124
+ const added = [...record.addedNodes];
125
+ const removed = [...record.removedNodes];
126
+ if (added.some((node) => !pending.added.get(node)) || removed.some((node) => !pending.removed.get(node))) {
127
+ return false;
128
+ }
129
+ for (const node of added) pending.added.set(node, (pending.added.get(node) ?? 0) - 1);
130
+ for (const node of removed) pending.removed.set(node, (pending.removed.get(node) ?? 0) - 1);
131
+ return true;
132
+ }
133
+ };
15
134
 
16
- // src/utils/field_mirror.ts
17
135
  function writeField(field, value) {
18
136
  if (field.value === value) return false;
19
137
  field.value = value;
@@ -23,7 +141,6 @@ function commitField(target) {
23
141
  target.dispatchEvent(new Event("change", { bubbles: true }));
24
142
  }
25
143
 
26
- // src/utils/focus_candidate.ts
27
144
  function inheritsFieldsetDisabled(control) {
28
145
  let fieldset = control.closest("fieldset[disabled]");
29
146
  while (fieldset) {
@@ -33,15 +150,25 @@ function inheritsFieldsetDisabled(control) {
33
150
  }
34
151
  return false;
35
152
  }
153
+ function unslotted(element) {
154
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
155
+ }
156
+ function flatTreeParent(element) {
157
+ if (unslotted(element)) return null;
158
+ const parent = element.assignedSlot ?? element.parentNode;
159
+ if (parent instanceof ShadowRoot) return parent.host;
160
+ return parent instanceof Element ? parent : null;
161
+ }
36
162
  function canTakeFocus(element) {
37
- if (element.closest("[hidden], [inert]")) return false;
163
+ for (let node = element; node; node = flatTreeParent(node)) {
164
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
165
+ }
38
166
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
39
167
  if (!("disabled" in element)) return true;
40
168
  if (element.disabled) return false;
41
169
  return !inheritsFieldsetDisabled(element);
42
170
  }
43
171
 
44
- // src/utils/interactive_host.ts
45
172
  var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
46
173
  function isInteractiveHost(element) {
47
174
  if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
@@ -58,27 +185,36 @@ function isInteractiveHost(element) {
58
185
  return false;
59
186
  }
60
187
 
61
- // src/utils/microtask_coalescer.ts
188
+ var ListenerSet = class {
189
+ #abort = new AbortController();
190
+ add(target, type, handler, options) {
191
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
192
+ }
193
+ dispose() {
194
+ this.#abort.abort();
195
+ this.#abort = new AbortController();
196
+ }
197
+ };
198
+
62
199
  var MicrotaskCoalescer = class {
63
200
  #run;
64
201
  #queued = false;
65
202
  #active = false;
66
203
  #generation = 0;
67
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
68
204
  constructor(run) {
69
205
  this.#run = run;
70
206
  }
71
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
72
207
  activate() {
73
208
  this.#active = true;
74
209
  }
75
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
76
210
  cancel() {
77
211
  this.#active = false;
78
212
  this.#queued = false;
79
213
  this.#generation += 1;
80
214
  }
81
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
215
+ get pending() {
216
+ return this.#queued;
217
+ }
82
218
  schedule() {
83
219
  if (!this.#active || this.#queued) return;
84
220
  this.#queued = true;
@@ -91,31 +227,53 @@ var MicrotaskCoalescer = class {
91
227
  }
92
228
  };
93
229
 
94
- // src/utils/roving_tabindex.ts
230
+ var MorphRenderWatcher = class {
231
+ #listeners = new ListenerSet();
232
+ #pass;
233
+ #element = null;
234
+ #onMorph = () => this.#pass.schedule();
235
+ constructor(run) {
236
+ this.#pass = new MicrotaskCoalescer(run);
237
+ }
238
+ observe(element) {
239
+ if (this.#element === element) return;
240
+ this.disconnect();
241
+ this.#element = element;
242
+ this.#pass.activate();
243
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
244
+ }
245
+ schedule() {
246
+ this.#pass.schedule();
247
+ }
248
+ get pending() {
249
+ return this.#pass.pending;
250
+ }
251
+ disconnect() {
252
+ this.#listeners.dispose();
253
+ this.#pass.cancel();
254
+ this.#element = null;
255
+ }
256
+ };
257
+
258
+ var MoveCounter = class {
259
+ #sequence = 0;
260
+ record() {
261
+ this.#sequence += 1;
262
+ return this.#sequence;
263
+ }
264
+ isLatest(token) {
265
+ return token === this.#sequence;
266
+ }
267
+ };
268
+
95
269
  var RovingTabindex = class {
96
- /** Returns the current ordered item elements; called on every operation. */
97
270
  #getItems;
98
- /**
99
- * @param getItems - Returns the current ordered item elements. Called on every
100
- * operation so the live target list is always used.
101
- */
102
271
  constructor(getItems) {
103
272
  this.#getItems = getItems;
104
273
  }
105
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
106
274
  get activeIndex() {
107
275
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
108
276
  }
109
- /**
110
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
111
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
112
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
113
- * "nothing is currently tabbable".
114
- *
115
- * @param index - Position of the item to make tabbable.
116
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
117
- * and `items` to reuse an event-scoped collection snapshot.
118
- */
119
277
  setActive(index, options = {}) {
120
278
  const { focus = false } = options;
121
279
  const items = options.items ?? this.#getItems();
@@ -126,43 +284,28 @@ var RovingTabindex = class {
126
284
  }
127
285
  };
128
286
 
129
- // src/utils/safe_timeout.ts
287
+ function targetSelector(identifier, name) {
288
+ return `[data-${identifier}-target~="${name}"]`;
289
+ }
290
+
130
291
  var TimerRegistry = class {
131
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
132
292
  ids = /* @__PURE__ */ new Set();
133
- /**
134
- * Cancels a single tracked timer.
135
- *
136
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
137
- * registry), so callers can clear defensively without guarding.
138
- */
139
293
  clear(id) {
140
294
  if (this.ids.delete(id)) {
141
295
  this.cancel(id);
142
296
  }
143
297
  }
144
- /**
145
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
146
- * to guarantee no timer outlives the element.
147
- */
148
298
  clearAll() {
149
299
  for (const id of this.ids) {
150
300
  this.cancel(id);
151
301
  }
152
302
  this.ids.clear();
153
303
  }
154
- /** Number of timers currently tracked (pending). */
155
304
  get size() {
156
305
  return this.ids.size;
157
306
  }
158
307
  };
159
308
  var SafeTimeout = class extends TimerRegistry {
160
- /**
161
- * Schedules `callback` after `delay` ms and returns the timer id.
162
- *
163
- * The id is removed from the registry automatically when the timeout fires,
164
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
165
- */
166
309
  set(callback, delay) {
167
310
  const id = this.schedule(() => {
168
311
  this.ids.delete(id);
@@ -179,29 +322,18 @@ var SafeTimeout = class extends TimerRegistry {
179
322
  }
180
323
  };
181
324
 
182
- // src/utils/typeahead.ts
183
325
  var TYPEAHEAD_RESET_MS = 500;
184
326
  var Typeahead = class {
185
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
186
327
  #timers = new SafeTimeout();
187
- /** Idle window before the query resets, in milliseconds. */
188
328
  #resetMs;
189
- /** The accumulated lowercase query, empty when idle. */
190
329
  #query = "";
191
- /** Id of the pending reset timer, `0` when none is scheduled. */
192
330
  #timerId = 0;
193
- /** @param options - Overrides for the idle window. */
194
331
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
195
332
  this.#resetMs = resetMs;
196
333
  }
197
- /** The query a search would currently run with; empty while idle. */
198
334
  get query() {
199
335
  return this.#query;
200
336
  }
201
- /**
202
- * Folds `key` into the query, restarts the idle window, and returns the query to
203
- * search with. A repeated character collapses the query to that one character.
204
- */
205
337
  push(key) {
206
338
  const char = key.toLowerCase();
207
339
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -210,7 +342,6 @@ var Typeahead = class {
210
342
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
211
343
  return this.#query;
212
344
  }
213
- /** Clears the query and cancels the pending idle reset. */
214
345
  reset() {
215
346
  this.#query = "";
216
347
  this.#timers.clear(this.#timerId);
@@ -237,62 +368,28 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
237
368
  return -1;
238
369
  }
239
370
 
240
- // src/controllers/tree_view_controller.ts
241
371
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
242
- function setAttributeIfChanged(element, name, value) {
243
- if (element.getAttribute(name) !== value) element.setAttribute(name, value);
244
- }
245
372
  var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
246
373
  var TreeViewController = class extends Controller {
374
+ #moves = new MoveCounter();
375
+ #connectionGeneration = 0;
247
376
  static targets = ["item", "group", "field"];
248
- static actions = ["onClick", "onKeydown", "toggle"];
377
+ static actions = ["onKeydown", "select", "toggle"];
249
378
  static events = ["select", "toggle", "reconcile"];
250
379
  #roving = new RovingTabindex(() => this.itemTargets);
251
380
  #typeahead = new Typeahead();
252
- #timers = new SafeTimeout();
253
- /**
254
- * Collapses the item and field callbacks and observed item writes of one DOM
255
- * mutation into a single selection pass; refused before `connect()` and after
256
- * `disconnect()`.
257
- */
258
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileSelection());
259
- /** Watches the item attributes a page can rewrite in place; set while connected. */
260
- #observer = null;
261
- /** The selection last settled: on connect, by the user, or by a reported pass. */
381
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileSelection());
382
+ #observer = new AuthoredMutationWatcher((records) => {
383
+ if (this.#concernsItems(records)) this.#reconcile.schedule();
384
+ });
262
385
  #settled = { item: null, value: "" };
263
386
  #connected = false;
264
- /**
265
- * Item targets in DOM order as of the last connect / target change. A removed
266
- * item is gone from `itemTargets` by the time Stimulus reports it, so this is
267
- * the only record of where in the order the gap opened.
268
- */
269
387
  #order = [];
270
- /** The `treeitem` that last took DOM focus inside this tree, if any. */
271
388
  #focused = null;
272
- /**
273
- * Records which row owns DOM focus. `focusin` bubbles, so one listener covers
274
- * rows and anything nested in them; the tree needs this because a removal is
275
- * only reported *after* the fact, when the browser has already reset
276
- * `document.activeElement`.
277
- */
278
389
  #onFocusIn = (event) => {
279
390
  const target = event.target;
280
391
  this.#focused = target?.closest('[role="treeitem"]') ?? null;
281
392
  };
282
- /**
283
- * Forgets the tracked row when focus genuinely leaves the tree, so a later
284
- * removal of that row cannot be mistaken for "focus died with it" and pull
285
- * focus back in.
286
- *
287
- * A *removed* focused row must not clear the record — that is exactly the case
288
- * {@link TreeViewController.itemTargetDisconnected} needs it for. The two are
289
- * indistinguishable while the event is dispatching: Chromium fires `focusout`
290
- * for a removed row *before* the detach lands — `isConnected` still reads
291
- * `true` — with `document.activeElement` already on `<body>`, exactly like a
292
- * plain `blur()`. Deciding on a microtask separates them — a row that is gone
293
- * by then was removed. Focus moving between two rows needs no handling:
294
- * `focusin` overwrites the record right after.
295
- */
296
393
  #onFocusOut = (event) => {
297
394
  const target = event.target;
298
395
  const row = target?.closest('[role="treeitem"]') ?? null;
@@ -303,15 +400,10 @@ var TreeViewController = class extends Controller {
303
400
  if (this.#focused === row && row.isConnected) this.#focused = null;
304
401
  });
305
402
  };
306
- /**
307
- * Reconciles authored expansion state, establishes the single tab stop, and
308
- * settles the selection without reporting it. Idempotent, so a Turbo cache
309
- * restore / morph re-runs it safely (re-adding the same listener reference is a
310
- * no-op per the DOM spec).
311
- */
403
+ #listeners = new ListenerSet();
312
404
  connect() {
313
- this.element.addEventListener("focusin", this.#onFocusIn);
314
- this.element.addEventListener("focusout", this.#onFocusOut);
405
+ this.#listeners.add(this.element, "focusin", this.#onFocusIn);
406
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
315
407
  this.#seedFocused();
316
408
  this.#reconcileExpansion();
317
409
  this.#normalizeSelection();
@@ -320,163 +412,77 @@ var TreeViewController = class extends Controller {
320
412
  this.#order = [...this.itemTargets];
321
413
  this.#settled = this.#selection();
322
414
  this.#connected = true;
323
- this.#reconcile.activate();
415
+ this.#reconcile.observe(this.element);
324
416
  this.#observeItems();
325
417
  }
326
- /** Fills a form field inserted or replaced at runtime with the current selection. */
327
418
  fieldTargetConnected() {
328
419
  this.#reconcile.schedule();
329
420
  }
330
- /**
331
- * Detaches the focus trackers and the item observer, drops a queued selection
332
- * pass, the typeahead buffer and its timer.
333
- */
421
+ fieldTargetDisconnected() {
422
+ this.#reconcile.schedule();
423
+ }
334
424
  disconnect() {
335
- this.element.removeEventListener("focusin", this.#onFocusIn);
336
- this.element.removeEventListener("focusout", this.#onFocusOut);
425
+ this.#connectionGeneration += 1;
426
+ this.#listeners.dispose();
337
427
  this.#connected = false;
338
- this.#reconcile.cancel();
339
- this.#observer?.disconnect();
340
- this.#observer = null;
428
+ this.#reconcile.disconnect();
429
+ this.#observer.disconnect();
341
430
  this.#typeahead.reset();
342
- this.#timers.clearAll();
343
431
  this.#order = [];
344
432
  this.#focused = null;
345
433
  }
346
- /**
347
- * Seeds the focus record from the live document. A row can already hold focus
348
- * when the controller connects (a Turbo restore, or `data-controller` added to
349
- * a tree the user was already in), and the `focusin` listener alone would
350
- * never learn about it.
351
- */
352
434
  #seedFocused() {
353
- const active = this.element.ownerDocument.activeElement;
354
- this.#focused = active instanceof HTMLElement && this.element.contains(active) ? active.closest('[role="treeitem"]') : null;
355
- }
356
- /**
357
- * Re-normalizes the tab stop when a `treeitem` is added at runtime, so an
358
- * appended item carrying `tabindex="0"` cannot turn the tree into two Tab
359
- * stops, and settles the selection once the batch is in. Skipped before
360
- * `connect()`: Stimulus registers the initial targets first, and `connect()`
361
- * owns the initial tab-stop policy.
362
- */
435
+ this.#focused = this.element.ownerDocument.activeElement?.closest('[role="treeitem"]') ?? null;
436
+ }
363
437
  itemTargetConnected() {
364
438
  if (!this.#connected) return;
365
439
  this.#normalizeTabStop();
366
440
  this.#trackOrder();
367
441
  this.#reconcile.schedule();
368
442
  }
369
- /**
370
- * Brings authored `aria-selected` to the shape the APG requires, without
371
- * changing which item the author chose.
372
- *
373
- * Every item gets an explicit value — an absent `aria-selected` means "not
374
- * selectable" in ARIA, so a forgotten attribute hides a selectable row — and a
375
- * single-select tree keeps at most one `true`, first in DOM order: an item the
376
- * page inserts ahead of the selection, already selected, takes it over.
377
- * `connect()` reconciles authored expansion the same way. The scan is the
378
- * `item` target set: a `role="treeitem"` without the target is outside the
379
- * contract and is neither counted nor written.
380
- */
381
443
  #normalizeSelection() {
382
444
  const items = this.itemTargets;
383
445
  const selected = items.find((item) => item.getAttribute("aria-selected") === "true");
384
446
  for (const item of items) {
385
- setAttributeIfChanged(item, "aria-selected", item === selected ? "true" : "false");
447
+ this.#writeSelection(item, item === selected ? "true" : "false");
386
448
  }
387
449
  }
388
- /**
389
- * Mirrors the selected item's `data-value` into the optional form field.
390
- * Nothing selected, and an item that carries no value, both submit `""`.
391
- */
392
450
  #mirrorField(notify) {
393
451
  if (!this.hasFieldTarget) return;
394
452
  const moved = writeField(this.fieldTarget, this.#selection().value);
395
453
  if (moved && notify) commitField(this.fieldTarget);
396
454
  }
397
- /** The selected item target, if any, and the value its field submits. */
398
455
  #selection() {
399
456
  const item = this.itemTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
400
457
  return { item, value: item?.dataset.value ?? "" };
401
458
  }
402
- /**
403
- * The pass the page's changes run: the selection is normalized and mirrored,
404
- * then reported if it moved. The report comes last, so items a subscriber
405
- * rewrites are the next pass's to settle.
406
- */
407
459
  #reconcileSelection() {
460
+ this.#reconcileExpansion();
408
461
  this.#normalizeSelection();
409
- this.#dropOwnRecords();
462
+ this.#normalizeTabStop();
463
+ this.#flushMutations();
410
464
  this.#mirrorField(false);
411
465
  this.#reportMove();
412
466
  }
413
- /**
414
- * Reports the selection as `reconcile` when its item or value differs from the
415
- * selection last settled. The settled selection is replaced before dispatching,
416
- * so a move a subscriber makes is measured against what it was told.
417
- */
418
467
  #reportMove() {
419
468
  const selection = this.#selection();
420
469
  if (selection.item === this.#settled.item && selection.value === this.#settled.value) return;
421
470
  this.#settled = selection;
422
471
  this.dispatch("reconcile", { detail: { item: selection.item } });
423
472
  }
424
- /**
425
- * Watches the items' `aria-selected` and `data-value`. The tree's own writes to
426
- * them are dropped from the queue as they happen, so each record the callback
427
- * receives is the page's, and only a record on one of the tree's own items
428
- * schedules a pass.
429
- */
430
473
  #observeItems() {
431
- const observer = new MutationObserver((records) => {
432
- if (this.#concernsItems(records)) this.#reconcile.schedule();
433
- });
434
- observer.observe(this.element, {
474
+ this.#observer.observe(this.element, {
435
475
  subtree: true,
436
- attributes: true,
437
- attributeFilter: OBSERVED_ATTRIBUTES
476
+ attributes: OBSERVED_ATTRIBUTES
438
477
  });
439
- this.#observer = observer;
440
478
  }
441
- /**
442
- * Whether any record is about one of this tree's own items. A widget nested in a
443
- * row writes the same attributes on its own elements, and those are no reason to
444
- * reconcile the tree.
445
- */
446
479
  #concernsItems(records) {
447
480
  const items = new Set(this.itemTargets);
448
481
  return records.some((record) => items.has(record.target));
449
482
  }
450
- /**
451
- * Drops the records the tree's own writes just queued, so the observer never
452
- * takes them for the page's. A page write queued just before goes with them and
453
- * owes nothing: every such write leaves every item's `aria-selected` at the value
454
- * the selection gives it, and the selection is then read back from the DOM, so it
455
- * already holds what the page wrote.
456
- */
457
- #dropOwnRecords() {
458
- this.#observer?.takeRecords();
459
- }
460
- /**
461
- * Restores the roving invariants when a `treeitem` leaves at runtime — a Turbo
462
- * Stream `remove`, a morph, or plain DOM surgery. The mirror of
463
- * {@link TreeViewController.itemTargetConnected}: without it, removing the row
464
- * that held `tabindex="0"` leaves every survivor at `-1`, i.e. a tree with
465
- * **no** Tab stop, and drops DOM focus to `<body>` when the removed subtree
466
- * held it.
467
- *
468
- * The replacement is the nearest surviving *visible* item in the pre-removal
469
- * order — the one after the gap, else the one before it — which is the row
470
- * `ArrowDown` would have reached. Removing a non-active item changes nothing.
471
- * When no visible item survives, a tree with zero Tab stops is the correct end
472
- * state.
473
- *
474
- * Focus is only *restored*, never *stolen*: DOM focus moves solely when it was
475
- * inside the removed subtree and the document has nowhere left to put it. The
476
- * roving recovery runs here, while the pre-removal order is still on record;
477
- * the selection is settled once the batch is in, and a selected item that left
478
- * is reported as `reconcile` with no item.
479
- */
483
+ #flushMutations() {
484
+ if (this.#concernsItems(this.#observer.take())) this.#reconcile.schedule();
485
+ }
480
486
  itemTargetDisconnected(item) {
481
487
  if (!this.#connected || !this.element.isConnected) return;
482
488
  const stranded = this.#focusDiedWith(item);
@@ -488,12 +494,6 @@ var TreeViewController = class extends Controller {
488
494
  this.#reconcile.schedule();
489
495
  this.#order = this.#order.filter((tracked) => tracked !== item);
490
496
  }
491
- /**
492
- * Folds newly connected targets into the DOM-order snapshot, keeping the slot
493
- * of any item whose disconnect callback has not arrived yet — a morph that
494
- * both adds and removes rows reports one batch as several callbacks, and
495
- * {@link TreeViewController.#neighborOf} needs those slots.
496
- */
497
497
  #trackOrder() {
498
498
  const live = this.itemTargets;
499
499
  const pending = this.#order.filter((tracked) => !live.includes(tracked));
@@ -507,15 +507,10 @@ var TreeViewController = class extends Controller {
507
507
  }
508
508
  this.#order = merged;
509
509
  }
510
- /**
511
- * Routes tree keyboard interaction. Because `treeitem`s nest, only the handler
512
- * on the nearest item to the event target acts; the same keydown bubbling to an
513
- * ancestor item's handler is ignored to avoid double moves / selections. Keys
514
- * from a nested interactive control (or an IME composition) are left untouched.
515
- */
516
510
  onKeydown(event) {
517
511
  if (event.defaultPrevented) return;
518
512
  if (isReservedArrowChord(event)) return;
513
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
519
514
  if (event.isComposing) return;
520
515
  const item = this.#ownerItem(event);
521
516
  if (!item) return;
@@ -528,15 +523,6 @@ var TreeViewController = class extends Controller {
528
523
  event.preventDefault();
529
524
  this.#moveBy(item, -1);
530
525
  break;
531
- // Logical, not physical: APG describes these as "to the child level" and
532
- // "to the parent level" — a spatial move — so they follow the writing
533
- // direction, exactly as horizontal roving does. `isRtl()` is the shared
534
- // detector; never introduce a second one.
535
- //
536
- // Direction comes from the tree, not from `item`: the tree is what lays
537
- // the rows out, and a row may carry its own `dir` (an LTR path inside an
538
- // RTL browser is ordinary bidi authoring). Probing the focused row instead
539
- // would let one row's arrows mean the opposite of its sibling's.
540
526
  case "ArrowRight":
541
527
  event.preventDefault();
542
528
  if (isRtl(this.element)) this.#collapseOrLeave(item);
@@ -568,53 +554,43 @@ var TreeViewController = class extends Controller {
568
554
  break;
569
555
  }
570
556
  }
571
- /** Selects the clicked item (nearest to the target only). */
572
- onClick(event) {
573
- const item = this.#ownerItem(event);
574
- if (!item) return;
557
+ select(source) {
558
+ const { event, host, origin, reason } = actionSource(source);
559
+ const item = host?.closest(targetSelector(this.identifier, "item"));
560
+ if (!item || !this.itemTargets.includes(item)) return;
561
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
562
+ return;
563
+ if (event && this.#ownerItem(event) !== item) return;
575
564
  const group = this.#childGroup(item);
576
- if (group?.contains(event.target)) return;
577
- this.#focusItem(item);
578
- this.#select(item);
579
- }
580
- /**
581
- * Expands or collapses the `treeitem` nearest to the event target. Optional:
582
- * wire it on a chevron control so pointer users can reach child nodes, which
583
- * otherwise only `ArrowRight` / `ArrowLeft` can do. A leaf item is a no-op.
584
- *
585
- * The toggled row then takes focus and the tab stop, matching what the keyboard
586
- * path guarantees. Without it, a browser that focuses buttons on mousedown leaves
587
- * focus on the clicked chevron: arrows would stop working until focus returns to a
588
- * row, and a decorative `aria-hidden` chevron would hold focus. The synchronous
589
- * `stimeo--tree-view:toggle` event is observed before this final focus hand-off.
590
- */
591
- toggle(event) {
592
- const item = event.target.closest('[role="treeitem"]');
593
- if (!item || !this.element.contains(item)) return;
594
- if (!this.#childGroup(item)) return;
595
- this.#setExpanded(item, !this.#isExpanded(item));
596
- this.#focusItem(item);
597
- }
598
- /**
599
- * The item a raw event belongs to, or `null` when the tree must not act: the
600
- * event came from a nested `treeitem` (it bubbled to an ancestor's handler) or
601
- * from an interactive control inside the item.
602
- */
565
+ if (event && origin && group?.contains(origin)) return;
566
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
567
+ this.#focusItem(item, moveFocus);
568
+ this.#select(item, reason);
569
+ }
570
+ toggle(source) {
571
+ const { event, origin, reason } = actionSource(source);
572
+ const item = origin?.closest(targetSelector(this.identifier, "item"));
573
+ if (!item || !this.itemTargets.includes(item)) return;
574
+ if (origin?.closest(`[data-controller~="${this.identifier}"]`) !== this.element) return;
575
+ const group = this.#childGroup(item);
576
+ if (!group) return;
577
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
578
+ this.#setExpanded(item, group, !this.#isExpanded(item), reason);
579
+ this.#focusItem(
580
+ item,
581
+ moveFocus && (event !== null || this.element.contains(document.activeElement))
582
+ );
583
+ }
603
584
  #ownerItem(event) {
604
- const item = event.currentTarget;
605
- const target = event.target;
585
+ const { host, origin: target } = actionSource(event);
586
+ const item = host?.closest('[role="treeitem"]');
587
+ if (!item || !target) return null;
606
588
  if (target.closest('[role="treeitem"]') !== item) return null;
607
589
  const control = target.closest(NESTED_INTERACTIVE);
608
590
  if (control && item.contains(control)) return null;
609
- if (isInteractiveHost(target)) return null;
591
+ if (target instanceof HTMLElement && isInteractiveHost(target)) return null;
610
592
  return item;
611
593
  }
612
- /**
613
- * The surviving visible item that inherits `removed`'s position: the next one
614
- * in the pre-removal order, else the previous one. Falls back to the first
615
- * visible item when `removed` is not in that order — a target that arrived and
616
- * left between two callbacks — and to `undefined` when nothing visible is left.
617
- */
618
594
  #neighborOf(removed) {
619
595
  const visible = this.#visibleItems;
620
596
  const index = this.#order.indexOf(removed);
@@ -628,14 +604,6 @@ var TreeViewController = class extends Controller {
628
604
  }
629
605
  return visible[0];
630
606
  }
631
- /**
632
- * Whether DOM focus went down with `removed`: it sat on that row (or inside
633
- * it) and the document now has nowhere to put focus. A browser falls back to
634
- * `<body>`; a still-referenced but detached `activeElement` is the same
635
- * condition seen from the other side. Focus that landed on a real element —
636
- * inside this tree or anywhere else on the page — is left alone, so a removal
637
- * can never steal it.
638
- */
639
607
  #focusDiedWith(removed) {
640
608
  const focused = this.#focused;
641
609
  if (!focused || focused !== removed && !removed.contains(focused)) return false;
@@ -644,7 +612,6 @@ var TreeViewController = class extends Controller {
644
612
  if (active === null || active === doc.body || active === doc.documentElement) return true;
645
613
  return !active.isConnected;
646
614
  }
647
- /** Moves focus to the next (`delta=1`) or previous visible item, if any. */
648
615
  #moveBy(item, delta) {
649
616
  const visible = this.#visibleItems;
650
617
  const current = visible.indexOf(item);
@@ -652,17 +619,11 @@ var TreeViewController = class extends Controller {
652
619
  const next = visible[current + delta];
653
620
  if (next) this.#focusItem(next);
654
621
  }
655
- /**
656
- * Expands a collapsed parent, else steps into the first child.
657
- *
658
- * Bound to `ArrowRight` under LTR and `ArrowLeft` under RTL — the caller picks;
659
- * this method only knows "toward the child level".
660
- */
661
622
  #expandOrEnter(item) {
662
623
  const group = this.#childGroup(item);
663
624
  if (!group) return;
664
625
  if (!this.#isExpanded(item)) {
665
- this.#setExpanded(item, true);
626
+ this.#setExpanded(item, group, true);
666
627
  return;
667
628
  }
668
629
  if (group.hidden) return;
@@ -671,34 +632,20 @@ var TreeViewController = class extends Controller {
671
632
  ).find((child) => !child.hidden);
672
633
  if (firstChild) this.#focusItem(firstChild);
673
634
  }
674
- /** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
675
635
  #collapseOrLeave(item) {
676
- if (this.#childGroup(item) && this.#isExpanded(item)) {
677
- this.#setExpanded(item, false);
636
+ const group = this.#childGroup(item);
637
+ if (group && this.#isExpanded(item)) {
638
+ this.#setExpanded(item, group, false);
678
639
  return;
679
640
  }
680
641
  const parent = this.#parentItem(item);
681
642
  if (parent) this.#focusItem(parent);
682
643
  }
683
- /** Focuses the first (`0`) or last (`-1`) visible item. */
684
644
  #focusEdge(index) {
685
645
  const visible = this.#visibleItems;
686
646
  const target = index < 0 ? visible[visible.length - 1] : visible[index];
687
647
  if (target) this.#focusItem(target);
688
648
  }
689
- /**
690
- * Whether the item is disabled — **its own attribute or an ancestor's**.
691
- *
692
- * ARIA is explicit that the state carries down: "The state of being disabled
693
- * applies to the current element *and all focusable descendant elements* of the
694
- * element on which the `aria-disabled` attribute is applied." A tree is the only
695
- * pattern here whose items nest, so it is the only place the inheritance is
696
- * observable — but the rule is the shared one.
697
- *
698
- * The walk stops at the controller element: an `aria-disabled` on the tree root
699
- * would otherwise disable every row, and a disabled *tree* is the consumer's
700
- * call to make with `inert`, not something this controller infers.
701
- */
702
649
  #isDisabled(item) {
703
650
  let current = item;
704
651
  while (current && current !== this.element) {
@@ -707,38 +654,31 @@ var TreeViewController = class extends Controller {
707
654
  }
708
655
  return false;
709
656
  }
710
- /**
711
- * Applies single selection and dispatches `select`. An `aria-disabled="true"`
712
- * item stays focusable (APG keeps disabled nodes reachable) but is never
713
- * activated, so consumers see no `select` for it.
714
- */
715
- #select(item) {
657
+ #select(item, reason = "user") {
716
658
  if (this.#isDisabled(item)) return;
659
+ const generation = this.#connectionGeneration;
717
660
  for (const candidate of this.itemTargets) {
718
- setAttributeIfChanged(candidate, "aria-selected", candidate === item ? "true" : "false");
661
+ this.#writeSelection(candidate, candidate === item ? "true" : "false");
719
662
  }
720
- this.#dropOwnRecords();
721
- this.#settled = this.#selection();
663
+ this.#flushMutations();
664
+ const selection = this.#selection();
665
+ const changed = selection.item !== this.#settled.item || selection.value !== this.#settled.value;
666
+ this.#settled = selection;
667
+ if (!changed) {
668
+ this.#mirrorField(false);
669
+ return;
670
+ }
671
+ const token = this.#moves.record();
722
672
  this.#mirrorField(true);
723
- this.dispatch("select", { detail: { item } });
673
+ if (generation !== this.#connectionGeneration || !this.#moves.isLatest(token)) return;
674
+ this.dispatch("select", { detail: { item, reason } });
724
675
  }
725
- /** Updates expansion, reconciles a collapsed subtree, then synchronously dispatches `toggle`. */
726
- #setExpanded(item, expanded) {
727
- const group = this.#childGroup(item);
728
- if (!group) return;
676
+ #setExpanded(item, group, expanded, reason = "user") {
729
677
  item.setAttribute("aria-expanded", String(expanded));
730
678
  group.hidden = !expanded;
731
679
  if (!expanded) this.#escapeCollapsedSubtree(item, group);
732
- this.dispatch("toggle", { detail: { item, expanded } });
733
- }
734
- /**
735
- * Reconciles focus and the roving tab stop before a synchronous `toggle` event
736
- * observes a newly collapsed group. If DOM focus remains inside the group,
737
- * both move to the parent. Otherwise only a stranded tab stop moves, preserving
738
- * DOM focus. A native chevron click takes this path when mousedown has already
739
- * focused the button. {@link TreeViewController.toggle} completes its focus
740
- * hand-off after this reconciliation and event dispatch.
741
- */
680
+ this.dispatch("toggle", { detail: { item, expanded, reason } });
681
+ }
742
682
  #escapeCollapsedSubtree(item, group) {
743
683
  const active = document.activeElement;
744
684
  if (active instanceof HTMLElement && group.contains(active)) {
@@ -746,23 +686,21 @@ var TreeViewController = class extends Controller {
746
686
  return;
747
687
  }
748
688
  const activeIndex = this.#roving.activeIndex;
749
- const tabbable = activeIndex === -1 ? void 0 : this.itemTargets[activeIndex];
689
+ const tabbable = this.itemTargets[activeIndex];
750
690
  if (!tabbable || !group.contains(tabbable)) return;
751
691
  const index = this.itemTargets.indexOf(item);
752
692
  if (index !== -1) this.#roving.setActive(index);
753
693
  }
754
- /** Makes `item` the single tab stop and moves DOM focus to it. */
755
- #focusItem(item) {
694
+ #focusItem(item, focus = true) {
756
695
  const index = this.itemTargets.indexOf(item);
757
696
  if (index !== -1) {
758
- this.#roving.setActive(index, { focus: true });
697
+ this.#roving.setActive(index, { focus });
759
698
  return;
760
699
  }
761
700
  if (!canTakeFocus(item)) return;
762
701
  item.tabIndex = -1;
763
702
  item.focus();
764
703
  }
765
- /** Advances the typeahead query and focuses the next matching visible item. */
766
704
  #typeaheadTo(item, char) {
767
705
  const query = this.#typeahead.push(char);
768
706
  const visible = this.#visibleItems;
@@ -775,13 +713,6 @@ var TreeViewController = class extends Controller {
775
713
  const match = visible[index];
776
714
  if (match) this.#focusItem(match);
777
715
  }
778
- /**
779
- * Aligns each parent's `aria-expanded` with its child `group`'s `hidden`.
780
- * `aria-expanded` wins when authored (it is what assistive tech reads); a
781
- * parent without it derives one from `hidden` (establish-only-when-absent, so
782
- * a restored snapshot is never clobbered). Disagreeing markup would otherwise
783
- * strand the only tab stop on an invisible item.
784
- */
785
716
  #reconcileExpansion() {
786
717
  for (const item of this.itemTargets) {
787
718
  const group = this.#childGroup(item);
@@ -793,32 +724,19 @@ var TreeViewController = class extends Controller {
793
724
  }
794
725
  }
795
726
  }
796
- /**
797
- * Leaves exactly one item tabbable. An existing, still-visible tab stop wins
798
- * (the DOM is the source of truth after a Turbo restore, so the user's roving
799
- * position survives); otherwise the selected item takes it (APG: a single-select
800
- * tree puts initial focus on its selected node), else the first visible item.
801
- */
802
727
  #normalizeTabStop() {
803
728
  const items = this.itemTargets;
804
729
  const visible = this.#visibleItems;
805
730
  const activeIndex = this.#roving.activeIndex;
806
- const active = activeIndex === -1 ? void 0 : items[activeIndex];
731
+ const active = items[activeIndex];
807
732
  if (active && visible.includes(active)) {
808
733
  this.#roving.setActive(activeIndex);
809
734
  return;
810
735
  }
811
736
  const selected = visible.find((item) => item.getAttribute("aria-selected") === "true");
812
737
  const next = selected ?? visible[0];
813
- this.#roving.setActive(next ? items.indexOf(next) : -1);
814
- }
815
- /**
816
- * The visible items: those with no `hidden` ancestor up to the tree, and not
817
- * `hidden` themselves. A hidden row is out of the move set — it can hold
818
- * neither DOM focus nor the only Tab stop — and neither can anything nested
819
- * under one, which is why the walk tests every ancestor rather than only the
820
- * collapsed `role="group"` containers.
821
- */
738
+ this.#roving.setActive(items.indexOf(next));
739
+ }
822
740
  get #visibleItems() {
823
741
  return this.itemTargets.filter((item) => {
824
742
  let node = item;
@@ -829,23 +747,15 @@ var TreeViewController = class extends Controller {
829
747
  return true;
830
748
  });
831
749
  }
832
- /** The child `group` owned directly by `item`, or `null` for a leaf. */
833
750
  #childGroup(item) {
834
751
  return item.querySelector(':scope > [role="group"]');
835
752
  }
836
- /** The nearest ancestor `treeitem`, or `null` at the root level. */
837
753
  #parentItem(item) {
838
754
  return item.parentElement?.closest('[role="treeitem"]') ?? null;
839
755
  }
840
- /** Whether a parent item is currently expanded. */
841
756
  #isExpanded(item) {
842
757
  return item.getAttribute("aria-expanded") === "true";
843
758
  }
844
- /**
845
- * `item`'s name for typeahead. The fallback is the item's *own* text: a parent
846
- * row's nested child group is rendered inside it, and folding a whole subtree
847
- * into one name would let "readme" match the folder containing it.
848
- */
849
759
  #label(item) {
850
760
  return typeaheadLabel(item, () => {
851
761
  let text = "";
@@ -858,8 +768,12 @@ var TreeViewController = class extends Controller {
858
768
  return text;
859
769
  });
860
770
  }
771
+ #writeSelection(target, value) {
772
+ setAttributeIfChanged(target, "aria-selected", value, () => {
773
+ this.#observer.claim(target, "aria-selected", value);
774
+ });
775
+ }
861
776
  };
862
777
 
863
778
  export { TreeViewController };
864
- //# sourceMappingURL=tree_view_controller.js.map
865
779
  //# sourceMappingURL=tree_view_controller.js.map