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,68 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/menubar_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
+ }
24
+
25
+ function isAriaDisabled(element) {
26
+ for (let current = element; current; current = current.parentElement) {
27
+ if (current.getAttribute("aria-disabled") === "true") return true;
28
+ }
29
+ return false;
30
+ }
4
31
 
5
- // src/utils/logical_scroll.ts
6
32
  function isRtl(element) {
7
33
  return window.getComputedStyle(element).direction === "rtl";
8
34
  }
9
35
 
10
- // src/utils/arrow_step.ts
11
36
  function isReservedArrowChord(event, allow = []) {
12
37
  if (!event.key.startsWith("Arrow")) return false;
13
38
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
14
39
  }
40
+ function hasModifierChord(event) {
41
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
42
+ }
43
+
44
+ var ClickVerdicts = class {
45
+ #verdicts = /* @__PURE__ */ new WeakMap();
46
+ record(event, entries, refused) {
47
+ this.#verdicts.set(event, { entries, refused });
48
+ }
49
+ for(event, entry) {
50
+ const verdict = event && this.#verdicts.get(event);
51
+ if (!verdict?.entries.includes(entry)) return void 0;
52
+ return verdict.refused;
53
+ }
54
+ };
55
+
56
+ function sharedRegistry(key, create) {
57
+ const symbol = Symbol.for(key);
58
+ const scope = globalThis;
59
+ const existing = scope[symbol];
60
+ if (existing !== void 0) return existing;
61
+ const registry = create();
62
+ Object.defineProperty(globalThis, symbol, { value: registry });
63
+ return registry;
64
+ }
15
65
 
16
- // src/utils/escape_layer.ts
17
66
  function claimsWhileFocusWithin(element) {
18
67
  return () => {
19
68
  const active = element.ownerDocument.activeElement;
@@ -21,17 +70,11 @@ function claimsWhileFocusWithin(element) {
21
70
  };
22
71
  }
23
72
  var EscapeLayer = class _EscapeLayer {
24
- static #registries = /* @__PURE__ */ new WeakMap();
25
- #ownerDocument = null;
26
- /** Dismissal callback while active; `null` when inactive. */
27
- #onDismiss = null;
28
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
29
- #claims = null;
30
- /**
31
- * Activates this layer at the top of its document's Escape stack, installing
32
- * the document's shared resolver listener if this is its first layer.
33
- * Re-activating an already-active layer moves it to the top.
34
- */
73
+ static #registries = sharedRegistry(
74
+ "stimeo-ui.escape-layer.registry.v1",
75
+ () => /* @__PURE__ */ new WeakMap()
76
+ );
77
+ #active = null;
35
78
  activate(ownerDocument = document, options) {
36
79
  this.deactivate();
37
80
  let registry = _EscapeLayer.#registries.get(ownerDocument);
@@ -40,44 +83,31 @@ var EscapeLayer = class _EscapeLayer {
40
83
  _EscapeLayer.#registries.set(ownerDocument, registry);
41
84
  ownerDocument.addEventListener("keydown", registry.onKeydown);
42
85
  }
43
- registry.stack.push(this);
44
- this.#ownerDocument = ownerDocument;
45
- this.#onDismiss = options.onDismiss;
46
- this.#claims = options.claims ?? null;
47
- }
48
- /**
49
- * Removes this layer from its document's Escape stack, uninstalling the
50
- * shared listener when the stack empties. Safe to call when inactive.
51
- */
86
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
87
+ registry.stack.push(entry);
88
+ this.#active = { document: ownerDocument, entry };
89
+ }
52
90
  deactivate() {
53
- const ownerDocument = this.#ownerDocument;
54
- if (!ownerDocument) return;
55
- const registry = _EscapeLayer.#registries.get(ownerDocument);
91
+ const active = this.#active;
92
+ if (!active) return;
93
+ const registry = _EscapeLayer.#registries.get(active.document);
56
94
  if (registry) {
57
- const index = registry.stack.lastIndexOf(this);
95
+ const index = registry.stack.lastIndexOf(active.entry);
58
96
  if (index >= 0) registry.stack.splice(index, 1);
59
97
  if (registry.stack.length === 0) {
60
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
61
- _EscapeLayer.#registries.delete(ownerDocument);
98
+ active.document.removeEventListener("keydown", registry.onKeydown);
99
+ _EscapeLayer.#registries.delete(active.document);
62
100
  }
63
101
  }
64
- this.#ownerDocument = null;
65
- this.#onDismiss = null;
66
- this.#claims = null;
67
- }
68
- /**
69
- * Whether this active layer would own a press right now: it is the topmost
70
- * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
71
- * and diagnostics — production dismissal goes through the shared listener.
72
- */
102
+ this.#active = null;
103
+ }
73
104
  get ownsEscape() {
74
- const ownerDocument = this.#ownerDocument;
75
- if (!ownerDocument) return false;
76
- const registry = _EscapeLayer.#registries.get(ownerDocument);
105
+ const active = this.#active;
106
+ if (!active) return false;
107
+ const registry = _EscapeLayer.#registries.get(active.document);
77
108
  if (!registry) return false;
78
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
109
+ return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
79
110
  }
