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,46 @@
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
+ }
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/escape_layer.ts
34
+ function sharedRegistry(key, create) {
35
+ const symbol = Symbol.for(key);
36
+ const scope = globalThis;
37
+ const existing = scope[symbol];
38
+ if (existing !== void 0) return existing;
39
+ const registry = create();
40
+ Object.defineProperty(globalThis, symbol, { value: registry });
41
+ return registry;
42
+ }
43
+
17
44
  function claimsWhileFocusWithin(element) {
18
45
  return () => {
19
46
  const active = element.ownerDocument.activeElement;
@@ -21,17 +48,11 @@ function claimsWhileFocusWithin(element) {
21
48
  };
22
49
  }
23
50
  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
- */
51
+ static #registries = sharedRegistry(
52
+ "stimeo-ui.escape-layer.registry.v1",
53
+ () => /* @__PURE__ */ new WeakMap()
54
+ );
55
+ #active = null;
35
56
  activate(ownerDocument = document, options) {
36
57
  this.deactivate();
37
58
  let registry = _EscapeLayer.#registries.get(ownerDocument);
@@ -40,44 +61,31 @@ var EscapeLayer = class _EscapeLayer {
40
61
  _EscapeLayer.#registries.set(ownerDocument, registry);
41
62
  ownerDocument.addEventListener("keydown", registry.onKeydown);
42
63
  }
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
- */
64
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
65
+ registry.stack.push(entry);
66
+ this.#active = { document: ownerDocument, entry };
67
+ }
52
68
  deactivate() {
53
- const ownerDocument = this.#ownerDocument;
54
- if (!ownerDocument) return;
55
- const registry = _EscapeLayer.#registries.get(ownerDocument);
69
+ const active = this.#active;
70
+ if (!active) return;
71
+ const registry = _EscapeLayer.#registries.get(active.document);
56
72
  if (registry) {
57
- const index = registry.stack.lastIndexOf(this);
73
+ const index = registry.stack.lastIndexOf(active.entry);
58
74
  if (index >= 0) registry.stack.splice(index, 1);
59
75
  if (registry.stack.length === 0) {
60
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
61
- _EscapeLayer.#registries.delete(ownerDocument);
76
+ active.document.removeEventListener("keydown", registry.onKeydown);
77
+ _EscapeLayer.#registries.delete(active.document);
62
78
  }
63
79
  }
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
- */
80
+ this.#active = null;
81
+ }
73
82
  get ownsEscape() {
74
- const ownerDocument = this.#ownerDocument;
75
- if (!ownerDocument) return false;
76
- const registry = _EscapeLayer.#registries.get(ownerDocument);
83
+ const active = this.#active;
84
+ if (!active) return false;
85
+ const registry = _EscapeLayer.#registries.get(active.document);
77
86
  if (!registry) return false;
78
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
87
+ return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
79
88
  }
80
- /** Builds a document's registry with its shared resolver listener. */
81
89
  static #createRegistry() {
82
90
  const registry = {
83
91
  stack: [],
@@ -86,24 +94,22 @@ var EscapeLayer = class _EscapeLayer {
86
94
  const owner = _EscapeLayer.#resolveOwner(registry.stack);
87
95
  if (!owner) return;
88
96
  event.preventDefault();
89
- owner.#onDismiss?.();
97
+ owner.onDismiss();
90
98
  }
91
99
  };
92
100
  return registry;
93
101
  }
94
- /** The topmost stack layer whose claims predicate passes, or `null`. */
95
102
  static #resolveOwner(stack) {
96
103
  for (let index = stack.length - 1; index >= 0; index--) {
97
104
  const layer = stack[index];
98
105
  if (!layer) continue;
99
- if (layer.#claims && !layer.#claims()) continue;
106
+ if (layer.claims && !layer.claims()) continue;
100
107
  return layer;
101
108
  }
102
109
  return null;
103
110
  }
104
111
  };
105
112
 
