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,1007 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
|
2
|
+
import PopoverController from "../../app/assets/javascripts/shadcn/controllers/popover_controller.js"
|
|
3
|
+
import { setupController, cleanupController, click, wait, nextFrame, keydown, waitForEvent } from '../helpers/stimulus-test-helper.js'
|
|
4
|
+
|
|
5
|
+
describe("PopoverController", () => {
|
|
6
|
+
let application
|
|
7
|
+
let element
|
|
8
|
+
let controller
|
|
9
|
+
|
|
10
|
+
const createPopoverHTML = (options = {}) => {
|
|
11
|
+
const {
|
|
12
|
+
open = false,
|
|
13
|
+
side = "bottom",
|
|
14
|
+
align = "center",
|
|
15
|
+
modal = false,
|
|
16
|
+
includeContent = true,
|
|
17
|
+
includeTrigger = true
|
|
18
|
+
} = options
|
|
19
|
+
|
|
20
|
+
const openAttr = open ? 'data-shadcn--popover-open-value="true"' : ''
|
|
21
|
+
const sideAttr = side !== "bottom" ? `data-shadcn--popover-side-value="${side}"` : ''
|
|
22
|
+
const alignAttr = align !== "center" ? `data-shadcn--popover-align-value="${align}"` : ''
|
|
23
|
+
const modalAttr = modal ? 'data-shadcn--popover-modal-value="true"' : ''
|
|
24
|
+
|
|
25
|
+
const triggerHTML = includeTrigger ? `
|
|
26
|
+
<button data-shadcn--popover-target="trigger" data-action="click->shadcn--popover#toggle">
|
|
27
|
+
Open
|
|
28
|
+
</button>
|
|
29
|
+
` : ''
|
|
30
|
+
|
|
31
|
+
const contentHTML = includeContent ? `
|
|
32
|
+
<div data-shadcn--popover-target="content" hidden>
|
|
33
|
+
Popover content
|
|
34
|
+
</div>
|
|
35
|
+
` : ''
|
|
36
|
+
|
|
37
|
+
return `
|
|
38
|
+
<div data-controller="shadcn--popover"
|
|
39
|
+
${openAttr}
|
|
40
|
+
${sideAttr}
|
|
41
|
+
${alignAttr}
|
|
42
|
+
${modalAttr}>
|
|
43
|
+
${triggerHTML}
|
|
44
|
+
${contentHTML}
|
|
45
|
+
</div>
|
|
46
|
+
`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
application = Application.start()
|
|
51
|
+
application.register("shadcn--popover", PopoverController)
|
|
52
|
+
document.body.innerHTML = createPopoverHTML()
|
|
53
|
+
|
|
54
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
55
|
+
|
|
56
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
57
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
if (application) {
|
|
62
|
+
application.stop()
|
|
63
|
+
}
|
|
64
|
+
document.body.innerHTML = ""
|
|
65
|
+
// Reset body styles
|
|
66
|
+
document.body.style.pointerEvents = ""
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("initialization", () => {
|
|
70
|
+
test("connects successfully", () => {
|
|
71
|
+
expect(controller).not.toBeNull()
|
|
72
|
+
expect(controller).toBeDefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("initializes with default values", () => {
|
|
76
|
+
expect(controller.openValue).toBe(false)
|
|
77
|
+
expect(controller.sideValue).toBe("bottom")
|
|
78
|
+
expect(controller.alignValue).toBe("center")
|
|
79
|
+
expect(controller.modalValue).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("initializes with custom values", async () => {
|
|
83
|
+
application.stop()
|
|
84
|
+
document.body.innerHTML = createPopoverHTML({
|
|
85
|
+
open: true,
|
|
86
|
+
side: "top",
|
|
87
|
+
align: "start",
|
|
88
|
+
modal: true
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
application = Application.start()
|
|
92
|
+
application.register("shadcn--popover", PopoverController)
|
|
93
|
+
|
|
94
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
95
|
+
|
|
96
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
97
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
98
|
+
|
|
99
|
+
expect(controller.openValue).toBe(true)
|
|
100
|
+
expect(controller.sideValue).toBe("top")
|
|
101
|
+
expect(controller.alignValue).toBe("start")
|
|
102
|
+
expect(controller.modalValue).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("respects open=true value on initialization", async () => {
|
|
106
|
+
application.stop()
|
|
107
|
+
document.body.innerHTML = createPopoverHTML({ open: true })
|
|
108
|
+
|
|
109
|
+
application = Application.start()
|
|
110
|
+
application.register("shadcn--popover", PopoverController)
|
|
111
|
+
|
|
112
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
113
|
+
|
|
114
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
115
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
116
|
+
|
|
117
|
+
// The openValue should be set to true from the data attribute
|
|
118
|
+
expect(controller.openValue).toBe(true)
|
|
119
|
+
|
|
120
|
+
// Note: Due to the guard clause in show(), when openValue is already true,
|
|
121
|
+
// the connect() method calls show() but it returns early, so the content
|
|
122
|
+
// state is not set. This is actual controller behavior.
|
|
123
|
+
// To properly open on init, the value would need to be set after connect.
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("keeps popover hidden when initialized with open=false", () => {
|
|
127
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
128
|
+
expect(content.hidden).toBe(true)
|
|
129
|
+
expect(content.dataset.state).toBeUndefined()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("handles missing trigger target gracefully", async () => {
|
|
133
|
+
application.stop()
|
|
134
|
+
document.body.innerHTML = createPopoverHTML({ includeTrigger: false })
|
|
135
|
+
|
|
136
|
+
application = Application.start()
|
|
137
|
+
application.register("shadcn--popover", PopoverController)
|
|
138
|
+
|
|
139
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
140
|
+
|
|
141
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
142
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
143
|
+
|
|
144
|
+
expect(controller).toBeDefined()
|
|
145
|
+
expect(controller.hasTriggerTarget).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("handles missing content target gracefully", async () => {
|
|
149
|
+
application.stop()
|
|
150
|
+
document.body.innerHTML = createPopoverHTML({ includeContent: false })
|
|
151
|
+
|
|
152
|
+
application = Application.start()
|
|
153
|
+
application.register("shadcn--popover", PopoverController)
|
|
154
|
+
|
|
155
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
156
|
+
|
|
157
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
158
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
159
|
+
|
|
160
|
+
expect(controller).toBeDefined()
|
|
161
|
+
expect(controller.hasContentTarget).toBe(false)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("toggle behavior", () => {
|
|
166
|
+
test("toggle opens closed popover", () => {
|
|
167
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
168
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
169
|
+
|
|
170
|
+
expect(controller.openValue).toBe(false)
|
|
171
|
+
|
|
172
|
+
click(trigger)
|
|
173
|
+
|
|
174
|
+
expect(controller.openValue).toBe(true)
|
|
175
|
+
expect(content.hidden).toBe(false)
|
|
176
|
+
expect(content.dataset.state).toBe("open")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("toggle closes open popover", async () => {
|
|
180
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
181
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
182
|
+
|
|
183
|
+
// Open first
|
|
184
|
+
click(trigger)
|
|
185
|
+
expect(controller.openValue).toBe(true)
|
|
186
|
+
|
|
187
|
+
// Close
|
|
188
|
+
click(trigger)
|
|
189
|
+
expect(controller.openValue).toBe(false)
|
|
190
|
+
expect(content.dataset.state).toBe("closed")
|
|
191
|
+
|
|
192
|
+
// Content should be hidden after animation delay
|
|
193
|
+
await wait(200)
|
|
194
|
+
expect(content.hidden).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test("toggle prevents default event behavior", () => {
|
|
198
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
199
|
+
|
|
200
|
+
let defaultPrevented = false
|
|
201
|
+
const event = new MouseEvent('click', {
|
|
202
|
+
bubbles: true,
|
|
203
|
+
cancelable: true,
|
|
204
|
+
view: window
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const originalPreventDefault = event.preventDefault
|
|
208
|
+
event.preventDefault = function() {
|
|
209
|
+
defaultPrevented = true
|
|
210
|
+
return originalPreventDefault.apply(this, arguments)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
trigger.dispatchEvent(event)
|
|
214
|
+
|
|
215
|
+
expect(defaultPrevented).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test("multiple toggles work correctly", async () => {
|
|
219
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
220
|
+
|
|
221
|
+
click(trigger) // Open
|
|
222
|
+
expect(controller.openValue).toBe(true)
|
|
223
|
+
|
|
224
|
+
click(trigger) // Close
|
|
225
|
+
expect(controller.openValue).toBe(false)
|
|
226
|
+
|
|
227
|
+
await wait(200)
|
|
228
|
+
|
|
229
|
+
click(trigger) // Open again
|
|
230
|
+
expect(controller.openValue).toBe(true)
|
|
231
|
+
|
|
232
|
+
click(trigger) // Close again
|
|
233
|
+
expect(controller.openValue).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe("show method", () => {
|
|
238
|
+
test("show opens popover", () => {
|
|
239
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
240
|
+
|
|
241
|
+
controller.show()
|
|
242
|
+
|
|
243
|
+
expect(controller.openValue).toBe(true)
|
|
244
|
+
expect(content.hidden).toBe(false)
|
|
245
|
+
expect(content.dataset.state).toBe("open")
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("show is idempotent when already open", () => {
|
|
249
|
+
controller.show()
|
|
250
|
+
expect(controller.openValue).toBe(true)
|
|
251
|
+
|
|
252
|
+
controller.show()
|
|
253
|
+
expect(controller.openValue).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test("show adds click outside listener", () => {
|
|
257
|
+
let clickListenerAdded = false
|
|
258
|
+
const originalAddEventListener = document.addEventListener
|
|
259
|
+
|
|
260
|
+
document.addEventListener = function(event) {
|
|
261
|
+
if (event === 'click') {
|
|
262
|
+
clickListenerAdded = true
|
|
263
|
+
}
|
|
264
|
+
return originalAddEventListener.apply(this, arguments)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
controller.show()
|
|
268
|
+
|
|
269
|
+
expect(clickListenerAdded).toBe(true)
|
|
270
|
+
|
|
271
|
+
document.addEventListener = originalAddEventListener
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test("show dispatches opened event", async () => {
|
|
275
|
+
const eventPromise = waitForEvent(element, "shadcn--popover:opened")
|
|
276
|
+
|
|
277
|
+
controller.show()
|
|
278
|
+
|
|
279
|
+
const event = await eventPromise
|
|
280
|
+
expect(event).toBeDefined()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test("show sets side data attribute on content", () => {
|
|
284
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
285
|
+
|
|
286
|
+
controller.show()
|
|
287
|
+
|
|
288
|
+
expect(content.dataset.side).toBe("bottom")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test("show calls positionContent", () => {
|
|
292
|
+
let positionContentCalled = false
|
|
293
|
+
const originalPositionContent = controller.positionContent.bind(controller)
|
|
294
|
+
|
|
295
|
+
controller.positionContent = function() {
|
|
296
|
+
positionContentCalled = true
|
|
297
|
+
return originalPositionContent()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
controller.show()
|
|
301
|
+
|
|
302
|
+
expect(positionContentCalled).toBe(true)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe("hide method", () => {
|
|
307
|
+
test("hide closes open popover", async () => {
|
|
308
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
309
|
+
|
|
310
|
+
controller.show()
|
|
311
|
+
expect(controller.openValue).toBe(true)
|
|
312
|
+
|
|
313
|
+
controller.hide()
|
|
314
|
+
|
|
315
|
+
expect(controller.openValue).toBe(false)
|
|
316
|
+
expect(content.dataset.state).toBe("closed")
|
|
317
|
+
|
|
318
|
+
await wait(200)
|
|
319
|
+
expect(content.hidden).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test("hide is idempotent when already closed", () => {
|
|
323
|
+
expect(controller.openValue).toBe(false)
|
|
324
|
+
|
|
325
|
+
controller.hide()
|
|
326
|
+
|
|
327
|
+
expect(controller.openValue).toBe(false)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test("hide removes click outside listener", () => {
|
|
331
|
+
let clickListenerRemoved = false
|
|
332
|
+
const originalRemoveEventListener = document.removeEventListener
|
|
333
|
+
|
|
334
|
+
document.removeEventListener = function(event) {
|
|
335
|
+
if (event === 'click') {
|
|
336
|
+
clickListenerRemoved = true
|
|
337
|
+
}
|
|
338
|
+
return originalRemoveEventListener.apply(this, arguments)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
controller.show()
|
|
342
|
+
controller.hide()
|
|
343
|
+
|
|
344
|
+
expect(clickListenerRemoved).toBe(true)
|
|
345
|
+
|
|
346
|
+
document.removeEventListener = originalRemoveEventListener
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test("hide dispatches closed event", async () => {
|
|
350
|
+
controller.show()
|
|
351
|
+
|
|
352
|
+
const eventPromise = waitForEvent(element, "shadcn--popover:closed")
|
|
353
|
+
|
|
354
|
+
controller.hide()
|
|
355
|
+
|
|
356
|
+
const event = await eventPromise
|
|
357
|
+
expect(event).toBeDefined()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test("hide delays hiding content for animation", async () => {
|
|
361
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
362
|
+
|
|
363
|
+
controller.show()
|
|
364
|
+
controller.hide()
|
|
365
|
+
|
|
366
|
+
// Should be marked as closed but not hidden yet
|
|
367
|
+
expect(content.dataset.state).toBe("closed")
|
|
368
|
+
expect(content.hidden).toBe(false)
|
|
369
|
+
|
|
370
|
+
// After delay, should be hidden
|
|
371
|
+
await wait(200)
|
|
372
|
+
expect(content.hidden).toBe(true)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test("hide animation is cancelled if reopened", async () => {
|
|
376
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
377
|
+
|
|
378
|
+
controller.show()
|
|
379
|
+
controller.hide()
|
|
380
|
+
|
|
381
|
+
// Reopen before animation completes
|
|
382
|
+
await wait(50)
|
|
383
|
+
controller.show()
|
|
384
|
+
|
|
385
|
+
await wait(150)
|
|
386
|
+
|
|
387
|
+
// Should still be visible
|
|
388
|
+
expect(content.hidden).toBe(false)
|
|
389
|
+
expect(content.dataset.state).toBe("open")
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
describe("close method", () => {
|
|
394
|
+
test("close is an alias for hide", () => {
|
|
395
|
+
controller.show()
|
|
396
|
+
expect(controller.openValue).toBe(true)
|
|
397
|
+
|
|
398
|
+
controller.close()
|
|
399
|
+
|
|
400
|
+
expect(controller.openValue).toBe(false)
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe("click outside behavior", () => {
|
|
405
|
+
test("clicking outside closes popover", async () => {
|
|
406
|
+
controller.show()
|
|
407
|
+
expect(controller.openValue).toBe(true)
|
|
408
|
+
|
|
409
|
+
// Click outside
|
|
410
|
+
await nextFrame()
|
|
411
|
+
click(document.body)
|
|
412
|
+
|
|
413
|
+
expect(controller.openValue).toBe(false)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test("clicking inside popover does not close it", async () => {
|
|
417
|
+
controller.show()
|
|
418
|
+
expect(controller.openValue).toBe(true)
|
|
419
|
+
|
|
420
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
421
|
+
|
|
422
|
+
await nextFrame()
|
|
423
|
+
click(content)
|
|
424
|
+
|
|
425
|
+
expect(controller.openValue).toBe(true)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test("clicking trigger does not trigger click outside", async () => {
|
|
429
|
+
controller.show()
|
|
430
|
+
expect(controller.openValue).toBe(true)
|
|
431
|
+
|
|
432
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
433
|
+
|
|
434
|
+
await nextFrame()
|
|
435
|
+
click(trigger)
|
|
436
|
+
|
|
437
|
+
// Should toggle to closed via toggle action, not click outside
|
|
438
|
+
expect(controller.openValue).toBe(false)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test("click outside listener is not added when closed", () => {
|
|
442
|
+
let clickListenerAdded = false
|
|
443
|
+
const originalAddEventListener = document.addEventListener
|
|
444
|
+
|
|
445
|
+
document.addEventListener = function(event) {
|
|
446
|
+
if (event === 'click') {
|
|
447
|
+
clickListenerAdded = true
|
|
448
|
+
}
|
|
449
|
+
return originalAddEventListener.apply(this, arguments)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Don't call show
|
|
453
|
+
expect(clickListenerAdded).toBe(false)
|
|
454
|
+
|
|
455
|
+
document.addEventListener = originalAddEventListener
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe("modal behavior", () => {
|
|
460
|
+
test("modal=true disables pointer events on body when open", async () => {
|
|
461
|
+
application.stop()
|
|
462
|
+
document.body.innerHTML = createPopoverHTML({ modal: true })
|
|
463
|
+
|
|
464
|
+
application = Application.start()
|
|
465
|
+
application.register("shadcn--popover", PopoverController)
|
|
466
|
+
|
|
467
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
468
|
+
|
|
469
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
470
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
471
|
+
|
|
472
|
+
controller.show()
|
|
473
|
+
|
|
474
|
+
expect(document.body.style.pointerEvents).toBe("none")
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
test("modal=true enables pointer events on content when open", async () => {
|
|
478
|
+
application.stop()
|
|
479
|
+
document.body.innerHTML = createPopoverHTML({ modal: true })
|
|
480
|
+
|
|
481
|
+
application = Application.start()
|
|
482
|
+
application.register("shadcn--popover", PopoverController)
|
|
483
|
+
|
|
484
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
485
|
+
|
|
486
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
487
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
488
|
+
|
|
489
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
490
|
+
|
|
491
|
+
controller.show()
|
|
492
|
+
|
|
493
|
+
expect(content.style.pointerEvents).toBe("auto")
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
test("modal=true restores pointer events on body when closed", async () => {
|
|
497
|
+
application.stop()
|
|
498
|
+
document.body.innerHTML = createPopoverHTML({ modal: true })
|
|
499
|
+
|
|
500
|
+
application = Application.start()
|
|
501
|
+
application.register("shadcn--popover", PopoverController)
|
|
502
|
+
|
|
503
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
504
|
+
|
|
505
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
506
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
507
|
+
|
|
508
|
+
controller.show()
|
|
509
|
+
expect(document.body.style.pointerEvents).toBe("none")
|
|
510
|
+
|
|
511
|
+
controller.hide()
|
|
512
|
+
expect(document.body.style.pointerEvents).toBe("")
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
test("modal=false does not affect pointer events", () => {
|
|
516
|
+
controller.show()
|
|
517
|
+
|
|
518
|
+
expect(document.body.style.pointerEvents).toBe("")
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe("positioning - side", () => {
|
|
523
|
+
test("positions content on bottom by default", () => {
|
|
524
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
525
|
+
|
|
526
|
+
controller.show()
|
|
527
|
+
|
|
528
|
+
expect(content.style.position).toBe("absolute")
|
|
529
|
+
expect(content.style.top).toBe("100%")
|
|
530
|
+
expect(content.style.bottom).toBe("auto")
|
|
531
|
+
expect(content.style.marginTop).toBe("8px")
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
test("positions content on top", async () => {
|
|
535
|
+
application.stop()
|
|
536
|
+
document.body.innerHTML = createPopoverHTML({ side: "top" })
|
|
537
|
+
|
|
538
|
+
application = Application.start()
|
|
539
|
+
application.register("shadcn--popover", PopoverController)
|
|
540
|
+
|
|
541
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
542
|
+
|
|
543
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
544
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
545
|
+
|
|
546
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
547
|
+
|
|
548
|
+
controller.show()
|
|
549
|
+
|
|
550
|
+
expect(content.style.position).toBe("absolute")
|
|
551
|
+
expect(content.style.bottom).toBe("100%")
|
|
552
|
+
expect(content.style.top).toBe("auto")
|
|
553
|
+
expect(content.style.marginBottom).toBe("8px")
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test("positions content on left", async () => {
|
|
557
|
+
application.stop()
|
|
558
|
+
document.body.innerHTML = createPopoverHTML({ side: "left" })
|
|
559
|
+
|
|
560
|
+
application = Application.start()
|
|
561
|
+
application.register("shadcn--popover", PopoverController)
|
|
562
|
+
|
|
563
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
564
|
+
|
|
565
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
566
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
567
|
+
|
|
568
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
569
|
+
|
|
570
|
+
controller.show()
|
|
571
|
+
|
|
572
|
+
expect(content.style.position).toBe("absolute")
|
|
573
|
+
expect(content.style.right).toBe("100%")
|
|
574
|
+
expect(content.style.left).toBe("auto")
|
|
575
|
+
expect(content.style.marginRight).toBe("8px")
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
test("positions content on right", async () => {
|
|
579
|
+
application.stop()
|
|
580
|
+
document.body.innerHTML = createPopoverHTML({ side: "right" })
|
|
581
|
+
|
|
582
|
+
application = Application.start()
|
|
583
|
+
application.register("shadcn--popover", PopoverController)
|
|
584
|
+
|
|
585
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
586
|
+
|
|
587
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
588
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
589
|
+
|
|
590
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
591
|
+
|
|
592
|
+
controller.show()
|
|
593
|
+
|
|
594
|
+
expect(content.style.position).toBe("absolute")
|
|
595
|
+
expect(content.style.left).toBe("100%")
|
|
596
|
+
expect(content.style.right).toBe("auto")
|
|
597
|
+
expect(content.style.marginLeft).toBe("8px")
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test("sets data-side attribute on content", async () => {
|
|
601
|
+
application.stop()
|
|
602
|
+
document.body.innerHTML = createPopoverHTML({ side: "right" })
|
|
603
|
+
|
|
604
|
+
application = Application.start()
|
|
605
|
+
application.register("shadcn--popover", PopoverController)
|
|
606
|
+
|
|
607
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
608
|
+
|
|
609
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
610
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
611
|
+
|
|
612
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
613
|
+
|
|
614
|
+
controller.show()
|
|
615
|
+
|
|
616
|
+
expect(content.dataset.side).toBe("right")
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
describe("positioning - align", () => {
|
|
621
|
+
test("aligns content to center by default on bottom side", () => {
|
|
622
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
623
|
+
|
|
624
|
+
controller.show()
|
|
625
|
+
|
|
626
|
+
expect(content.style.left).toBe("50%")
|
|
627
|
+
expect(content.style.transform).toBe("translateX(-50%)")
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
test("aligns content to start", async () => {
|
|
631
|
+
application.stop()
|
|
632
|
+
document.body.innerHTML = createPopoverHTML({ align: "start" })
|
|
633
|
+
|
|
634
|
+
application = Application.start()
|
|
635
|
+
application.register("shadcn--popover", PopoverController)
|
|
636
|
+
|
|
637
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
638
|
+
|
|
639
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
640
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
641
|
+
|
|
642
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
643
|
+
|
|
644
|
+
controller.show()
|
|
645
|
+
|
|
646
|
+
expect(content.style.left).toBe("0px")
|
|
647
|
+
expect(content.style.right).toBe("auto")
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
test("aligns content to end", async () => {
|
|
651
|
+
application.stop()
|
|
652
|
+
document.body.innerHTML = createPopoverHTML({ align: "end" })
|
|
653
|
+
|
|
654
|
+
application = Application.start()
|
|
655
|
+
application.register("shadcn--popover", PopoverController)
|
|
656
|
+
|
|
657
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
658
|
+
|
|
659
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
660
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
661
|
+
|
|
662
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
663
|
+
|
|
664
|
+
controller.show()
|
|
665
|
+
|
|
666
|
+
expect(content.style.right).toBe("0px")
|
|
667
|
+
expect(content.style.left).toBe("auto")
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test("center alignment only applies transform on top/bottom sides", async () => {
|
|
671
|
+
application.stop()
|
|
672
|
+
document.body.innerHTML = createPopoverHTML({ side: "left", align: "center" })
|
|
673
|
+
|
|
674
|
+
application = Application.start()
|
|
675
|
+
application.register("shadcn--popover", PopoverController)
|
|
676
|
+
|
|
677
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
678
|
+
|
|
679
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
680
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
681
|
+
|
|
682
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
683
|
+
|
|
684
|
+
controller.show()
|
|
685
|
+
|
|
686
|
+
// Should not have transform for left/right sides
|
|
687
|
+
expect(content.style.transform).toBe("")
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
describe("positionContent edge cases", () => {
|
|
692
|
+
test("does not position if content target missing", async () => {
|
|
693
|
+
application.stop()
|
|
694
|
+
document.body.innerHTML = createPopoverHTML({ includeContent: false })
|
|
695
|
+
|
|
696
|
+
application = Application.start()
|
|
697
|
+
application.register("shadcn--popover", PopoverController)
|
|
698
|
+
|
|
699
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
700
|
+
|
|
701
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
702
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
703
|
+
|
|
704
|
+
// Should not throw
|
|
705
|
+
expect(() => controller.positionContent()).not.toThrow()
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
test("does not position if trigger target missing", async () => {
|
|
709
|
+
application.stop()
|
|
710
|
+
document.body.innerHTML = createPopoverHTML({ includeTrigger: false })
|
|
711
|
+
|
|
712
|
+
application = Application.start()
|
|
713
|
+
application.register("shadcn--popover", PopoverController)
|
|
714
|
+
|
|
715
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
716
|
+
|
|
717
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
718
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
719
|
+
|
|
720
|
+
// Should not throw
|
|
721
|
+
expect(() => controller.positionContent()).not.toThrow()
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
describe("event dispatching", () => {
|
|
726
|
+
test("dispatches opened event when opening", async () => {
|
|
727
|
+
const eventPromise = waitForEvent(element, "shadcn--popover:opened")
|
|
728
|
+
|
|
729
|
+
controller.show()
|
|
730
|
+
|
|
731
|
+
const event = await eventPromise
|
|
732
|
+
expect(event.type).toBe("shadcn--popover:opened")
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
test("dispatches closed event when closing", async () => {
|
|
736
|
+
controller.show()
|
|
737
|
+
|
|
738
|
+
const eventPromise = waitForEvent(element, "shadcn--popover:closed")
|
|
739
|
+
|
|
740
|
+
controller.hide()
|
|
741
|
+
|
|
742
|
+
const event = await eventPromise
|
|
743
|
+
expect(event.type).toBe("shadcn--popover:closed")
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
test("opened event bubbles", async () => {
|
|
747
|
+
let eventCaught = false
|
|
748
|
+
|
|
749
|
+
document.body.addEventListener("shadcn--popover:opened", () => {
|
|
750
|
+
eventCaught = true
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
controller.show()
|
|
754
|
+
|
|
755
|
+
await nextFrame()
|
|
756
|
+
|
|
757
|
+
expect(eventCaught).toBe(true)
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
test("closed event bubbles", async () => {
|
|
761
|
+
controller.show()
|
|
762
|
+
|
|
763
|
+
let eventCaught = false
|
|
764
|
+
|
|
765
|
+
document.body.addEventListener("shadcn--popover:closed", () => {
|
|
766
|
+
eventCaught = true
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
controller.hide()
|
|
770
|
+
|
|
771
|
+
await nextFrame()
|
|
772
|
+
|
|
773
|
+
expect(eventCaught).toBe(true)
|
|
774
|
+
})
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
describe("disconnect cleanup", () => {
|
|
778
|
+
test("closes popover on disconnect", () => {
|
|
779
|
+
controller.show()
|
|
780
|
+
expect(controller.openValue).toBe(true)
|
|
781
|
+
|
|
782
|
+
controller.disconnect()
|
|
783
|
+
|
|
784
|
+
expect(controller.openValue).toBe(false)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test("removes click outside listener on disconnect", () => {
|
|
788
|
+
let clickListenerRemoved = false
|
|
789
|
+
const originalRemoveEventListener = document.removeEventListener
|
|
790
|
+
|
|
791
|
+
document.removeEventListener = function(event) {
|
|
792
|
+
if (event === 'click') {
|
|
793
|
+
clickListenerRemoved = true
|
|
794
|
+
}
|
|
795
|
+
return originalRemoveEventListener.apply(this, arguments)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
controller.show()
|
|
799
|
+
controller.disconnect()
|
|
800
|
+
|
|
801
|
+
expect(clickListenerRemoved).toBe(true)
|
|
802
|
+
|
|
803
|
+
document.removeEventListener = originalRemoveEventListener
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
test("restores body pointer events on disconnect when modal", async () => {
|
|
807
|
+
application.stop()
|
|
808
|
+
document.body.innerHTML = createPopoverHTML({ modal: true })
|
|
809
|
+
|
|
810
|
+
application = Application.start()
|
|
811
|
+
application.register("shadcn--popover", PopoverController)
|
|
812
|
+
|
|
813
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
814
|
+
|
|
815
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
816
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
817
|
+
|
|
818
|
+
controller.show()
|
|
819
|
+
expect(document.body.style.pointerEvents).toBe("none")
|
|
820
|
+
|
|
821
|
+
controller.disconnect()
|
|
822
|
+
|
|
823
|
+
expect(document.body.style.pointerEvents).toBe("")
|
|
824
|
+
})
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
describe("ARIA attributes", () => {
|
|
828
|
+
test("trigger can have aria-haspopup", () => {
|
|
829
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
830
|
+
|
|
831
|
+
// Set manually as this would typically be in the HTML
|
|
832
|
+
trigger.setAttribute('aria-haspopup', 'dialog')
|
|
833
|
+
|
|
834
|
+
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
test("trigger can have aria-expanded", () => {
|
|
838
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
839
|
+
|
|
840
|
+
// Set manually as this would typically be managed in the HTML/component
|
|
841
|
+
trigger.setAttribute('aria-expanded', 'false')
|
|
842
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
843
|
+
|
|
844
|
+
controller.show()
|
|
845
|
+
trigger.setAttribute('aria-expanded', 'true')
|
|
846
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('true')
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
test("content can have role dialog", () => {
|
|
850
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
851
|
+
|
|
852
|
+
// Set manually as this would typically be in the HTML
|
|
853
|
+
content.setAttribute('role', 'dialog')
|
|
854
|
+
|
|
855
|
+
expect(content.getAttribute('role')).toBe('dialog')
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
describe("edge cases", () => {
|
|
860
|
+
test("rapid open/close transitions", async () => {
|
|
861
|
+
controller.show()
|
|
862
|
+
controller.hide()
|
|
863
|
+
controller.show()
|
|
864
|
+
controller.hide()
|
|
865
|
+
controller.show()
|
|
866
|
+
|
|
867
|
+
expect(controller.openValue).toBe(true)
|
|
868
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
869
|
+
expect(content.hidden).toBe(false)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
test("handles getBoundingClientRect on trigger", () => {
|
|
873
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
874
|
+
|
|
875
|
+
let getBoundingClientRectCalled = false
|
|
876
|
+
|
|
877
|
+
// Mock getBoundingClientRect
|
|
878
|
+
const originalGetBoundingClientRect = trigger.getBoundingClientRect.bind(trigger)
|
|
879
|
+
trigger.getBoundingClientRect = function() {
|
|
880
|
+
getBoundingClientRectCalled = true
|
|
881
|
+
return originalGetBoundingClientRect()
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
controller.show()
|
|
885
|
+
|
|
886
|
+
expect(getBoundingClientRectCalled).toBe(true)
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
test("handles null event in toggle", () => {
|
|
890
|
+
// Should not throw when called without event
|
|
891
|
+
expect(() => controller.toggle()).not.toThrow()
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
test("handles undefined event in toggle", () => {
|
|
895
|
+
expect(() => controller.toggle(undefined)).not.toThrow()
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
describe("integration scenarios", () => {
|
|
900
|
+
test("complete interaction flow: open, click outside, reopen", async () => {
|
|
901
|
+
const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
|
|
902
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
903
|
+
|
|
904
|
+
// Open
|
|
905
|
+
click(trigger)
|
|
906
|
+
expect(controller.openValue).toBe(true)
|
|
907
|
+
expect(content.hidden).toBe(false)
|
|
908
|
+
|
|
909
|
+
// Click outside
|
|
910
|
+
await nextFrame()
|
|
911
|
+
click(document.body)
|
|
912
|
+
expect(controller.openValue).toBe(false)
|
|
913
|
+
|
|
914
|
+
await wait(200)
|
|
915
|
+
expect(content.hidden).toBe(true)
|
|
916
|
+
|
|
917
|
+
// Reopen
|
|
918
|
+
click(trigger)
|
|
919
|
+
expect(controller.openValue).toBe(true)
|
|
920
|
+
expect(content.hidden).toBe(false)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
test("modal popover prevents background interaction", async () => {
|
|
924
|
+
application.stop()
|
|
925
|
+
document.body.innerHTML = `
|
|
926
|
+
<div>
|
|
927
|
+
<button id="background-button">Background</button>
|
|
928
|
+
${createPopoverHTML({ modal: true })}
|
|
929
|
+
</div>
|
|
930
|
+
`
|
|
931
|
+
|
|
932
|
+
application = Application.start()
|
|
933
|
+
application.register("shadcn--popover", PopoverController)
|
|
934
|
+
|
|
935
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
936
|
+
|
|
937
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
938
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
|
|
939
|
+
|
|
940
|
+
controller.show()
|
|
941
|
+
|
|
942
|
+
// Body should block pointer events
|
|
943
|
+
expect(document.body.style.pointerEvents).toBe("none")
|
|
944
|
+
|
|
945
|
+
// Content should allow pointer events
|
|
946
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
947
|
+
expect(content.style.pointerEvents).toBe("auto")
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
test("changing side value while open repositions content", async () => {
|
|
951
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
952
|
+
|
|
953
|
+
controller.show()
|
|
954
|
+
expect(content.style.top).toBe("100%")
|
|
955
|
+
expect(content.dataset.side).toBe("bottom")
|
|
956
|
+
|
|
957
|
+
// Change side value
|
|
958
|
+
controller.sideValue = "top"
|
|
959
|
+
// Need to update data attribute manually since positionContent doesn't do it
|
|
960
|
+
content.dataset.side = controller.sideValue
|
|
961
|
+
controller.positionContent()
|
|
962
|
+
|
|
963
|
+
expect(content.style.bottom).toBe("100%")
|
|
964
|
+
expect(content.dataset.side).toBe("top")
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
test("changing align value while open repositions content", () => {
|
|
968
|
+
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
969
|
+
|
|
970
|
+
controller.show()
|
|
971
|
+
expect(content.style.left).toBe("50%")
|
|
972
|
+
expect(content.style.transform).toBe("translateX(-50%)")
|
|
973
|
+
|
|
974
|
+
// Change align value
|
|
975
|
+
controller.alignValue = "start"
|
|
976
|
+
controller.positionContent()
|
|
977
|
+
|
|
978
|
+
expect(content.style.left).toBe("0px")
|
|
979
|
+
expect(content.style.right).toBe("auto")
|
|
980
|
+
})
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
describe("snapshots", () => {
|
|
984
|
+
test("renders closed popover correctly", () => {
|
|
985
|
+
expect(element.innerHTML).toMatchSnapshot()
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
test("renders open popover correctly", () => {
|
|
989
|
+
controller.show()
|
|
990
|
+
expect(element.innerHTML).toMatchSnapshot()
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
test("renders modal popover correctly", async () => {
|
|
994
|
+
application.stop()
|
|
995
|
+
document.body.innerHTML = createPopoverHTML({ modal: true, open: true })
|
|
996
|
+
|
|
997
|
+
application = Application.start()
|
|
998
|
+
application.register("shadcn--popover", PopoverController)
|
|
999
|
+
|
|
1000
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
1001
|
+
|
|
1002
|
+
element = document.querySelector('[data-controller="shadcn--popover"]')
|
|
1003
|
+
|
|
1004
|
+
expect(element.innerHTML).toMatchSnapshot()
|
|
1005
|
+
})
|
|
1006
|
+
})
|
|
1007
|
+
})
|