stimeo-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +8 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +363 -436
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +4 -2
  7. package/dist/controllers/accordion_controller.js +166 -55
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +35 -5
  13. package/dist/controllers/announcer_controller.js +135 -198
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +271 -127
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +171 -105
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.js +0 -130
  23. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  24. package/dist/controllers/bulk_select_controller.d.ts +18 -0
  25. package/dist/controllers/bulk_select_controller.js +262 -52
  26. package/dist/controllers/bulk_select_controller.js.map +1 -1
  27. package/dist/controllers/calendar_controller.d.ts +24 -5
  28. package/dist/controllers/calendar_controller.js +175 -209
  29. package/dist/controllers/calendar_controller.js.map +1 -1
  30. package/dist/controllers/carousel_controller.d.ts +34 -7
  31. package/dist/controllers/carousel_controller.js +461 -399
  32. package/dist/controllers/carousel_controller.js.map +1 -1
  33. package/dist/controllers/character_counter_controller.d.ts +17 -1
  34. package/dist/controllers/character_counter_controller.js +246 -156
  35. package/dist/controllers/character_counter_controller.js.map +1 -1
  36. package/dist/controllers/checkbox_controller.d.ts +7 -2
  37. package/dist/controllers/checkbox_controller.js +32 -74
  38. package/dist/controllers/checkbox_controller.js.map +1 -1
  39. package/dist/controllers/clipboard_controller.d.ts +16 -5
  40. package/dist/controllers/clipboard_controller.js +168 -147
  41. package/dist/controllers/clipboard_controller.js.map +1 -1
  42. package/dist/controllers/collapsible_controller.d.ts +16 -4
  43. package/dist/controllers/collapsible_controller.js +301 -120
  44. package/dist/controllers/collapsible_controller.js.map +1 -1
  45. package/dist/controllers/color_picker_controller.d.ts +18 -6
  46. package/dist/controllers/color_picker_controller.js +149 -139
  47. package/dist/controllers/color_picker_controller.js.map +1 -1
  48. package/dist/controllers/combobox_controller.d.ts +23 -5
  49. package/dist/controllers/combobox_controller.js +360 -133
  50. package/dist/controllers/combobox_controller.js.map +1 -1
  51. package/dist/controllers/command_palette_controller.d.ts +45 -14
  52. package/dist/controllers/command_palette_controller.js +970 -358
  53. package/dist/controllers/command_palette_controller.js.map +1 -1
  54. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  55. package/dist/controllers/conditional_fields_controller.js +166 -124
  56. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  57. package/dist/controllers/confirm_controller.d.ts +49 -12
  58. package/dist/controllers/confirm_controller.js +939 -248
  59. package/dist/controllers/confirm_controller.js.map +1 -1
  60. package/dist/controllers/context_menu_controller.d.ts +31 -4
  61. package/dist/controllers/context_menu_controller.js +256 -128
  62. package/dist/controllers/context_menu_controller.js.map +1 -1
  63. package/dist/controllers/count_up_controller.d.ts +13 -1
  64. package/dist/controllers/count_up_controller.js +73 -21
  65. package/dist/controllers/count_up_controller.js.map +1 -1
  66. package/dist/controllers/countdown_controller.d.ts +45 -1
  67. package/dist/controllers/countdown_controller.js +183 -148
  68. package/dist/controllers/countdown_controller.js.map +1 -1
  69. package/dist/controllers/currency_input_controller.d.ts +21 -2
  70. package/dist/controllers/currency_input_controller.js +134 -158
  71. package/dist/controllers/currency_input_controller.js.map +1 -1
  72. package/dist/controllers/data_grid_controller.d.ts +15 -4
  73. package/dist/controllers/data_grid_controller.js +114 -186
  74. package/dist/controllers/data_grid_controller.js.map +1 -1
  75. package/dist/controllers/date_range_picker_controller.d.ts +30 -5
  76. package/dist/controllers/date_range_picker_controller.js +288 -206
  77. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  78. package/dist/controllers/dialog_controller.d.ts +13 -1
  79. package/dist/controllers/dialog_controller.js +858 -223
  80. package/dist/controllers/dialog_controller.js.map +1 -1
  81. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  82. package/dist/controllers/direct_upload_controller.js +73 -153
  83. package/dist/controllers/direct_upload_controller.js.map +1 -1
  84. package/dist/controllers/dirty_form_controller.js +14 -48
  85. package/dist/controllers/dirty_form_controller.js.map +1 -1
  86. package/dist/controllers/dismissible_controller.d.ts +19 -0
  87. package/dist/controllers/dismissible_controller.js +175 -21
  88. package/dist/controllers/dismissible_controller.js.map +1 -1
  89. package/dist/controllers/drawer_controller.d.ts +39 -11
  90. package/dist/controllers/drawer_controller.js +933 -341
  91. package/dist/controllers/drawer_controller.js.map +1 -1
  92. package/dist/controllers/dropdown_controller.d.ts +31 -1
  93. package/dist/controllers/dropdown_controller.js +312 -82
  94. package/dist/controllers/dropdown_controller.js.map +1 -1
  95. package/dist/controllers/editable_controller.d.ts +12 -1
  96. package/dist/controllers/editable_controller.js +158 -68
  97. package/dist/controllers/editable_controller.js.map +1 -1
  98. package/dist/controllers/empty_state_controller.js +67 -53
  99. package/dist/controllers/empty_state_controller.js.map +1 -1
  100. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  101. package/dist/controllers/file_dropzone_controller.js +311 -207
  102. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  103. package/dist/controllers/filter_controller.js +67 -39
  104. package/dist/controllers/filter_controller.js.map +1 -1
  105. package/dist/controllers/flash_controller.d.ts +31 -8
  106. package/dist/controllers/flash_controller.js +172 -314
  107. package/dist/controllers/flash_controller.js.map +1 -1
  108. package/dist/controllers/focus_controller.d.ts +12 -3
  109. package/dist/controllers/focus_controller.js +654 -240
  110. package/dist/controllers/focus_controller.js.map +1 -1
  111. package/dist/controllers/form_field_controller.js +147 -132
  112. package/dist/controllers/form_field_controller.js.map +1 -1
  113. package/dist/controllers/form_validation_controller.js +13 -86
  114. package/dist/controllers/form_validation_controller.js.map +1 -1
  115. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  116. package/dist/controllers/frame_loading_controller.js +264 -213
  117. package/dist/controllers/frame_loading_controller.js.map +1 -1
  118. package/dist/controllers/highlight_controller.d.ts +22 -2
  119. package/dist/controllers/highlight_controller.js +107 -75
  120. package/dist/controllers/highlight_controller.js.map +1 -1
  121. package/dist/controllers/hover_card_controller.d.ts +56 -3
  122. package/dist/controllers/hover_card_controller.js +309 -118
  123. package/dist/controllers/hover_card_controller.js.map +1 -1
  124. package/dist/controllers/idle_controller.d.ts +42 -1
  125. package/dist/controllers/idle_controller.js +267 -101
  126. package/dist/controllers/idle_controller.js.map +1 -1
  127. package/dist/controllers/input_mask_controller.js +71 -72
  128. package/dist/controllers/input_mask_controller.js.map +1 -1
  129. package/dist/controllers/intersection_controller.d.ts +32 -13
  130. package/dist/controllers/intersection_controller.js +147 -106
  131. package/dist/controllers/intersection_controller.js.map +1 -1
  132. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  133. package/dist/controllers/lazy_frame_controller.js +9 -65
  134. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  135. package/dist/controllers/listbox_controller.d.ts +34 -2
  136. package/dist/controllers/listbox_controller.js +306 -204
  137. package/dist/controllers/listbox_controller.js.map +1 -1
  138. package/dist/controllers/local_time_controller.d.ts +1 -1
  139. package/dist/controllers/local_time_controller.js +54 -64
  140. package/dist/controllers/local_time_controller.js.map +1 -1
  141. package/dist/controllers/masonry_controller.d.ts +11 -3
  142. package/dist/controllers/masonry_controller.js +70 -93
  143. package/dist/controllers/masonry_controller.js.map +1 -1
  144. package/dist/controllers/menu_controller.d.ts +32 -7
  145. package/dist/controllers/menu_controller.js +214 -156
  146. package/dist/controllers/menu_controller.js.map +1 -1
  147. package/dist/controllers/menubar_controller.d.ts +4 -4
  148. package/dist/controllers/menubar_controller.js +170 -329
  149. package/dist/controllers/menubar_controller.js.map +1 -1
  150. package/dist/controllers/meter_controller.d.ts +20 -0
  151. package/dist/controllers/meter_controller.js +151 -67
  152. package/dist/controllers/meter_controller.js.map +1 -1
  153. package/dist/controllers/multi_select_controller.d.ts +32 -4
  154. package/dist/controllers/multi_select_controller.js +593 -358
  155. package/dist/controllers/multi_select_controller.js.map +1 -1
  156. package/dist/controllers/navigation_menu_controller.d.ts +8 -1
  157. package/dist/controllers/navigation_menu_controller.js +124 -232
  158. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  159. package/dist/controllers/nested_form_controller.d.ts +10 -0
  160. package/dist/controllers/nested_form_controller.js +352 -182
  161. package/dist/controllers/nested_form_controller.js.map +1 -1
  162. package/dist/controllers/network_status_controller.d.ts +23 -0
  163. package/dist/controllers/network_status_controller.js +242 -54
  164. package/dist/controllers/network_status_controller.js.map +1 -1
  165. package/dist/controllers/number_input_controller.d.ts +26 -2
  166. package/dist/controllers/number_input_controller.js +320 -263
  167. package/dist/controllers/number_input_controller.js.map +1 -1
  168. package/dist/controllers/optimistic_controller.js +16 -76
  169. package/dist/controllers/optimistic_controller.js.map +1 -1
  170. package/dist/controllers/otp_controller.d.ts +20 -4
  171. package/dist/controllers/otp_controller.js +198 -193
  172. package/dist/controllers/otp_controller.js.map +1 -1
  173. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  174. package/dist/controllers/overflow_indicator_controller.js +116 -113
  175. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  176. package/dist/controllers/overflow_menu_controller.d.ts +48 -11
  177. package/dist/controllers/overflow_menu_controller.js +343 -283
  178. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  179. package/dist/controllers/pagination_controller.d.ts +35 -6
  180. package/dist/controllers/pagination_controller.js +295 -119
  181. package/dist/controllers/pagination_controller.js.map +1 -1
  182. package/dist/controllers/password_reveal_controller.d.ts +45 -8
  183. package/dist/controllers/password_reveal_controller.js +389 -121
  184. package/dist/controllers/password_reveal_controller.js.map +1 -1
  185. package/dist/controllers/password_strength_controller.d.ts +14 -5
  186. package/dist/controllers/password_strength_controller.js +99 -153
  187. package/dist/controllers/password_strength_controller.js.map +1 -1
  188. package/dist/controllers/persist_controller.d.ts +11 -4
  189. package/dist/controllers/persist_controller.js +237 -103
  190. package/dist/controllers/persist_controller.js.map +1 -1
  191. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  192. package/dist/controllers/pointer_drag_controller.js +185 -165
  193. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  194. package/dist/controllers/popover_controller.d.ts +38 -2
  195. package/dist/controllers/popover_controller.js +236 -97
  196. package/dist/controllers/popover_controller.js.map +1 -1
  197. package/dist/controllers/portal_controller.d.ts +44 -4
  198. package/dist/controllers/portal_controller.js +209 -76
  199. package/dist/controllers/portal_controller.js.map +1 -1
  200. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  201. package/dist/controllers/preview_guard_controller.js +232 -116
  202. package/dist/controllers/preview_guard_controller.js.map +1 -1
  203. package/dist/controllers/progress_controller.d.ts +11 -0
  204. package/dist/controllers/progress_controller.js +125 -56
  205. package/dist/controllers/progress_controller.js.map +1 -1
  206. package/dist/controllers/radio_group_controller.d.ts +14 -5
  207. package/dist/controllers/radio_group_controller.js +117 -137
  208. package/dist/controllers/radio_group_controller.js.map +1 -1
  209. package/dist/controllers/range_slider_controller.d.ts +43 -3
  210. package/dist/controllers/range_slider_controller.js +192 -105
  211. package/dist/controllers/range_slider_controller.js.map +1 -1
  212. package/dist/controllers/rating_controller.d.ts +20 -4
  213. package/dist/controllers/rating_controller.js +418 -207
  214. package/dist/controllers/rating_controller.js.map +1 -1
  215. package/dist/controllers/read_more_controller.d.ts +2 -0
  216. package/dist/controllers/read_more_controller.js +134 -63
  217. package/dist/controllers/read_more_controller.js.map +1 -1
  218. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  219. package/dist/controllers/reading_progress_controller.js +147 -135
  220. package/dist/controllers/reading_progress_controller.js.map +1 -1
  221. package/dist/controllers/relative_time_controller.d.ts +18 -4
  222. package/dist/controllers/relative_time_controller.js +145 -110
  223. package/dist/controllers/relative_time_controller.js.map +1 -1
  224. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  225. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  226. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  227. package/dist/controllers/resizable_controller.d.ts +28 -2
  228. package/dist/controllers/resizable_controller.js +255 -139
  229. package/dist/controllers/resizable_controller.js.map +1 -1
  230. package/dist/controllers/roving_controller.js +39 -84
  231. package/dist/controllers/roving_controller.js.map +1 -1
  232. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  233. package/dist/controllers/scroll_area_controller.js +428 -225
  234. package/dist/controllers/scroll_area_controller.js.map +1 -1
  235. package/dist/controllers/scroll_restore_controller.js +0 -53
  236. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  237. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  238. package/dist/controllers/scroll_visibility_controller.js +395 -145
  239. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  240. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  241. package/dist/controllers/scrollspy_controller.js +181 -242
  242. package/dist/controllers/scrollspy_controller.js.map +1 -1
  243. package/dist/controllers/separator_controller.d.ts +19 -4
  244. package/dist/controllers/separator_controller.js +267 -140
  245. package/dist/controllers/separator_controller.js.map +1 -1
  246. package/dist/controllers/sidebar_controller.d.ts +64 -7
  247. package/dist/controllers/sidebar_controller.js +1023 -386
  248. package/dist/controllers/sidebar_controller.js.map +1 -1
  249. package/dist/controllers/skeleton_controller.d.ts +23 -0
  250. package/dist/controllers/skeleton_controller.js +248 -97
  251. package/dist/controllers/skeleton_controller.js.map +1 -1
  252. package/dist/controllers/slider_controller.d.ts +27 -1
  253. package/dist/controllers/slider_controller.js +159 -79
  254. package/dist/controllers/slider_controller.js.map +1 -1
  255. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  256. package/dist/controllers/smart_sticky_header_controller.js +162 -63
  257. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  258. package/dist/controllers/sortable_controller.js +0 -112
  259. package/dist/controllers/sortable_controller.js.map +1 -1
  260. package/dist/controllers/spinner_controller.d.ts +39 -4
  261. package/dist/controllers/spinner_controller.js +277 -169
  262. package/dist/controllers/spinner_controller.js.map +1 -1
  263. package/dist/controllers/step_indicator_controller.d.ts +5 -0
  264. package/dist/controllers/step_indicator_controller.js +99 -46
  265. package/dist/controllers/step_indicator_controller.js.map +1 -1
  266. package/dist/controllers/stepper_controller.d.ts +12 -3
  267. package/dist/controllers/stepper_controller.js +114 -73
  268. package/dist/controllers/stepper_controller.js.map +1 -1
  269. package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
  270. package/dist/controllers/stick_to_bottom_controller.js +262 -138
  271. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  272. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  273. package/dist/controllers/sticky_observer_controller.js +214 -49
  274. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  275. package/dist/controllers/submit_once_controller.d.ts +23 -8
  276. package/dist/controllers/submit_once_controller.js +294 -205
  277. package/dist/controllers/submit_once_controller.js.map +1 -1
  278. package/dist/controllers/switch_controller.d.ts +12 -1
  279. package/dist/controllers/switch_controller.js +33 -60
  280. package/dist/controllers/switch_controller.js.map +1 -1
  281. package/dist/controllers/tabs_controller.d.ts +2 -2
  282. package/dist/controllers/tabs_controller.js +35 -29
  283. package/dist/controllers/tabs_controller.js.map +1 -1
  284. package/dist/controllers/tags_input_controller.d.ts +12 -0
  285. package/dist/controllers/tags_input_controller.js +136 -145
  286. package/dist/controllers/tags_input_controller.js.map +1 -1
  287. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  288. package/dist/controllers/textarea_autosize_controller.js +71 -58
  289. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  290. package/dist/controllers/theme_controller.d.ts +21 -9
  291. package/dist/controllers/theme_controller.js +130 -112
  292. package/dist/controllers/theme_controller.js.map +1 -1
  293. package/dist/controllers/time_picker_controller.d.ts +18 -1
  294. package/dist/controllers/time_picker_controller.js +146 -80
  295. package/dist/controllers/time_picker_controller.js.map +1 -1
  296. package/dist/controllers/toast_controller.d.ts +36 -11
  297. package/dist/controllers/toast_controller.js +186 -297
  298. package/dist/controllers/toast_controller.js.map +1 -1
  299. package/dist/controllers/toggle_group_controller.d.ts +12 -3
  300. package/dist/controllers/toggle_group_controller.js +95 -136
  301. package/dist/controllers/toggle_group_controller.js.map +1 -1
  302. package/dist/controllers/toolbar_controller.js +68 -98
  303. package/dist/controllers/toolbar_controller.js.map +1 -1
  304. package/dist/controllers/tooltip_controller.d.ts +45 -3
  305. package/dist/controllers/tooltip_controller.js +158 -124
  306. package/dist/controllers/tooltip_controller.js.map +1 -1
  307. package/dist/controllers/transition_controller.d.ts +22 -4
  308. package/dist/controllers/transition_controller.js +123 -119
  309. package/dist/controllers/transition_controller.js.map +1 -1
  310. package/dist/controllers/tree_view_controller.d.ts +21 -11
  311. package/dist/controllers/tree_view_controller.js +150 -345
  312. package/dist/controllers/tree_view_controller.js.map +1 -1
  313. package/dist/index.d.ts +8 -5
  314. package/dist/index.js +6370 -9346
  315. package/dist/index.js.map +1 -1
  316. package/dist/inspector/cli.d.ts +23 -3
  317. package/dist/inspector/cli.js +92 -37
  318. package/dist/inspector/cli.js.map +1 -1
  319. package/dist/inspector/cli_bin.js +92 -56
  320. package/dist/inspector/cli_bin.js.map +1 -1
  321. package/dist/inspector/examples.json +8 -8
  322. package/dist/inspector/manifest.json +787 -100
  323. package/dist/positioning/index.d.ts +8 -0
  324. package/dist/positioning/index.js +148 -47
  325. package/dist/positioning/index.js.map +1 -1
  326. package/package.json +6 -6
  327. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  328. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,19 +1,36 @@
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
  }