106
- // src/utils/event_owner.ts
107
113
  function ownerIndex(candidates, node) {
108
114
  if (!(node instanceof Node)) return -1;
109
115
  return candidates.findIndex((candidate) => candidate.contains(node));
@@ -112,31 +118,78 @@ function ownerOf(candidates, node) {
112
118
  return candidates[ownerIndex(candidates, node)] ?? null;
113
119
  }
114
120
 
115
- // src/utils/roving_tabindex.ts
121
+ var ListenerSet = class {
122
+ #abort = new AbortController();
123
+ add(target, type, handler, options) {
124
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
125
+ }
126
+ dispose() {
127
+ this.#abort.abort();
128
+ this.#abort = new AbortController();
129
+ }
130
+ };
131
+
132
+ var MicrotaskCoalescer = class {
133
+ #run;
134
+ #queued = false;
135
+ #active = false;
136
+ #generation = 0;
137
+ constructor(run) {
138
+ this.#run = run;
139
+ }
140
+ activate() {
141
+ this.#active = true;
142
+ }
143
+ cancel() {
144
+ this.#active = false;
145
+ this.#queued = false;
146
+ this.#generation += 1;
147
+ }
148
+ schedule() {
149
+ if (!this.#active || this.#queued) return;
150
+ this.#queued = true;
151
+ const generation = this.#generation;
152
+ queueMicrotask(() => {
153
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
154
+ this.#queued = false;
155
+ this.#run();
156
+ });
157
+ }
158
+ };
159
+
160
+ var MorphRenderWatcher = class {
161
+ #listeners = new ListenerSet();
162
+ #pass;
163
+ #element = null;
164
+ #onMorph = () => this.#pass.schedule();
165
+ constructor(run) {
166
+ this.#pass = new MicrotaskCoalescer(run);
167
+ }
168
+ observe(element) {
169
+ if (this.#element === element) return;
170
+ this.disconnect();
171
+ this.#element = element;
172
+ this.#pass.activate();
173
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
174
+ }
175
+ schedule() {
176
+ this.#pass.schedule();
177
+ }
178
+ disconnect() {
179
+ this.#listeners.dispose();
180
+ this.#pass.cancel();
181
+ this.#element = null;
182
+ }
183
+ };
184
+
116
185
  var RovingTabindex = class {
117
- /** Returns the current ordered item elements; called on every operation. */
118
186
  #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
187
  constructor(getItems) {
124
188
  this.#getItems = getItems;
125
189
  }
126
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
127
190
  get activeIndex() {
128
191
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
129
192
  }
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
193
  setActive(index, options = {}) {
141
194
  const { focus = false } = options;
142
195
  const items = options.items ?? this.#getItems();
@@ -147,43 +200,24 @@ var RovingTabindex = class {
147
200
  }
148
201
  };
149
202
 
150
- // src/utils/safe_timeout.ts
151
203
  var TimerRegistry = class {
152
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
153
204
  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
205
  clear(id) {
161
206
  if (this.ids.delete(id)) {
162
207
  this.cancel(id);
163
208
  }
164
209
  }
165
- /**
166
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
167
- * to guarantee no timer outlives the element.
168
- */
169
210
  clearAll() {
170
211
  for (const id of this.ids) {
171
212
  this.cancel(id);
172
213
  }
173
214
  this.ids.clear();
174
215
  }
175
- /** Number of timers currently tracked (pending). */
176
216
  get size() {
177
217
  return this.ids.size;
178
218
  }
179
219
  };
180
220
  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
221
  set(callback, delay) {
188
222
  const id = this.schedule(() => {
189
223
  this.ids.delete(id);
@@ -200,39 +234,22 @@ var SafeTimeout = class extends TimerRegistry {
200
234
  }
201
235
  };
202
236
 
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";
237
+ function targetSelector(identifier, name) {
238
+ return `[data-${identifier}-target~="${name}"]`;
211
239
  }
212
240
 
213
- // src/utils/typeahead.ts
214
241
  var TYPEAHEAD_RESET_MS = 500;
215
242
  var Typeahead = class {
216
- /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
217
243
  #timers = new SafeTimeout();
218
- /** Idle window before the query resets, in milliseconds. */
219
244
  #resetMs;
220
- /** The accumulated lowercase query, empty when idle. */
221
245
  #query = "";
222
- /** Id of the pending reset timer, `0` when none is scheduled. */
223
246
  #timerId = 0;
224
- /** @param options - Overrides for the idle window. */
225
247
  constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
226
248
  this.#resetMs = resetMs;
227
249
  }
228
- /** The query a search would currently run with; empty while idle. */
229
250
  get query() {
230
251
  return this.#query;
231
252
  }
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
253
  push(key) {
237
254
  const char = key.toLowerCase();
238
255
  const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
@@ -241,7 +258,6 @@ var Typeahead = class {
241
258
  this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
242
259
  return this.#query;
243
260
  }
244
- /** Clears the query and cancels the pending idle reset. */
245
261
  reset() {
246
262
  this.#query = "";
247
263
  this.#timers.clear(this.#timerId);
@@ -268,65 +284,30 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
268
284
  return -1;
269
285
  }
270
286
 
271
- // src/controllers/menubar_controller.ts
272
287
  var STATE_ATTRIBUTES = ["disabled", "hidden"];
273
288
  var MenubarController = class extends Controller {
274
289
  static targets = ["top", "menu", "item"];
275
290
  static actions = ["activate", "onItemKeydown", "onTopKeydown", "toggle"];
276
291
  static events = ["close", "open"];
277
- /** Roving tabindex across the top-level menuitems (one Tab stop). */
278
292
  #roving = new RovingTabindex(() => this.topTargets);
279
- /** Typeahead query and its idle-reset timer (scoped to the open menu). */
280
293
  #typeahead = new Typeahead();
281
294
  #timers = new SafeTimeout();
282
- /** Escape-stack membership while a menu is open; the shared resolver dismisses via it. */
283
295
  #escapeLayer = new EscapeLayer();
284
- /** Whether {@link #escapeLayer} is currently on the stack (see {@link #syncEscapeLayer}). */
285
296
  #layerActive = false;
286
- /** Watches `disabled`/`hidden` under the menubar; `null` while disconnected. */
287
297
  #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
298
  #connected = false;
295
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
296
299
  #reporting = false;
297
- /** The element inside the menubar that last took DOM focus, if any. */
298
300
  #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
- */
306
301
  #onFocusIn = (event) => {
307
302
  this.#focused = event.target;
308
303
  };
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
304
  #onFocusOut = (event) => {
316
305
  const next = event.relatedTarget;
317
306
  if (next && !this.element.contains(next)) this.#focused = null;
318
307
  };
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
- */
308
+ #morph = new MorphRenderWatcher(() => this.#reconcile());
329
309
  connect() {
310
+ this.#morph.observe(this.element);
330
311
  this.#closeAllMenus("api");
331
312
  this.#reconcile();
332
313
  this.element.addEventListener("click", this.#onDisabledClickCapture, true);
@@ -334,7 +315,7 @@ var MenubarController = class extends Controller {
334
315
  this.element.addEventListener("focusout", this.#onFocusOut);
335
316
  document.addEventListener("click", this.#onOutsideClick, true);
336
317
  if (typeof MutationObserver !== "undefined") {
337
- this.#observer = new MutationObserver(() => this.#reconcile());
318
+ this.#observer = new MutationObserver(() => this.#morph.schedule());
338
319
  this.#observer.observe(this.element, {
339
320
  subtree: true,
340
321
  childList: true,
@@ -345,8 +326,8 @@ var MenubarController = class extends Controller {
345
326
  this.#connected = true;
346
327
  this.#reporting = true;
347
328
  }
348
- /** Removes the listeners, stack membership, and any pending timer (typeahead / Tab close). */
349
329
  disconnect() {
330
+ this.#morph.disconnect();
350
331
  this.#connected = false;
351
332
  this.#reporting = false;
352
333
  this.#escapeLayer.deactivate();
@@ -361,63 +342,39 @@ var MenubarController = class extends Controller {
361
342
  this.#typeahead.reset();
362
343
  this.#timers.clearAll();
363
344
  }
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
345
  topTargetConnected(top) {
370
346
  if (!this.#connected) return;
371
347
  top.tabIndex = -1;
372
348
  this.#reconcile();
373
349
  }
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
350
  topTargetDisconnected(top) {
386
351
  if (!this.#connected) return;
387
352
  top.tabIndex = -1;
388
353
  if (this.#isExpanded(top)) top.setAttribute("aria-expanded", "false");
389
354
  this.#reconcile();
390
355
  }
391
- /** See {@link MenubarController.menuTargetDisconnected}. */
392
356
  menuTargetConnected() {
393
357
  if (!this.#connected) return;
394
358
  this.#reconcile();
395
359
  }
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
360
  menuTargetDisconnected(menu) {
406
361
  if (!this.#connected) return;
407
362
  if (!menu.hidden) menu.hidden = true;
408
363
  this.#reconcile();
409
364
  }
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);
365
+ toggle(source) {
366
+ const { event, host, origin, reason } = actionSource(source);
367
+ const top = host?.closest(targetSelector(this.identifier, "top"));
368
+ if (!top || !this.topTargets.includes(top)) return;
369
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
370
+ return;
371
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
414
372
  if (this.#isExpanded(top)) {
415
373
  this.#closeMenu(top, reason);
416
374
  } else {
417
- this.#openMenu(top, "first", reason);
375
+ this.#openMenu(top, "first", reason, moveFocus);
418
376
  }
419
377
  }
420
- /** Keyboard handling while focus is on a top item. */
421
378
  onTopKeydown(event) {
422
379
  if (event.defaultPrevented) return;
423
380
  if (isReservedArrowChord(event)) return;
@@ -457,7 +414,6 @@ var MenubarController = class extends Controller {
457
414
  break;
458
415
  }
459
416
  }
460
- /** Keyboard handling while focus is on a menu item. */
461
417
  onItemKeydown(event) {
462
418
  if (event.defaultPrevented) return;
463
419
  if (isReservedArrowChord(event)) return;
@@ -503,76 +459,60 @@ var MenubarController = class extends Controller {
503
459
  break;
504
460
  }
505
461
  }
506
- /** Closes the owning menu after an item is activated and refocuses its top. */
507
- activate(event) {
508
- const item = event.currentTarget;
462
+ activate(source) {
463
+ const { event, host, origin } = actionSource(source);
464
+ const item = host?.closest(targetSelector(this.identifier, "item"));
465
+ if (!item || !this.itemTargets.includes(item)) return;
466
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
467
+ return;
468
+ const moveFocus = event !== null || this.element.contains(document.activeElement);
509
469
  const menu = item.closest("[role='menu']");
510
470
  const top = this.#topFor(menu);
511
471
  this.#closeAllMenus("select");
512
- this.#focusTop(top);
472
+ this.#focusTop(
473
+ top,
474
+ moveFocus && (event !== null || this.element.contains(document.activeElement))
475
+ );
513
476
  }
514
- /** Moves the roving focus to `top`, opening its menu when one was open. */
515
- #gotoTop(top, reopen, reason) {
516
- if (!top) return;
477
+ #gotoTop(destination, reopen, reason) {
478
+ if (!destination) return;
517
479
  if (reopen) {
518
- this.#openMenu(top, "first", reason);
480
+ this.#openMenu(destination, "first", reason);
519
481
  } else {
520
- this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
482
+ this.#roving.setActive(this.topTargets.indexOf(destination), { focus: true });
521
483
  }
522
484
  }
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")) {
485
+ #openMenu(opener, focus, reason, moveFocus = true) {
486
+ if (!opener) return;
487
+ const menu = this.#menuFor(opener);
488
+ if (!menu || this.#isActivationBlocked(opener)) {
489
+ if (menu || !opener.hasAttribute("aria-controls")) {
550
490
  this.#closeAllMenus(reason);
551
- this.#focusTop(top);
491
+ this.#focusTop(
492
+ opener,
493
+ moveFocus && (reason !== "api" || this.element.contains(document.activeElement))
494
+ );
552
495
  }
553
496
  return;
554
497
  }
555
498
  this.#timers.clearAll();
556
- const was = this.#isExpanded(top);
557
- this.#closeAllMenus(reason, top);
499
+ const was = this.#isExpanded(opener);
500
+ this.#closeAllMenus(reason, opener);
558
501
  menu.hidden = false;
559
- top.setAttribute("aria-expanded", "true");
560
- if (!was) this.#report("open", top, menu, reason);
561
- if (!this.#isExpanded(top)) return;
502
+ opener.setAttribute("aria-expanded", "true");
503
+ if (!was) this.#report("open", opener, menu, reason);
504
+ if (!this.#isExpanded(opener)) return;
562
505
  this.#escapeLayer.activate(document, {
563
506
  onDismiss: () => this.#dismissOpenMenu(),
564
507
  claims: claimsWhileFocusWithin(this.element)
565
508
  });
566
509
  this.#layerActive = true;
567
- this.#roving.setActive(this.topTargets.indexOf(top));
510
+ this.#roving.setActive(this.topTargets.indexOf(opener));
568
511
  const items = this.#itemsIn(menu);
569
- this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
512
+ if (moveFocus && (reason !== "api" || this.element.contains(document.activeElement))) {
513
+ this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
514
+ }
570
515
  }
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
516
  #closeMenu(top, reason, silent = false) {
577
517
  if (!top) return;
578
518
  const menu = this.#menuFor(top);
@@ -583,40 +523,21 @@ var MenubarController = class extends Controller {
583
523
  if (was && !silent) this.#report("close", top, menu, reason);
584
524
  this.#syncEscapeLayer();
585
525
  }
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
526
  #closeAllMenus(reason, except = null) {
592
527
  for (const top of this.topTargets) this.#closeMenu(top, reason, top === except);
593
528
  this.#typeahead.reset();
594
529
  }
595
- /** Names the menu that moved, so a subscriber does not have to re-derive it. */
596
530
  #report(name, top, menu, reason) {
597
531
  if (!this.#reporting) return;
598
532
  const detail = { reason, index: this.topTargets.indexOf(top), menu };
599
533
  if (name === "open") this.dispatch("open", { detail, cancelable: false });
600
534
  else this.dispatch("close", { detail, cancelable: false });
601
535
  }
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
536
  #closeMenusSoon() {
610
537
  if (!this.#isAnyOpen) return;
611
538
  this.#timers.clearAll();
612
539
  this.#timers.set(() => this.#closeAllMenus("focus"), 0);
613
540
  }
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
541
  #dismissOpenMenu() {
621
542
  const menu = ownerOf(this.menuTargets, document.activeElement);
622
543
  if (menu) {
@@ -627,32 +548,20 @@ var MenubarController = class extends Controller {
627
548
  }
628
549
  this.#closeAllMenus("escape");
629
550
  }
630
- /** Opens the menu of the navigable top item `delta` steps from `menu`'s owner. */
631
551
  #moveToAdjacentMenu(menu, delta) {
632
- const top = this.#topFor(menu);
633
- if (!top) return;
552
+ const ownerTop = this.#topFor(menu);
553
+ if (!ownerTop) return;
634
554
  const tops = this.#navigableTops;
635
- const current = tops.indexOf(top);
555
+ const current = tops.indexOf(ownerTop);
636
556
  if (current === -1) return;
637
557
  const next = (current + delta + tops.length) % tops.length;
638
558
  this.#openMenu(tops[next], "first", "user");
639
559
  }
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
560
  #onOutsideClick = (event) => {
646
561
  if (this.#isAnyOpen && !this.element.contains(event.target)) {
647
562
  this.#closeAllMenus("outside");
648
563
  }
649
564
  };
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
565
  #onDisabledClickCapture = (event) => {
657
566
  const target = event.target;
658
567
  if (!(target instanceof Node)) return;
@@ -663,18 +572,6 @@ var MenubarController = class extends Controller {
663
572
  event.preventDefault();
664
573
  event.stopImmediatePropagation();
665
574
  };
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
575
  #reconcile() {
679
576
  for (const top of this.topTargets) {
680
577
  const own = this.#menuFor(top);
@@ -691,20 +588,6 @@ var MenubarController = class extends Controller {
691
588
  this.#rescueFocus();
692
589
  this.#syncEscapeLayer();
693
590
  }
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
591
  #rescueFocus() {
709
592
  const focused = this.#focused;
710
593
  if (!focused) return;
@@ -720,22 +603,6 @@ var MenubarController = class extends Controller {
720
603
  const top = index === -1 ? void 0 : this.topTargets[index];
721
604
  if (top && top !== focused && !focused.contains(top)) top.focus();
722
605
  }
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
606
  #syncEscapeLayer() {
740
607
  const open = this.#isAnyOpen;
741
608
  if (open === this.#layerActive) return;
@@ -749,13 +616,6 @@ var MenubarController = class extends Controller {
749
616
  }
750
617
  this.#layerActive = open;
751
618
  }
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
619
  #ensureTabStop() {
760
620
  const active = this.#roving.activeIndex;
761
621
  const activeTop = active === -1 ? void 0 : this.topTargets[active];
@@ -766,58 +626,40 @@ var MenubarController = class extends Controller {
766
626
  const first = this.#navigableTops[0];
767
627
  this.#roving.setActive(first ? this.topTargets.indexOf(first) : -1);
768
628
  }
769
- /** The menu element controlled by `top` (matched by `aria-controls`/`id`). */
770
629
  #menuFor(top) {
771
630
  const id = top.getAttribute("aria-controls");
772
631
  return id ? this.menuTargets.find((menu) => menu.id === id) ?? null : null;
773
632
  }
774
- /** The top item that controls `menu` (reverse of `#menuFor`). */
775
633
  #topFor(menu) {
776
634
  if (!menu) return null;
777
635
  return this.topTargets.find((top) => top.getAttribute("aria-controls") === menu.id) ?? null;
778
636
  }
779
- /** The navigable item targets that live inside `menu`, in DOM order. */
780
637
  #itemsIn(menu) {
781
638
  return this.itemTargets.filter((item) => menu.contains(item) && this.#isNavigable(item));
782
639
  }
783
- /** Top items eligible for the roving tab stop and the arrow keys. */
784
640
  get #navigableTops() {
785
641
  return this.topTargets.filter((top) => this.#isNavigable(top));
786
642
  }
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
643
  #isNavigable(element) {
796
644
  if (element.hasAttribute("hidden")) return false;
797
645
  return !element.disabled;
798
646
  }
799
- /** Whether `element` is `aria-disabled`: reachable, but never activated. */
800
647
  #isActivationBlocked(element) {
801
648
  return element.getAttribute("aria-disabled") === "true";
802
649
  }
803
- /** Moves DOM focus to the item at `index` (no-op if out of range). */
804
650
  #focusAt(items, index) {
805
651
  items[index]?.focus();
806
652
  }
807
- /** Returns roving focus to a top item (and makes it the single tab stop). */
808
- #focusTop(top) {
653
+ #focusTop(top, focus = true) {
809
654
  if (!top) return;
810
- this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
655
+ this.#roving.setActive(this.topTargets.indexOf(top), { focus });
811
656
  }
812
- /** Whether `top`'s menu is currently expanded. */
813
657
  #isExpanded(top) {
814
658
  return top.getAttribute("aria-expanded") === "true";
815
659
  }
816
- /** Whether any top item's menu is currently open. */
817
660
  get #isAnyOpen() {
818
661
  return this.topTargets.some((top) => this.#isExpanded(top));
819
662
  }
820
- /** Advances focus to the next item in `items` matching the accumulated query. */
821
663
  #typeaheadTo(items, current, key) {
822
664
  const index = findTypeaheadMatch(items, current, this.#typeahead.push(key));
823
665
  if (index !== -1) items[index]?.focus();
@@ -825,5 +667,4 @@ var MenubarController = class extends Controller {
825
667
  };
826
668
 
827
669
  export { MenubarController };
828
- //# sourceMappingURL=menubar_controller.js.map
829
670
  //# sourceMappingURL=menubar_controller.js.map