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.
- checksums.yaml +7 -0
- data/.dockerignore +40 -0
- data/CHANGELOG.md +54 -0
- data/CLAUDE.md +463 -0
- data/PROGRESS.md +485 -0
- data/README.md +1483 -0
- data/Rakefile +29 -0
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
- data/__tests__/controllers/accordion_controller.test.js +904 -0
- data/__tests__/controllers/calendar_controller.test.js +1370 -0
- data/__tests__/controllers/carousel_controller.test.js +912 -0
- data/__tests__/controllers/checkbox_controller.test.js +454 -0
- data/__tests__/controllers/collapsible_controller.test.js +407 -0
- data/__tests__/controllers/combobox_controller.test.js +966 -0
- data/__tests__/controllers/context_menu_controller.test.js +627 -0
- data/__tests__/controllers/date_picker_controller.test.js +636 -0
- data/__tests__/controllers/dialog_controller.test.js +878 -0
- data/__tests__/controllers/drawer_controller.test.js +995 -0
- data/__tests__/controllers/menubar_controller.test.js +736 -0
- data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
- data/__tests__/controllers/popover_controller.test.js +1007 -0
- data/__tests__/controllers/radio_group_controller.test.js +640 -0
- data/__tests__/controllers/resizable_controller.test.js +680 -0
- data/__tests__/controllers/select_controller.test.js +674 -0
- data/__tests__/controllers/sheet_controller.test.js +986 -0
- data/__tests__/controllers/slider_controller.test.js +1036 -0
- data/__tests__/controllers/switch_controller.test.js +424 -0
- data/__tests__/controllers/tabs_controller.test.js +907 -0
- data/__tests__/controllers/toggle_group_controller.test.js +839 -0
- data/__tests__/controllers/tooltip_controller.test.js +808 -0
- data/__tests__/helpers/stimulus-test-helper.js +203 -0
- data/app/assets/config/manifest.js +1 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
- data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
- data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
- data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
- data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
- data/app/assets/javascripts/shadcn/index.d.ts +74 -0
- data/app/assets/javascripts/shadcn/index.js +133 -0
- data/app/assets/stylesheets/.keep +0 -0
- data/app/assets/stylesheets/shadcn/base.css +445 -0
- data/app/assets/stylesheets/shadcn/components.css +513 -0
- data/app/assets/stylesheets/shadcn/index.css +18 -0
- data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
- data/app/components/shadcn/accordion_component.rb +63 -0
- data/app/components/shadcn/accordion_content_component.rb +29 -0
- data/app/components/shadcn/accordion_item_component.rb +40 -0
- data/app/components/shadcn/accordion_trigger_component.rb +49 -0
- data/app/components/shadcn/alert_component.rb +75 -0
- data/app/components/shadcn/alert_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_component.rb +71 -0
- data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
- data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
- data/app/components/shadcn/alert_title_component.rb +12 -0
- data/app/components/shadcn/aspect_ratio_component.rb +49 -0
- data/app/components/shadcn/avatar_component.rb +107 -0
- data/app/components/shadcn/avatar_fallback_component.rb +17 -0
- data/app/components/shadcn/badge_component.rb +49 -0
- data/app/components/shadcn/base_component.rb +100 -0
- data/app/components/shadcn/breadcrumb_component.rb +70 -0
- data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
- data/app/components/shadcn/button_component.rb +141 -0
- data/app/components/shadcn/button_group_component.rb +69 -0
- data/app/components/shadcn/calendar_component.rb +337 -0
- data/app/components/shadcn/card_action_component.rb +10 -0
- data/app/components/shadcn/card_component.rb +63 -0
- data/app/components/shadcn/card_content_component.rb +19 -0
- data/app/components/shadcn/card_description_component.rb +12 -0
- data/app/components/shadcn/card_footer_component.rb +12 -0
- data/app/components/shadcn/card_header_component.rb +24 -0
- data/app/components/shadcn/card_title_component.rb +18 -0
- data/app/components/shadcn/carousel_component.rb +275 -0
- data/app/components/shadcn/checkbox_component.rb +103 -0
- data/app/components/shadcn/collapsible_component.rb +66 -0
- data/app/components/shadcn/collapsible_content_component.rb +28 -0
- data/app/components/shadcn/combobox_component.rb +322 -0
- data/app/components/shadcn/command_component.rb +52 -0
- data/app/components/shadcn/command_dialog_component.rb +76 -0
- data/app/components/shadcn/command_empty_component.rb +12 -0
- data/app/components/shadcn/command_group_component.rb +34 -0
- data/app/components/shadcn/command_input_component.rb +59 -0
- data/app/components/shadcn/command_item_component.rb +48 -0
- data/app/components/shadcn/command_list_component.rb +38 -0
- data/app/components/shadcn/command_separator_component.rb +12 -0
- data/app/components/shadcn/command_shortcut_component.rb +12 -0
- data/app/components/shadcn/context_menu_component.rb +64 -0
- data/app/components/shadcn/context_menu_content_component.rb +44 -0
- data/app/components/shadcn/context_menu_item_component.rb +63 -0
- data/app/components/shadcn/context_menu_label_component.rb +18 -0
- data/app/components/shadcn/context_menu_separator_component.rb +12 -0
- data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/date_picker_component.rb +368 -0
- data/app/components/shadcn/dialog_component.rb +77 -0
- data/app/components/shadcn/dialog_content_component.rb +91 -0
- data/app/components/shadcn/dialog_description_component.rb +12 -0
- data/app/components/shadcn/dialog_footer_component.rb +12 -0
- data/app/components/shadcn/dialog_header_component.rb +19 -0
- data/app/components/shadcn/dialog_title_component.rb +12 -0
- data/app/components/shadcn/drawer_component.rb +72 -0
- data/app/components/shadcn/drawer_content_component.rb +76 -0
- data/app/components/shadcn/drawer_description_component.rb +12 -0
- data/app/components/shadcn/drawer_footer_component.rb +12 -0
- data/app/components/shadcn/drawer_header_component.rb +19 -0
- data/app/components/shadcn/drawer_title_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_component.rb +75 -0
- data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
- data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
- data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
- data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
- data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/empty_component.rb +48 -0
- data/app/components/shadcn/empty_content_component.rb +12 -0
- data/app/components/shadcn/empty_description_component.rb +12 -0
- data/app/components/shadcn/empty_header_component.rb +29 -0
- data/app/components/shadcn/empty_media_component.rb +21 -0
- data/app/components/shadcn/empty_title_component.rb +12 -0
- data/app/components/shadcn/field_component.rb +113 -0
- data/app/components/shadcn/hover_card_component.rb +64 -0
- data/app/components/shadcn/hover_card_content_component.rb +36 -0
- data/app/components/shadcn/input_component.rb +108 -0
- data/app/components/shadcn/input_group_component.rb +70 -0
- data/app/components/shadcn/input_otp_component.rb +183 -0
- data/app/components/shadcn/item_actions_component.rb +12 -0
- data/app/components/shadcn/item_component.rb +98 -0
- data/app/components/shadcn/item_content_component.rb +24 -0
- data/app/components/shadcn/item_description_component.rb +12 -0
- data/app/components/shadcn/item_footer_component.rb +12 -0
- data/app/components/shadcn/item_group_component.rb +24 -0
- data/app/components/shadcn/item_header_component.rb +12 -0
- data/app/components/shadcn/item_media_component.rb +22 -0
- data/app/components/shadcn/item_separator_component.rb +12 -0
- data/app/components/shadcn/item_title_component.rb +12 -0
- data/app/components/shadcn/kbd_component.rb +36 -0
- data/app/components/shadcn/label_component.rb +49 -0
- data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/menubar_component.rb +56 -0
- data/app/components/shadcn/menubar_content_component.rb +64 -0
- data/app/components/shadcn/menubar_item_component.rb +65 -0
- data/app/components/shadcn/menubar_label_component.rb +27 -0
- data/app/components/shadcn/menubar_menu_component.rb +34 -0
- data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
- data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
- data/app/components/shadcn/menubar_separator_component.rb +22 -0
- data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
- data/app/components/shadcn/menubar_sub_component.rb +38 -0
- data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
- data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
- data/app/components/shadcn/menubar_trigger_component.rb +31 -0
- data/app/components/shadcn/native_select_component.rb +150 -0
- data/app/components/shadcn/navigation_menu_component.rb +76 -0
- data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
- data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
- data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
- data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
- data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
- data/app/components/shadcn/pagination_component.rb +195 -0
- data/app/components/shadcn/pagination_content_component.rb +47 -0
- data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
- data/app/components/shadcn/pagination_item_component.rb +53 -0
- data/app/components/shadcn/pagination_next_component.rb +48 -0
- data/app/components/shadcn/pagination_previous_component.rb +48 -0
- data/app/components/shadcn/popover_component.rb +76 -0
- data/app/components/shadcn/popover_content_component.rb +25 -0
- data/app/components/shadcn/progress_component.rb +77 -0
- data/app/components/shadcn/radio_group_component.rb +129 -0
- data/app/components/shadcn/radio_group_item_component.rb +109 -0
- data/app/components/shadcn/resizable_handle_component.rb +98 -0
- data/app/components/shadcn/resizable_panel_component.rb +56 -0
- data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
- data/app/components/shadcn/scroll_area_component.rb +110 -0
- data/app/components/shadcn/select_component.rb +151 -0
- data/app/components/shadcn/select_group_component.rb +32 -0
- data/app/components/shadcn/select_item_component.rb +59 -0
- data/app/components/shadcn/select_separator_component.rb +12 -0
- data/app/components/shadcn/separator_component.rb +54 -0
- data/app/components/shadcn/sheet_component.rb +82 -0
- data/app/components/shadcn/sheet_content_component.rb +95 -0
- data/app/components/shadcn/sheet_description_component.rb +12 -0
- data/app/components/shadcn/sheet_footer_component.rb +12 -0
- data/app/components/shadcn/sheet_header_component.rb +19 -0
- data/app/components/shadcn/sheet_title_component.rb +12 -0
- data/app/components/shadcn/sidebar_component.rb +180 -0
- data/app/components/shadcn/sidebar_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_footer_component.rb +24 -0
- data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
- data/app/components/shadcn/sidebar_group_component.rb +38 -0
- data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
- data/app/components/shadcn/sidebar_header_component.rb +24 -0
- data/app/components/shadcn/sidebar_inset_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
- data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
- data/app/components/shadcn/sidebar_menu_component.rb +32 -0
- data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
- data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
- data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
- data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
- data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
- data/app/components/shadcn/sidebar_provider_component.rb +57 -0
- data/app/components/shadcn/sidebar_rail_component.rb +30 -0
- data/app/components/shadcn/sidebar_separator_component.rb +24 -0
- data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
- data/app/components/shadcn/skeleton_component.rb +29 -0
- data/app/components/shadcn/slider_component.rb +76 -0
- data/app/components/shadcn/spinner_component.rb +67 -0
- data/app/components/shadcn/switch_component.rb +147 -0
- data/app/components/shadcn/table_body_component.rb +16 -0
- data/app/components/shadcn/table_caption_component.rb +12 -0
- data/app/components/shadcn/table_cell_component.rb +12 -0
- data/app/components/shadcn/table_component.rb +57 -0
- data/app/components/shadcn/table_footer_component.rb +16 -0
- data/app/components/shadcn/table_head_component.rb +12 -0
- data/app/components/shadcn/table_header_component.rb +16 -0
- data/app/components/shadcn/table_row_component.rb +40 -0
- data/app/components/shadcn/tabs_component.rb +78 -0
- data/app/components/shadcn/tabs_content_component.rb +32 -0
- data/app/components/shadcn/tabs_list_component.rb +30 -0
- data/app/components/shadcn/tabs_trigger_component.rb +37 -0
- data/app/components/shadcn/textarea_component.rb +84 -0
- data/app/components/shadcn/toast_action_component.rb +18 -0
- data/app/components/shadcn/toast_component.rb +114 -0
- data/app/components/shadcn/toast_description_component.rb +12 -0
- data/app/components/shadcn/toast_title_component.rb +12 -0
- data/app/components/shadcn/toast_viewport_component.rb +12 -0
- data/app/components/shadcn/toggle_component.rb +77 -0
- data/app/components/shadcn/toggle_group_component.rb +96 -0
- data/app/components/shadcn/toggle_group_item_component.rb +62 -0
- data/app/components/shadcn/tooltip_component.rb +89 -0
- data/app/components/shadcn/typography_component.rb +112 -0
- data/babel.config.cjs +5 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/config/importmap.rb +5 -0
- data/fly.toml +26 -0
- data/jest.config.js +19 -0
- data/jest.setup.js +8 -0
- data/lib/generators/shadcn/component/component_generator.rb +188 -0
- data/lib/generators/shadcn/install/install_generator.rb +140 -0
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
- data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
- data/lib/shadcn/rails/class_merger.rb +228 -0
- data/lib/shadcn/rails/configuration.rb +341 -0
- data/lib/shadcn/rails/engine.rb +59 -0
- data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
- data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
- data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
- data/lib/shadcn/rails/version.rb +7 -0
- data/lib/shadcn/rails.rb +179 -0
- data/package-lock.json +7415 -0
- data/package.json +68 -0
- data/rollup.config.js +29 -0
- data/sig/shadcn/rails.rbs +6 -0
- 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
|
+
})
|