shadcn-rails 0.1.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 (315) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +40 -0
  3. data/CHANGELOG.md +54 -0
  4. data/CLAUDE.md +463 -0
  5. data/PROGRESS.md +485 -0
  6. data/README.md +1483 -0
  7. data/Rakefile +29 -0
  8. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
  9. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
  10. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
  11. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
  12. data/__tests__/controllers/accordion_controller.test.js +904 -0
  13. data/__tests__/controllers/calendar_controller.test.js +1370 -0
  14. data/__tests__/controllers/carousel_controller.test.js +912 -0
  15. data/__tests__/controllers/checkbox_controller.test.js +454 -0
  16. data/__tests__/controllers/collapsible_controller.test.js +407 -0
  17. data/__tests__/controllers/combobox_controller.test.js +966 -0
  18. data/__tests__/controllers/context_menu_controller.test.js +627 -0
  19. data/__tests__/controllers/date_picker_controller.test.js +636 -0
  20. data/__tests__/controllers/dialog_controller.test.js +878 -0
  21. data/__tests__/controllers/drawer_controller.test.js +995 -0
  22. data/__tests__/controllers/menubar_controller.test.js +736 -0
  23. data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
  24. data/__tests__/controllers/popover_controller.test.js +1007 -0
  25. data/__tests__/controllers/radio_group_controller.test.js +640 -0
  26. data/__tests__/controllers/resizable_controller.test.js +680 -0
  27. data/__tests__/controllers/select_controller.test.js +674 -0
  28. data/__tests__/controllers/sheet_controller.test.js +986 -0
  29. data/__tests__/controllers/slider_controller.test.js +1036 -0
  30. data/__tests__/controllers/switch_controller.test.js +424 -0
  31. data/__tests__/controllers/tabs_controller.test.js +907 -0
  32. data/__tests__/controllers/toggle_group_controller.test.js +839 -0
  33. data/__tests__/controllers/tooltip_controller.test.js +808 -0
  34. data/__tests__/helpers/stimulus-test-helper.js +203 -0
  35. data/app/assets/config/manifest.js +1 -0
  36. data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
  37. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
  38. data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
  39. data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
  40. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
  41. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
  42. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
  43. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
  44. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
  45. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
  46. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
  47. data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
  48. data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
  49. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
  50. data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
  51. data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
  52. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
  53. data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
  54. data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
  55. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
  56. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
  57. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
  58. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
  59. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
  60. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
  61. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
  62. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
  63. data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
  64. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
  65. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
  66. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
  67. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
  68. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
  69. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
  70. data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
  71. data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
  72. data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
  73. data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
  74. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
  75. data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
  76. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
  77. data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
  78. data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
  79. data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
  80. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
  81. data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
  82. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
  83. data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
  84. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
  85. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
  86. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
  87. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
  88. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
  89. data/app/assets/javascripts/shadcn/index.d.ts +74 -0
  90. data/app/assets/javascripts/shadcn/index.js +133 -0
  91. data/app/assets/stylesheets/.keep +0 -0
  92. data/app/assets/stylesheets/shadcn/base.css +445 -0
  93. data/app/assets/stylesheets/shadcn/components.css +513 -0
  94. data/app/assets/stylesheets/shadcn/index.css +18 -0
  95. data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
  96. data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
  97. data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
  98. data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
  99. data/app/components/shadcn/accordion_component.rb +63 -0
  100. data/app/components/shadcn/accordion_content_component.rb +29 -0
  101. data/app/components/shadcn/accordion_item_component.rb +40 -0
  102. data/app/components/shadcn/accordion_trigger_component.rb +49 -0
  103. data/app/components/shadcn/alert_component.rb +75 -0
  104. data/app/components/shadcn/alert_description_component.rb +12 -0
  105. data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
  106. data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
  107. data/app/components/shadcn/alert_dialog_component.rb +71 -0
  108. data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
  109. data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
  110. data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
  111. data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
  112. data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
  113. data/app/components/shadcn/alert_title_component.rb +12 -0
  114. data/app/components/shadcn/aspect_ratio_component.rb +49 -0
  115. data/app/components/shadcn/avatar_component.rb +107 -0
  116. data/app/components/shadcn/avatar_fallback_component.rb +17 -0
  117. data/app/components/shadcn/badge_component.rb +49 -0
  118. data/app/components/shadcn/base_component.rb +100 -0
  119. data/app/components/shadcn/breadcrumb_component.rb +70 -0
  120. data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
  121. data/app/components/shadcn/button_component.rb +141 -0
  122. data/app/components/shadcn/button_group_component.rb +69 -0
  123. data/app/components/shadcn/calendar_component.rb +337 -0
  124. data/app/components/shadcn/card_action_component.rb +10 -0
  125. data/app/components/shadcn/card_component.rb +63 -0
  126. data/app/components/shadcn/card_content_component.rb +19 -0
  127. data/app/components/shadcn/card_description_component.rb +12 -0
  128. data/app/components/shadcn/card_footer_component.rb +12 -0
  129. data/app/components/shadcn/card_header_component.rb +24 -0
  130. data/app/components/shadcn/card_title_component.rb +18 -0
  131. data/app/components/shadcn/carousel_component.rb +275 -0
  132. data/app/components/shadcn/checkbox_component.rb +103 -0
  133. data/app/components/shadcn/collapsible_component.rb +66 -0
  134. data/app/components/shadcn/collapsible_content_component.rb +28 -0
  135. data/app/components/shadcn/combobox_component.rb +322 -0
  136. data/app/components/shadcn/command_component.rb +52 -0
  137. data/app/components/shadcn/command_dialog_component.rb +76 -0
  138. data/app/components/shadcn/command_empty_component.rb +12 -0
  139. data/app/components/shadcn/command_group_component.rb +34 -0
  140. data/app/components/shadcn/command_input_component.rb +59 -0
  141. data/app/components/shadcn/command_item_component.rb +48 -0
  142. data/app/components/shadcn/command_list_component.rb +38 -0
  143. data/app/components/shadcn/command_separator_component.rb +12 -0
  144. data/app/components/shadcn/command_shortcut_component.rb +12 -0
  145. data/app/components/shadcn/context_menu_component.rb +64 -0
  146. data/app/components/shadcn/context_menu_content_component.rb +44 -0
  147. data/app/components/shadcn/context_menu_item_component.rb +63 -0
  148. data/app/components/shadcn/context_menu_label_component.rb +18 -0
  149. data/app/components/shadcn/context_menu_separator_component.rb +12 -0
  150. data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
  151. data/app/components/shadcn/date_picker_component.rb +368 -0
  152. data/app/components/shadcn/dialog_component.rb +77 -0
  153. data/app/components/shadcn/dialog_content_component.rb +91 -0
  154. data/app/components/shadcn/dialog_description_component.rb +12 -0
  155. data/app/components/shadcn/dialog_footer_component.rb +12 -0
  156. data/app/components/shadcn/dialog_header_component.rb +19 -0
  157. data/app/components/shadcn/dialog_title_component.rb +12 -0
  158. data/app/components/shadcn/drawer_component.rb +72 -0
  159. data/app/components/shadcn/drawer_content_component.rb +76 -0
  160. data/app/components/shadcn/drawer_description_component.rb +12 -0
  161. data/app/components/shadcn/drawer_footer_component.rb +12 -0
  162. data/app/components/shadcn/drawer_header_component.rb +19 -0
  163. data/app/components/shadcn/drawer_title_component.rb +12 -0
  164. data/app/components/shadcn/dropdown_menu_component.rb +75 -0
  165. data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
  166. data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
  167. data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
  168. data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
  169. data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
  170. data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
  171. data/app/components/shadcn/empty_component.rb +48 -0
  172. data/app/components/shadcn/empty_content_component.rb +12 -0
  173. data/app/components/shadcn/empty_description_component.rb +12 -0
  174. data/app/components/shadcn/empty_header_component.rb +29 -0
  175. data/app/components/shadcn/empty_media_component.rb +21 -0
  176. data/app/components/shadcn/empty_title_component.rb +12 -0
  177. data/app/components/shadcn/field_component.rb +113 -0
  178. data/app/components/shadcn/hover_card_component.rb +64 -0
  179. data/app/components/shadcn/hover_card_content_component.rb +36 -0
  180. data/app/components/shadcn/input_component.rb +108 -0
  181. data/app/components/shadcn/input_group_component.rb +70 -0
  182. data/app/components/shadcn/input_otp_component.rb +183 -0
  183. data/app/components/shadcn/item_actions_component.rb +12 -0
  184. data/app/components/shadcn/item_component.rb +98 -0
  185. data/app/components/shadcn/item_content_component.rb +24 -0
  186. data/app/components/shadcn/item_description_component.rb +12 -0
  187. data/app/components/shadcn/item_footer_component.rb +12 -0
  188. data/app/components/shadcn/item_group_component.rb +24 -0
  189. data/app/components/shadcn/item_header_component.rb +12 -0
  190. data/app/components/shadcn/item_media_component.rb +22 -0
  191. data/app/components/shadcn/item_separator_component.rb +12 -0
  192. data/app/components/shadcn/item_title_component.rb +12 -0
  193. data/app/components/shadcn/kbd_component.rb +36 -0
  194. data/app/components/shadcn/label_component.rb +49 -0
  195. data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
  196. data/app/components/shadcn/menubar_component.rb +56 -0
  197. data/app/components/shadcn/menubar_content_component.rb +64 -0
  198. data/app/components/shadcn/menubar_item_component.rb +65 -0
  199. data/app/components/shadcn/menubar_label_component.rb +27 -0
  200. data/app/components/shadcn/menubar_menu_component.rb +34 -0
  201. data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
  202. data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
  203. data/app/components/shadcn/menubar_separator_component.rb +22 -0
  204. data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
  205. data/app/components/shadcn/menubar_sub_component.rb +38 -0
  206. data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
  207. data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
  208. data/app/components/shadcn/menubar_trigger_component.rb +31 -0
  209. data/app/components/shadcn/native_select_component.rb +150 -0
  210. data/app/components/shadcn/navigation_menu_component.rb +76 -0
  211. data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
  212. data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
  213. data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
  214. data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
  215. data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
  216. data/app/components/shadcn/pagination_component.rb +195 -0
  217. data/app/components/shadcn/pagination_content_component.rb +47 -0
  218. data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
  219. data/app/components/shadcn/pagination_item_component.rb +53 -0
  220. data/app/components/shadcn/pagination_next_component.rb +48 -0
  221. data/app/components/shadcn/pagination_previous_component.rb +48 -0
  222. data/app/components/shadcn/popover_component.rb +76 -0
  223. data/app/components/shadcn/popover_content_component.rb +25 -0
  224. data/app/components/shadcn/progress_component.rb +77 -0
  225. data/app/components/shadcn/radio_group_component.rb +129 -0
  226. data/app/components/shadcn/radio_group_item_component.rb +109 -0
  227. data/app/components/shadcn/resizable_handle_component.rb +98 -0
  228. data/app/components/shadcn/resizable_panel_component.rb +56 -0
  229. data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
  230. data/app/components/shadcn/scroll_area_component.rb +110 -0
  231. data/app/components/shadcn/select_component.rb +151 -0
  232. data/app/components/shadcn/select_group_component.rb +32 -0
  233. data/app/components/shadcn/select_item_component.rb +59 -0
  234. data/app/components/shadcn/select_separator_component.rb +12 -0
  235. data/app/components/shadcn/separator_component.rb +54 -0
  236. data/app/components/shadcn/sheet_component.rb +82 -0
  237. data/app/components/shadcn/sheet_content_component.rb +95 -0
  238. data/app/components/shadcn/sheet_description_component.rb +12 -0
  239. data/app/components/shadcn/sheet_footer_component.rb +12 -0
  240. data/app/components/shadcn/sheet_header_component.rb +19 -0
  241. data/app/components/shadcn/sheet_title_component.rb +12 -0
  242. data/app/components/shadcn/sidebar_component.rb +180 -0
  243. data/app/components/shadcn/sidebar_content_component.rb +32 -0
  244. data/app/components/shadcn/sidebar_footer_component.rb +24 -0
  245. data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
  246. data/app/components/shadcn/sidebar_group_component.rb +38 -0
  247. data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
  248. data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
  249. data/app/components/shadcn/sidebar_header_component.rb +24 -0
  250. data/app/components/shadcn/sidebar_inset_component.rb +25 -0
  251. data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
  252. data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
  253. data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
  254. data/app/components/shadcn/sidebar_menu_component.rb +32 -0
  255. data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
  256. data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
  257. data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
  258. data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
  259. data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
  260. data/app/components/shadcn/sidebar_provider_component.rb +57 -0
  261. data/app/components/shadcn/sidebar_rail_component.rb +30 -0
  262. data/app/components/shadcn/sidebar_separator_component.rb +24 -0
  263. data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
  264. data/app/components/shadcn/skeleton_component.rb +29 -0
  265. data/app/components/shadcn/slider_component.rb +76 -0
  266. data/app/components/shadcn/spinner_component.rb +67 -0
  267. data/app/components/shadcn/switch_component.rb +147 -0
  268. data/app/components/shadcn/table_body_component.rb +16 -0
  269. data/app/components/shadcn/table_caption_component.rb +12 -0
  270. data/app/components/shadcn/table_cell_component.rb +12 -0
  271. data/app/components/shadcn/table_component.rb +57 -0
  272. data/app/components/shadcn/table_footer_component.rb +16 -0
  273. data/app/components/shadcn/table_head_component.rb +12 -0
  274. data/app/components/shadcn/table_header_component.rb +16 -0
  275. data/app/components/shadcn/table_row_component.rb +40 -0
  276. data/app/components/shadcn/tabs_component.rb +78 -0
  277. data/app/components/shadcn/tabs_content_component.rb +32 -0
  278. data/app/components/shadcn/tabs_list_component.rb +30 -0
  279. data/app/components/shadcn/tabs_trigger_component.rb +37 -0
  280. data/app/components/shadcn/textarea_component.rb +84 -0
  281. data/app/components/shadcn/toast_action_component.rb +18 -0
  282. data/app/components/shadcn/toast_component.rb +114 -0
  283. data/app/components/shadcn/toast_description_component.rb +12 -0
  284. data/app/components/shadcn/toast_title_component.rb +12 -0
  285. data/app/components/shadcn/toast_viewport_component.rb +12 -0
  286. data/app/components/shadcn/toggle_component.rb +77 -0
  287. data/app/components/shadcn/toggle_group_component.rb +96 -0
  288. data/app/components/shadcn/toggle_group_item_component.rb +62 -0
  289. data/app/components/shadcn/tooltip_component.rb +89 -0
  290. data/app/components/shadcn/typography_component.rb +112 -0
  291. data/babel.config.cjs +5 -0
  292. data/bin/console +11 -0
  293. data/bin/setup +8 -0
  294. data/config/importmap.rb +5 -0
  295. data/fly.toml +26 -0
  296. data/jest.config.js +19 -0
  297. data/jest.setup.js +8 -0
  298. data/lib/generators/shadcn/component/component_generator.rb +188 -0
  299. data/lib/generators/shadcn/install/install_generator.rb +140 -0
  300. data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
  301. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
  302. data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
  303. data/lib/shadcn/rails/class_merger.rb +228 -0
  304. data/lib/shadcn/rails/configuration.rb +341 -0
  305. data/lib/shadcn/rails/engine.rb +59 -0
  306. data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
  307. data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
  308. data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
  309. data/lib/shadcn/rails/version.rb +7 -0
  310. data/lib/shadcn/rails.rb +179 -0
  311. data/package-lock.json +7415 -0
  312. data/package.json +68 -0
  313. data/rollup.config.js +29 -0
  314. data/sig/shadcn/rails.rbs +6 -0
  315. metadata +526 -0