80
- /** Builds a document's registry with its shared resolver listener. */
81
111
  static #createRegistry() {
82
112
  const registry = {
83
113
  stack: [],
@@ -86,24 +116,22 @@ var EscapeLayer = class _EscapeLayer {
86
116
  const owner = _EscapeLayer.#resolveOwner(registry.stack);
87
117
  if (!owner) return;
88
118
  event.preventDefault();
89
- owner.#onDismiss?.();
119
+ owner.onDismiss();
90
120
  }
91
121
  };
92
122
  return registry;
93
123
  }
94
- /** The topmost stack layer whose claims predicate passes, or `null`. */
95
124
  static #resolveOwner(stack) {
96
125
  for (let index = stack.length - 1; index >= 0; index--) {
97
126
  const layer = stack[index];
98
127
  if (!layer) continue;
99
- if (layer.#claims && !layer.#claims()) continue;
128
+ if (layer.claims && !layer.claims()) continue;
100
129
  return layer;
101
130
  }
102
131
  return null;
103
132
  }
104
133
  };
105
134
 
106
- // src/utils/event_owner.ts
107
135
  function ownerIndex(candidates, node) {
108
136
  if (!(node instanceof Node)) return -1;
109
137
  return candidates.findIndex((candidate) => candidate.contains(node));
@@ -111,32 +139,89 @@ function ownerIndex(candidates, node) {
111
139
  function ownerOf(candidates, node) {
112
140
  return candidates[ownerIndex(candidates, node)] ?? null;
113
141
  }
142
+ function ownersOf(candidates, node) {
143
+ if (!(node instanceof Node)) return [];
144
+ return candidates.filter((candidate) => candidate.contains(node));
145
+ }
146
+
147
+ var ListenerSet = class {
148
+ #abort = new AbortController();
149
+ add(target, type, handler, options) {
150
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
151
+ }
152
+ dispose() {
153
+ this.#abort.abort();
154
+ this.#abort = new AbortController();
155
+ }
156
+ };
157
+
158
+ var MicrotaskCoalescer = class {
159
+ #run;
160
+ #queued = false;
161
+ #active = false;
162
+ #generation = 0;
163
+ constructor(run) {
164
+ this.#run = run;
165
+ }
166
+ activate() {
167
+ this.#active = true;
168
+ }
169
+ cancel() {
170
+ this.#active = false;
171
+ this.#queued = false;
172
+ this.#generation += 1;
173
+ }
174
+ get pending() {
175
+ return this.#queued;
176
+ }
177
+ schedule() {
178
+ if (!this.#active || this.#queued) return;
179
+ this.#queued = true;
180
+ const generation = this.#generation;
181
+ queueMicrotask(() => {
182
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
183
+ this.#queued = false;
184
+ this.#run();
185
+ });
186
+ }
187
+ };
188
+
189
+ var MorphRenderWatcher = class {
190
+ #listeners = new ListenerSet();
191
+ #pass;
192
+ #element = null;
193
+ #onMorph = () => this.#pass.schedule();
194
+ constructor(run) {
195
+ this.#pass = new MicrotaskCoalescer(run);
196
+ }
197
+ observe(element) {
198
+ if (this.#element === element) return;
199
+ this.disconnect();
200
+ this.#element = element;
201
+ this.#pass.activate();
202
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
203
+ }
204
+ schedule() {
205
+ this.#pass.schedule();
206
+ }
207
+ get pending() {
208
+ return this.#pass.pending;
209
+ }
210
+ disconnect() {
211
+ this.#listeners.dispose();
212
+ this.#pass.cancel();
213
+ this.#element = null;
214
+ }
215
+ };
114
216
 
115
- // src/utils/roving_tabindex.ts
116
217
  var RovingTabindex = class {
117
- /** Returns the current ordered item elements; called on every operation. */
118
218
  #getItems;
119
- /**
120
- * @param getItems - Returns the current ordered item elements. Called on every
121
- * operation so the live target list is always used.
122
- */
123
219
  constructor(getItems) {
124
220
  this.#getItems = getItems;
125
221
  }
126
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
127
222
  get activeIndex() {
128
223
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
129
224
  }
130
- /**
131
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
132
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
133
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
134
- * "nothing is currently tabbable".
135
- *
136
- * @param index - Position of the item to make tabbable.
137
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
138
- * and `items` to reuse an event-scoped collection snapshot.
139
- */
140
225
  setActive(index, options = {}) {
141
226
  const { focus = false } = options;
142
227
  const items = options.items ?? this.#getItems();
@@ -147,43 +232,24 @@ var RovingTabindex = class {
147
232
  }
148
233
  };
149
234
 
150
- // src/utils/safe_timeout.ts
151
235
  var TimerRegistry = class {
152
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
153
236
  ids = /* @__PURE__ */ new Set();
154
- /**
155
- * Cancels a single tracked timer.
156
- *
157
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
158
- * registry), so callers can clear defensively without guarding.
159
- */
160
237
  clear(id) {
161
238
  if (this.ids.delete(id)) {
162
239
  this.cancel(id);
163
240
  }
164
241
  }
165
- /**
166
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
167
- * to guarantee no timer outlives the element.
168
- */
169
242
  clearAll() {
170
243
  for (const id of this.ids) {
171
244
  this.cancel(id);
172
245
  }
173
246
  this.ids.clear();
174
247
  }
175
- /** Number of timers currently tracked (pending). */
176
248
  get size() {
177
249
  return this.ids.size;
178
250
  }
179
251
  };
180
252
  var SafeTimeout = class extends TimerRegistry {
181
- /**
182
- * Schedules `callback` after `delay` ms and returns the timer id.
183
- *
184
- * The id is removed from the registry automatically when the timeout fires,
185
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
186
- */
187
253
  set(callback, delay) {
188
254
  const id = this.schedule(() => {
189
255
  this.ids.delete(id);
@@ -200,39 +266,22 @@ var SafeTimeout = class extends TimerRegistry {
200
266
  }
201
267
  };
202
268
 
203
- // src/utils/state_reason.ts
204
- var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
205
- var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
206
- function stateReasonFor(event) {
207
- if (!event) return "api";
208
- if (FOCUS_EVENTS.has(event.type)) return "focus";
209
- if (POINTER_EVENTS.has(event.type)) return "pointer";
210
- return "user";
269
+ function targetSelector(identifier, name) {
270
+ return `[data-${identifier}-target~="${name}"]`;
211
271
  }
212
272
 
213
- // src/utils/typeahead.ts
214
273
  var TYPEAHEAD_RESET_MS = 500;
215
274
  var Typeahead = class {
216
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
217
275
  #timers = new SafeTimeout();
218
- /** Idle window before the query resets, in milliseconds. */
219
276
  #resetMs;
220
- /** The accumulated lowercase query, empty when idle. */
221
277
  #query = "";
222
- /** Id of the pending reset timer, `0` when none is scheduled. */
223
278
  #timerId = 0;
224
- /** @param options - Overrides for the idle window. */
225
279
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
226
280
  this.#resetMs = resetMs;
227
281
  }
228
- /** The query a search would currently run with; empty while idle. */
229
282
  get query() {
230
283
  return this.#query;
231
284
  }
232
- /**
233
- * Folds `key` into the query, restarts the idle window, and returns the query to
234
- * search with. A repeated character collapses the query to that one character.
235
- */
236
285
  push(key) {
237
286
  const char = key.toLowerCase();
238
287
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -241,7 +290,6 @@ var Typeahead = class {
241
290
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
242
291
  return this.#query;
243
292
  }
244
- /** Clears the query and cancels the pending idle reset. */
245
293
  reset() {
246
294
  this.#query = "";
247
295
  this.#timers.clear(this.#timerId);
@@ -268,73 +316,40 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
268
316
  return -1;
269
317
  }
270
318
 
271
- // src/controllers/menubar_controller.ts
272
319
  var STATE_ATTRIBUTES = ["disabled", "hidden"];
273
320
  var MenubarController = class extends Controller {
274
321
  static targets = ["top", "menu", "item"];
275
322
  static actions = ["activate", "onItemKeydown", "onTopKeydown", "toggle"];
276
323
  static events = ["close", "open"];
277
- /** Roving tabindex across the top-level menuitems (one Tab stop). */
278
324
  #roving = new RovingTabindex(() => this.topTargets);
279
- /** Typeahead query and its idle-reset timer (scoped to the open menu). */
280
325
  #typeahead = new Typeahead();
281
326
  #timers = new SafeTimeout();
282
- /** Escape-stack membership while a menu is open; the shared resolver dismisses via it. */
283
327
  #escapeLayer = new EscapeLayer();
284
- /** Whether {@link #escapeLayer} is currently on the stack (see {@link #syncEscapeLayer}). */
285
328
  #layerActive = false;
286
- /** Watches `disabled`/`hidden` under the menubar; `null` while disconnected. */
287
329
  #observer = null;
288
- /**
289
- * Live between `connect()` and `disconnect()`. Stimulus reports the *initial*
290
- * targets before `connect()` and re-reports them as disconnected afterwards;
291
- * gating on this keeps the target callbacks from clobbering the authored Tab
292
- * stop on mount and from resurrecting one after teardown.
293
- */
294
330
  #connected = false;
295
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
296
331
  #reporting = false;
297
- /** The element inside the menubar that last took DOM focus, if any. */
298
332
  #focused = null;
299
- /**
300
- * Records where DOM focus sits inside the menubar. `focusin` bubbles, so one
301
- * listener covers the top items and everything nested in their menus. The
302
- * record is needed because a top or menu becomes unreachable *before* this
303
- * controller hears about it, by which point the browser has already reset
304
- * `document.activeElement`.
305
- */
333
+ #clickVerdicts = new ClickVerdicts();
306
334
  #onFocusIn = (event) => {
307
335
  this.#focused = event.target;
308
336
  };
309
- /**
310
- * Forgets the record once focus genuinely lands on something else, so a later
311
- * mutation cannot pull focus back in. A `relatedTarget` of `null` is the case
312
- * that must be kept: it is what "focus went nowhere" looks like, which is
313
- * exactly the state the rescue exists for.
314
- */
315
337
  #onFocusOut = (event) => {
316
338
  const next = event.relatedTarget;
317
339
  if (next && !this.element.contains(next)) this.#focused = null;
318
340
  };
319
- /**
320
- * Establishes the closed baseline and the single tab stop: keep an existing tab
321
- * stop when it is still navigable (so a Turbo cache restore preserves the user's
322
- * position), else fall back to the first navigable top item.
323
- *
324
- * The outside-click listener is registered in the **capture** phase: in the
325
- * bubble phase an inside handler that removes its own click target detaches the
326
- * node before `contains()` runs, so an *inside* click would read as outside and
327
- * close the menu.
328
- */
341
+ #morph = new MorphRenderWatcher(() => this.#reconcile());
342
+ #listeners = new ListenerSet();
329
343
  connect() {
344
+ this.#morph.observe(this.element);
330
345
  this.#closeAllMenus("api");
331
346
  this.#reconcile();
332
- this.element.addEventListener("click", this.#onDisabledClickCapture, true);
333
- this.element.addEventListener("focusin", this.#onFocusIn);
334
- this.element.addEventListener("focusout", this.#onFocusOut);
335
- document.addEventListener("click", this.#onOutsideClick, true);
347
+ this.#listeners.add(this.element, "click", this.#onClickCapture, { capture: true });
348
+ this.#listeners.add(this.element, "focusin", this.#onFocusIn);
349
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
350
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
336
351
  if (typeof MutationObserver !== "undefined") {
337
- this.#observer = new MutationObserver(() => this.#reconcile());
352
+ this.#observer = new MutationObserver(() => this.#morph.schedule());
338
353
  this.#observer.observe(this.element, {
339
354
  subtree: true,
340
355
  childList: true,
@@ -345,82 +360,57 @@ var MenubarController = class extends Controller {
345
360
  this.#connected = true;
346
361
  this.#reporting = true;
347
362
  }
348
- /** Removes the listeners, stack membership, and any pending timer (typeahead / Tab close). */
349
363
  disconnect() {
364
+ this.#morph.disconnect();
350
365
  this.#connected = false;
351
366
  this.#reporting = false;
352
367
  this.#escapeLayer.deactivate();
353
368
  this.#layerActive = false;
354
- this.element.removeEventListener("click", this.#onDisabledClickCapture, true);
355
- this.element.removeEventListener("focusin", this.#onFocusIn);
356
- this.element.removeEventListener("focusout", this.#onFocusOut);
357
- document.removeEventListener("click", this.#onOutsideClick, true);
369
+ this.#listeners.dispose();
358
370
  this.#focused = null;
359
371
  this.#observer?.disconnect();
360
372
  this.#observer = null;
361
373
  this.#typeahead.reset();
362
374
  this.#timers.clearAll();
363
375
  }
364
- /**
365
- * A top item added at runtime is dropped out of the Tab sequence first — a fresh
366
- * `<button>` is tabbable by default, which would leave the menubar with two Tab
367
- * stops — before the lone stop is re-established.
368
- */
369
376
  topTargetConnected(top) {
370
377
  if (!this.#connected) return;
371
378
  top.tabIndex = -1;
372
379
  this.#reconcile();
373
380
  }
374
- /**
375
- * Removing a top item can strand two things: the Tab stop it held, and the menu
376
- * it owned (which nothing could close afterwards).
377
- *
378
- * The departing element is handed back in a neutral state first. A target can
379
- * leave without leaving the document — a morph that only drops the
380
- * `data-*-target` token keeps the node — and it would then sit in the page
381
- * carrying this controller's `tabindex="0"` (a second Tab stop next to the one
382
- * re-established below) and an `aria-expanded="true"` nothing can collapse.
383
- * When the node really is gone these writes are harmless no-ops.
384
- */
385
381
  topTargetDisconnected(top) {
386
382
  if (!this.#connected) return;
387
383
  top.tabIndex = -1;
388
384
  if (this.#isExpanded(top)) top.setAttribute("aria-expanded", "false");
389
385
  this.#reconcile();
390
386
  }
391
- /** See {@link MenubarController.menuTargetDisconnected}. */
392
387
  menuTargetConnected() {
393
388
  if (!this.#connected) return;
394
389
  this.#reconcile();
395
390
  }
396
- /**
397
- * A menu removed while open leaves its owner claiming `aria-expanded="true"` for
398
- * a popup that no longer exists, and the menubar holding an Escape layer that
399
- * would swallow presses meant for something else.
400
- *
401
- * As with {@link MenubarController.topTargetDisconnected}, the departing menu is
402
- * closed first: a token-only removal leaves a visible popup in the page that no
403
- * key and no click can dismiss any more.
404
- */
405
391
  menuTargetDisconnected(menu) {
406
392
  if (!this.#connected) return;
407
393
  if (!menu.hidden) menu.hidden = true;
408
394
  this.#reconcile();
409
395
  }
410
- /** Toggles a top item's menu. Bound via `data-action` (click on the top item). */
411
- toggle(event) {
412
- const top = event.currentTarget;
413
- const reason = stateReasonFor(event);
396
+ toggle(source) {
397
+ const { event, host, origin, reason } = actionSource(source);
398
+ const top = host?.closest(targetSelector(this.identifier, "top"));
399
+ if (!top || !this.topTargets.includes(top)) return;
400
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
401
+ return;
402
+ if (this.#refuses(top, event, origin)) return;
403
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
414
404
  if (this.#isExpanded(top)) {
415
405
  this.#closeMenu(top, reason);
416
406
  } else {
417
- this.#openMenu(top, "first", reason);
407
+ this.#openMenu(top, "first", reason, moveFocus, true);
418
408
  }
419
409
  }
420
- /** Keyboard handling while focus is on a top item. */
421
410
  onTopKeydown(event) {
422
411
  if (event.defaultPrevented) return;
423
412
  if (isReservedArrowChord(event)) return;
413
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
424
414
  const tops = this.#navigableTops;
425
415
  const index = tops.indexOf(event.currentTarget);
426
416
  if (index === -1) return;
@@ -457,10 +447,10 @@ var MenubarController = class extends Controller {
457
447
  break;
458
448
  }
459
449
  }
460
- /** Keyboard handling while focus is on a menu item. */
461
450
  onItemKeydown(event) {
462
451
  if (event.defaultPrevented) return;
463
452
  if (isReservedArrowChord(event)) return;
453
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
464
454
  const item = event.currentTarget;
465
455
  const menu = item.closest("[role='menu']");
466
456
  if (!menu) return;
@@ -503,76 +493,61 @@ var MenubarController = class extends Controller {
503
493
  break;
504
494
  }
505
495
  }
506
- /** Closes the owning menu after an item is activated and refocuses its top. */
507
- activate(event) {
508
- const item = event.currentTarget;
496
+ activate(source) {
497
+ const { event, host, origin } = actionSource(source);
498
+ const item = host?.closest(targetSelector(this.identifier, "item"));
499
+ if (!item || !this.itemTargets.includes(item)) return;
500
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
501
+ return;
502
+ if (this.#refuses(item, event, origin)) return;
503
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
509
504
  const menu = item.closest("[role='menu']");
510
505
  const top = this.#topFor(menu);
511
506
  this.#closeAllMenus("select");
512
- this.#focusTop(top);
507
+ this.#focusTop(
508
+ top,
509
+ moveFocus && (event !== null || this.element.contains(document.activeElement))
510
+ );
513
511
  }
514
- /** Moves the roving focus to `top`, opening its menu when one was open. */
515
- #gotoTop(top, reopen, reason) {
516
- if (!top) return;
512
+ #gotoTop(destination, reopen, reason) {
513
+ if (!destination) return;
517
514
  if (reopen) {
518
- this.#openMenu(top, "first", reason);
515
+ this.#openMenu(destination, "first", reason);
519
516
  } else {
520
- this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
517
+ this.#roving.setActive(this.topTargets.indexOf(destination), { focus: true });
521
518
  }
522
519
  }
523
- /**
524
- * Opens `top`'s menu (closing others) and focuses its first/last item.
525
- *
526
- * Three kinds of top item never open a menu:
527
- * - **`aria-disabled`** — focusable but never activated, and opening a popup is
528
- * activation.
529
- * - **plain command** (no `aria-controls` at all) — a legitimate top item that
530
- * simply has no popup.
531
- * - **dangling `aria-controls`** (names a menu that is not a target) — broken
532
- * markup.
533
- *
534
- * The first two still take the roving focus, and any open menu closes: the APG
535
- * makes closing unconditional for the horizontal move ("closes the submenu,
536
- * moves focus to the next menubar item, and *optionally* opens that item's
537
- * submenu"), so only the opening half is skipped. Leaving the old menu open
538
- * would strand a popup that no longer contains focus.
539
- *
540
- * The dangling case instead leaves the open/closed state and focus untouched
541
- * rather than closing everything and dropping focus to the body — but only on
542
- * this "open a menu" path. A dangling top is still an ordinary roving
543
- * destination while nothing is open.
544
- */
545
- #openMenu(top, focus, reason) {
546
- if (!top) return;
547
- const menu = this.#menuFor(top);
548
- if (!menu || this.#isActivationBlocked(top)) {
549
- if (menu || !top.hasAttribute("aria-controls")) {
520
+ #openMenu(opener, focus, reason, moveFocus = true, judged = false) {
521
+ if (!opener) return;
522
+ const menu = this.#menuFor(opener);
523
+ if (!menu || !judged && isAriaDisabled(opener)) {
524
+ if (menu || !opener.hasAttribute("aria-controls")) {
550
525
  this.#closeAllMenus(reason);
551
- this.#focusTop(top);
526
+ this.#focusTop(
527
+ opener,
528
+ moveFocus && (reason !== "api" || this.element.contains(document.activeElement))
529
+ );
552
530
  }
553
531
  return;
554
532
  }
555
533
  this.#timers.clearAll();
556
- const was = this.#isExpanded(top);
557
- this.#closeAllMenus(reason, top);
534
+ const was = this.#isExpanded(opener);
535
+ this.#closeAllMenus(reason, opener);
558
536
  menu.hidden = false;
559
- top.setAttribute("aria-expanded", "true");
560
- if (!was) this.#report("open", top, menu, reason);
561
- if (!this.#isExpanded(top)) return;
537
+ opener.setAttribute("aria-expanded", "true");
538
+ if (!was) this.#report("open", opener, menu, reason);
539
+ if (!this.#isExpanded(opener)) return;
562
540
  this.#escapeLayer.activate(document, {
563
541
  onDismiss: () => this.#dismissOpenMenu(),
564
542
  claims: claimsWhileFocusWithin(this.element)
565
543
  });
566
544
  this.#layerActive = true;
567
- this.#roving.setActive(this.topTargets.indexOf(top));
545
+ this.#roving.setActive(this.topTargets.indexOf(opener));
568
546
  const items = this.#itemsIn(menu);
569
- this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
547
+ if (moveFocus && (reason !== "api" || this.element.contains(document.activeElement))) {
548
+ this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
549
+ }
570
550
  }
571
- /**
572
- * Hides `top`'s menu and reflects the collapsed state. A top item that controls
573
- * no menu (a plain command in an otherwise popup-bearing menubar) is left alone
574
- * — stamping `aria-expanded="false"` on it would announce a popup it lacks.
575
- */
576
551
  #closeMenu(top, reason, silent = false) {
577
552
  if (!top) return;
578
553
  const menu = this.#menuFor(top);
@@ -583,40 +558,21 @@ var MenubarController = class extends Controller {
583
558
  if (was && !silent) this.#report("close", top, menu, reason);
584
559
  this.#syncEscapeLayer();
585
560
  }
586
- /**
587
- * Closes every menu and resets the typeahead query. `except` still has its
588
- * attributes rewritten — the caller is about to reopen it — but its move is
589
- * left for that caller to report.
590
- */
591
561
  #closeAllMenus(reason, except = null) {
592
562
  for (const top of this.topTargets) this.#closeMenu(top, reason, top === except);
593
563
  this.#typeahead.reset();
594
564
  }
595
- /** Names the menu that moved, so a subscriber does not have to re-derive it. */
596
565
  #report(name, top, menu, reason) {
597
566
  if (!this.#reporting) return;
598
567
  const detail = { reason, index: this.topTargets.indexOf(top), menu };
599
568
  if (name === "open") this.dispatch("open", { detail, cancelable: false });
600
569
  else this.dispatch("close", { detail, cancelable: false });
601
570
  }
602
- /**
603
- * Closes every menu on the next task instead of synchronously.
604
- *
605
- * Used by `Tab`: closing right away removes the focused element before the
606
- * browser performs its own Tab move, which can restart traversal at the
607
- * document head and lose the user's place in the Tab order.
608
- */
609
571
  #closeMenusSoon() {
610
572
  if (!this.#isAnyOpen) return;
611
573
  this.#timers.clearAll();
612
574
  this.#timers.set(() => this.#closeAllMenus("focus"), 0);
613
575
  }
614
- /**
615
- * Escape path, invoked by the shared resolver: pressed inside an open menu it
616
- * closes that menu and returns focus to its top item (the APG behavior);
617
- * pressed while focus is on a top item (or fell to the body) it closes every
618
- * menu without moving focus.
619
- */
620
576
  #dismissOpenMenu() {
621
577
  const menu = ownerOf(this.menuTargets, document.activeElement);
622
578
  if (menu) {
@@ -627,54 +583,31 @@ var MenubarController = class extends Controller {
627
583
  }
628
584
  this.#closeAllMenus("escape");
629
585
  }
630
- /** Opens the menu of the navigable top item `delta` steps from `menu`'s owner. */
631
586
  #moveToAdjacentMenu(menu, delta) {
632
- const top = this.#topFor(menu);
633
- if (!top) return;
587
+ const ownerTop = this.#topFor(menu);
588
+ if (!ownerTop) return;
634
589
  const tops = this.#navigableTops;
635
- const current = tops.indexOf(top);
590
+ const current = tops.indexOf(ownerTop);
636
591
  if (current === -1) return;
637
592
  const next = (current + delta + tops.length) % tops.length;
638
593
  this.#openMenu(tops[next], "first", "user");
639
594
  }
640
- /**
641
- * Closes when a click lands outside the controller's element. Focus is left on
642
- * whatever the user clicked — see the focus-restoration contract in the class
643
- * docs.
644
- */
645
595
  #onOutsideClick = (event) => {
646
596
  if (this.#isAnyOpen && !this.element.contains(event.target)) {
647
597
  this.#closeAllMenus("outside");
648
598
  }
649
599
  };
650
- /**
651
- * Captures clicks so `aria-disabled` top items and commands cannot reach
652
- * consumer handlers (nor `toggle`/`activate`). Native Enter/Space activation
653
- * also synthesizes a click and is blocked here; natively `disabled` buttons
654
- * dispatch no click at all.
655
- */
656
- #onDisabledClickCapture = (event) => {
657
- const target = event.target;
658
- if (!(target instanceof Node)) return;
659
- const blocked = [...this.topTargets, ...this.itemTargets].some(
660
- (element) => this.#isActivationBlocked(element) && element.contains(target)
600
+ #onClickCapture = (event) => {
601
+ const entries = ownersOf(this.#entries, event.target);
602
+ this.#clickVerdicts.record(
603
+ event,
604
+ entries,
605
+ entries.some((entry) => this.#isActivationDisabled(entry))
661
606
  );
662
- if (!blocked) return;
607
+ if (!entries.some((entry) => isAriaDisabled(entry))) return;
663
608
  event.preventDefault();
664
609
  event.stopImmediatePropagation();
665
610
  };
666
- /**
667
- * Re-derives every piece of state this controller owns from the live targets:
668
- * expanded flags, menu visibility, the single Tab stop, and Escape-stack
669
- * membership. Runtime DOM edits can leave those out of step with each other — a
670
- * top can claim `aria-expanded="true"` after its menu target was removed or after
671
- * being hidden itself, a menu can stay visible after its owning top was removed,
672
- * an open pair can appear from a morph with no layer registered for it, and the
673
- * Tab stop can end up on a now-inert top, on a runtime-added one, or on none at
674
- * all. The only state carried between calls is the tracked focus record and which side
675
- * of the Escape stack this layer is on (which the stack itself does not expose), so the outcome is the
676
- * same whichever mutation arrived and calling it more often than needed is free.
677
- */
678
611
  #reconcile() {
679
612
  for (const top of this.topTargets) {
680
613
  const own = this.#menuFor(top);
@@ -691,20 +624,6 @@ var MenubarController = class extends Controller {
691
624
  this.#rescueFocus();
692
625
  this.#syncEscapeLayer();
693
626
  }
694
- /**
695
- * Returns DOM focus to the menubar when the element holding it became
696
- * unreachable — hidden, natively disabled, or removed — and the document had
697
- * nowhere to put it. The destination is the top item that now owns the Tab
698
- * stop, which is where `Escape` from a closed menu would have left the user.
699
- *
700
- * Focus is only *restored*, never *stolen*: it moves solely when the tracked
701
- * element can no longer take it **and** focus is either already gone or still
702
- * sitting on that element. The second half is what makes a `hidden` ancestor
703
- * work — a browser blurs the element it hides on its next style pass, not when
704
- * the attribute is written, so waiting to observe `<body>` here would always
705
- * come too early. Removal is the other side of the same condition: the browser
706
- * has already fallen back to the body, or left a detached `activeElement`.
707
- */
708
627
  #rescueFocus() {
709
628
  const focused = this.#focused;
710
629
  if (!focused) return;
@@ -720,22 +639,6 @@ var MenubarController = class extends Controller {
720
639
  const top = index === -1 ? void 0 : this.topTargets[index];
721
640
  if (top && top !== focused && !focused.contains(top)) top.focus();
722
641
  }
723
- /**
724
- * Brings Escape-stack membership back in line with what the DOM now says is
725
- * open, in both directions.
726
- *
727
- * The deactivate half is the common one (something closed). The activate half
728
- * covers a menu that became open *without* going through {@link #openMenu} —
729
- * a Turbo morph that patches `aria-expanded` and the menu's `hidden` in place,
730
- * which is legitimate here because the DOM is this controller's only source of
731
- * truth. Without it the popup is visible but `Escape` does nothing, since no
732
- * layer is registered to claim the press.
733
- *
734
- * `#layerActive` exists because re-activating an already-active layer moves it
735
- * to the top of the stack, which would reshuffle nested layers on every
736
- * unrelated mutation. Registering only on the false→true edge keeps activation
737
- * ordered by when each layer actually opened.
738
- */
739
642
  #syncEscapeLayer() {
740
643
  const open = this.#isAnyOpen;
741
644
  if (open === this.#layerActive) return;
@@ -749,13 +652,6 @@ var MenubarController = class extends Controller {
749
652
  }
750
653
  this.#layerActive = open;
751
654
  }
752
- /**
753
- * Re-establishes the single Tab stop: keep the current one while it is still
754
- * navigable, else hand it to the first navigable top item. Every other top is
755
- * explicitly removed from the Tab sequence, so a top that arrived tabbable
756
- * cannot leave two stops behind. No Tab stop at all (`-1`) happens only when
757
- * every top is inert, and is recovered from as soon as one becomes navigable.
758
- */
759
655
  #ensureTabStop() {
760
656
  const active = this.#roving.activeIndex;
761
657
  const activeTop = active === -1 ? void 0 : this.topTargets[active];
@@ -766,58 +662,50 @@ var MenubarController = class extends Controller {
766
662
  const first = this.#navigableTops[0];
767
663
  this.#roving.setActive(first ? this.topTargets.indexOf(first) : -1);
768
664
  }
769
- /** The menu element controlled by `top` (matched by `aria-controls`/`id`). */
770
665
  #menuFor(top) {
771
666
  const id = top.getAttribute("aria-controls");
772
667
  return id ? this.menuTargets.find((menu) => menu.id === id) ?? null : null;
773
668
  }
774
- /** The top item that controls `menu` (reverse of `#menuFor`). */
775
669
  #topFor(menu) {
776
670
  if (!menu) return null;
777
671
  return this.topTargets.find((top) => top.getAttribute("aria-controls") === menu.id) ?? null;
778
672
  }
779
- /** The navigable item targets that live inside `menu`, in DOM order. */
780
673
  #itemsIn(menu) {
781
674
  return this.itemTargets.filter((item) => menu.contains(item) && this.#isNavigable(item));
782
675
  }
783
- /** Top items eligible for the roving tab stop and the arrow keys. */
676
+ get #entries() {
677
+ return [...this.topTargets, ...this.itemTargets];
678
+ }
784
679
  get #navigableTops() {
785
680
  return this.topTargets.filter((top) => this.#isNavigable(top));
786
681
  }
787
- /**
788
- * Whether `element` can take roving focus: `hidden` and natively `disabled`
789
- * controls are out of reach and are skipped. `aria-disabled` is deliberately
790
- * **not** checked — the APG keeps such items focusable so they stay
791
- * discoverable, and {@link MenubarController.#isActivationBlocked} is what
792
- * suppresses acting on them. CSS-only visibility is not detectable headlessly
793
- * and stays the consumer's responsibility.
794
- */
795
682
  #isNavigable(element) {
796
683
  if (element.hasAttribute("hidden")) return false;
797
684
  return !element.disabled;
798
685
  }
799
- /** Whether `element` is `aria-disabled`: reachable, but never activated. */
800
- #isActivationBlocked(element) {
801
- return element.getAttribute("aria-disabled") === "true";
686
+ #isActivationDisabled(element) {
687
+ return !this.#isNavigable(element) || isAriaDisabled(element);
688
+ }
689
+ #refuses(entry, event, origin) {
690
+ const recorded = this.#clickVerdicts.for(event, entry);
691
+ if (recorded !== void 0) return recorded;
692
+ if (this.#isActivationDisabled(entry)) return true;
693
+ const target = event ? event.target : origin;
694
+ return ownersOf(this.#entries, target).some((owner) => this.#isActivationDisabled(owner));
802
695
  }
803
- /** Moves DOM focus to the item at `index` (no-op if out of range). */
804
696
  #focusAt(items, index) {
805
697
  items[index]?.focus();
806
698
  }
807
- /** Returns roving focus to a top item (and makes it the single tab stop). */
808
- #focusTop(top) {
699
+ #focusTop(top, focus = true) {
809
700
  if (!top) return;
810
- this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
701
+ this.#roving.setActive(this.topTargets.indexOf(top), { focus });
811
702
  }
812
- /** Whether `top`'s menu is currently expanded. */
813
703
  #isExpanded(top) {
814
704
  return top.getAttribute("aria-expanded") === "true";
815
705
  }
816
- /** Whether any top item's menu is currently open. */
817
706
  get #isAnyOpen() {
818
707
  return this.topTargets.some((top) => this.#isExpanded(top));
819
708
  }
820
- /** Advances focus to the next item in `items` matching the accumulated query. */
821
709
  #typeaheadTo(items, current, key) {
822
710
  const index = findTypeaheadMatch(items, current, this.#typeahead.push(key));
823
711
  if (index !== -1) items[index]?.focus();
@@ -825,5 +713,4 @@ var MenubarController = class extends Controller {
825
713
  };
826
714
 
827
715
  export { MenubarController };
828
- //# sourceMappingURL=menubar_controller.js.map
829
716
  //# sourceMappingURL=menubar_controller.js.map