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,627 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import ContextMenuController from "../../app/assets/javascripts/shadcn/controllers/context_menu_controller.js"
3
+ import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
4
+
5
+ describe("ContextMenuController", () => {
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--context-menu"
17
+ data-shadcn--context-menu-open-value="false">
18
+ <div data-shadcn--context-menu-target="trigger"
19
+ data-action="contextmenu->shadcn--context-menu#show">
20
+ Right click here
21
+ </div>
22
+ <div data-shadcn--context-menu-target="content" hidden>
23
+ <button data-shadcn--context-menu-target="item"
24
+ data-action="click->shadcn--context-menu#selectItem">Item 1</button>
25
+ <button data-shadcn--context-menu-target="item"
26
+ data-action="click->shadcn--context-menu#selectItem">Item 2</button>
27
+ </div>
28
+ </div>
29
+ `
30
+
31
+ beforeEach(async () => {
32
+ const setup = await setupController(ContextMenuController, basicHTML, 'shadcn--context-menu')
33
+ application = setup.application
34
+ element = setup.element
35
+ controller = setup.controller
36
+ })
37
+
38
+ test("initializes with closed state", () => {
39
+ expect(controller.openValue).toBe(false)
40
+ })
41
+
42
+ test("initializes focusedIndex to -1", () => {
43
+ expect(controller.focusedIndex).toBe(-1)
44
+ })
45
+
46
+ test("content is hidden initially", () => {
47
+ expect(controller.contentTarget.hidden).toBe(true)
48
+ })
49
+
50
+ test("has trigger target", () => {
51
+ expect(controller.hasTriggerTarget).toBe(true)
52
+ })
53
+
54
+ test("has content target", () => {
55
+ expect(controller.hasContentTarget).toBe(true)
56
+ })
57
+
58
+ test("has item targets", () => {
59
+ expect(controller.itemTargets.length).toBe(2)
60
+ })
61
+ })
62
+
63
+ describe("show functionality", () => {
64
+ const showHTML = `
65
+ <div data-controller="shadcn--context-menu"
66
+ data-shadcn--context-menu-open-value="false">
67
+ <div data-shadcn--context-menu-target="trigger"
68
+ data-action="contextmenu->shadcn--context-menu#show">
69
+ Right click here
70
+ </div>
71
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
72
+ <button data-shadcn--context-menu-target="item"
73
+ data-action="click->shadcn--context-menu#selectItem">Item 1</button>
74
+ <button data-shadcn--context-menu-target="item"
75
+ data-action="click->shadcn--context-menu#selectItem">Item 2</button>
76
+ </div>
77
+ </div>
78
+ `
79
+
80
+ beforeEach(async () => {
81
+ const setup = await setupController(ContextMenuController, showHTML, 'shadcn--context-menu')
82
+ application = setup.application
83
+ element = setup.element
84
+ controller = setup.controller
85
+ })
86
+
87
+ test("sets openValue to true", async () => {
88
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
89
+ controller.show(event)
90
+ await nextFrame()
91
+
92
+ expect(controller.openValue).toBe(true)
93
+ })
94
+
95
+ test("prevents default on event", async () => {
96
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
97
+ controller.show(event)
98
+
99
+ expect(event.preventDefault).toHaveBeenCalled()
100
+ })
101
+
102
+ test("stores mouse position", async () => {
103
+ const event = { preventDefault: jest.fn(), clientX: 150, clientY: 200 }
104
+ controller.show(event)
105
+
106
+ expect(controller.mouseX).toBe(150)
107
+ expect(controller.mouseY).toBe(200)
108
+ })
109
+
110
+ test("shows content", async () => {
111
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
112
+ controller.show(event)
113
+ await nextFrame()
114
+
115
+ expect(controller.contentTarget.hidden).toBe(false)
116
+ })
117
+
118
+ test("sets content data-state to open", async () => {
119
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
120
+ controller.show(event)
121
+ await nextFrame()
122
+
123
+ expect(controller.contentTarget.dataset.state).toBe("open")
124
+ })
125
+
126
+ test("dispatches opened event", async () => {
127
+ let eventFired = false
128
+ element.addEventListener("shadcn--context-menu:opened", () => {
129
+ eventFired = true
130
+ })
131
+
132
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
133
+ controller.show(event)
134
+ await nextFrame()
135
+
136
+ expect(eventFired).toBe(true)
137
+ })
138
+
139
+ test("focuses first item on show", async () => {
140
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
141
+ controller.show(event)
142
+ await nextFrame()
143
+
144
+ expect(controller.focusedIndex).toBe(0)
145
+ })
146
+ })
147
+
148
+ describe("hide functionality", () => {
149
+ const hideHTML = `
150
+ <div data-controller="shadcn--context-menu"
151
+ data-shadcn--context-menu-open-value="false">
152
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
153
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
154
+ <button data-shadcn--context-menu-target="item">Item 1</button>
155
+ </div>
156
+ </div>
157
+ `
158
+
159
+ beforeEach(async () => {
160
+ const setup = await setupController(ContextMenuController, hideHTML, 'shadcn--context-menu')
161
+ application = setup.application
162
+ element = setup.element
163
+ controller = setup.controller
164
+ })
165
+
166
+ test("sets openValue to false", async () => {
167
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
168
+ controller.show(event)
169
+ await nextFrame()
170
+
171
+ controller.hide()
172
+ await nextFrame()
173
+
174
+ expect(controller.openValue).toBe(false)
175
+ })
176
+
177
+ test("sets content data-state to closed", async () => {
178
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
179
+ controller.show(event)
180
+ await nextFrame()
181
+
182
+ controller.hide()
183
+ await nextFrame()
184
+
185
+ expect(controller.contentTarget.dataset.state).toBe("closed")
186
+ })
187
+
188
+ test("dispatches closed event", async () => {
189
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
190
+ controller.show(event)
191
+ await nextFrame()
192
+
193
+ let eventFired = false
194
+ element.addEventListener("shadcn--context-menu:closed", () => {
195
+ eventFired = true
196
+ })
197
+
198
+ controller.hide()
199
+ await nextFrame()
200
+
201
+ expect(eventFired).toBe(true)
202
+ })
203
+
204
+ test("resets focusedIndex to -1", async () => {
205
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
206
+ controller.show(event)
207
+ await nextFrame()
208
+
209
+ controller.hide()
210
+ await nextFrame()
211
+
212
+ expect(controller.focusedIndex).toBe(-1)
213
+ })
214
+
215
+ test("does nothing if already closed", async () => {
216
+ let eventFired = false
217
+ element.addEventListener("shadcn--context-menu:closed", () => {
218
+ eventFired = true
219
+ })
220
+
221
+ controller.hide()
222
+ await nextFrame()
223
+
224
+ expect(eventFired).toBe(false)
225
+ })
226
+
227
+ test("close() is an alias for hide()", async () => {
228
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
229
+ controller.show(event)
230
+ await nextFrame()
231
+
232
+ controller.close()
233
+ await nextFrame()
234
+
235
+ expect(controller.openValue).toBe(false)
236
+ })
237
+ })
238
+
239
+ describe("item selection", () => {
240
+ const selectHTML = `
241
+ <div data-controller="shadcn--context-menu"
242
+ data-shadcn--context-menu-open-value="false">
243
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
244
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
245
+ <button data-shadcn--context-menu-target="item"
246
+ data-action="click->shadcn--context-menu#selectItem">Item 1</button>
247
+ <button data-shadcn--context-menu-target="item"
248
+ data-action="click->shadcn--context-menu#selectItem"
249
+ data-disabled>Disabled Item</button>
250
+ <button data-shadcn--context-menu-target="item"
251
+ data-action="click->shadcn--context-menu#selectItem">Item 3</button>
252
+ </div>
253
+ </div>
254
+ `
255
+
256
+ beforeEach(async () => {
257
+ const setup = await setupController(ContextMenuController, selectHTML, 'shadcn--context-menu')
258
+ application = setup.application
259
+ element = setup.element
260
+ controller = setup.controller
261
+ })
262
+
263
+ test("dispatches select event with item", async () => {
264
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
265
+ controller.show(event)
266
+ await nextFrame()
267
+
268
+ let selectedItem = null
269
+ element.addEventListener("shadcn--context-menu:select", (e) => {
270
+ selectedItem = e.detail.item
271
+ })
272
+
273
+ const item = controller.itemTargets[0]
274
+ controller.selectItem({ currentTarget: item })
275
+ await nextFrame()
276
+
277
+ expect(selectedItem).toBe(item)
278
+ })
279
+
280
+ test("closes menu after selection", async () => {
281
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
282
+ controller.show(event)
283
+ await nextFrame()
284
+
285
+ const item = controller.itemTargets[0]
286
+ controller.selectItem({ currentTarget: item })
287
+ await nextFrame()
288
+
289
+ expect(controller.openValue).toBe(false)
290
+ })
291
+
292
+ test("does not select disabled items", async () => {
293
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
294
+ controller.show(event)
295
+ await nextFrame()
296
+
297
+ let selectFired = false
298
+ element.addEventListener("shadcn--context-menu:select", () => {
299
+ selectFired = true
300
+ })
301
+
302
+ const disabledItem = controller.itemTargets[1]
303
+ controller.selectItem({ currentTarget: disabledItem })
304
+ await nextFrame()
305
+
306
+ expect(selectFired).toBe(false)
307
+ })
308
+
309
+ test("enabled items getter filters disabled items", () => {
310
+ const enabledItems = controller.enabledItems
311
+ expect(enabledItems.length).toBe(2)
312
+ })
313
+ })
314
+
315
+ describe("keyboard navigation", () => {
316
+ const keyboardHTML = `
317
+ <div data-controller="shadcn--context-menu"
318
+ data-shadcn--context-menu-open-value="false">
319
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
320
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
321
+ <button data-shadcn--context-menu-target="item">Item 1</button>
322
+ <button data-shadcn--context-menu-target="item" data-disabled>Disabled</button>
323
+ <button data-shadcn--context-menu-target="item">Item 3</button>
324
+ <button data-shadcn--context-menu-target="item">Item 4</button>
325
+ </div>
326
+ </div>
327
+ `
328
+
329
+ beforeEach(async () => {
330
+ const setup = await setupController(ContextMenuController, keyboardHTML, 'shadcn--context-menu')
331
+ application = setup.application
332
+ element = setup.element
333
+ controller = setup.controller
334
+
335
+ // Open the menu first
336
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
337
+ controller.show(event)
338
+ await nextFrame()
339
+ })
340
+
341
+ test("ArrowDown moves to next item", async () => {
342
+ // Already at first item (index 0) from show()
343
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
344
+ await nextFrame()
345
+
346
+ expect(controller.focusedIndex).toBe(1)
347
+ })
348
+
349
+ test("ArrowDown wraps to first item", async () => {
350
+ // Move to last enabled item
351
+ controller.focusedIndex = 2 // Last enabled item (index 2 in enabledItems)
352
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
353
+ await nextFrame()
354
+
355
+ expect(controller.focusedIndex).toBe(0)
356
+ })
357
+
358
+ test("ArrowUp moves to previous item", async () => {
359
+ controller.focusedIndex = 1
360
+ controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
361
+ await nextFrame()
362
+
363
+ expect(controller.focusedIndex).toBe(0)
364
+ })
365
+
366
+ test("ArrowUp wraps to last item from first", async () => {
367
+ controller.focusedIndex = 0
368
+ controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
369
+ await nextFrame()
370
+
371
+ expect(controller.focusedIndex).toBe(2) // Last enabled item
372
+ })
373
+
374
+ test("Home moves to first item", async () => {
375
+ controller.focusedIndex = 2
376
+ controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
377
+ await nextFrame()
378
+
379
+ expect(controller.focusedIndex).toBe(0)
380
+ })
381
+
382
+ test("End moves to last item", async () => {
383
+ controller.focusedIndex = 0
384
+ controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
385
+ await nextFrame()
386
+
387
+ expect(controller.focusedIndex).toBe(2) // Last enabled item
388
+ })
389
+
390
+ test("Escape closes the menu", async () => {
391
+ controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
392
+ await nextFrame()
393
+
394
+ expect(controller.openValue).toBe(false)
395
+ })
396
+
397
+ test("Enter triggers click on focused item", async () => {
398
+ const enabledItems = controller.enabledItems
399
+ const clickSpy = jest.spyOn(enabledItems[0], 'click')
400
+
401
+ controller.focusedIndex = 0
402
+ controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
403
+ await nextFrame()
404
+
405
+ expect(clickSpy).toHaveBeenCalled()
406
+ })
407
+
408
+ test("Space triggers click on focused item", async () => {
409
+ const enabledItems = controller.enabledItems
410
+ const clickSpy = jest.spyOn(enabledItems[0], 'click')
411
+
412
+ controller.focusedIndex = 0
413
+ controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
414
+ await nextFrame()
415
+
416
+ expect(clickSpy).toHaveBeenCalled()
417
+ })
418
+
419
+ test("prevents default on navigation keys", () => {
420
+ const preventDefault = jest.fn()
421
+
422
+ controller.handleKeydown({ key: "ArrowDown", preventDefault })
423
+ expect(preventDefault).toHaveBeenCalled()
424
+
425
+ preventDefault.mockClear()
426
+ controller.handleKeydown({ key: "ArrowUp", preventDefault })
427
+ expect(preventDefault).toHaveBeenCalled()
428
+
429
+ preventDefault.mockClear()
430
+ controller.handleKeydown({ key: "Home", preventDefault })
431
+ expect(preventDefault).toHaveBeenCalled()
432
+
433
+ preventDefault.mockClear()
434
+ controller.handleKeydown({ key: "End", preventDefault })
435
+ expect(preventDefault).toHaveBeenCalled()
436
+ })
437
+ })
438
+
439
+ describe("click outside handling", () => {
440
+ const clickOutsideHTML = `
441
+ <div data-controller="shadcn--context-menu"
442
+ data-shadcn--context-menu-open-value="false">
443
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
444
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
445
+ <button data-shadcn--context-menu-target="item">Item 1</button>
446
+ </div>
447
+ </div>
448
+ `
449
+
450
+ beforeEach(async () => {
451
+ const setup = await setupController(ContextMenuController, clickOutsideHTML, 'shadcn--context-menu')
452
+ application = setup.application
453
+ element = setup.element
454
+ controller = setup.controller
455
+ })
456
+
457
+ test("closes on click outside", async () => {
458
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
459
+ controller.show(event)
460
+ await nextFrame()
461
+
462
+ // Simulate click outside
463
+ const outsideElement = document.createElement("div")
464
+ document.body.appendChild(outsideElement)
465
+ controller.handleClickOutside({ target: outsideElement })
466
+ await nextFrame()
467
+
468
+ expect(controller.openValue).toBe(false)
469
+
470
+ document.body.removeChild(outsideElement)
471
+ })
472
+
473
+ test("does not close on click inside content", async () => {
474
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
475
+ controller.show(event)
476
+ await nextFrame()
477
+
478
+ // Simulate click inside content
479
+ controller.handleClickOutside({ target: controller.contentTarget })
480
+ await nextFrame()
481
+
482
+ expect(controller.openValue).toBe(true)
483
+ })
484
+ })
485
+
486
+ describe("positioning", () => {
487
+ const positionHTML = `
488
+ <div data-controller="shadcn--context-menu"
489
+ data-shadcn--context-menu-open-value="false">
490
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
491
+ <div data-shadcn--context-menu-target="content" hidden
492
+ style="position: fixed; width: 200px; height: 150px;">
493
+ <button data-shadcn--context-menu-target="item">Item 1</button>
494
+ </div>
495
+ </div>
496
+ `
497
+
498
+ beforeEach(async () => {
499
+ const setup = await setupController(ContextMenuController, positionHTML, 'shadcn--context-menu')
500
+ application = setup.application
501
+ element = setup.element
502
+ controller = setup.controller
503
+ })
504
+
505
+ test("positions content at mouse location", async () => {
506
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 150 }
507
+ controller.show(event)
508
+ await nextFrame()
509
+
510
+ const content = controller.contentTarget
511
+ expect(content.style.left).toBe("100px")
512
+ expect(content.style.top).toBe("150px")
513
+ })
514
+
515
+ test("positions content with minimum offset from edges", async () => {
516
+ const event = { preventDefault: jest.fn(), clientX: 5, clientY: 5 }
517
+ controller.show(event)
518
+ await nextFrame()
519
+
520
+ const content = controller.contentTarget
521
+ // Should be at least 8px from edge
522
+ expect(parseInt(content.style.left)).toBeGreaterThanOrEqual(8)
523
+ expect(parseInt(content.style.top)).toBeGreaterThanOrEqual(8)
524
+ })
525
+ })
526
+
527
+ describe("disconnect cleanup", () => {
528
+ const disconnectHTML = `
529
+ <div data-controller="shadcn--context-menu"
530
+ data-shadcn--context-menu-open-value="false">
531
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
532
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
533
+ <button data-shadcn--context-menu-target="item">Item 1</button>
534
+ </div>
535
+ </div>
536
+ `
537
+
538
+ beforeEach(async () => {
539
+ const setup = await setupController(ContextMenuController, disconnectHTML, 'shadcn--context-menu')
540
+ application = setup.application
541
+ element = setup.element
542
+ controller = setup.controller
543
+ })
544
+
545
+ test("hides menu on disconnect", async () => {
546
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
547
+ controller.show(event)
548
+ await nextFrame()
549
+
550
+ controller.disconnect()
551
+ await nextFrame()
552
+
553
+ expect(controller.openValue).toBe(false)
554
+ })
555
+ })
556
+
557
+ describe("without items", () => {
558
+ const noItemsHTML = `
559
+ <div data-controller="shadcn--context-menu"
560
+ data-shadcn--context-menu-open-value="false">
561
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
562
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
563
+ <p>No items here</p>
564
+ </div>
565
+ </div>
566
+ `
567
+
568
+ beforeEach(async () => {
569
+ const setup = await setupController(ContextMenuController, noItemsHTML, 'shadcn--context-menu')
570
+ application = setup.application
571
+ element = setup.element
572
+ controller = setup.controller
573
+ })
574
+
575
+ test("handles empty items gracefully", async () => {
576
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
577
+
578
+ expect(() => {
579
+ controller.show(event)
580
+ }).not.toThrow()
581
+
582
+ expect(controller.openValue).toBe(true)
583
+ })
584
+
585
+ test("navigation does nothing with no items", async () => {
586
+ const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
587
+ controller.show(event)
588
+ await nextFrame()
589
+
590
+ expect(() => {
591
+ controller.focusNextItem()
592
+ controller.focusPreviousItem()
593
+ controller.focusFirstItem()
594
+ controller.focusLastItem()
595
+ }).not.toThrow()
596
+ })
597
+ })
598
+
599
+ describe("show without event", () => {
600
+ const noEventHTML = `
601
+ <div data-controller="shadcn--context-menu"
602
+ data-shadcn--context-menu-open-value="false">
603
+ <div data-shadcn--context-menu-target="trigger">Trigger</div>
604
+ <div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
605
+ <button data-shadcn--context-menu-target="item">Item 1</button>
606
+ </div>
607
+ </div>
608
+ `
609
+
610
+ beforeEach(async () => {
611
+ const setup = await setupController(ContextMenuController, noEventHTML, 'shadcn--context-menu')
612
+ application = setup.application
613
+ element = setup.element
614
+ controller = setup.controller
615
+ })
616
+
617
+ test("handles show called without event", async () => {
618
+ expect(() => {
619
+ controller.show()
620
+ }).not.toThrow()
621
+
622
+ expect(controller.openValue).toBe(true)
623
+ expect(controller.mouseX).toBe(0)
624
+ expect(controller.mouseY).toBe(0)
625
+ })
626
+ })
627
+ })