@@ -0,0 +1,736 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import MenubarController from "../../app/assets/javascripts/shadcn/controllers/menubar_controller.js"
3
+ import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
4
+
5
+ describe("MenubarController", () => {
6
+ let application
7
+ let element
8
+ let controller
9
+
10
+ afterEach(() => {
11
+ cleanupController(application)
12
+ })
13
+
14
+ describe("basic rendering and initialization", () => {
15
+ const basicHTML = `
16
+ <div data-controller="shadcn--menubar"
17
+ data-shadcn--menubar-open-index-value="-1">
18
+ <div data-shadcn--menubar-target="menu">
19
+ <button data-shadcn--menubar-target="trigger"
20
+ data-action="click->shadcn--menubar#toggle mouseenter->shadcn--menubar#hoverOpen"
21
+ aria-expanded="false">File</button>
22
+ <div data-shadcn--menubar-target="content" hidden>
23
+ <button data-shadcn--menubar-target="item"
24
+ data-action="click->shadcn--menubar#selectItem">New</button>
25
+ <button data-shadcn--menubar-target="item"
26
+ data-action="click->shadcn--menubar#selectItem">Open</button>
27
+ </div>
28
+ </div>
29
+ <div data-shadcn--menubar-target="menu">
30
+ <button data-shadcn--menubar-target="trigger"
31
+ data-action="click->shadcn--menubar#toggle mouseenter->shadcn--menubar#hoverOpen"
32
+ aria-expanded="false">Edit</button>
33
+ <div data-shadcn--menubar-target="content" hidden>
34
+ <button data-shadcn--menubar-target="item"
35
+ data-action="click->shadcn--menubar#selectItem">Undo</button>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ `
40
+
41
+ beforeEach(async () => {
42
+ const setup = await setupController(MenubarController, basicHTML, 'shadcn--menubar')
43
+ application = setup.application
44
+ element = setup.element
45
+ controller = setup.controller
46
+ })
47
+
48
+ test("initializes with closed state", () => {
49
+ expect(controller.openIndexValue).toBe(-1)
50
+ })
51
+
52
+ test("initializes isMenuOpen to false", () => {
53
+ expect(controller.isMenuOpen).toBe(false)
54
+ })
55
+
56
+ test("initializes focusedIndex to -1", () => {
57
+ expect(controller.focusedIndex).toBe(-1)
58
+ })
59
+
60
+ test("has menu targets", () => {
61
+ expect(controller.menuTargets.length).toBe(2)
62
+ })
63
+
64
+ test("has trigger targets", () => {
65
+ expect(controller.triggerTargets.length).toBe(2)
66
+ })
67
+
68
+ test("has content targets", () => {
69
+ expect(controller.contentTargets.length).toBe(2)
70
+ })
71
+
72
+ test("has item targets", () => {
73
+ expect(controller.itemTargets.length).toBe(3)
74
+ })
75
+
76
+ test("all content is initially hidden", () => {
77
+ controller.contentTargets.forEach(content => {
78
+ expect(content.hidden).toBe(true)
79
+ })
80
+ })
81
+ })
82
+
83
+ describe("toggle functionality", () => {
84
+ const toggleHTML = `
85
+ <div data-controller="shadcn--menubar"
86
+ data-shadcn--menubar-open-index-value="-1">
87
+ <div data-shadcn--menubar-target="menu">
88
+ <button data-shadcn--menubar-target="trigger"
89
+ data-action="click->shadcn--menubar#toggle"
90
+ aria-expanded="false">File</button>
91
+ <div data-shadcn--menubar-target="content" hidden>
92
+ <button data-shadcn--menubar-target="item">New</button>
93
+ </div>
94
+ </div>
95
+ <div data-shadcn--menubar-target="menu">
96
+ <button data-shadcn--menubar-target="trigger"
97
+ data-action="click->shadcn--menubar#toggle"
98
+ aria-expanded="false">Edit</button>
99
+ <div data-shadcn--menubar-target="content" hidden>
100
+ <button data-shadcn--menubar-target="item">Undo</button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ `
105
+
106
+ beforeEach(async () => {
107
+ const setup = await setupController(MenubarController, toggleHTML, 'shadcn--menubar')
108
+ application = setup.application
109
+ element = setup.element
110
+ controller = setup.controller
111
+ })
112
+
113
+ test("opens menu on toggle", async () => {
114
+ const trigger = controller.triggerTargets[0]
115
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
116
+ await nextFrame()
117
+
118
+ expect(controller.openIndexValue).toBe(0)
119
+ expect(controller.isMenuOpen).toBe(true)
120
+ })
121
+
122
+ test("sets aria-expanded to true", async () => {
123
+ const trigger = controller.triggerTargets[0]
124
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
125
+ await nextFrame()
126
+
127
+ expect(trigger.getAttribute("aria-expanded")).toBe("true")
128
+ })
129
+
130
+ test("shows content when opened", async () => {
131
+ const trigger = controller.triggerTargets[0]
132
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
133
+ await nextFrame()
134
+
135
+ const content = controller.contentTargets[0]
136
+ expect(content.hidden).toBe(false)
137
+ })
138
+
139
+ test("sets content data-state to open", async () => {
140
+ const trigger = controller.triggerTargets[0]
141
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
142
+ await nextFrame()
143
+
144
+ const content = controller.contentTargets[0]
145
+ expect(content.dataset.state).toBe("open")
146
+ })
147
+
148
+ test("closes menu on second toggle", async () => {
149
+ const trigger = controller.triggerTargets[0]
150
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
151
+ await nextFrame()
152
+ controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
153
+ await nextFrame()
154
+
155
+ expect(controller.openIndexValue).toBe(-1)
156
+ expect(controller.isMenuOpen).toBe(false)
157
+ })
158
+
159
+ test("switches to different menu on toggle", async () => {
160
+ const trigger1 = controller.triggerTargets[0]
161
+ const trigger2 = controller.triggerTargets[1]
162
+
163
+ controller.toggle({ currentTarget: trigger1, preventDefault: jest.fn() })
164
+ await nextFrame()
165
+ expect(controller.openIndexValue).toBe(0)
166
+
167
+ controller.toggle({ currentTarget: trigger2, preventDefault: jest.fn() })
168
+ await nextFrame()
169
+ expect(controller.openIndexValue).toBe(1)
170
+ })
171
+ })
172
+
173
+ describe("hover functionality", () => {
174
+ const hoverHTML = `
175
+ <div data-controller="shadcn--menubar"
176
+ data-shadcn--menubar-open-index-value="-1">
177
+ <div data-shadcn--menubar-target="menu">
178
+ <button data-shadcn--menubar-target="trigger"
179
+ data-action="mouseenter->shadcn--menubar#hoverOpen"
180
+ aria-expanded="false">File</button>
181
+ <div data-shadcn--menubar-target="content" hidden>Content 1</div>
182
+ </div>
183
+ <div data-shadcn--menubar-target="menu">
184
+ <button data-shadcn--menubar-target="trigger"
185
+ data-action="mouseenter->shadcn--menubar#hoverOpen"
186
+ aria-expanded="false">Edit</button>
187
+ <div data-shadcn--menubar-target="content" hidden>Content 2</div>
188
+ </div>
189
+ </div>
190
+ `
191
+
192
+ beforeEach(async () => {
193
+ const setup = await setupController(MenubarController, hoverHTML, 'shadcn--menubar')
194
+ application = setup.application
195
+ element = setup.element
196
+ controller = setup.controller
197
+ })
198
+
199
+ test("does not open on hover when no menu is open", async () => {
200
+ const trigger = controller.triggerTargets[0]
201
+ controller.hoverOpen({ currentTarget: trigger })
202
+ await nextFrame()
203
+
204
+ expect(controller.isMenuOpen).toBe(false)
205
+ })
206
+
207
+ test("opens different menu on hover when one is already open", async () => {
208
+ // First open a menu
209
+ controller.openMenu(0)
210
+ await nextFrame()
211
+
212
+ const trigger2 = controller.triggerTargets[1]
213
+ controller.hoverOpen({ currentTarget: trigger2 })
214
+ await nextFrame()
215
+
216
+ expect(controller.openIndexValue).toBe(1)
217
+ })
218
+ })
219
+
220
+ describe("item selection", () => {
221
+ const selectHTML = `
222
+ <div data-controller="shadcn--menubar"
223
+ data-shadcn--menubar-open-index-value="-1">
224
+ <div data-shadcn--menubar-target="menu">
225
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
226
+ <div data-shadcn--menubar-target="content" hidden>
227
+ <button data-shadcn--menubar-target="item"
228
+ data-action="click->shadcn--menubar#selectItem">New</button>
229
+ <button data-shadcn--menubar-target="item"
230
+ data-action="click->shadcn--menubar#selectItem"
231
+ data-disabled>Disabled</button>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ `
236
+
237
+ beforeEach(async () => {
238
+ const setup = await setupController(MenubarController, selectHTML, 'shadcn--menubar')
239
+ application = setup.application
240
+ element = setup.element
241
+ controller = setup.controller
242
+ })
243
+
244
+ test("dispatches select event", async () => {
245
+ controller.openMenu(0)
246
+ await nextFrame()
247
+
248
+ let selectedItem = null
249
+ element.addEventListener("shadcn--menubar:select", (e) => {
250
+ selectedItem = e.detail.item
251
+ })
252
+
253
+ const item = controller.itemTargets[0]
254
+ controller.selectItem({ currentTarget: item })
255
+ await nextFrame()
256
+
257
+ expect(selectedItem).toBe(item)
258
+ })
259
+
260
+ test("closes menu after selection", async () => {
261
+ controller.openMenu(0)
262
+ await nextFrame()
263
+
264
+ const item = controller.itemTargets[0]
265
+ controller.selectItem({ currentTarget: item })
266
+ await nextFrame()
267
+
268
+ expect(controller.isMenuOpen).toBe(false)
269
+ })
270
+
271
+ test("does not select disabled items", async () => {
272
+ controller.openMenu(0)
273
+ await nextFrame()
274
+
275
+ let selectFired = false
276
+ element.addEventListener("shadcn--menubar:select", () => {
277
+ selectFired = true
278
+ })
279
+
280
+ const disabledItem = controller.itemTargets[1]
281
+ controller.selectItem({ currentTarget: disabledItem })
282
+ await nextFrame()
283
+
284
+ expect(selectFired).toBe(false)
285
+ })
286
+ })
287
+
288
+ describe("checkbox items", () => {
289
+ const checkboxHTML = `
290
+ <div data-controller="shadcn--menubar"
291
+ data-shadcn--menubar-open-index-value="-1">
292
+ <div data-shadcn--menubar-target="menu">
293
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">View</button>
294
+ <div data-shadcn--menubar-target="content" hidden>
295
+ <button data-shadcn--menubar-target="item"
296
+ role="menuitemcheckbox"
297
+ aria-checked="false"
298
+ data-state="unchecked"
299
+ data-action="click->shadcn--menubar#toggleCheckbox">
300
+ <span><svg style="display: none;">✓</svg></span>
301
+ Show Toolbar
302
+ </button>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ `
307
+
308
+ beforeEach(async () => {
309
+ const setup = await setupController(MenubarController, checkboxHTML, 'shadcn--menubar')
310
+ application = setup.application
311
+ element = setup.element
312
+ controller = setup.controller
313
+ })
314
+
315
+ test("toggles checkbox state", async () => {
316
+ controller.openMenu(0)
317
+ await nextFrame()
318
+
319
+ const item = controller.itemTargets[0]
320
+ controller.toggleCheckbox({ currentTarget: item })
321
+ await nextFrame()
322
+
323
+ expect(item.dataset.state).toBe("checked")
324
+ expect(item.getAttribute("aria-checked")).toBe("true")
325
+ })
326
+
327
+ test("toggles checkbox back to unchecked", async () => {
328
+ controller.openMenu(0)
329
+ await nextFrame()
330
+
331
+ const item = controller.itemTargets[0]
332
+ controller.toggleCheckbox({ currentTarget: item })
333
+ await nextFrame()
334
+ controller.toggleCheckbox({ currentTarget: item })
335
+ await nextFrame()
336
+
337
+ expect(item.dataset.state).toBe("unchecked")
338
+ expect(item.getAttribute("aria-checked")).toBe("false")
339
+ })
340
+
341
+ test("dispatches check event", async () => {
342
+ controller.openMenu(0)
343
+ await nextFrame()
344
+
345
+ let checkDetail = null
346
+ element.addEventListener("shadcn--menubar:check", (e) => {
347
+ checkDetail = e.detail
348
+ })
349
+
350
+ const item = controller.itemTargets[0]
351
+ controller.toggleCheckbox({ currentTarget: item })
352
+ await nextFrame()
353
+
354
+ expect(checkDetail.item).toBe(item)
355
+ expect(checkDetail.checked).toBe(true)
356
+ })
357
+ })
358
+
359
+ describe("radio items", () => {
360
+ const radioHTML = `
361
+ <div data-controller="shadcn--menubar"
362
+ data-shadcn--menubar-open-index-value="-1">
363
+ <div data-shadcn--menubar-target="menu">
364
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">View</button>
365
+ <div data-shadcn--menubar-target="content" hidden>
366
+ <div role="group">
367
+ <button data-shadcn--menubar-target="item"
368
+ role="menuitemradio"
369
+ aria-checked="true"
370
+ data-state="checked"
371
+ data-value="small"
372
+ data-action="click->shadcn--menubar#selectRadio">
373
+ <span><svg style="display: block;">●</svg></span>
374
+ Small
375
+ </button>
376
+ <button data-shadcn--menubar-target="item"
377
+ role="menuitemradio"
378
+ aria-checked="false"
379
+ data-state="unchecked"
380
+ data-value="medium"
381
+ data-action="click->shadcn--menubar#selectRadio">
382
+ <span><svg style="display: none;">●</svg></span>
383
+ Medium
384
+ </button>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ `
390
+
391
+ beforeEach(async () => {
392
+ const setup = await setupController(MenubarController, radioHTML, 'shadcn--menubar')
393
+ application = setup.application
394
+ element = setup.element
395
+ controller = setup.controller
396
+ })
397
+
398
+ test("selects radio item", async () => {
399
+ controller.openMenu(0)
400
+ await nextFrame()
401
+
402
+ const mediumItem = controller.itemTargets[1]
403
+ controller.selectRadio({ currentTarget: mediumItem })
404
+ await nextFrame()
405
+
406
+ expect(mediumItem.dataset.state).toBe("checked")
407
+ expect(mediumItem.getAttribute("aria-checked")).toBe("true")
408
+ })
409
+
410
+ test("unchecks other radio items in group", async () => {
411
+ controller.openMenu(0)
412
+ await nextFrame()
413
+
414
+ const smallItem = controller.itemTargets[0]
415
+ const mediumItem = controller.itemTargets[1]
416
+
417
+ controller.selectRadio({ currentTarget: mediumItem })
418
+ await nextFrame()
419
+
420
+ expect(smallItem.dataset.state).toBe("unchecked")
421
+ expect(smallItem.getAttribute("aria-checked")).toBe("false")
422
+ })
423
+
424
+ test("dispatches radioChange event", async () => {
425
+ controller.openMenu(0)
426
+ await nextFrame()
427
+
428
+ let radioDetail = null
429
+ element.addEventListener("shadcn--menubar:radioChange", (e) => {
430
+ radioDetail = e.detail
431
+ })
432
+
433
+ const mediumItem = controller.itemTargets[1]
434
+ controller.selectRadio({ currentTarget: mediumItem })
435
+ await nextFrame()
436
+
437
+ expect(radioDetail.item).toBe(mediumItem)
438
+ expect(radioDetail.value).toBe("medium")
439
+ })
440
+ })
441
+
442
+ describe("keyboard navigation", () => {
443
+ const keyboardHTML = `
444
+ <div data-controller="shadcn--menubar"
445
+ data-shadcn--menubar-open-index-value="-1">
446
+ <div data-shadcn--menubar-target="menu">
447
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
448
+ <div data-shadcn--menubar-target="content" hidden>
449
+ <button data-shadcn--menubar-target="item">New</button>
450
+ <button data-shadcn--menubar-target="item" data-disabled>Disabled</button>
451
+ <button data-shadcn--menubar-target="item">Save</button>
452
+ </div>
453
+ </div>
454
+ <div data-shadcn--menubar-target="menu">
455
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">Edit</button>
456
+ <div data-shadcn--menubar-target="content" hidden>
457
+ <button data-shadcn--menubar-target="item">Undo</button>
458
+ </div>
459
+ </div>
460
+ </div>
461
+ `
462
+
463
+ beforeEach(async () => {
464
+ const setup = await setupController(MenubarController, keyboardHTML, 'shadcn--menubar')
465
+ application = setup.application
466
+ element = setup.element
467
+ controller = setup.controller
468
+
469
+ // Open the first menu
470
+ controller.openMenu(0)
471
+ await nextFrame()
472
+ })
473
+
474
+ test("ArrowDown moves to next item", async () => {
475
+ // Initially focused on first item (index 0)
476
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
477
+ await nextFrame()
478
+
479
+ expect(controller.focusedIndex).toBe(1)
480
+ })
481
+
482
+ test("ArrowDown wraps to first item", async () => {
483
+ // Move to last item
484
+ controller.focusedIndex = 1 // Last enabled item in current menu
485
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
486
+ await nextFrame()
487
+
488
+ expect(controller.focusedIndex).toBe(0)
489
+ })
490
+
491
+ test("ArrowUp moves to previous item", async () => {
492
+ controller.focusedIndex = 1
493
+ controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
494
+ await nextFrame()
495
+
496
+ expect(controller.focusedIndex).toBe(0)
497
+ })
498
+
499
+ test("ArrowRight opens next menu", async () => {
500
+ controller.handleKeydown({ key: "ArrowRight", preventDefault: jest.fn() })
501
+ await nextFrame()
502
+
503
+ expect(controller.openIndexValue).toBe(1)
504
+ })
505
+
506
+ test("ArrowLeft opens previous menu", async () => {
507
+ controller.openMenu(1)
508
+ await nextFrame()
509
+
510
+ controller.handleKeydown({ key: "ArrowLeft", preventDefault: jest.fn() })
511
+ await nextFrame()
512
+
513
+ expect(controller.openIndexValue).toBe(0)
514
+ })
515
+
516
+ test("Home moves to first item", async () => {
517
+ controller.focusedIndex = 1
518
+ controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
519
+ await nextFrame()
520
+
521
+ expect(controller.focusedIndex).toBe(0)
522
+ })
523
+
524
+ test("End moves to last item", async () => {
525
+ controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
526
+ await nextFrame()
527
+
528
+ expect(controller.focusedIndex).toBe(1) // Last enabled item
529
+ })
530
+
531
+ test("Escape closes menu", async () => {
532
+ controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
533
+ await nextFrame()
534
+
535
+ expect(controller.isMenuOpen).toBe(false)
536
+ })
537
+
538
+ test("Enter/Space triggers click on focused item", async () => {
539
+ const items = controller.currentMenuItems
540
+ const clickSpy = jest.spyOn(items[0], 'click')
541
+
542
+ controller.focusedIndex = 0
543
+ controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
544
+ await nextFrame()
545
+
546
+ expect(clickSpy).toHaveBeenCalled()
547
+ })
548
+
549
+ test("prevents default on navigation keys", () => {
550
+ const preventDefault = jest.fn()
551
+
552
+ controller.handleKeydown({ key: "ArrowDown", preventDefault })
553
+ expect(preventDefault).toHaveBeenCalled()
554
+
555
+ preventDefault.mockClear()
556
+ controller.handleKeydown({ key: "ArrowUp", preventDefault })
557
+ expect(preventDefault).toHaveBeenCalled()
558
+
559
+ preventDefault.mockClear()
560
+ controller.handleKeydown({ key: "ArrowRight", preventDefault })
561
+ expect(preventDefault).toHaveBeenCalled()
562
+
563
+ preventDefault.mockClear()
564
+ controller.handleKeydown({ key: "ArrowLeft", preventDefault })
565
+ expect(preventDefault).toHaveBeenCalled()
566
+ })
567
+ })
568
+
569
+ describe("submenu functionality", () => {
570
+ const submenuHTML = `
571
+ <div data-controller="shadcn--menubar"
572
+ data-shadcn--menubar-open-index-value="-1">
573
+ <div data-shadcn--menubar-target="menu">
574
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
575
+ <div data-shadcn--menubar-target="content" hidden>
576
+ <div data-shadcn--menubar-target="sub">
577
+ <button data-shadcn--menubar-target="subTrigger"
578
+ data-action="mouseenter->shadcn--menubar#openSub mouseleave->shadcn--menubar#startCloseSubTimer"
579
+ aria-expanded="false">Share</button>
580
+ <div data-shadcn--menubar-target="subContent" hidden>
581
+ <button data-shadcn--menubar-target="item">Email</button>
582
+ <button data-shadcn--menubar-target="item">Twitter</button>
583
+ </div>
584
+ </div>
585
+ </div>
586
+ </div>
587
+ </div>
588
+ `
589
+
590
+ beforeEach(async () => {
591
+ const setup = await setupController(MenubarController, submenuHTML, 'shadcn--menubar')
592
+ application = setup.application
593
+ element = setup.element
594
+ controller = setup.controller
595
+ })
596
+
597
+ test("opens submenu", async () => {
598
+ controller.openMenu(0)
599
+ await nextFrame()
600
+
601
+ const subTrigger = controller.subTriggerTargets[0]
602
+ controller.openSub({ currentTarget: subTrigger })
603
+ await nextFrame()
604
+
605
+ expect(subTrigger.getAttribute("aria-expanded")).toBe("true")
606
+ expect(controller.subContentTargets[0].hidden).toBe(false)
607
+ })
608
+
609
+ test("closeAllSubs closes all submenus", async () => {
610
+ controller.openMenu(0)
611
+ await nextFrame()
612
+
613
+ const subTrigger = controller.subTriggerTargets[0]
614
+ controller.openSub({ currentTarget: subTrigger })
615
+ await nextFrame()
616
+
617
+ controller.closeAllSubs()
618
+ await nextFrame()
619
+
620
+ expect(subTrigger.getAttribute("aria-expanded")).toBe("false")
621
+ expect(controller.subContentTargets[0].hidden).toBe(true)
622
+ })
623
+ })
624
+
625
+ describe("click outside handling", () => {
626
+ const clickOutsideHTML = `
627
+ <div data-controller="shadcn--menubar"
628
+ data-shadcn--menubar-open-index-value="-1">
629
+ <div data-shadcn--menubar-target="menu">
630
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
631
+ <div data-shadcn--menubar-target="content" hidden>
632
+ <button data-shadcn--menubar-target="item">New</button>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ `
637
+
638
+ beforeEach(async () => {
639
+ const setup = await setupController(MenubarController, clickOutsideHTML, 'shadcn--menubar')
640
+ application = setup.application
641
+ element = setup.element
642
+ controller = setup.controller
643
+ })
644
+
645
+ test("closes on click outside", async () => {
646
+ controller.openMenu(0)
647
+ await nextFrame()
648
+
649
+ const outsideElement = document.createElement("div")
650
+ document.body.appendChild(outsideElement)
651
+
652
+ controller.handleClickOutside({ target: outsideElement })
653
+ await nextFrame()
654
+
655
+ expect(controller.isMenuOpen).toBe(false)
656
+
657
+ document.body.removeChild(outsideElement)
658
+ })
659
+
660
+ test("does not close on click inside", async () => {
661
+ controller.openMenu(0)
662
+ await nextFrame()
663
+
664
+ controller.handleClickOutside({ target: element })
665
+ await nextFrame()
666
+
667
+ expect(controller.isMenuOpen).toBe(true)
668
+ })
669
+ })
670
+
671
+ describe("currentMenuItems getter", () => {
672
+ const itemsHTML = `
673
+ <div data-controller="shadcn--menubar"
674
+ data-shadcn--menubar-open-index-value="-1">
675
+ <div data-shadcn--menubar-target="menu">
676
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
677
+ <div data-shadcn--menubar-target="content" hidden>
678
+ <button data-shadcn--menubar-target="item">New</button>
679
+ <button data-shadcn--menubar-target="item" data-disabled>Disabled</button>
680
+ <button data-shadcn--menubar-target="item">Save</button>
681
+ </div>
682
+ </div>
683
+ </div>
684
+ `
685
+
686
+ beforeEach(async () => {
687
+ const setup = await setupController(MenubarController, itemsHTML, 'shadcn--menubar')
688
+ application = setup.application
689
+ element = setup.element
690
+ controller = setup.controller
691
+ })
692
+
693
+ test("returns empty array when no menu open", () => {
694
+ expect(controller.currentMenuItems).toEqual([])
695
+ })
696
+
697
+ test("returns enabled items when menu is open", async () => {
698
+ controller.openMenu(0)
699
+ await nextFrame()
700
+
701
+ const items = controller.currentMenuItems
702
+ expect(items.length).toBe(2) // Only enabled items
703
+ })
704
+ })
705
+
706
+ describe("disconnect cleanup", () => {
707
+ const disconnectHTML = `
708
+ <div data-controller="shadcn--menubar"
709
+ data-shadcn--menubar-open-index-value="-1">
710
+ <div data-shadcn--menubar-target="menu">
711
+ <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
712
+ <div data-shadcn--menubar-target="content" hidden>
713
+ <button data-shadcn--menubar-target="item">New</button>
714
+ </div>
715
+ </div>
716
+ </div>
717
+ `
718
+
719
+ beforeEach(async () => {
720
+ const setup = await setupController(MenubarController, disconnectHTML, 'shadcn--menubar')
721
+ application = setup.application
722
+ element = setup.element
723
+ controller = setup.controller
724
+ })
725
+
726
+ test("closes all on disconnect", async () => {
727
+ controller.openMenu(0)
728
+ await nextFrame()
729
+
730
+ controller.disconnect()
731
+ await nextFrame()
732
+
733
+ expect(controller.openIndexValue).toBe(-1)
734
+ })
735
+ })
736
+ })