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,674 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import SelectController from "../../app/assets/javascripts/shadcn/controllers/select_controller.js"
3
+ import { setupController, cleanupController, click, nextFrame, keydown } from '../helpers/stimulus-test-helper.js'
4
+
5
+ describe("SelectController", () => {
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--select"
17
+ data-shadcn--select-value-value="">
18
+ <button data-shadcn--select-target="trigger"
19
+ role="combobox"
20
+ aria-expanded="false"
21
+ data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
22
+ <span data-shadcn--select-target="display">Select...</span>
23
+ </button>
24
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
25
+ <div data-shadcn--select-target="content"
26
+ role="listbox"
27
+ hidden
28
+ data-state="closed">
29
+ <div data-shadcn--select-target="item"
30
+ data-value="apple"
31
+ role="option"
32
+ tabindex="-1"
33
+ data-action="click->shadcn--select#select">Apple</div>
34
+ <div data-shadcn--select-target="item"
35
+ data-value="banana"
36
+ role="option"
37
+ tabindex="-1"
38
+ data-action="click->shadcn--select#select">Banana</div>
39
+ <div data-shadcn--select-target="item"
40
+ data-value="cherry"
41
+ role="option"
42
+ tabindex="-1"
43
+ data-action="click->shadcn--select#select">Cherry</div>
44
+ </div>
45
+ </div>
46
+ `
47
+
48
+ beforeEach(async () => {
49
+ const setup = await setupController(SelectController, basicHTML, 'shadcn--select')
50
+ application = setup.application
51
+ element = setup.element
52
+ controller = setup.controller
53
+ })
54
+
55
+ test("initializes with closed state", () => {
56
+ expect(controller.isOpen).toBe(false)
57
+ })
58
+
59
+ test("initializes with empty value", () => {
60
+ expect(controller.valueValue).toBe("")
61
+ })
62
+
63
+ test("content is hidden by default", () => {
64
+ expect(controller.contentTarget.hidden).toBe(true)
65
+ expect(controller.contentTarget.dataset.state).toBe("closed")
66
+ })
67
+
68
+ test("trigger has correct aria-expanded", () => {
69
+ expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
70
+ })
71
+ })
72
+
73
+ describe("opening and closing", () => {
74
+ const basicHTML = `
75
+ <div data-controller="shadcn--select">
76
+ <button data-shadcn--select-target="trigger"
77
+ role="combobox"
78
+ aria-expanded="false"
79
+ data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
80
+ <span data-shadcn--select-target="display">Select...</span>
81
+ </button>
82
+ <div data-shadcn--select-target="content"
83
+ role="listbox"
84
+ hidden
85
+ data-state="closed">
86
+ <div data-shadcn--select-target="item"
87
+ data-value="apple"
88
+ tabindex="-1"
89
+ data-action="click->shadcn--select#select">Apple</div>
90
+ </div>
91
+ </div>
92
+ `
93
+
94
+ beforeEach(async () => {
95
+ const setup = await setupController(SelectController, basicHTML, 'shadcn--select')
96
+ application = setup.application
97
+ element = setup.element
98
+ controller = setup.controller
99
+ })
100
+
101
+ test("opens on toggle", () => {
102
+ controller.toggle()
103
+
104
+ expect(controller.isOpen).toBe(true)
105
+ expect(controller.contentTarget.hidden).toBe(false)
106
+ expect(controller.contentTarget.dataset.state).toBe("open")
107
+ expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("true")
108
+ })
109
+
110
+ test("closes when already open", () => {
111
+ controller.open()
112
+ controller.toggle()
113
+
114
+ expect(controller.isOpen).toBe(false)
115
+ expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
116
+ })
117
+
118
+ test("dispatches opened event on open", () => {
119
+ let eventFired = false
120
+ element.addEventListener("shadcn--select:opened", () => {
121
+ eventFired = true
122
+ })
123
+
124
+ controller.open()
125
+ expect(eventFired).toBe(true)
126
+ })
127
+
128
+ test("dispatches closed event on close", () => {
129
+ let eventFired = false
130
+ element.addEventListener("shadcn--select:closed", () => {
131
+ eventFired = true
132
+ })
133
+
134
+ controller.open()
135
+ controller.close()
136
+ expect(eventFired).toBe(true)
137
+ })
138
+
139
+ test("opens with trigger click", async () => {
140
+ click(controller.triggerTarget)
141
+ await nextFrame()
142
+
143
+ expect(controller.isOpen).toBe(true)
144
+ })
145
+
146
+ test("does not re-open if already open", () => {
147
+ controller.open()
148
+ const dispatchSpy = jest.spyOn(controller, 'dispatch')
149
+
150
+ controller.open()
151
+
152
+ // Should not dispatch opened event again
153
+ expect(dispatchSpy).not.toHaveBeenCalledWith("opened")
154
+ })
155
+ })
156
+
157
+ describe("item selection", () => {
158
+ const selectionHTML = `
159
+ <div data-controller="shadcn--select"
160
+ data-shadcn--select-value-value="">
161
+ <button data-shadcn--select-target="trigger"
162
+ data-action="click->shadcn--select#toggle">
163
+ <span data-shadcn--select-target="display">Select...</span>
164
+ </button>
165
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
166
+ <div data-shadcn--select-target="content"
167
+ hidden
168
+ data-state="closed">
169
+ <div data-shadcn--select-target="item"
170
+ data-value="apple"
171
+ tabindex="-1"
172
+ data-action="click->shadcn--select#select">Apple</div>
173
+ <div data-shadcn--select-target="item"
174
+ data-value="banana"
175
+ tabindex="-1"
176
+ data-action="click->shadcn--select#select">Banana</div>
177
+ </div>
178
+ </div>
179
+ `
180
+
181
+ beforeEach(async () => {
182
+ const setup = await setupController(SelectController, selectionHTML, 'shadcn--select')
183
+ application = setup.application
184
+ element = setup.element
185
+ controller = setup.controller
186
+ })
187
+
188
+ test("selects item when clicked", async () => {
189
+ controller.open()
190
+ const appleItem = controller.itemTargets[0]
191
+ click(appleItem)
192
+ await nextFrame()
193
+
194
+ expect(controller.valueValue).toBe("apple")
195
+ })
196
+
197
+ test("updates display text on selection", async () => {
198
+ controller.open()
199
+ const appleItem = controller.itemTargets[0]
200
+ click(appleItem)
201
+ await nextFrame()
202
+
203
+ expect(controller.displayTarget.textContent).toBe("Apple")
204
+ })
205
+
206
+ test("updates hidden input value on selection", async () => {
207
+ controller.open()
208
+ const bananaItem = controller.itemTargets[1]
209
+ click(bananaItem)
210
+ await nextFrame()
211
+
212
+ expect(controller.inputTarget.value).toBe("banana")
213
+ })
214
+
215
+ test("closes dropdown after selection", async () => {
216
+ controller.open()
217
+ const appleItem = controller.itemTargets[0]
218
+ click(appleItem)
219
+ await nextFrame()
220
+
221
+ expect(controller.isOpen).toBe(false)
222
+ })
223
+
224
+ test("dispatches change event on selection", async () => {
225
+ let eventDetail = null
226
+ element.addEventListener("shadcn--select:change", (e) => {
227
+ eventDetail = e.detail
228
+ })
229
+
230
+ controller.open()
231
+ const appleItem = controller.itemTargets[0]
232
+ click(appleItem)
233
+ await nextFrame()
234
+
235
+ expect(eventDetail).not.toBeNull()
236
+ expect(eventDetail.value).toBe("apple")
237
+ })
238
+
239
+ test("updates aria-selected on items", async () => {
240
+ controller.open()
241
+ const appleItem = controller.itemTargets[0]
242
+ const bananaItem = controller.itemTargets[1]
243
+ click(appleItem)
244
+ await nextFrame()
245
+
246
+ expect(appleItem.getAttribute("aria-selected")).toBe("true")
247
+ expect(bananaItem.getAttribute("aria-selected")).toBe("false")
248
+ })
249
+ })
250
+
251
+ describe("disabled items", () => {
252
+ const disabledHTML = `
253
+ <div data-controller="shadcn--select">
254
+ <button data-shadcn--select-target="trigger">
255
+ <span data-shadcn--select-target="display">Select...</span>
256
+ </button>
257
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
258
+ <div data-shadcn--select-target="content">
259
+ <div data-shadcn--select-target="item"
260
+ data-value="apple"
261
+ data-action="click->shadcn--select#select">Apple</div>
262
+ <div data-shadcn--select-target="item"
263
+ data-value="banana"
264
+ data-disabled
265
+ data-action="click->shadcn--select#select">Banana (Disabled)</div>
266
+ </div>
267
+ </div>
268
+ `
269
+
270
+ beforeEach(async () => {
271
+ const setup = await setupController(SelectController, disabledHTML, 'shadcn--select')
272
+ application = setup.application
273
+ element = setup.element
274
+ controller = setup.controller
275
+ })
276
+
277
+ test("does not select disabled item", async () => {
278
+ controller.open()
279
+ const disabledItem = controller.itemTargets[1]
280
+ click(disabledItem)
281
+ await nextFrame()
282
+
283
+ expect(controller.valueValue).toBe("")
284
+ })
285
+
286
+ test("enabledItems excludes disabled items", () => {
287
+ const enabled = controller.enabledItems
288
+ expect(enabled.length).toBe(1)
289
+ expect(enabled[0].dataset.value).toBe("apple")
290
+ })
291
+ })
292
+
293
+ describe("keyboard navigation", () => {
294
+ const keyboardHTML = `
295
+ <div data-controller="shadcn--select"
296
+ data-action="keydown.escape->shadcn--select#close">
297
+ <button data-shadcn--select-target="trigger"
298
+ data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
299
+ <span data-shadcn--select-target="display">Select...</span>
300
+ </button>
301
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
302
+ <div data-shadcn--select-target="content"
303
+ hidden
304
+ data-state="closed">
305
+ <div data-shadcn--select-target="item"
306
+ data-value="apple"
307
+ tabindex="-1"
308
+ data-action="click->shadcn--select#select">Apple</div>
309
+ <div data-shadcn--select-target="item"
310
+ data-value="banana"
311
+ tabindex="-1"
312
+ data-action="click->shadcn--select#select">Banana</div>
313
+ <div data-shadcn--select-target="item"
314
+ data-value="cherry"
315
+ tabindex="-1"
316
+ data-action="click->shadcn--select#select">Cherry</div>
317
+ </div>
318
+ </div>
319
+ `
320
+
321
+ beforeEach(async () => {
322
+ const setup = await setupController(SelectController, keyboardHTML, 'shadcn--select')
323
+ application = setup.application
324
+ element = setup.element
325
+ controller = setup.controller
326
+ })
327
+
328
+ test("opens on Enter key when closed", () => {
329
+ controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
330
+
331
+ expect(controller.isOpen).toBe(true)
332
+ })
333
+
334
+ test("opens on Space key when closed", () => {
335
+ controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
336
+
337
+ expect(controller.isOpen).toBe(true)
338
+ })
339
+
340
+ test("opens on ArrowDown key when closed", () => {
341
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
342
+
343
+ expect(controller.isOpen).toBe(true)
344
+ })
345
+
346
+ test("closes on Escape key when open", () => {
347
+ controller.open()
348
+ controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
349
+
350
+ expect(controller.isOpen).toBe(false)
351
+ })
352
+
353
+ test("navigates down with ArrowDown", () => {
354
+ controller.open()
355
+ controller.focusedIndex = 0
356
+
357
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
358
+
359
+ expect(controller.focusedIndex).toBe(1)
360
+ })
361
+
362
+ test("navigates up with ArrowUp", () => {
363
+ controller.open()
364
+ controller.focusedIndex = 1
365
+
366
+ controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
367
+
368
+ expect(controller.focusedIndex).toBe(0)
369
+ })
370
+
371
+ test("wraps to first item from last with ArrowDown", () => {
372
+ controller.open()
373
+ controller.focusedIndex = 2
374
+
375
+ controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
376
+
377
+ expect(controller.focusedIndex).toBe(0)
378
+ })
379
+
380
+ test("wraps to last item from first with ArrowUp", () => {
381
+ controller.open()
382
+ controller.focusedIndex = 0
383
+
384
+ controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
385
+
386
+ expect(controller.focusedIndex).toBe(2)
387
+ })
388
+
389
+ test("jumps to first item with Home", () => {
390
+ controller.open()
391
+ controller.focusedIndex = 2
392
+
393
+ controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
394
+
395
+ expect(controller.focusedIndex).toBe(0)
396
+ })
397
+
398
+ test("jumps to last item with End", () => {
399
+ controller.open()
400
+ controller.focusedIndex = 0
401
+
402
+ controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
403
+
404
+ expect(controller.focusedIndex).toBe(2)
405
+ })
406
+
407
+ test("selects focused item on Enter", async () => {
408
+ controller.open()
409
+ controller.focusedIndex = 1
410
+
411
+ controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
412
+ await nextFrame()
413
+
414
+ expect(controller.valueValue).toBe("banana")
415
+ })
416
+
417
+ test("selects focused item on Space", async () => {
418
+ controller.open()
419
+ controller.focusedIndex = 0
420
+
421
+ controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
422
+ await nextFrame()
423
+
424
+ expect(controller.valueValue).toBe("apple")
425
+ })
426
+ })
427
+
428
+ describe("initial value", () => {
429
+ const initialValueHTML = `
430
+ <div data-controller="shadcn--select"
431
+ data-shadcn--select-value-value="banana">
432
+ <button data-shadcn--select-target="trigger">
433
+ <span data-shadcn--select-target="display">Select...</span>
434
+ </button>
435
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
436
+ <div data-shadcn--select-target="content">
437
+ <div data-shadcn--select-target="item"
438
+ data-value="apple"
439
+ data-action="click->shadcn--select#select">Apple</div>
440
+ <div data-shadcn--select-target="item"
441
+ data-value="banana"
442
+ data-action="click->shadcn--select#select">Banana</div>
443
+ </div>
444
+ </div>
445
+ `
446
+
447
+ beforeEach(async () => {
448
+ const setup = await setupController(SelectController, initialValueHTML, 'shadcn--select')
449
+ application = setup.application
450
+ element = setup.element
451
+ controller = setup.controller
452
+ })
453
+
454
+ test("initializes with pre-set value", () => {
455
+ expect(controller.valueValue).toBe("banana")
456
+ })
457
+
458
+ test("sets display text from initial value", () => {
459
+ expect(controller.displayTarget.textContent).toBe("Banana")
460
+ })
461
+
462
+ test("marks correct item as selected on init", () => {
463
+ const bananaItem = controller.itemTargets[1]
464
+ expect(bananaItem.getAttribute("aria-selected")).toBe("true")
465
+ })
466
+
467
+ test("focuses current value item when opening", async () => {
468
+ const focusSpy = jest.spyOn(controller.itemTargets[1], 'focus')
469
+ controller.open()
470
+ await nextFrame()
471
+
472
+ expect(focusSpy).toHaveBeenCalled()
473
+ })
474
+ })
475
+
476
+ describe("click outside", () => {
477
+ const clickOutsideHTML = `
478
+ <div data-controller="shadcn--select">
479
+ <button data-shadcn--select-target="trigger"
480
+ data-action="click->shadcn--select#toggle">
481
+ <span data-shadcn--select-target="display">Select...</span>
482
+ </button>
483
+ <div data-shadcn--select-target="content" hidden>
484
+ <div data-shadcn--select-target="item" data-value="apple">Apple</div>
485
+ </div>
486
+ </div>
487
+ <div id="outside">Outside Element</div>
488
+ `
489
+
490
+ beforeEach(async () => {
491
+ const setup = await setupController(SelectController, clickOutsideHTML, 'shadcn--select')
492
+ application = setup.application
493
+ element = setup.element
494
+ controller = setup.controller
495
+ })
496
+
497
+ test("closes when clicking outside", async () => {
498
+ controller.open()
499
+ await nextFrame()
500
+
501
+ const outsideElement = document.getElementById("outside")
502
+ click(outsideElement)
503
+ await nextFrame()
504
+
505
+ expect(controller.isOpen).toBe(false)
506
+ })
507
+
508
+ test("does not close when clicking inside", async () => {
509
+ controller.open()
510
+ await nextFrame()
511
+
512
+ click(controller.contentTarget)
513
+ await nextFrame()
514
+
515
+ expect(controller.isOpen).toBe(true)
516
+ })
517
+ })
518
+
519
+ describe("trigger width synchronization", () => {
520
+ const widthSyncHTML = `
521
+ <div data-controller="shadcn--select">
522
+ <button data-shadcn--select-target="trigger"
523
+ style="width: 200px;"
524
+ data-action="click->shadcn--select#toggle">
525
+ <span data-shadcn--select-target="display">Select...</span>
526
+ </button>
527
+ <div data-shadcn--select-target="content" hidden>
528
+ <div data-shadcn--select-target="item" data-value="apple">Apple</div>
529
+ </div>
530
+ </div>
531
+ `
532
+
533
+ beforeEach(async () => {
534
+ const setup = await setupController(SelectController, widthSyncHTML, 'shadcn--select')
535
+ application = setup.application
536
+ element = setup.element
537
+ controller = setup.controller
538
+ })
539
+
540
+ test("sets trigger width CSS variable on open", async () => {
541
+ controller.open()
542
+ await nextFrame()
543
+
544
+ const cssVar = controller.contentTarget.style.getPropertyValue('--radix-select-trigger-width')
545
+ expect(cssVar).toBeTruthy()
546
+ })
547
+ })
548
+
549
+ describe("check icon visibility", () => {
550
+ const checkIconHTML = `
551
+ <div data-controller="shadcn--select">
552
+ <button data-shadcn--select-target="trigger">
553
+ <span data-shadcn--select-target="display">Select...</span>
554
+ </button>
555
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
556
+ <div data-shadcn--select-target="content">
557
+ <div data-shadcn--select-target="item"
558
+ data-value="apple"
559
+ data-action="click->shadcn--select#select">
560
+ Apple
561
+ <span data-shadcn--select-target="checkIcon" style="opacity: 0;">✓</span>
562
+ </div>
563
+ <div data-shadcn--select-target="item"
564
+ data-value="banana"
565
+ data-action="click->shadcn--select#select">
566
+ Banana
567
+ <span data-shadcn--select-target="checkIcon" style="opacity: 0;">✓</span>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ `
572
+
573
+ beforeEach(async () => {
574
+ const setup = await setupController(SelectController, checkIconHTML, 'shadcn--select')
575
+ application = setup.application
576
+ element = setup.element
577
+ controller = setup.controller
578
+ })
579
+
580
+ test("shows check icon for selected item", async () => {
581
+ controller.open()
582
+ const appleItem = controller.itemTargets[0]
583
+ click(appleItem)
584
+ await nextFrame()
585
+
586
+ const checkIcon = appleItem.querySelector('[data-shadcn--select-target="checkIcon"]')
587
+ expect(checkIcon.style.opacity).toBe("1")
588
+ })
589
+
590
+ test("hides check icon for non-selected items", async () => {
591
+ controller.open()
592
+ const appleItem = controller.itemTargets[0]
593
+ click(appleItem)
594
+ await nextFrame()
595
+
596
+ const bananaItem = controller.itemTargets[1]
597
+ const checkIcon = bananaItem.querySelector('[data-shadcn--select-target="checkIcon"]')
598
+ expect(checkIcon.style.opacity).toBe("0")
599
+ })
600
+ })
601
+
602
+ describe("programmatic value change", () => {
603
+ const programmaticHTML = `
604
+ <div data-controller="shadcn--select">
605
+ <button data-shadcn--select-target="trigger">
606
+ <span data-shadcn--select-target="display">Select...</span>
607
+ </button>
608
+ <input type="hidden" data-shadcn--select-target="input" name="fruit">
609
+ <div data-shadcn--select-target="content">
610
+ <div data-shadcn--select-target="item" data-value="apple">Apple</div>
611
+ <div data-shadcn--select-target="item" data-value="banana">Banana</div>
612
+ </div>
613
+ </div>
614
+ `
615
+
616
+ beforeEach(async () => {
617
+ const setup = await setupController(SelectController, programmaticHTML, 'shadcn--select')
618
+ application = setup.application
619
+ element = setup.element
620
+ controller = setup.controller
621
+ })
622
+
623
+ test("selectByValue updates value without dispatch when specified", () => {
624
+ let eventFired = false
625
+ element.addEventListener("shadcn--select:change", () => {
626
+ eventFired = true
627
+ })
628
+
629
+ controller.selectByValue("apple", false)
630
+
631
+ expect(controller.valueValue).toBe("apple")
632
+ expect(eventFired).toBe(false)
633
+ })
634
+
635
+ test("selectByValue dispatches change when dispatch is true", () => {
636
+ let eventDetail = null
637
+ element.addEventListener("shadcn--select:change", (e) => {
638
+ eventDetail = e.detail
639
+ })
640
+
641
+ controller.selectByValue("banana", true)
642
+
643
+ expect(eventDetail).not.toBeNull()
644
+ expect(eventDetail.value).toBe("banana")
645
+ })
646
+ })
647
+
648
+ describe("disconnect cleanup", () => {
649
+ const disconnectHTML = `
650
+ <div data-controller="shadcn--select">
651
+ <button data-shadcn--select-target="trigger">Select...</button>
652
+ <div data-shadcn--select-target="content" hidden>
653
+ <div data-shadcn--select-target="item" data-value="apple">Apple</div>
654
+ </div>
655
+ </div>
656
+ `
657
+
658
+ beforeEach(async () => {
659
+ const setup = await setupController(SelectController, disconnectHTML, 'shadcn--select')
660
+ application = setup.application
661
+ element = setup.element
662
+ controller = setup.controller
663
+ })
664
+
665
+ test("cleans up event listeners on disconnect", () => {
666
+ controller.open()
667
+
668
+ const closeSpy = jest.spyOn(controller, 'close')
669
+ controller.disconnect()
670
+
671
+ expect(closeSpy).toHaveBeenCalled()
672
+ })
673
+ })
674
+ })