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,912 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
|
2
|
+
import CarouselController from "../../app/assets/javascripts/shadcn/controllers/carousel_controller.js"
|
|
3
|
+
import { setupController, cleanupController, click, wait, nextFrame, keydown, waitForEvent } from '../helpers/stimulus-test-helper.js'
|
|
4
|
+
|
|
5
|
+
describe("CarouselController", () => {
|
|
6
|
+
let application
|
|
7
|
+
let element
|
|
8
|
+
let controller
|
|
9
|
+
|
|
10
|
+
const createCarouselHTML = ({
|
|
11
|
+
orientation = "horizontal",
|
|
12
|
+
loop = false,
|
|
13
|
+
autoplay = false,
|
|
14
|
+
autoplayInterval = 4000,
|
|
15
|
+
align = "start",
|
|
16
|
+
selectedIndex = 0,
|
|
17
|
+
itemCount = 3
|
|
18
|
+
} = {}) => {
|
|
19
|
+
const items = Array.from({ length: itemCount }, (_, i) =>
|
|
20
|
+
`<div data-shadcn--carousel-target="item" data-index="${i}">Slide ${i + 1}</div>`
|
|
21
|
+
).join('')
|
|
22
|
+
|
|
23
|
+
return `
|
|
24
|
+
<div data-controller="shadcn--carousel"
|
|
25
|
+
data-shadcn--carousel-orientation-value="${orientation}"
|
|
26
|
+
data-shadcn--carousel-loop-value="${loop}"
|
|
27
|
+
data-shadcn--carousel-autoplay-value="${autoplay}"
|
|
28
|
+
data-shadcn--carousel-autoplay-interval-value="${autoplayInterval}"
|
|
29
|
+
data-shadcn--carousel-align-value="${align}"
|
|
30
|
+
data-shadcn--carousel-selected-index-value="${selectedIndex}">
|
|
31
|
+
<div data-shadcn--carousel-target="viewport">
|
|
32
|
+
<div data-shadcn--carousel-target="content">
|
|
33
|
+
${items}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<button data-shadcn--carousel-target="prevButton" data-action="click->shadcn--carousel#previous">Prev</button>
|
|
37
|
+
<button data-shadcn--carousel-target="nextButton" data-action="click->shadcn--carousel#next">Next</button>
|
|
38
|
+
</div>
|
|
39
|
+
`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const setupCarousel = async (options = {}) => {
|
|
43
|
+
application = Application.start()
|
|
44
|
+
application.register("shadcn--carousel", CarouselController)
|
|
45
|
+
document.body.innerHTML = createCarouselHTML(options)
|
|
46
|
+
|
|
47
|
+
await nextFrame()
|
|
48
|
+
|
|
49
|
+
element = document.querySelector('[data-controller="shadcn--carousel"]')
|
|
50
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--carousel")
|
|
51
|
+
|
|
52
|
+
return { application, element, controller }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
if (application) {
|
|
57
|
+
application.stop()
|
|
58
|
+
}
|
|
59
|
+
document.body.innerHTML = ""
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("Value Initialization", () => {
|
|
63
|
+
test("initializes with default values", async () => {
|
|
64
|
+
await setupCarousel()
|
|
65
|
+
|
|
66
|
+
expect(controller.orientationValue).toBe("horizontal")
|
|
67
|
+
expect(controller.loopValue).toBe(false)
|
|
68
|
+
expect(controller.autoplayValue).toBe(false)
|
|
69
|
+
expect(controller.autoplayIntervalValue).toBe(4000)
|
|
70
|
+
expect(controller.alignValue).toBe("start")
|
|
71
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("initializes with custom orientation value", async () => {
|
|
75
|
+
await setupCarousel({ orientation: "vertical" })
|
|
76
|
+
|
|
77
|
+
expect(controller.orientationValue).toBe("vertical")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("initializes with loop enabled", async () => {
|
|
81
|
+
await setupCarousel({ loop: true })
|
|
82
|
+
|
|
83
|
+
expect(controller.loopValue).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("initializes with autoplay enabled", async () => {
|
|
87
|
+
await setupCarousel({ autoplay: true })
|
|
88
|
+
|
|
89
|
+
expect(controller.autoplayValue).toBe(true)
|
|
90
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("initializes with custom autoplay interval", async () => {
|
|
94
|
+
await setupCarousel({ autoplayInterval: 2000 })
|
|
95
|
+
|
|
96
|
+
expect(controller.autoplayIntervalValue).toBe(2000)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("initializes with custom align value", async () => {
|
|
100
|
+
await setupCarousel({ align: "center" })
|
|
101
|
+
|
|
102
|
+
expect(controller.alignValue).toBe("center")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("initializes with custom selected index", async () => {
|
|
106
|
+
await setupCarousel({ selectedIndex: 1 })
|
|
107
|
+
|
|
108
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("Navigation - next()", () => {
|
|
113
|
+
test("advances to next slide", async () => {
|
|
114
|
+
await setupCarousel()
|
|
115
|
+
|
|
116
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
117
|
+
|
|
118
|
+
controller.next()
|
|
119
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
120
|
+
|
|
121
|
+
controller.next()
|
|
122
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("stops at last slide when loop is disabled", async () => {
|
|
126
|
+
await setupCarousel({ loop: false })
|
|
127
|
+
|
|
128
|
+
controller.selectedIndexValue = 2 // Last slide
|
|
129
|
+
|
|
130
|
+
controller.next()
|
|
131
|
+
expect(controller.selectedIndexValue).toBe(2) // Should stay at 2
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("wraps around to first slide when loop is enabled", async () => {
|
|
135
|
+
await setupCarousel({ loop: true })
|
|
136
|
+
|
|
137
|
+
controller.selectedIndexValue = 2 // Last slide
|
|
138
|
+
|
|
139
|
+
controller.next()
|
|
140
|
+
expect(controller.selectedIndexValue).toBe(0) // Should wrap to first
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("dispatches select event with correct index", async () => {
|
|
144
|
+
await setupCarousel()
|
|
145
|
+
|
|
146
|
+
const selectPromise = waitForEvent(element, "shadcn--carousel:select")
|
|
147
|
+
|
|
148
|
+
controller.next()
|
|
149
|
+
|
|
150
|
+
const event = await selectPromise
|
|
151
|
+
expect(event.detail.index).toBe(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("next button triggers next slide", async () => {
|
|
155
|
+
await setupCarousel()
|
|
156
|
+
|
|
157
|
+
const nextButton = element.querySelector('[data-shadcn--carousel-target="nextButton"]')
|
|
158
|
+
|
|
159
|
+
click(nextButton)
|
|
160
|
+
await nextFrame()
|
|
161
|
+
|
|
162
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe("Navigation - previous()", () => {
|
|
167
|
+
test("goes back to previous slide", async () => {
|
|
168
|
+
await setupCarousel({ selectedIndex: 2 })
|
|
169
|
+
|
|
170
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
171
|
+
|
|
172
|
+
controller.previous()
|
|
173
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
174
|
+
|
|
175
|
+
controller.previous()
|
|
176
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("stops at first slide when loop is disabled", async () => {
|
|
180
|
+
await setupCarousel({ loop: false, selectedIndex: 0 })
|
|
181
|
+
|
|
182
|
+
controller.previous()
|
|
183
|
+
expect(controller.selectedIndexValue).toBe(0) // Should stay at 0
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("wraps around to last slide when loop is enabled", async () => {
|
|
187
|
+
await setupCarousel({ loop: true, selectedIndex: 0 })
|
|
188
|
+
|
|
189
|
+
controller.previous()
|
|
190
|
+
expect(controller.selectedIndexValue).toBe(2) // Should wrap to last (index 2)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test("dispatches select event with correct index", async () => {
|
|
194
|
+
await setupCarousel({ selectedIndex: 1 })
|
|
195
|
+
|
|
196
|
+
const selectPromise = waitForEvent(element, "shadcn--carousel:select")
|
|
197
|
+
|
|
198
|
+
controller.previous()
|
|
199
|
+
|
|
200
|
+
const event = await selectPromise
|
|
201
|
+
expect(event.detail.index).toBe(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("previous button triggers previous slide", async () => {
|
|
205
|
+
await setupCarousel({ selectedIndex: 2 })
|
|
206
|
+
|
|
207
|
+
const prevButton = element.querySelector('[data-shadcn--carousel-target="prevButton"]')
|
|
208
|
+
|
|
209
|
+
click(prevButton)
|
|
210
|
+
await nextFrame()
|
|
211
|
+
|
|
212
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe("Loop Behavior", () => {
|
|
217
|
+
test("loop disabled - buttons disabled at boundaries", async () => {
|
|
218
|
+
await setupCarousel({ loop: false, selectedIndex: 0 })
|
|
219
|
+
|
|
220
|
+
const prevButton = element.querySelector('[data-shadcn--carousel-target="prevButton"]')
|
|
221
|
+
const nextButton = element.querySelector('[data-shadcn--carousel-target="nextButton"]')
|
|
222
|
+
|
|
223
|
+
// At first slide, prev should be disabled
|
|
224
|
+
expect(prevButton.disabled).toBe(true)
|
|
225
|
+
expect(nextButton.disabled).toBe(false)
|
|
226
|
+
|
|
227
|
+
// Navigate to last slide
|
|
228
|
+
controller.selectedIndexValue = 2
|
|
229
|
+
controller.updateButtonStates()
|
|
230
|
+
|
|
231
|
+
// At last slide, next should be disabled
|
|
232
|
+
expect(prevButton.disabled).toBe(false)
|
|
233
|
+
expect(nextButton.disabled).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test("loop enabled - buttons never disabled", async () => {
|
|
237
|
+
await setupCarousel({ loop: true, selectedIndex: 0 })
|
|
238
|
+
|
|
239
|
+
const prevButton = element.querySelector('[data-shadcn--carousel-target="prevButton"]')
|
|
240
|
+
const nextButton = element.querySelector('[data-shadcn--carousel-target="nextButton"]')
|
|
241
|
+
|
|
242
|
+
// At first slide
|
|
243
|
+
expect(prevButton.disabled).toBe(false)
|
|
244
|
+
expect(nextButton.disabled).toBe(false)
|
|
245
|
+
|
|
246
|
+
// At last slide
|
|
247
|
+
controller.selectedIndexValue = 2
|
|
248
|
+
controller.updateButtonStates()
|
|
249
|
+
|
|
250
|
+
expect(prevButton.disabled).toBe(false)
|
|
251
|
+
expect(nextButton.disabled).toBe(false)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test("loop enabled - continuous navigation forward", async () => {
|
|
255
|
+
await setupCarousel({ loop: true, itemCount: 3 })
|
|
256
|
+
|
|
257
|
+
const indices = []
|
|
258
|
+
for (let i = 0; i < 5; i++) {
|
|
259
|
+
indices.push(controller.selectedIndexValue)
|
|
260
|
+
controller.next()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
expect(indices).toEqual([0, 1, 2, 0, 1])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test("loop enabled - continuous navigation backward", async () => {
|
|
267
|
+
await setupCarousel({ loop: true, selectedIndex: 0, itemCount: 3 })
|
|
268
|
+
|
|
269
|
+
const indices = []
|
|
270
|
+
for (let i = 0; i < 5; i++) {
|
|
271
|
+
indices.push(controller.selectedIndexValue)
|
|
272
|
+
controller.previous()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(indices).toEqual([0, 2, 1, 0, 2])
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe("Autoplay", () => {
|
|
280
|
+
test("starts autoplay on connect when enabled", async () => {
|
|
281
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
282
|
+
|
|
283
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
284
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
285
|
+
|
|
286
|
+
// Wait for autoplay to advance
|
|
287
|
+
await wait(110)
|
|
288
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
289
|
+
|
|
290
|
+
await wait(100)
|
|
291
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test("does not start autoplay when disabled", async () => {
|
|
295
|
+
await setupCarousel({ autoplay: false })
|
|
296
|
+
|
|
297
|
+
expect(controller.autoplayTimer).toBeUndefined()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test("respects custom autoplay interval", async () => {
|
|
301
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 150 })
|
|
302
|
+
|
|
303
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
304
|
+
|
|
305
|
+
// Wait a bit less than the interval - should still be at 0
|
|
306
|
+
await wait(100)
|
|
307
|
+
expect(controller.selectedIndexValue).toBe(0) // Still at 0
|
|
308
|
+
|
|
309
|
+
// Wait for the interval to complete
|
|
310
|
+
await wait(60)
|
|
311
|
+
expect(controller.selectedIndexValue).toBe(1) // Now at 1
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test("pauses autoplay on mouse enter", async () => {
|
|
315
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
316
|
+
|
|
317
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
318
|
+
|
|
319
|
+
controller.mouseEnter()
|
|
320
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
321
|
+
|
|
322
|
+
// Timer should not advance slides
|
|
323
|
+
await wait(110)
|
|
324
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test("resumes autoplay on mouse leave", async () => {
|
|
328
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
329
|
+
|
|
330
|
+
controller.mouseEnter()
|
|
331
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
332
|
+
|
|
333
|
+
controller.mouseLeave()
|
|
334
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
335
|
+
|
|
336
|
+
await wait(110)
|
|
337
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test("pauses autoplay on touch start", async () => {
|
|
341
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
342
|
+
|
|
343
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
344
|
+
|
|
345
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
346
|
+
touches: [{ clientX: 100, clientY: 100 }]
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
350
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test("resumes autoplay after touch end", async () => {
|
|
354
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
355
|
+
|
|
356
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
357
|
+
|
|
358
|
+
// Touch start
|
|
359
|
+
controller.touchStartX = 100
|
|
360
|
+
controller.touchStartY = 100
|
|
361
|
+
controller.stopAutoplay()
|
|
362
|
+
|
|
363
|
+
// Touch end (small swipe, below threshold)
|
|
364
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
365
|
+
changedTouches: [{ clientX: 110, clientY: 100 }]
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
369
|
+
|
|
370
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test("autoplay wraps around with loop enabled", async () => {
|
|
374
|
+
await setupCarousel({ autoplay: true, loop: true, autoplayInterval: 100 })
|
|
375
|
+
|
|
376
|
+
controller.selectedIndexValue = 2 // Last slide
|
|
377
|
+
|
|
378
|
+
await wait(110)
|
|
379
|
+
expect(controller.selectedIndexValue).toBe(0) // Should wrap to first
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test("autoplay stops at end with loop disabled", async () => {
|
|
383
|
+
await setupCarousel({ autoplay: true, loop: false, autoplayInterval: 100 })
|
|
384
|
+
|
|
385
|
+
controller.selectedIndexValue = 2 // Last slide
|
|
386
|
+
|
|
387
|
+
await wait(110)
|
|
388
|
+
expect(controller.selectedIndexValue).toBe(2) // Should stay at last
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe("Orientation", () => {
|
|
393
|
+
test("horizontal orientation - ArrowLeft goes to previous", async () => {
|
|
394
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
395
|
+
|
|
396
|
+
keydown(element, "ArrowLeft")
|
|
397
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test("horizontal orientation - ArrowRight goes to next", async () => {
|
|
401
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 0 })
|
|
402
|
+
|
|
403
|
+
keydown(element, "ArrowRight")
|
|
404
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test("horizontal orientation - ArrowUp/Down do nothing", async () => {
|
|
408
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
409
|
+
|
|
410
|
+
keydown(element, "ArrowUp")
|
|
411
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
412
|
+
|
|
413
|
+
keydown(element, "ArrowDown")
|
|
414
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test("vertical orientation - ArrowUp goes to previous", async () => {
|
|
418
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 1 })
|
|
419
|
+
|
|
420
|
+
keydown(element, "ArrowUp")
|
|
421
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test("vertical orientation - ArrowDown goes to next", async () => {
|
|
425
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 0 })
|
|
426
|
+
|
|
427
|
+
keydown(element, "ArrowDown")
|
|
428
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test("vertical orientation - ArrowLeft/Right do nothing", async () => {
|
|
432
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 1 })
|
|
433
|
+
|
|
434
|
+
keydown(element, "ArrowLeft")
|
|
435
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
436
|
+
|
|
437
|
+
keydown(element, "ArrowRight")
|
|
438
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
describe("Selected Index", () => {
|
|
443
|
+
test("updates aria-hidden on slides when index changes", async () => {
|
|
444
|
+
await setupCarousel()
|
|
445
|
+
|
|
446
|
+
const items = element.querySelectorAll('[data-shadcn--carousel-target="item"]')
|
|
447
|
+
|
|
448
|
+
// Initial state - first slide visible
|
|
449
|
+
expect(items[0].getAttribute('aria-hidden')).toBe('false')
|
|
450
|
+
expect(items[1].getAttribute('aria-hidden')).toBe('true')
|
|
451
|
+
expect(items[2].getAttribute('aria-hidden')).toBe('true')
|
|
452
|
+
|
|
453
|
+
// Navigate to second slide
|
|
454
|
+
controller.selectedIndexValue = 1
|
|
455
|
+
controller.scrollToIndex(1)
|
|
456
|
+
|
|
457
|
+
expect(items[0].getAttribute('aria-hidden')).toBe('true')
|
|
458
|
+
expect(items[1].getAttribute('aria-hidden')).toBe('false')
|
|
459
|
+
expect(items[2].getAttribute('aria-hidden')).toBe('true')
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test("updates inert property on slides when index changes", async () => {
|
|
463
|
+
await setupCarousel()
|
|
464
|
+
|
|
465
|
+
const items = element.querySelectorAll('[data-shadcn--carousel-target="item"]')
|
|
466
|
+
|
|
467
|
+
// Initial state
|
|
468
|
+
expect(items[0].inert).toBe(false)
|
|
469
|
+
expect(items[1].inert).toBe(true)
|
|
470
|
+
expect(items[2].inert).toBe(true)
|
|
471
|
+
|
|
472
|
+
// Navigate to third slide
|
|
473
|
+
controller.selectedIndexValue = 2
|
|
474
|
+
controller.scrollToIndex(2)
|
|
475
|
+
|
|
476
|
+
expect(items[0].inert).toBe(true)
|
|
477
|
+
expect(items[1].inert).toBe(true)
|
|
478
|
+
expect(items[2].inert).toBe(false)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test("selectedIndexValueChanged triggers scrollToIndex", async () => {
|
|
482
|
+
await setupCarousel()
|
|
483
|
+
|
|
484
|
+
// Track the current index before change
|
|
485
|
+
const previousIndex = controller.selectedIndexValue
|
|
486
|
+
|
|
487
|
+
controller.selectedIndexValue = 1
|
|
488
|
+
|
|
489
|
+
// Verify index changed
|
|
490
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
491
|
+
expect(previousIndex).toBe(0)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test("persists selected index through interactions", async () => {
|
|
495
|
+
await setupCarousel({ selectedIndex: 1 })
|
|
496
|
+
|
|
497
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
498
|
+
|
|
499
|
+
controller.next()
|
|
500
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
501
|
+
|
|
502
|
+
controller.previous()
|
|
503
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe("Timer Cleanup", () => {
|
|
508
|
+
test("clears autoplay timer on disconnect", async () => {
|
|
509
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
510
|
+
|
|
511
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
512
|
+
|
|
513
|
+
// Stop autoplay manually before disconnecting
|
|
514
|
+
controller.stopAutoplay()
|
|
515
|
+
|
|
516
|
+
// Timer should be cleared
|
|
517
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
518
|
+
|
|
519
|
+
// Wait - if timer wasn't cleared, it would cause errors
|
|
520
|
+
await wait(110)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
test("clears autoplay timer when stopAutoplay is called", async () => {
|
|
524
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
525
|
+
|
|
526
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
527
|
+
|
|
528
|
+
controller.stopAutoplay()
|
|
529
|
+
|
|
530
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test("no memory leaks - multiple start/stop cycles", async () => {
|
|
534
|
+
await setupCarousel({ autoplay: false })
|
|
535
|
+
|
|
536
|
+
// Start and stop multiple times
|
|
537
|
+
for (let i = 0; i < 5; i++) {
|
|
538
|
+
controller.autoplayValue = true
|
|
539
|
+
controller.startAutoplay()
|
|
540
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
541
|
+
|
|
542
|
+
controller.stopAutoplay()
|
|
543
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
test("clears old timer when starting new autoplay", async () => {
|
|
548
|
+
await setupCarousel({ autoplay: true, autoplayInterval: 100 })
|
|
549
|
+
|
|
550
|
+
const firstTimerId = controller.autoplayTimer
|
|
551
|
+
|
|
552
|
+
// Start autoplay again (should clear old timer)
|
|
553
|
+
controller.startAutoplay()
|
|
554
|
+
|
|
555
|
+
const secondTimerId = controller.autoplayTimer
|
|
556
|
+
|
|
557
|
+
expect(secondTimerId).toBeDefined()
|
|
558
|
+
expect(firstTimerId).not.toBe(secondTimerId)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test("removes event listeners on disconnect", async () => {
|
|
562
|
+
await setupCarousel()
|
|
563
|
+
|
|
564
|
+
// Verify bound event handlers exist
|
|
565
|
+
expect(controller.boundHandleKeydown).toBeDefined()
|
|
566
|
+
expect(controller.boundHandleTouchStart).toBeDefined()
|
|
567
|
+
expect(controller.boundHandleTouchEnd).toBeDefined()
|
|
568
|
+
|
|
569
|
+
// Test that disconnect actually calls stopAutoplay
|
|
570
|
+
controller.autoplayValue = true
|
|
571
|
+
controller.startAutoplay()
|
|
572
|
+
expect(controller.autoplayTimer).toBeDefined()
|
|
573
|
+
|
|
574
|
+
// Manually call disconnect to test cleanup
|
|
575
|
+
controller.disconnect()
|
|
576
|
+
|
|
577
|
+
// Timer should be cleared after disconnect
|
|
578
|
+
expect(controller.autoplayTimer).toBeNull()
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe("Keyboard Navigation", () => {
|
|
583
|
+
test("prevents default on arrow key navigation", async () => {
|
|
584
|
+
await setupCarousel({ orientation: "horizontal" })
|
|
585
|
+
|
|
586
|
+
let defaultPrevented = false
|
|
587
|
+
|
|
588
|
+
const event = new KeyboardEvent('keydown', {
|
|
589
|
+
key: 'ArrowRight',
|
|
590
|
+
bubbles: true,
|
|
591
|
+
cancelable: true
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
// Override preventDefault to track if it was called
|
|
595
|
+
const originalPreventDefault = event.preventDefault
|
|
596
|
+
event.preventDefault = function() {
|
|
597
|
+
defaultPrevented = true
|
|
598
|
+
originalPreventDefault.call(this)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
element.dispatchEvent(event)
|
|
602
|
+
|
|
603
|
+
expect(defaultPrevented).toBe(true)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test("horizontal carousel ignores non-arrow keys", async () => {
|
|
607
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
608
|
+
|
|
609
|
+
keydown(element, "Enter")
|
|
610
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
611
|
+
|
|
612
|
+
keydown(element, "Space")
|
|
613
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
614
|
+
|
|
615
|
+
keydown(element, "Tab")
|
|
616
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test("vertical carousel ignores non-arrow keys", async () => {
|
|
620
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 1 })
|
|
621
|
+
|
|
622
|
+
keydown(element, "Enter")
|
|
623
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
624
|
+
|
|
625
|
+
keydown(element, "Space")
|
|
626
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
test("keyboard navigation respects loop setting", async () => {
|
|
630
|
+
await setupCarousel({ orientation: "horizontal", loop: false, selectedIndex: 0 })
|
|
631
|
+
|
|
632
|
+
keydown(element, "ArrowLeft")
|
|
633
|
+
expect(controller.selectedIndexValue).toBe(0) // Should not wrap
|
|
634
|
+
|
|
635
|
+
controller.loopValue = true
|
|
636
|
+
keydown(element, "ArrowLeft")
|
|
637
|
+
expect(controller.selectedIndexValue).toBe(2) // Should wrap to last
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
describe("goToSlide", () => {
|
|
642
|
+
test("navigates to specific slide by index", async () => {
|
|
643
|
+
await setupCarousel()
|
|
644
|
+
|
|
645
|
+
const mockEvent = {
|
|
646
|
+
currentTarget: { dataset: { index: "2" } }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
controller.goToSlide(mockEvent)
|
|
650
|
+
|
|
651
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test("dispatches select event when going to slide", async () => {
|
|
655
|
+
await setupCarousel()
|
|
656
|
+
|
|
657
|
+
const selectPromise = waitForEvent(element, "shadcn--carousel:select")
|
|
658
|
+
|
|
659
|
+
const mockEvent = {
|
|
660
|
+
currentTarget: { dataset: { index: "1" } }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
controller.goToSlide(mockEvent)
|
|
664
|
+
|
|
665
|
+
const event = await selectPromise
|
|
666
|
+
expect(event.detail.index).toBe(1)
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test("ignores invalid index values", async () => {
|
|
670
|
+
await setupCarousel({ selectedIndex: 1 })
|
|
671
|
+
|
|
672
|
+
const mockEvent = {
|
|
673
|
+
currentTarget: { dataset: { index: "invalid" } }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
controller.goToSlide(mockEvent)
|
|
677
|
+
|
|
678
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test("ignores out of range index values", async () => {
|
|
682
|
+
await setupCarousel({ selectedIndex: 1 })
|
|
683
|
+
|
|
684
|
+
// Index too high
|
|
685
|
+
let mockEvent = {
|
|
686
|
+
currentTarget: { dataset: { index: "10" } }
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
controller.goToSlide(mockEvent)
|
|
690
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
691
|
+
|
|
692
|
+
// Index negative
|
|
693
|
+
mockEvent = {
|
|
694
|
+
currentTarget: { dataset: { index: "-1" } }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
controller.goToSlide(mockEvent)
|
|
698
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
699
|
+
})
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
describe("Touch/Swipe Support", () => {
|
|
703
|
+
test("horizontal swipe left triggers next", async () => {
|
|
704
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 0 })
|
|
705
|
+
|
|
706
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
707
|
+
|
|
708
|
+
// Swipe left (large enough to trigger - threshold is 50)
|
|
709
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
710
|
+
touches: [{ clientX: 200, clientY: 100 }]
|
|
711
|
+
})
|
|
712
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
713
|
+
|
|
714
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
715
|
+
changedTouches: [{ clientX: 100, clientY: 100 }]
|
|
716
|
+
})
|
|
717
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
718
|
+
|
|
719
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
test("horizontal swipe right triggers previous", async () => {
|
|
723
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
724
|
+
|
|
725
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
726
|
+
|
|
727
|
+
// Swipe right
|
|
728
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
729
|
+
touches: [{ clientX: 100, clientY: 100 }]
|
|
730
|
+
})
|
|
731
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
732
|
+
|
|
733
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
734
|
+
changedTouches: [{ clientX: 200, clientY: 100 }]
|
|
735
|
+
})
|
|
736
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
737
|
+
|
|
738
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
test("vertical swipe down triggers previous", async () => {
|
|
742
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 1 })
|
|
743
|
+
|
|
744
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
745
|
+
|
|
746
|
+
// Swipe down (positive deltaY)
|
|
747
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
748
|
+
touches: [{ clientX: 100, clientY: 100 }]
|
|
749
|
+
})
|
|
750
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
751
|
+
|
|
752
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
753
|
+
changedTouches: [{ clientX: 100, clientY: 200 }]
|
|
754
|
+
})
|
|
755
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
756
|
+
|
|
757
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
test("vertical swipe up triggers next", async () => {
|
|
761
|
+
await setupCarousel({ orientation: "vertical", selectedIndex: 0 })
|
|
762
|
+
|
|
763
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
764
|
+
|
|
765
|
+
// Swipe up (negative deltaY)
|
|
766
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
767
|
+
touches: [{ clientX: 100, clientY: 200 }]
|
|
768
|
+
})
|
|
769
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
770
|
+
|
|
771
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
772
|
+
changedTouches: [{ clientX: 100, clientY: 100 }]
|
|
773
|
+
})
|
|
774
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
775
|
+
|
|
776
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
test("small swipe below threshold does not trigger navigation", async () => {
|
|
780
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
781
|
+
|
|
782
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
783
|
+
|
|
784
|
+
// Small swipe (threshold is 50)
|
|
785
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
786
|
+
touches: [{ clientX: 100, clientY: 100 }]
|
|
787
|
+
})
|
|
788
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
789
|
+
|
|
790
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
791
|
+
changedTouches: [{ clientX: 130, clientY: 100 }]
|
|
792
|
+
})
|
|
793
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
794
|
+
|
|
795
|
+
expect(controller.selectedIndexValue).toBe(1) // Unchanged
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
test("diagonal swipe with stronger horizontal component triggers horizontal navigation", async () => {
|
|
799
|
+
await setupCarousel({ orientation: "horizontal", selectedIndex: 1 })
|
|
800
|
+
|
|
801
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
802
|
+
|
|
803
|
+
// Diagonal swipe with stronger horizontal
|
|
804
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
805
|
+
touches: [{ clientX: 200, clientY: 100 }]
|
|
806
|
+
})
|
|
807
|
+
viewport.dispatchEvent(touchStartEvent)
|
|
808
|
+
|
|
809
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
810
|
+
changedTouches: [{ clientX: 100, clientY: 120 }]
|
|
811
|
+
})
|
|
812
|
+
viewport.dispatchEvent(touchEndEvent)
|
|
813
|
+
|
|
814
|
+
expect(controller.selectedIndexValue).toBe(2) // Next slide
|
|
815
|
+
})
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
describe("Align Offset Calculation", () => {
|
|
819
|
+
test("start alignment returns 0 offset", async () => {
|
|
820
|
+
await setupCarousel({ align: "start" })
|
|
821
|
+
|
|
822
|
+
const mockItem = { offsetWidth: 100, offsetHeight: 100 }
|
|
823
|
+
const offset = controller.getAlignOffset(mockItem, "width")
|
|
824
|
+
|
|
825
|
+
expect(offset).toBe(0)
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
test("center alignment calculates correct offset", async () => {
|
|
829
|
+
await setupCarousel({ align: "center" })
|
|
830
|
+
|
|
831
|
+
// Mock viewport size
|
|
832
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
833
|
+
Object.defineProperty(viewport, 'offsetWidth', { value: 500, configurable: true })
|
|
834
|
+
|
|
835
|
+
const mockItem = { offsetWidth: 100 }
|
|
836
|
+
const offset = controller.getAlignOffset(mockItem, "width")
|
|
837
|
+
|
|
838
|
+
expect(offset).toBe(200) // (500 - 100) / 2
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
test("end alignment calculates correct offset", async () => {
|
|
842
|
+
await setupCarousel({ align: "end" })
|
|
843
|
+
|
|
844
|
+
const viewport = element.querySelector('[data-shadcn--carousel-target="viewport"]')
|
|
845
|
+
Object.defineProperty(viewport, 'offsetWidth', { value: 500, configurable: true })
|
|
846
|
+
|
|
847
|
+
const mockItem = { offsetWidth: 100 }
|
|
848
|
+
const offset = controller.getAlignOffset(mockItem, "width")
|
|
849
|
+
|
|
850
|
+
expect(offset).toBe(400) // 500 - 100
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
describe("Edge Cases", () => {
|
|
855
|
+
test("handles carousel with single item", async () => {
|
|
856
|
+
await setupCarousel({ itemCount: 1 })
|
|
857
|
+
|
|
858
|
+
controller.next()
|
|
859
|
+
expect(controller.selectedIndexValue).toBe(0) // Stays at 0
|
|
860
|
+
|
|
861
|
+
controller.previous()
|
|
862
|
+
expect(controller.selectedIndexValue).toBe(0) // Stays at 0
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
test("handles carousel with no items gracefully", async () => {
|
|
866
|
+
await setupCarousel({ itemCount: 0 })
|
|
867
|
+
|
|
868
|
+
expect(() => {
|
|
869
|
+
controller.next()
|
|
870
|
+
controller.previous()
|
|
871
|
+
controller.scrollToIndex(0)
|
|
872
|
+
}).not.toThrow()
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
test("updateButtonStates handles missing button targets", async () => {
|
|
876
|
+
application = Application.start()
|
|
877
|
+
application.register("shadcn--carousel", CarouselController)
|
|
878
|
+
|
|
879
|
+
// Create carousel without button targets
|
|
880
|
+
document.body.innerHTML = `
|
|
881
|
+
<div data-controller="shadcn--carousel">
|
|
882
|
+
<div data-shadcn--carousel-target="viewport">
|
|
883
|
+
<div data-shadcn--carousel-target="content">
|
|
884
|
+
<div data-shadcn--carousel-target="item">Slide 1</div>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
`
|
|
889
|
+
|
|
890
|
+
await nextFrame()
|
|
891
|
+
|
|
892
|
+
element = document.querySelector('[data-controller="shadcn--carousel"]')
|
|
893
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--carousel")
|
|
894
|
+
|
|
895
|
+
expect(() => {
|
|
896
|
+
controller.updateButtonStates()
|
|
897
|
+
}).not.toThrow()
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
test("scrollToIndex handles missing content target", async () => {
|
|
901
|
+
await setupCarousel()
|
|
902
|
+
|
|
903
|
+
// Remove content target
|
|
904
|
+
const content = element.querySelector('[data-shadcn--carousel-target="content"]')
|
|
905
|
+
content.removeAttribute('data-shadcn--carousel-target')
|
|
906
|
+
|
|
907
|
+
expect(() => {
|
|
908
|
+
controller.scrollToIndex(1)
|
|
909
|
+
}).not.toThrow()
|
|
910
|
+
})
|
|
911
|
+
})
|
|
912
|
+
})
|