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,995 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import DrawerController from "../../app/assets/javascripts/shadcn/controllers/drawer_controller.js"
3
+ import { click, wait, nextFrame, keydown, waitForEvent } from '../helpers/stimulus-test-helper.js'
4
+
5
+ describe("DrawerController", () => {
6
+ let application
7
+ let element
8
+ let controller
9
+
10
+ const createDrawerHTML = (open = false, direction = "bottom") => {
11
+ const openAttr = open ? `data-shadcn--drawer-open-value="true"` : ''
12
+
13
+ return `
14
+ <div data-controller="shadcn--drawer"
15
+ data-shadcn--drawer-direction-value="${direction}"
16
+ ${openAttr}>
17
+ <button data-shadcn--drawer-target="trigger"
18
+ data-action="click->shadcn--drawer#toggle">
19
+ Open Drawer
20
+ </button>
21
+ <template data-shadcn--drawer-target="template">
22
+ <div data-shadcn--drawer-target="overlay" data-state="closed"></div>
23
+ <div data-shadcn--drawer-target="content" data-state="closed" tabindex="-1">
24
+ <h2>Drawer Content</h2>
25
+ <button class="close-btn">Close</button>
26
+ <input type="text" placeholder="Focus test" />
27
+ </div>
28
+ </template>
29
+ </div>
30
+ `
31
+ }
32
+
33
+ beforeEach(async () => {
34
+ application = Application.start()
35
+ application.register("shadcn--drawer", DrawerController)
36
+ document.body.innerHTML = createDrawerHTML()
37
+
38
+ await nextFrame()
39
+
40
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
41
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
42
+ })
43
+
44
+ afterEach(() => {
45
+ // Clean up any portals
46
+ const portals = document.querySelectorAll('body > div:not([data-controller])')
47
+ portals.forEach(portal => portal.remove())
48
+
49
+ if (application) {
50
+ application.stop()
51
+ }
52
+ document.body.innerHTML = ""
53
+ document.body.style.overflow = ""
54
+ })
55
+
56
+ describe("value initialization", () => {
57
+ test("initializes with default open value of false", () => {
58
+ expect(controller.openValue).toBe(false)
59
+ })
60
+
61
+ test("initializes with default direction value of 'bottom'", () => {
62
+ expect(controller.directionValue).toBe("bottom")
63
+ })
64
+
65
+ test("accepts custom direction value", async () => {
66
+ application.stop()
67
+ document.body.innerHTML = createDrawerHTML(false, "right")
68
+
69
+ application = Application.start()
70
+ application.register("shadcn--drawer", DrawerController)
71
+ await nextFrame()
72
+
73
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
74
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
75
+
76
+ expect(controller.directionValue).toBe("right")
77
+ })
78
+
79
+ test("accepts custom open value", async () => {
80
+ application.stop()
81
+ document.body.innerHTML = createDrawerHTML(true, "bottom")
82
+
83
+ application = Application.start()
84
+ application.register("shadcn--drawer", DrawerController)
85
+ await nextFrame()
86
+
87
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
88
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
89
+
90
+ expect(controller.openValue).toBe(true)
91
+ })
92
+ })
93
+
94
+ describe("targets", () => {
95
+ test("has trigger target", () => {
96
+ expect(controller.hasTriggerTarget).toBe(true)
97
+ })
98
+
99
+ test("has template target", () => {
100
+ expect(controller.hasTemplateTarget).toBe(true)
101
+ })
102
+
103
+ test("trigger target is correct element", () => {
104
+ const trigger = element.querySelector('[data-shadcn--drawer-target="trigger"]')
105
+ expect(controller.triggerTarget).toBe(trigger)
106
+ })
107
+
108
+ test("template target is correct element", () => {
109
+ const template = element.querySelector('[data-shadcn--drawer-target="template"]')
110
+ expect(controller.templateTarget).toBe(template)
111
+ })
112
+ })
113
+
114
+ describe("connect", () => {
115
+ test("initializes portal as null", () => {
116
+ expect(controller.portal).toBeNull()
117
+ })
118
+
119
+ test("opens drawer if openValue is true on connect", async () => {
120
+ application.stop()
121
+ document.body.innerHTML = createDrawerHTML(true, "bottom")
122
+
123
+ application = Application.start()
124
+ application.register("shadcn--drawer", DrawerController)
125
+ await nextFrame()
126
+ await nextFrame() // Wait for requestAnimationFrame in open()
127
+
128
+ const portal = document.querySelector('body > div:not([data-controller])')
129
+ expect(portal).toBeTruthy()
130
+ })
131
+
132
+ test("does not open drawer if openValue is false on connect", async () => {
133
+ application.stop()
134
+ document.body.innerHTML = createDrawerHTML(false, "bottom")
135
+
136
+ application = Application.start()
137
+ application.register("shadcn--drawer", DrawerController)
138
+ await nextFrame()
139
+
140
+ const portal = document.querySelector('body > div:not([data-controller])')
141
+ expect(portal).toBeFalsy()
142
+ })
143
+ })
144
+
145
+ describe("toggle behavior", () => {
146
+ test("toggle opens drawer when closed", async () => {
147
+ const trigger = controller.triggerTarget
148
+
149
+ click(trigger)
150
+ await nextFrame()
151
+
152
+ expect(controller.openValue).toBe(true)
153
+ expect(controller.portal).toBeTruthy()
154
+ })
155
+
156
+ test("toggle closes drawer when open", async () => {
157
+ const trigger = controller.triggerTarget
158
+
159
+ // Open
160
+ click(trigger)
161
+ await nextFrame()
162
+ expect(controller.openValue).toBe(true)
163
+
164
+ // Close
165
+ click(trigger)
166
+ await wait(250) // Wait for closing animation
167
+
168
+ expect(controller.openValue).toBe(false)
169
+ expect(controller.portal).toBeNull()
170
+ })
171
+
172
+ test("multiple toggles work correctly", async () => {
173
+ const trigger = controller.triggerTarget
174
+
175
+ // Open
176
+ click(trigger)
177
+ await nextFrame()
178
+ expect(controller.openValue).toBe(true)
179
+
180
+ // Close
181
+ click(trigger)
182
+ await wait(250)
183
+ expect(controller.openValue).toBe(false)
184
+
185
+ // Open again
186
+ click(trigger)
187
+ await nextFrame()
188
+ expect(controller.openValue).toBe(true)
189
+
190
+ // Close again
191
+ click(trigger)
192
+ await wait(250)
193
+ expect(controller.openValue).toBe(false)
194
+ })
195
+ })
196
+
197
+ describe("portal rendering", () => {
198
+ test("creates portal in document body when opened", async () => {
199
+ controller.open()
200
+ await nextFrame()
201
+
202
+ const portal = document.querySelector('body > div:not([data-controller])')
203
+ expect(portal).toBeTruthy()
204
+ expect(portal).toBe(controller.portal)
205
+ })
206
+
207
+ test("portal contains overlay element", async () => {
208
+ controller.open()
209
+ await nextFrame()
210
+
211
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
212
+ expect(overlay).toBeTruthy()
213
+ })
214
+
215
+ test("portal contains content element", async () => {
216
+ controller.open()
217
+ await nextFrame()
218
+
219
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
220
+ expect(content).toBeTruthy()
221
+ })
222
+
223
+ test("portal contains template innerHTML", async () => {
224
+ controller.open()
225
+ await nextFrame()
226
+
227
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
228
+ expect(content.innerHTML).toContain("Drawer Content")
229
+ })
230
+
231
+ test("removes portal from DOM when closed", async () => {
232
+ controller.open()
233
+ await nextFrame()
234
+
235
+ const portalBefore = document.querySelector('body > div:not([data-controller])')
236
+ expect(portalBefore).toBeTruthy()
237
+
238
+ controller.close()
239
+ await wait(250) // Wait for closing animation
240
+
241
+ const portalAfter = document.querySelector('body > div:not([data-controller])')
242
+ expect(portalAfter).toBeFalsy()
243
+ })
244
+
245
+ test("does not open if template target is missing", async () => {
246
+ // Remove template
247
+ const template = element.querySelector('[data-shadcn--drawer-target="template"]')
248
+ template.remove()
249
+
250
+ controller.open()
251
+ await nextFrame()
252
+
253
+ expect(controller.portal).toBeNull()
254
+ })
255
+ })
256
+
257
+ describe("data-state attributes", () => {
258
+ test("overlay has data-state='closed' initially in portal", async () => {
259
+ controller.open()
260
+
261
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
262
+ expect(overlay.getAttribute("data-state")).toBe("closed")
263
+ })
264
+
265
+ test("content has data-state='closed' initially in portal", async () => {
266
+ controller.open()
267
+
268
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
269
+ expect(content.getAttribute("data-state")).toBe("closed")
270
+ })
271
+
272
+ test("overlay has data-state='open' after animation frame", async () => {
273
+ controller.open()
274
+ await nextFrame()
275
+
276
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
277
+ expect(overlay.getAttribute("data-state")).toBe("open")
278
+ })
279
+
280
+ test("content has data-state='open' after animation frame", async () => {
281
+ controller.open()
282
+ await nextFrame()
283
+
284
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
285
+ expect(content.getAttribute("data-state")).toBe("open")
286
+ })
287
+
288
+ test("overlay has data-state='closed' when closing", async () => {
289
+ controller.open()
290
+ await nextFrame()
291
+
292
+ controller.close()
293
+
294
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
295
+ expect(overlay.getAttribute("data-state")).toBe("closed")
296
+ })
297
+
298
+ test("content has data-state='closed' when closing", async () => {
299
+ controller.open()
300
+ await nextFrame()
301
+
302
+ controller.close()
303
+
304
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
305
+ expect(content.getAttribute("data-state")).toBe("closed")
306
+ })
307
+ })
308
+
309
+ describe("direction variants", () => {
310
+ const directions = ["top", "right", "bottom", "left"]
311
+
312
+ directions.forEach(direction => {
313
+ test(`supports direction='${direction}'`, async () => {
314
+ application.stop()
315
+ document.body.innerHTML = createDrawerHTML(false, direction)
316
+
317
+ application = Application.start()
318
+ application.register("shadcn--drawer", DrawerController)
319
+ await nextFrame()
320
+
321
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
322
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
323
+
324
+ expect(controller.directionValue).toBe(direction)
325
+ })
326
+
327
+ test(`opens drawer with direction='${direction}'`, async () => {
328
+ application.stop()
329
+ document.body.innerHTML = createDrawerHTML(false, direction)
330
+
331
+ application = Application.start()
332
+ application.register("shadcn--drawer", DrawerController)
333
+ await nextFrame()
334
+
335
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
336
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
337
+
338
+ controller.open()
339
+ await nextFrame()
340
+
341
+ const portal = document.querySelector('body > div:not([data-controller])')
342
+ expect(portal).toBeTruthy()
343
+ })
344
+ })
345
+ })
346
+
347
+ describe("focus management", () => {
348
+ test("focuses content when drawer opens", async () => {
349
+ controller.open()
350
+ await nextFrame()
351
+
352
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
353
+ expect(document.activeElement).toBe(content)
354
+ })
355
+
356
+ test("content is focusable with tabindex", async () => {
357
+ controller.open()
358
+ await nextFrame()
359
+
360
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
361
+ expect(content.getAttribute("tabindex")).toBe("-1")
362
+ })
363
+
364
+ test("maintains focus within drawer when open", async () => {
365
+ controller.open()
366
+ await nextFrame()
367
+
368
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
369
+ const input = controller.portal.querySelector('input')
370
+
371
+ // Focus moves to content first
372
+ expect(document.activeElement).toBe(content)
373
+
374
+ // Can focus elements within content
375
+ input.focus()
376
+ expect(document.activeElement).toBe(input)
377
+ })
378
+ })
379
+
380
+ describe("escape key handling", () => {
381
+ test("closes drawer when Escape key is pressed", async () => {
382
+ controller.open()
383
+ await nextFrame()
384
+
385
+ expect(controller.openValue).toBe(true)
386
+
387
+ keydown(document, 'Escape')
388
+ await wait(250)
389
+
390
+ expect(controller.openValue).toBe(false)
391
+ expect(controller.portal).toBeNull()
392
+ })
393
+
394
+ test("escape key listener is added when drawer opens", async () => {
395
+ expect(controller.boundHandleKeydown).toBeDefined()
396
+
397
+ controller.open()
398
+ await nextFrame()
399
+
400
+ // Verify that escape works (indirectly confirms listener is attached)
401
+ keydown(document, 'Escape')
402
+ await wait(250)
403
+
404
+ expect(controller.openValue).toBe(false)
405
+ })
406
+
407
+ test("escape key listener is removed when drawer closes", async () => {
408
+ controller.open()
409
+ await nextFrame()
410
+
411
+ controller.close()
412
+ await wait(250)
413
+
414
+ // Try to close again with escape - shouldn't do anything since already closed
415
+ keydown(document, 'Escape')
416
+ await nextFrame()
417
+
418
+ expect(controller.openValue).toBe(false)
419
+ })
420
+
421
+ test("escape key only affects open drawer", async () => {
422
+ // Try escape when drawer is closed
423
+ keydown(document, 'Escape')
424
+ await nextFrame()
425
+
426
+ expect(controller.openValue).toBe(false)
427
+ })
428
+ })
429
+
430
+ describe("overlay click handling", () => {
431
+ test("closes drawer when overlay is clicked", async () => {
432
+ controller.open()
433
+ await nextFrame()
434
+
435
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
436
+
437
+ click(overlay)
438
+ await wait(250)
439
+
440
+ expect(controller.openValue).toBe(false)
441
+ expect(controller.portal).toBeNull()
442
+ })
443
+
444
+ test("overlay click listener is added when drawer opens", async () => {
445
+ controller.open()
446
+ await nextFrame()
447
+
448
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
449
+ expect(overlay).toBeTruthy()
450
+
451
+ // Verify overlay click works (indirectly confirms listener is attached)
452
+ click(overlay)
453
+ await wait(250)
454
+
455
+ expect(controller.openValue).toBe(false)
456
+ })
457
+
458
+ test("clicking content does not close drawer", async () => {
459
+ controller.open()
460
+ await nextFrame()
461
+
462
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
463
+
464
+ click(content)
465
+ await nextFrame()
466
+
467
+ expect(controller.openValue).toBe(true)
468
+ expect(controller.portal).toBeTruthy()
469
+ })
470
+ })
471
+
472
+ describe("body scroll lock", () => {
473
+ test("locks body scroll when drawer opens", async () => {
474
+ expect(document.body.style.overflow).toBe("")
475
+
476
+ controller.open()
477
+ await nextFrame()
478
+
479
+ expect(document.body.style.overflow).toBe("hidden")
480
+ })
481
+
482
+ test("restores body scroll when drawer closes", async () => {
483
+ controller.open()
484
+ await nextFrame()
485
+ expect(document.body.style.overflow).toBe("hidden")
486
+
487
+ controller.close()
488
+ await wait(250)
489
+
490
+ expect(document.body.style.overflow).toBe("")
491
+ })
492
+
493
+ test("restores body scroll even if closed quickly", async () => {
494
+ controller.open()
495
+ await nextFrame()
496
+
497
+ controller.close()
498
+ // Don't wait for animation
499
+
500
+ expect(document.body.style.overflow).toBe("")
501
+ })
502
+
503
+ test("body scroll is locked for multiple open/close cycles", async () => {
504
+ // First cycle
505
+ controller.open()
506
+ await nextFrame()
507
+ expect(document.body.style.overflow).toBe("hidden")
508
+
509
+ controller.close()
510
+ await wait(250)
511
+ expect(document.body.style.overflow).toBe("")
512
+
513
+ // Second cycle
514
+ controller.open()
515
+ await nextFrame()
516
+ expect(document.body.style.overflow).toBe("hidden")
517
+
518
+ controller.close()
519
+ await wait(250)
520
+ expect(document.body.style.overflow).toBe("")
521
+ })
522
+ })
523
+
524
+ describe("event dispatch", () => {
525
+ test("dispatches 'open' event when drawer opens", async () => {
526
+ const eventPromise = waitForEvent(element, 'shadcn--drawer:open')
527
+
528
+ controller.open()
529
+
530
+ const event = await eventPromise
531
+ expect(event).toBeTruthy()
532
+ })
533
+
534
+ test("dispatches 'close' event when drawer closes", async () => {
535
+ controller.open()
536
+ await nextFrame()
537
+
538
+ const eventPromise = waitForEvent(element, 'shadcn--drawer:close')
539
+
540
+ controller.close()
541
+
542
+ const event = await eventPromise
543
+ expect(event).toBeTruthy()
544
+ })
545
+
546
+ test("events bubble up correctly", async () => {
547
+ let openEventFired = false
548
+ let closeEventFired = false
549
+
550
+ element.addEventListener('shadcn--drawer:open', () => {
551
+ openEventFired = true
552
+ })
553
+
554
+ element.addEventListener('shadcn--drawer:close', () => {
555
+ closeEventFired = true
556
+ })
557
+
558
+ controller.open()
559
+ await nextFrame()
560
+ expect(openEventFired).toBe(true)
561
+
562
+ controller.close()
563
+ await nextFrame()
564
+ expect(closeEventFired).toBe(true)
565
+ })
566
+
567
+ test("open event is dispatched before portal is shown", async () => {
568
+ let eventTime = null
569
+ let portalStateAtEvent = null
570
+
571
+ element.addEventListener('shadcn--drawer:open', () => {
572
+ eventTime = Date.now()
573
+ const portalOverlay = controller.portal?.querySelector('[data-shadcn--drawer-target="overlay"]')
574
+ portalStateAtEvent = portalOverlay?.getAttribute('data-state')
575
+ })
576
+
577
+ controller.open()
578
+ await nextFrame()
579
+
580
+ expect(eventTime).toBeTruthy()
581
+ // Portal exists but might still be in closed state when event fires
582
+ expect(portalStateAtEvent).toBe('closed')
583
+ })
584
+ })
585
+
586
+ describe("openValueChanged", () => {
587
+ test("opens drawer when openValue changes from false to true", async () => {
588
+ expect(controller.portal).toBeNull()
589
+
590
+ controller.openValue = true
591
+ await nextFrame()
592
+
593
+ expect(controller.portal).toBeTruthy()
594
+ })
595
+
596
+ test("closes drawer when openValue changes from true to false", async () => {
597
+ controller.openValue = true
598
+ await nextFrame()
599
+ expect(controller.portal).toBeTruthy()
600
+
601
+ controller.openValue = false
602
+ await wait(250)
603
+
604
+ expect(controller.portal).toBeNull()
605
+ })
606
+
607
+ test("does not open if already has portal", async () => {
608
+ controller.open()
609
+ await nextFrame()
610
+
611
+ const firstPortal = controller.portal
612
+
613
+ controller.openValue = true
614
+ await nextFrame()
615
+
616
+ // Should be same portal
617
+ expect(controller.portal).toBe(firstPortal)
618
+ })
619
+
620
+ test("does not close if already closed", async () => {
621
+ expect(controller.portal).toBeNull()
622
+
623
+ controller.openValue = false
624
+ await nextFrame()
625
+
626
+ // Should still be null
627
+ expect(controller.portal).toBeNull()
628
+ })
629
+ })
630
+
631
+ describe("disconnect", () => {
632
+ test("calls removePortal on disconnect", async () => {
633
+ controller.open()
634
+ await nextFrame()
635
+
636
+ expect(controller.portal).toBeTruthy()
637
+ const portal = controller.portal
638
+
639
+ // Manually call disconnect to test
640
+ controller.disconnect()
641
+
642
+ // removePortal should have been called
643
+ expect(controller.portal).toBeNull()
644
+ expect(document.body.contains(portal)).toBe(false)
645
+ })
646
+
647
+ test("removes keydown event listener on disconnect", async () => {
648
+ controller.open()
649
+ await nextFrame()
650
+
651
+ // Manually call disconnect
652
+ controller.disconnect()
653
+
654
+ // Create new controller to verify listener was removed
655
+ // (Cannot directly test listener removal, but can verify no errors)
656
+ keydown(document, 'Escape')
657
+ await nextFrame()
658
+
659
+ // No errors means success
660
+ expect(true).toBe(true)
661
+ })
662
+
663
+ test("cleans up even if drawer is open", async () => {
664
+ controller.open()
665
+ await nextFrame()
666
+
667
+ const portal = controller.portal
668
+ expect(portal).toBeTruthy()
669
+
670
+ const portalInBody = document.body.contains(portal)
671
+ expect(portalInBody).toBe(true)
672
+
673
+ // Manually call disconnect
674
+ controller.disconnect()
675
+
676
+ // Portal should be removed from DOM
677
+ expect(controller.portal).toBeNull()
678
+ expect(document.body.contains(portal)).toBe(false)
679
+ })
680
+
681
+ test("restores body overflow on disconnect", async () => {
682
+ controller.open()
683
+ await nextFrame()
684
+
685
+ expect(document.body.style.overflow).toBe("hidden")
686
+
687
+ // Disconnect removes event listener but doesn't restore overflow
688
+ // We need to close first
689
+ controller.close()
690
+ await nextFrame()
691
+
692
+ application.stop()
693
+
694
+ // Overflow should be restored from close, not disconnect
695
+ expect(document.body.style.overflow).toBe("")
696
+ })
697
+ })
698
+
699
+ describe("removePortal", () => {
700
+ test("removes portal from DOM", async () => {
701
+ controller.open()
702
+ await nextFrame()
703
+
704
+ const portal = controller.portal
705
+ expect(document.body.contains(portal)).toBe(true)
706
+
707
+ controller.removePortal()
708
+
709
+ expect(document.body.contains(portal)).toBe(false)
710
+ })
711
+
712
+ test("sets portal to null", async () => {
713
+ controller.open()
714
+ await nextFrame()
715
+
716
+ expect(controller.portal).toBeTruthy()
717
+
718
+ controller.removePortal()
719
+
720
+ expect(controller.portal).toBeNull()
721
+ })
722
+
723
+ test("does nothing if portal is null", () => {
724
+ expect(controller.portal).toBeNull()
725
+
726
+ expect(() => {
727
+ controller.removePortal()
728
+ }).not.toThrow()
729
+
730
+ expect(controller.portal).toBeNull()
731
+ })
732
+
733
+ test("can be called multiple times safely", async () => {
734
+ controller.open()
735
+ await nextFrame()
736
+
737
+ controller.removePortal()
738
+ expect(controller.portal).toBeNull()
739
+
740
+ controller.removePortal()
741
+ expect(controller.portal).toBeNull()
742
+ })
743
+ })
744
+
745
+ describe("animation timing", () => {
746
+ test("waits 200ms before removing portal when closing", async () => {
747
+ controller.open()
748
+ await nextFrame()
749
+
750
+ const portal = controller.portal
751
+
752
+ controller.close()
753
+
754
+ // Portal should still exist immediately after close
755
+ expect(document.body.contains(portal)).toBe(true)
756
+
757
+ // Wait less than 200ms
758
+ await wait(100)
759
+ expect(document.body.contains(portal)).toBe(true)
760
+
761
+ // Wait for full duration
762
+ await wait(150)
763
+ expect(document.body.contains(portal)).toBe(false)
764
+ })
765
+
766
+ test("state changes after animation frame", async () => {
767
+ controller.open()
768
+
769
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
770
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
771
+
772
+ // Before animation frame
773
+ expect(overlay.getAttribute('data-state')).toBe('closed')
774
+ expect(content.getAttribute('data-state')).toBe('closed')
775
+
776
+ await nextFrame()
777
+
778
+ // After animation frame
779
+ expect(overlay.getAttribute('data-state')).toBe('open')
780
+ expect(content.getAttribute('data-state')).toBe('open')
781
+ })
782
+
783
+ test("state is open after requestAnimationFrame completes", async () => {
784
+ controller.open()
785
+ await nextFrame()
786
+
787
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
788
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
789
+
790
+ // Verify animation has completed
791
+ expect(overlay.getAttribute('data-state')).toBe('open')
792
+ expect(content.getAttribute('data-state')).toBe('open')
793
+ })
794
+ })
795
+
796
+ describe("edge cases", () => {
797
+ test("handles missing overlay gracefully", async () => {
798
+ // Modify template to not have overlay
799
+ const template = element.querySelector('[data-shadcn--drawer-target="template"]')
800
+ template.innerHTML = `
801
+ <div data-shadcn--drawer-target="content" data-state="closed" tabindex="-1">
802
+ <h2>Drawer Content</h2>
803
+ </div>
804
+ `
805
+
806
+ expect(() => {
807
+ controller.open()
808
+ }).not.toThrow()
809
+
810
+ await nextFrame()
811
+
812
+ expect(controller.portal).toBeTruthy()
813
+ })
814
+
815
+ test("handles missing content gracefully", async () => {
816
+ // Modify template to not have content
817
+ const template = element.querySelector('[data-shadcn--drawer-target="template"]')
818
+ template.innerHTML = `
819
+ <div data-shadcn--drawer-target="overlay" data-state="closed"></div>
820
+ `
821
+
822
+ expect(() => {
823
+ controller.open()
824
+ }).not.toThrow()
825
+
826
+ await nextFrame()
827
+
828
+ expect(controller.portal).toBeTruthy()
829
+ })
830
+
831
+ test("handles rapid open/close cycles", async () => {
832
+ // Open
833
+ controller.open()
834
+ await nextFrame()
835
+
836
+ // Close immediately
837
+ controller.close()
838
+
839
+ // Open again before close animation finishes
840
+ controller.open()
841
+ await nextFrame()
842
+
843
+ // Should have a portal
844
+ expect(controller.portal).toBeTruthy()
845
+ })
846
+
847
+ test("close does nothing if portal is null", () => {
848
+ expect(controller.portal).toBeNull()
849
+
850
+ expect(() => {
851
+ controller.close()
852
+ }).not.toThrow()
853
+
854
+ expect(controller.portal).toBeNull()
855
+ })
856
+
857
+ test("handles empty template gracefully", async () => {
858
+ const template = element.querySelector('[data-shadcn--drawer-target="template"]')
859
+ template.innerHTML = ''
860
+
861
+ controller.open()
862
+ await nextFrame()
863
+
864
+ // Portal exists but is empty
865
+ expect(controller.portal).toBeTruthy()
866
+ expect(controller.portal.innerHTML).toBe('')
867
+ })
868
+ })
869
+
870
+ describe("integration scenarios", () => {
871
+ test("complete open and close cycle", async () => {
872
+ // Initial state
873
+ expect(controller.openValue).toBe(false)
874
+ expect(controller.portal).toBeNull()
875
+ expect(document.body.style.overflow).toBe("")
876
+
877
+ // Open
878
+ const trigger = controller.triggerTarget
879
+ click(trigger)
880
+ await nextFrame()
881
+
882
+ expect(controller.openValue).toBe(true)
883
+ expect(controller.portal).toBeTruthy()
884
+ expect(document.body.style.overflow).toBe("hidden")
885
+
886
+ const overlay = controller.portal.querySelector('[data-shadcn--drawer-target="overlay"]')
887
+ expect(overlay.getAttribute('data-state')).toBe('open')
888
+
889
+ // Close via overlay
890
+ click(overlay)
891
+ await wait(250)
892
+
893
+ expect(controller.openValue).toBe(false)
894
+ expect(controller.portal).toBeNull()
895
+ expect(document.body.style.overflow).toBe("")
896
+ })
897
+
898
+ test("open via trigger, close via escape", async () => {
899
+ const trigger = controller.triggerTarget
900
+
901
+ click(trigger)
902
+ await nextFrame()
903
+
904
+ expect(controller.openValue).toBe(true)
905
+
906
+ keydown(document, 'Escape')
907
+ await wait(250)
908
+
909
+ expect(controller.openValue).toBe(false)
910
+ expect(controller.portal).toBeNull()
911
+ })
912
+
913
+ test("multiple drawers can coexist", async () => {
914
+ // Create second drawer
915
+ const drawer2HTML = createDrawerHTML(false, "right")
916
+ const tempDiv = document.createElement('div')
917
+ tempDiv.innerHTML = drawer2HTML
918
+ document.body.appendChild(tempDiv.firstElementChild)
919
+
920
+ await nextFrame()
921
+
922
+ const element2 = document.querySelectorAll('[data-controller="shadcn--drawer"]')[1]
923
+ const controller2 = application.getControllerForElementAndIdentifier(element2, "shadcn--drawer")
924
+
925
+ // Open first drawer
926
+ controller.open()
927
+ await nextFrame()
928
+
929
+ // Open second drawer
930
+ controller2.open()
931
+ await nextFrame()
932
+
933
+ // Both should be open
934
+ expect(controller.portal).toBeTruthy()
935
+ expect(controller2.portal).toBeTruthy()
936
+
937
+ // Close first
938
+ controller.close()
939
+ await wait(250)
940
+
941
+ expect(controller.portal).toBeNull()
942
+ expect(controller2.portal).toBeTruthy()
943
+
944
+ // Clean up
945
+ controller2.close()
946
+ await wait(250)
947
+ })
948
+
949
+ test("works with different directions in sequence", async () => {
950
+ const directions = ["top", "right", "bottom", "left"]
951
+
952
+ for (const direction of directions) {
953
+ application.stop()
954
+ document.body.innerHTML = createDrawerHTML(false, direction)
955
+
956
+ application = Application.start()
957
+ application.register("shadcn--drawer", DrawerController)
958
+ await nextFrame()
959
+
960
+ element = document.querySelector('[data-controller="shadcn--drawer"]')
961
+ controller = application.getControllerForElementAndIdentifier(element, "shadcn--drawer")
962
+
963
+ controller.open()
964
+ await nextFrame()
965
+
966
+ expect(controller.directionValue).toBe(direction)
967
+ expect(controller.portal).toBeTruthy()
968
+
969
+ controller.close()
970
+ await wait(250)
971
+
972
+ expect(controller.portal).toBeNull()
973
+ }
974
+ })
975
+
976
+ test("focus returns to trigger after closing", async () => {
977
+ const trigger = controller.triggerTarget
978
+
979
+ trigger.focus()
980
+ expect(document.activeElement).toBe(trigger)
981
+
982
+ click(trigger)
983
+ await nextFrame()
984
+
985
+ const content = controller.portal.querySelector('[data-shadcn--drawer-target="content"]')
986
+ expect(document.activeElement).toBe(content)
987
+
988
+ controller.close()
989
+ await wait(250)
990
+
991
+ // Note: This behavior may need to be implemented in the controller
992
+ // Currently it doesn't restore focus automatically
993
+ })
994
+ })
995
+ })