15
33
 
16
- // src/utils/field_mirror.ts
17
34
  function writeField(field, value) {
18
35
  if (field.value === value) return false;
19
36
  field.value = value;
@@ -23,7 +40,6 @@ function commitField(target) {
23
40
  target.dispatchEvent(new Event("change", { bubbles: true }));
24
41
  }
25
42
 
26
- // src/utils/focus_candidate.ts
27
43
  function inheritsFieldsetDisabled(control) {
28
44
  let fieldset = control.closest("fieldset[disabled]");
29
45
  while (fieldset) {
@@ -33,15 +49,25 @@ function inheritsFieldsetDisabled(control) {
33
49
  }
34
50
  return false;
35
51
  }
52
+ function unslotted(element) {
53
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
54
+ }
55
+ function flatTreeParent(element) {
56
+ if (unslotted(element)) return null;
57
+ const parent = element.assignedSlot ?? element.parentNode;
58
+ if (parent instanceof ShadowRoot) return parent.host;
59
+ return parent instanceof Element ? parent : null;
60
+ }
36
61
  function canTakeFocus(element) {
37
- if (element.closest("[hidden], [inert]")) return false;
62
+ for (let node = element; node; node = flatTreeParent(node)) {
63
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
64
+ }
38
65
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
39
66
  if (!("disabled" in element)) return true;
40
67
  if (element.disabled) return false;
41
68
  return !inheritsFieldsetDisabled(element);
42
69
  }
43
70
 
44
- // src/utils/interactive_host.ts
45
71
  var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
46
72
  function isInteractiveHost(element) {
47
73
  if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
@@ -58,27 +84,33 @@ function isInteractiveHost(element) {
58
84
  return false;
59
85
  }
60
86
 
61
- // src/utils/microtask_coalescer.ts
87
+ var ListenerSet = class {
88
+ #abort = new AbortController();
89
+ add(target, type, handler, options) {
90
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
91
+ }
92
+ dispose() {
93
+ this.#abort.abort();
94
+ this.#abort = new AbortController();
95
+ }
96
+ };
97
+
62
98
  var MicrotaskCoalescer = class {
63
99
  #run;
64
100
  #queued = false;
65
101
  #active = false;
66
102
  #generation = 0;
67
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
68
103
  constructor(run) {
69
104
  this.#run = run;
70
105
  }
71
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
72
106
  activate() {
73
107
  this.#active = true;
74
108
  }
75
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
76
109
  cancel() {
77
110
  this.#active = false;
78
111
  this.#queued = false;
79
112
  this.#generation += 1;
80
113
  }
81
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
82
114
  schedule() {
83
115
  if (!this.#active || this.#queued) return;
84
116
  this.#queued = true;
@@ -91,31 +123,50 @@ var MicrotaskCoalescer = class {
91
123
  }
92
124
  };
93
125
 
94
- // src/utils/roving_tabindex.ts
126
+ var MorphRenderWatcher = class {
127
+ #listeners = new ListenerSet();
128
+ #pass;
129
+ #element = null;
130
+ #onMorph = () => this.#pass.schedule();
131
+ constructor(run) {
132
+ this.#pass = new MicrotaskCoalescer(run);
133
+ }
134
+ observe(element) {
135
+ if (this.#element === element) return;
136
+ this.disconnect();
137
+ this.#element = element;
138
+ this.#pass.activate();
139
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
140
+ }
141
+ schedule() {
142
+ this.#pass.schedule();
143
+ }
144
+ disconnect() {
145
+ this.#listeners.dispose();
146
+ this.#pass.cancel();
147
+ this.#element = null;
148
+ }
149
+ };
150
+
151
+ var MoveCounter = class {
152
+ #sequence = 0;
153
+ record() {
154
+ this.#sequence += 1;
155
+ return this.#sequence;
156
+ }
157
+ isLatest(token) {
158
+ return token === this.#sequence;
159
+ }
160
+ };
161
+
95
162
  var RovingTabindex = class {
96
- /** Returns the current ordered item elements; called on every operation. */
97
163
  #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
164
  constructor(getItems) {
103
165
  this.#getItems = getItems;
104
166
  }
105
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
106
167
  get activeIndex() {
107
168
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
108
169
  }
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
170
  setActive(index, options = {}) {
120
171
  const { focus = false } = options;
121
172
  const items = options.items ?? this.#getItems();
@@ -126,43 +177,28 @@ var RovingTabindex = class {
126
177
  }
127
178
  };
128
179
 
129
- // src/utils/safe_timeout.ts
180
+ function targetSelector(identifier, name) {
181
+ return `[data-${identifier}-target~="${name}"]`;
182
+ }
183
+
130
184
  var TimerRegistry = class {
131
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
132
185
  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
186
  clear(id) {
140
187
  if (this.ids.delete(id)) {
141
188
  this.cancel(id);
142
189
  }
143
190
  }
144
- /**
145
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
146
- * to guarantee no timer outlives the element.
147
- */
148
191
  clearAll() {
149
192
  for (const id of this.ids) {
150
193
  this.cancel(id);
151
194
  }
152
195
  this.ids.clear();
153
196
  }
154
- /** Number of timers currently tracked (pending). */
155
197
  get size() {
156
198
  return this.ids.size;
157
199
  }
158
200
  };
159
201
  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
202
  set(callback, delay) {
167
203
  const id = this.schedule(() => {
168
204
  this.ids.delete(id);
@@ -179,29 +215,18 @@ var SafeTimeout = class extends TimerRegistry {
179
215
  }
180
216
  };
181
217
 
182
- // src/utils/typeahead.ts
183
218
  var TYPEAHEAD_RESET_MS = 500;
184
219
  var Typeahead = class {
185
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
186
220
  #timers = new SafeTimeout();
187
- /** Idle window before the query resets, in milliseconds. */
188
221
  #resetMs;
189
- /** The accumulated lowercase query, empty when idle. */
190
222
  #query = "";
191
- /** Id of the pending reset timer, `0` when none is scheduled. */
192
223
  #timerId = 0;
193
- /** @param options - Overrides for the idle window. */
194
224
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
195
225
  this.#resetMs = resetMs;
196
226
  }
197
- /** The query a search would currently run with; empty while idle. */
198
227
  get query() {
199
228
  return this.#query;
200
229
  }
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
230
  push(key) {
206
231
  const char = key.toLowerCase();
207
232
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -210,7 +235,6 @@ var Typeahead = class {
210
235
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
211
236
  return this.#query;
212
237
  }
213
- /** Clears the query and cancels the pending idle reset. */
214
238
  reset() {
215
239
  this.#query = "";
216
240
  this.#timers.clear(this.#timerId);
@@ -237,62 +261,28 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
237
261
  return -1;
238
262
  }
239
263
 
240
- // src/controllers/tree_view_controller.ts
241
264
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
242
265
  function setAttributeIfChanged(element, name, value) {
243
266
  if (element.getAttribute(name) !== value) element.setAttribute(name, value);
244
267
  }
245
268
  var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
246
269
  var TreeViewController = class extends Controller {
270
+ #moves = new MoveCounter();
247
271
  static targets = ["item", "group", "field"];
248
- static actions = ["onClick", "onKeydown", "toggle"];
272
+ static actions = ["onKeydown", "select", "toggle"];
249
273
  static events = ["select", "toggle", "reconcile"];
250
274
  #roving = new RovingTabindex(() => this.itemTargets);
251
275
  #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. */
276
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileSelection());
260
277
  #observer = null;
261
- /** The selection last settled: on connect, by the user, or by a reported pass. */
262
278
  #settled = { item: null, value: "" };
263
279
  #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
280
  #order = [];
270
- /** The `treeitem` that last took DOM focus inside this tree, if any. */
271
281
  #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
282
  #onFocusIn = (event) => {
279
283
  const target = event.target;
280
284
  this.#focused = target?.closest('[role="treeitem"]') ?? null;
281
285
  };
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
286
  #onFocusOut = (event) => {
297
287
  const target = event.target;
298
288
  const row = target?.closest('[role="treeitem"]') ?? null;
@@ -303,12 +293,6 @@ var TreeViewController = class extends Controller {
303
293
  if (this.#focused === row && row.isConnected) this.#focused = null;
304
294
  });
305
295
  };
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
- */
312
296
  connect() {
313
297
  this.element.addEventListener("focusin", this.#onFocusIn);
314
298
  this.element.addEventListener("focusout", this.#onFocusOut);
@@ -320,64 +304,35 @@ var TreeViewController = class extends Controller {
320
304
  this.#order = [...this.itemTargets];
321
305
  this.#settled = this.#selection();
322
306
  this.#connected = true;
323
- this.#reconcile.activate();
307
+ this.#reconcile.observe(this.element);
324
308
  this.#observeItems();
325
309
  }
326
- /** Fills a form field inserted or replaced at runtime with the current selection. */
327
310
  fieldTargetConnected() {
328
311
  this.#reconcile.schedule();
329
312
  }
330
- /**
331
- * Detaches the focus trackers and the item observer, drops a queued selection
332
- * pass, the typeahead buffer and its timer.
333
- */
313
+ fieldTargetDisconnected() {
314
+ this.#reconcile.schedule();
315
+ }
334
316
  disconnect() {
335
317
  this.element.removeEventListener("focusin", this.#onFocusIn);
336
318
  this.element.removeEventListener("focusout", this.#onFocusOut);
337
319
  this.#connected = false;
338
- this.#reconcile.cancel();
320
+ this.#reconcile.disconnect();
339
321
  this.#observer?.disconnect();
340
322
  this.#observer = null;
341
323
  this.#typeahead.reset();
342
- this.#timers.clearAll();
343
324
  this.#order = [];
344
325
  this.#focused = null;
345
326
  }
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
327
  #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
- */
328
+ this.#focused = this.element.ownerDocument.activeElement?.closest('[role="treeitem"]') ?? null;
329
+ }
363
330
  itemTargetConnected() {
364
331
  if (!this.#connected) return;
365
332
  this.#normalizeTabStop();
366
333
  this.#trackOrder();
367
334
  this.#reconcile.schedule();
368
335
  }
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
336
  #normalizeSelection() {
382
337
  const items = this.itemTargets;
383
338
  const selected = items.find((item) => item.getAttribute("aria-selected") === "true");
@@ -385,48 +340,29 @@ var TreeViewController = class extends Controller {
385
340
  setAttributeIfChanged(item, "aria-selected", item === selected ? "true" : "false");
386
341
  }
387
342
  }
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
343
  #mirrorField(notify) {
393
344
  if (!this.hasFieldTarget) return;
394
345
  const moved = writeField(this.fieldTarget, this.#selection().value);
395
346
  if (moved && notify) commitField(this.fieldTarget);
396
347
  }
397
- /** The selected item target, if any, and the value its field submits. */
398
348
  #selection() {
399
349
  const item = this.itemTargets.find((candidate) => candidate.getAttribute("aria-selected") === "true") ?? null;
400
350
  return { item, value: item?.dataset.value ?? "" };
401
351
  }
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
352
  #reconcileSelection() {
353
+ this.#reconcileExpansion();
408
354
  this.#normalizeSelection();
355
+ this.#normalizeTabStop();
409
356
  this.#dropOwnRecords();
410
357
  this.#mirrorField(false);
411
358
  this.#reportMove();
412
359
  }
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
360
  #reportMove() {
419
361
  const selection = this.#selection();
420
362
  if (selection.item === this.#settled.item && selection.value === this.#settled.value) return;
421
363
  this.#settled = selection;
422
364
  this.dispatch("reconcile", { detail: { item: selection.item } });
423
365
  }
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
366
  #observeItems() {
431
367
  const observer = new MutationObserver((records) => {
432
368
  if (this.#concernsItems(records)) this.#reconcile.schedule();
@@ -438,45 +374,13 @@ var TreeViewController = class extends Controller {
438
374
  });
439
375
  this.#observer = observer;
440
376
  }
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
377
  #concernsItems(records) {
447
378
  const items = new Set(this.itemTargets);
448
379
  return records.some((record) => items.has(record.target));
449
380
  }
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
381
  #dropOwnRecords() {
458
382
  this.#observer?.takeRecords();
459
383
  }
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
- */
480
384
  itemTargetDisconnected(item) {
481
385
  if (!this.#connected || !this.element.isConnected) return;
482
386
  const stranded = this.#focusDiedWith(item);
@@ -488,12 +392,6 @@ var TreeViewController = class extends Controller {
488
392
  this.#reconcile.schedule();
489
393
  this.#order = this.#order.filter((tracked) => tracked !== item);
490
394
  }
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
395
  #trackOrder() {
498
396
  const live = this.itemTargets;
499
397
  const pending = this.#order.filter((tracked) => !live.includes(tracked));
@@ -507,12 +405,6 @@ var TreeViewController = class extends Controller {
507
405
  }
508
406
  this.#order = merged;
509
407
  }
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
408
  onKeydown(event) {
517
409
  if (event.defaultPrevented) return;
518
410
  if (isReservedArrowChord(event)) return;
@@ -528,15 +420,6 @@ var TreeViewController = class extends Controller {
528
420
  event.preventDefault();
529
421
  this.#moveBy(item, -1);
530
422
  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
423
  case "ArrowRight":
541
424
  event.preventDefault();
542
425
  if (isRtl(this.element)) this.#collapseOrLeave(item);
@@ -568,53 +451,43 @@ var TreeViewController = class extends Controller {
568
451
  break;
569
452
  }
570
453
  }
571
- /** Selects the clicked item (nearest to the target only). */
572
- onClick(event) {
573
- const item = this.#ownerItem(event);
574
- if (!item) return;
454
+ select(source) {
455
+ const { event, host, origin, reason } = actionSource(source);
456
+ const item = host?.closest(targetSelector(this.identifier, "item"));
457
+ if (!item || !this.itemTargets.includes(item)) return;
458
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
459
+ return;
460
+ if (event && this.#ownerItem(event) !== item) return;
575
461
  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
- */
462
+ if (event && origin && group?.contains(origin)) return;
463
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
464
+ this.#focusItem(item, moveFocus);
465
+ this.#select(item, reason);
466
+ }
467
+ toggle(source) {
468
+ const { event, origin, reason } = actionSource(source);
469
+ const item = origin?.closest(targetSelector(this.identifier, "item"));
470
+ if (!item || !this.itemTargets.includes(item)) return;
471
+ if (origin?.closest(`[data-controller~="${this.identifier}"]`) !== this.element) return;
472
+ const group = this.#childGroup(item);
473
+ if (!group) return;
474
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
475
+ this.#setExpanded(item, group, !this.#isExpanded(item), reason);
476
+ this.#focusItem(
477
+ item,
478
+ moveFocus && (event !== null || this.element.contains(document.activeElement))
479
+ );
480
+ }
603
481
  #ownerItem(event) {
604
- const item = event.currentTarget;
605
- const target = event.target;
482
+ const { host, origin: target } = actionSource(event);
483
+ const item = host?.closest('[role="treeitem"]');
484
+ if (!item || !target) return null;
606
485
  if (target.closest('[role="treeitem"]') !== item) return null;
607
486
  const control = target.closest(NESTED_INTERACTIVE);
608
487
  if (control && item.contains(control)) return null;
609
- if (isInteractiveHost(target)) return null;
488
+ if (target instanceof HTMLElement && isInteractiveHost(target)) return null;
610
489
  return item;
611
490
  }
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
491
  #neighborOf(removed) {
619
492
  const visible = this.#visibleItems;
620
493
  const index = this.#order.indexOf(removed);
@@ -628,14 +501,6 @@ var TreeViewController = class extends Controller {
628
501
  }
629
502
  return visible[0];
630
503
  }
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
504
  #focusDiedWith(removed) {
640
505
  const focused = this.#focused;
641
506
  if (!focused || focused !== removed && !removed.contains(focused)) return false;
@@ -644,7 +509,6 @@ var TreeViewController = class extends Controller {
644
509
  if (active === null || active === doc.body || active === doc.documentElement) return true;
645
510
  return !active.isConnected;
646
511
  }
647
- /** Moves focus to the next (`delta=1`) or previous visible item, if any. */
648
512
  #moveBy(item, delta) {
649
513
  const visible = this.#visibleItems;
650
514
  const current = visible.indexOf(item);
@@ -652,17 +516,11 @@ var TreeViewController = class extends Controller {
652
516
  const next = visible[current + delta];
653
517
  if (next) this.#focusItem(next);
654
518
  }
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
519
  #expandOrEnter(item) {
662
520
  const group = this.#childGroup(item);
663
521
  if (!group) return;
664
522
  if (!this.#isExpanded(item)) {
665
- this.#setExpanded(item, true);
523
+ this.#setExpanded(item, group, true);
666
524
  return;
667
525
  }
668
526
  if (group.hidden) return;
@@ -671,34 +529,20 @@ var TreeViewController = class extends Controller {
671
529
  ).find((child) => !child.hidden);
672
530
  if (firstChild) this.#focusItem(firstChild);
673
531
  }
674
- /** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
675
532
  #collapseOrLeave(item) {
676
- if (this.#childGroup(item) && this.#isExpanded(item)) {
677
- this.#setExpanded(item, false);
533
+ const group = this.#childGroup(item);
534
+ if (group && this.#isExpanded(item)) {
535
+ this.#setExpanded(item, group, false);
678
536
  return;
679
537
  }
680
538
  const parent = this.#parentItem(item);
681
539
  if (parent) this.#focusItem(parent);
682
540
  }
683
- /** Focuses the first (`0`) or last (`-1`) visible item. */
684
541
  #focusEdge(index) {
685
542
  const visible = this.#visibleItems;
686
543
  const target = index < 0 ? visible[visible.length - 1] : visible[index];
687
544
  if (target) this.#focusItem(target);
688
545
  }
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
546
  #isDisabled(item) {
703
547
  let current = item;
704
548
  while (current && current !== this.element) {
@@ -707,38 +551,30 @@ var TreeViewController = class extends Controller {
707
551
  }
708
552
  return false;
709
553
  }
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) {
554
+ #select(item, reason = "user") {
716
555
  if (this.#isDisabled(item)) return;
717
556
  for (const candidate of this.itemTargets) {
718
557
  setAttributeIfChanged(candidate, "aria-selected", candidate === item ? "true" : "false");
719
558
  }
720
559
  this.#dropOwnRecords();
721
- this.#settled = this.#selection();
560
+ const selection = this.#selection();
561
+ const changed = selection.item !== this.#settled.item || selection.value !== this.#settled.value;
562
+ this.#settled = selection;
563
+ if (!changed) {
564
+ this.#mirrorField(false);
565
+ return;
566
+ }
567
+ const token = this.#moves.record();
722
568
  this.#mirrorField(true);
723
- this.dispatch("select", { detail: { item } });
569
+ if (!this.#moves.isLatest(token)) return;
570
+ this.dispatch("select", { detail: { item, reason } });
724
571
  }
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;
572
+ #setExpanded(item, group, expanded, reason = "user") {
729
573
  item.setAttribute("aria-expanded", String(expanded));
730
574
  group.hidden = !expanded;
731
575
  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
- */
576
+ this.dispatch("toggle", { detail: { item, expanded, reason } });
577
+ }
742
578
  #escapeCollapsedSubtree(item, group) {
743
579
  const active = document.activeElement;
744
580
  if (active instanceof HTMLElement && group.contains(active)) {
@@ -746,23 +582,21 @@ var TreeViewController = class extends Controller {
746
582
  return;
747
583
  }
748
584
  const activeIndex = this.#roving.activeIndex;
749
- const tabbable = activeIndex === -1 ? void 0 : this.itemTargets[activeIndex];
585
+ const tabbable = this.itemTargets[activeIndex];
750
586
  if (!tabbable || !group.contains(tabbable)) return;
751
587
  const index = this.itemTargets.indexOf(item);
752
588
  if (index !== -1) this.#roving.setActive(index);
753
589
  }
754
- /** Makes `item` the single tab stop and moves DOM focus to it. */
755
- #focusItem(item) {
590
+ #focusItem(item, focus = true) {
756
591
  const index = this.itemTargets.indexOf(item);
757
592
  if (index !== -1) {
758
- this.#roving.setActive(index, { focus: true });
593
+ this.#roving.setActive(index, { focus });
759
594
  return;
760
595
  }
761
596
  if (!canTakeFocus(item)) return;
762
597
  item.tabIndex = -1;
763
598
  item.focus();
764
599
  }
765
- /** Advances the typeahead query and focuses the next matching visible item. */
766
600
  #typeaheadTo(item, char) {
767
601
  const query = this.#typeahead.push(char);
768
602
  const visible = this.#visibleItems;
@@ -775,13 +609,6 @@ var TreeViewController = class extends Controller {
775
609
  const match = visible[index];
776
610
  if (match) this.#focusItem(match);
777
611
  }
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
612
  #reconcileExpansion() {
786
613
  for (const item of this.itemTargets) {
787
614
  const group = this.#childGroup(item);
@@ -793,32 +620,19 @@ var TreeViewController = class extends Controller {
793
620
  }
794
621
  }
795
622
  }
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
623
  #normalizeTabStop() {
803
624
  const items = this.itemTargets;
804
625
  const visible = this.#visibleItems;
805
626
  const activeIndex = this.#roving.activeIndex;
806
- const active = activeIndex === -1 ? void 0 : items[activeIndex];
627
+ const active = items[activeIndex];
807
628
  if (active && visible.includes(active)) {
808
629
  this.#roving.setActive(activeIndex);
809
630
  return;
810
631
  }
811
632
  const selected = visible.find((item) => item.getAttribute("aria-selected") === "true");
812
633
  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
- */
634
+ this.#roving.setActive(items.findIndex((item) => item === next));
635
+ }
822
636
  get #visibleItems() {
823
637
  return this.itemTargets.filter((item) => {
824
638
  let node = item;
@@ -829,23 +643,15 @@ var TreeViewController = class extends Controller {
829
643
  return true;
830
644
  });
831
645
  }
832
- /** The child `group` owned directly by `item`, or `null` for a leaf. */
833
646
  #childGroup(item) {
834
647
  return item.querySelector(':scope > [role="group"]');
835
648
  }
836
- /** The nearest ancestor `treeitem`, or `null` at the root level. */
837
649
  #parentItem(item) {
838
650
  return item.parentElement?.closest('[role="treeitem"]') ?? null;
839
651
  }
840
- /** Whether a parent item is currently expanded. */
841
652
  #isExpanded(item) {
842
653
  return item.getAttribute("aria-expanded") === "true";
843
654
  }
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
655
  #label(item) {
850
656
  return typeaheadLabel(item, () => {
851
657
  let text = "";
@@ -861,5 +667,4 @@ var TreeViewController = class extends Controller {
861
667
  };
862
668
 
863
669
  export { TreeViewController };
864
- //# sourceMappingURL=tree_view_controller.js.map
865
670
  //# sourceMappingURL=tree_view_controller.js.map