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,907 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
|
2
|
+
import TabsController from "../../app/assets/javascripts/shadcn/controllers/tabs_controller.js"
|
|
3
|
+
import { setupController, cleanupController, click, wait, nextFrame, keydown, mockLocation, mockHistory, waitForEvent } from '../helpers/stimulus-test-helper.js'
|
|
4
|
+
|
|
5
|
+
describe("TabsController", () => {
|
|
6
|
+
let application
|
|
7
|
+
let element
|
|
8
|
+
let controller
|
|
9
|
+
|
|
10
|
+
const createTabsHTML = (options = {}) => {
|
|
11
|
+
const {
|
|
12
|
+
defaultValue = "tab1",
|
|
13
|
+
urlParam = null,
|
|
14
|
+
tabCount = 3
|
|
15
|
+
} = options
|
|
16
|
+
|
|
17
|
+
const urlParamAttr = urlParam ? `data-shadcn--tabs-url-param-value="${urlParam}"` : ''
|
|
18
|
+
|
|
19
|
+
const triggers = Array.from({ length: tabCount }, (_, i) => {
|
|
20
|
+
const tabNum = i + 1
|
|
21
|
+
return `<button data-shadcn--tabs-target="trigger" data-value="tab${tabNum}" role="tab" data-action="click->shadcn--tabs#selectTab keydown->shadcn--tabs#handleKeydown">Tab ${tabNum}</button>`
|
|
22
|
+
}).join('\n')
|
|
23
|
+
|
|
24
|
+
const contents = Array.from({ length: tabCount }, (_, i) => {
|
|
25
|
+
const tabNum = i + 1
|
|
26
|
+
const hidden = tabNum === 1 ? '' : 'hidden'
|
|
27
|
+
return `<div data-shadcn--tabs-target="content" data-value="tab${tabNum}" role="tabpanel" ${hidden}>Content ${tabNum}</div>`
|
|
28
|
+
}).join('\n')
|
|
29
|
+
|
|
30
|
+
return `
|
|
31
|
+
<div data-controller="shadcn--tabs"
|
|
32
|
+
data-shadcn--tabs-default-value-value="${defaultValue}"
|
|
33
|
+
${urlParamAttr}>
|
|
34
|
+
<div data-shadcn--tabs-target="list" role="tablist">
|
|
35
|
+
${triggers}
|
|
36
|
+
</div>
|
|
37
|
+
${contents}
|
|
38
|
+
</div>
|
|
39
|
+
`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
application = Application.start()
|
|
44
|
+
application.register("shadcn--tabs", TabsController)
|
|
45
|
+
document.body.innerHTML = createTabsHTML()
|
|
46
|
+
|
|
47
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
48
|
+
|
|
49
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
50
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--tabs")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
if (application) {
|
|
55
|
+
application.stop()
|
|
56
|
+
}
|
|
57
|
+
document.body.innerHTML = ""
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("initialization", () => {
|
|
61
|
+
test("connects successfully", () => {
|
|
62
|
+
expect(controller).not.toBeNull()
|
|
63
|
+
expect(controller).toBeDefined()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("initializes with default value", () => {
|
|
67
|
+
const trigger = element.querySelector('[data-value="tab1"]')
|
|
68
|
+
expect(trigger.dataset.state).toBe("active")
|
|
69
|
+
expect(trigger.getAttribute("aria-selected")).toBe("true")
|
|
70
|
+
expect(trigger.tabIndex).toBe(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("sets first tab as active when no default value", async () => {
|
|
74
|
+
application.stop()
|
|
75
|
+
document.body.innerHTML = createTabsHTML({ defaultValue: "" })
|
|
76
|
+
|
|
77
|
+
application = Application.start()
|
|
78
|
+
application.register("shadcn--tabs", TabsController)
|
|
79
|
+
|
|
80
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
81
|
+
|
|
82
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
83
|
+
const trigger = element.querySelector('[data-value="tab1"]')
|
|
84
|
+
|
|
85
|
+
expect(trigger.dataset.state).toBe("active")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("initializes inactive tabs correctly", () => {
|
|
89
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
90
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
91
|
+
|
|
92
|
+
expect(tab2.dataset.state).toBe("inactive")
|
|
93
|
+
expect(tab2.getAttribute("aria-selected")).toBe("false")
|
|
94
|
+
expect(tab2.tabIndex).toBe(-1)
|
|
95
|
+
|
|
96
|
+
expect(tab3.dataset.state).toBe("inactive")
|
|
97
|
+
expect(tab3.getAttribute("aria-selected")).toBe("false")
|
|
98
|
+
expect(tab3.tabIndex).toBe(-1)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("shows correct content panel on initialization", () => {
|
|
102
|
+
const content1 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab1"]')
|
|
103
|
+
const content2 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab2"]')
|
|
104
|
+
const content3 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab3"]')
|
|
105
|
+
|
|
106
|
+
expect(content1.dataset.state).toBe("active")
|
|
107
|
+
expect(content1.hidden).toBe(false)
|
|
108
|
+
|
|
109
|
+
expect(content2.dataset.state).toBe("inactive")
|
|
110
|
+
expect(content2.hidden).toBe(true)
|
|
111
|
+
|
|
112
|
+
expect(content3.dataset.state).toBe("inactive")
|
|
113
|
+
expect(content3.hidden).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("validates initial value and falls back to default if invalid", async () => {
|
|
117
|
+
application.stop()
|
|
118
|
+
|
|
119
|
+
const restoreLocation = mockLocation("http://localhost?tab=invalid")
|
|
120
|
+
|
|
121
|
+
document.body.innerHTML = `
|
|
122
|
+
<div data-controller="shadcn--tabs"
|
|
123
|
+
data-shadcn--tabs-default-value-value="tab2"
|
|
124
|
+
data-shadcn--tabs-url-param-value="tab">
|
|
125
|
+
<div data-shadcn--tabs-target="list" role="tablist">
|
|
126
|
+
<button data-shadcn--tabs-target="trigger" data-value="tab1" role="tab" data-action="click->shadcn--tabs#selectTab">Tab 1</button>
|
|
127
|
+
<button data-shadcn--tabs-target="trigger" data-value="tab2" role="tab" data-action="click->shadcn--tabs#selectTab">Tab 2</button>
|
|
128
|
+
</div>
|
|
129
|
+
<div data-shadcn--tabs-target="content" data-value="tab1" role="tabpanel">Content 1</div>
|
|
130
|
+
<div data-shadcn--tabs-target="content" data-value="tab2" role="tabpanel" hidden>Content 2</div>
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
|
|
134
|
+
application = Application.start()
|
|
135
|
+
application.register("shadcn--tabs", TabsController)
|
|
136
|
+
|
|
137
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
138
|
+
|
|
139
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
140
|
+
|
|
141
|
+
// Should fall back to default value "tab2"
|
|
142
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
143
|
+
expect(tab2.dataset.state).toBe("active")
|
|
144
|
+
|
|
145
|
+
restoreLocation()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("tab switching", () => {
|
|
150
|
+
test("switches to clicked tab", () => {
|
|
151
|
+
const tab2Trigger = element.querySelector('[data-value="tab2"]')
|
|
152
|
+
click(tab2Trigger)
|
|
153
|
+
|
|
154
|
+
expect(tab2Trigger.dataset.state).toBe("active")
|
|
155
|
+
expect(tab2Trigger.getAttribute("aria-selected")).toBe("true")
|
|
156
|
+
expect(tab2Trigger.tabIndex).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("deactivates previously active tab", () => {
|
|
160
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
161
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
162
|
+
|
|
163
|
+
expect(tab1.dataset.state).toBe("active")
|
|
164
|
+
|
|
165
|
+
click(tab2)
|
|
166
|
+
|
|
167
|
+
expect(tab1.dataset.state).toBe("inactive")
|
|
168
|
+
expect(tab1.getAttribute("aria-selected")).toBe("false")
|
|
169
|
+
expect(tab1.tabIndex).toBe(-1)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test("shows correct content panel when tab is clicked", () => {
|
|
173
|
+
const tab2Trigger = element.querySelector('[data-value="tab2"]')
|
|
174
|
+
const content1 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab1"]')
|
|
175
|
+
const content2 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab2"]')
|
|
176
|
+
|
|
177
|
+
click(tab2Trigger)
|
|
178
|
+
|
|
179
|
+
expect(content1.dataset.state).toBe("inactive")
|
|
180
|
+
expect(content1.hidden).toBe(true)
|
|
181
|
+
|
|
182
|
+
expect(content2.dataset.state).toBe("active")
|
|
183
|
+
expect(content2.hidden).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("can switch between multiple tabs", () => {
|
|
187
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
188
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
189
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
190
|
+
|
|
191
|
+
click(tab2)
|
|
192
|
+
expect(tab2.dataset.state).toBe("active")
|
|
193
|
+
|
|
194
|
+
click(tab3)
|
|
195
|
+
expect(tab3.dataset.state).toBe("active")
|
|
196
|
+
expect(tab2.dataset.state).toBe("inactive")
|
|
197
|
+
|
|
198
|
+
click(tab1)
|
|
199
|
+
expect(tab1.dataset.state).toBe("active")
|
|
200
|
+
expect(tab3.dataset.state).toBe("inactive")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test("dispatches change event with correct value", async () => {
|
|
204
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
205
|
+
|
|
206
|
+
const eventPromise = waitForEvent(element, "shadcn--tabs:change")
|
|
207
|
+
|
|
208
|
+
click(tab2)
|
|
209
|
+
|
|
210
|
+
const event = await eventPromise
|
|
211
|
+
expect(event.detail.value).toBe("tab2")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("dispatches change event when switching tabs", async () => {
|
|
215
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
216
|
+
|
|
217
|
+
let changeEventFired = false
|
|
218
|
+
element.addEventListener("shadcn--tabs:change", () => {
|
|
219
|
+
changeEventFired = true
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
click(tab2)
|
|
223
|
+
|
|
224
|
+
await nextFrame()
|
|
225
|
+
|
|
226
|
+
expect(changeEventFired).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe("content visibility", () => {
|
|
231
|
+
test("only selected tab content is visible", () => {
|
|
232
|
+
const allContents = element.querySelectorAll('[data-shadcn--tabs-target="content"]')
|
|
233
|
+
|
|
234
|
+
// Initially tab1 is active
|
|
235
|
+
expect(allContents[0].hidden).toBe(false)
|
|
236
|
+
expect(allContents[1].hidden).toBe(true)
|
|
237
|
+
expect(allContents[2].hidden).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test("content visibility updates when switching tabs", () => {
|
|
241
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
242
|
+
const allContents = element.querySelectorAll('[data-shadcn--tabs-target="content"]')
|
|
243
|
+
|
|
244
|
+
click(tab3)
|
|
245
|
+
|
|
246
|
+
expect(allContents[0].hidden).toBe(true)
|
|
247
|
+
expect(allContents[1].hidden).toBe(true)
|
|
248
|
+
expect(allContents[2].hidden).toBe(false)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test("content state attribute matches visibility", () => {
|
|
252
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
253
|
+
const content2 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab2"]')
|
|
254
|
+
|
|
255
|
+
click(tab2)
|
|
256
|
+
|
|
257
|
+
expect(content2.dataset.state).toBe("active")
|
|
258
|
+
expect(content2.hidden).toBe(false)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe("URL sync", () => {
|
|
263
|
+
let historyMock
|
|
264
|
+
let restoreLocation
|
|
265
|
+
|
|
266
|
+
beforeEach(async () => {
|
|
267
|
+
application.stop()
|
|
268
|
+
document.body.innerHTML = ""
|
|
269
|
+
|
|
270
|
+
// Mock window.location
|
|
271
|
+
restoreLocation = mockLocation("http://localhost/")
|
|
272
|
+
|
|
273
|
+
// Mock history methods
|
|
274
|
+
historyMock = mockHistory()
|
|
275
|
+
|
|
276
|
+
// Create tabs with URL param
|
|
277
|
+
document.body.innerHTML = createTabsHTML({ urlParam: "tab" })
|
|
278
|
+
|
|
279
|
+
application = Application.start()
|
|
280
|
+
application.register("shadcn--tabs", TabsController)
|
|
281
|
+
|
|
282
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
283
|
+
|
|
284
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
285
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--tabs")
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
historyMock.restore()
|
|
290
|
+
restoreLocation()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test("updates URL when tab is clicked", () => {
|
|
294
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
295
|
+
click(tab2)
|
|
296
|
+
|
|
297
|
+
expect(historyMock.calls.replaceState.length).toBe(1)
|
|
298
|
+
expect(historyMock.calls.replaceState[0].url).toContain("tab=tab2")
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test("does not update URL on initial load", () => {
|
|
302
|
+
// Should not have called replaceState during initialization
|
|
303
|
+
expect(historyMock.calls.replaceState.length).toBe(0)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test("reads initial tab from URL parameter", async () => {
|
|
307
|
+
application.stop()
|
|
308
|
+
historyMock.restore()
|
|
309
|
+
restoreLocation()
|
|
310
|
+
|
|
311
|
+
const newRestoreLocation = mockLocation("http://localhost?tab=tab3")
|
|
312
|
+
const newHistoryMock = mockHistory()
|
|
313
|
+
|
|
314
|
+
document.body.innerHTML = createTabsHTML({ urlParam: "tab" })
|
|
315
|
+
|
|
316
|
+
application = Application.start()
|
|
317
|
+
application.register("shadcn--tabs", TabsController)
|
|
318
|
+
|
|
319
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
320
|
+
|
|
321
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
322
|
+
|
|
323
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
324
|
+
expect(tab3.dataset.state).toBe("active")
|
|
325
|
+
|
|
326
|
+
newHistoryMock.restore()
|
|
327
|
+
newRestoreLocation()
|
|
328
|
+
|
|
329
|
+
// Re-initialize for next tests
|
|
330
|
+
historyMock = mockHistory()
|
|
331
|
+
restoreLocation = mockLocation("http://localhost/")
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test("URL parameter takes precedence over default value", async () => {
|
|
335
|
+
application.stop()
|
|
336
|
+
historyMock.restore()
|
|
337
|
+
restoreLocation()
|
|
338
|
+
|
|
339
|
+
const newRestoreLocation = mockLocation("http://localhost?tab=tab2")
|
|
340
|
+
const newHistoryMock = mockHistory()
|
|
341
|
+
|
|
342
|
+
document.body.innerHTML = createTabsHTML({ defaultValue: "tab1", urlParam: "tab" })
|
|
343
|
+
|
|
344
|
+
application = Application.start()
|
|
345
|
+
application.register("shadcn--tabs", TabsController)
|
|
346
|
+
|
|
347
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
348
|
+
|
|
349
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
350
|
+
|
|
351
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
352
|
+
expect(tab2.dataset.state).toBe("active")
|
|
353
|
+
|
|
354
|
+
newHistoryMock.restore()
|
|
355
|
+
newRestoreLocation()
|
|
356
|
+
|
|
357
|
+
// Re-initialize for next tests
|
|
358
|
+
historyMock = mockHistory()
|
|
359
|
+
restoreLocation = mockLocation("http://localhost/")
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test("does not update URL when urlParam is not set", async () => {
|
|
363
|
+
application.stop()
|
|
364
|
+
historyMock.restore()
|
|
365
|
+
historyMock = mockHistory()
|
|
366
|
+
|
|
367
|
+
document.body.innerHTML = createTabsHTML() // No urlParam
|
|
368
|
+
|
|
369
|
+
application = Application.start()
|
|
370
|
+
application.register("shadcn--tabs", TabsController)
|
|
371
|
+
|
|
372
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
373
|
+
|
|
374
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
375
|
+
|
|
376
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
377
|
+
click(tab2)
|
|
378
|
+
|
|
379
|
+
expect(historyMock.calls.replaceState.length).toBe(0)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test("handles popstate event for browser navigation", async () => {
|
|
383
|
+
// Start with tab1 active
|
|
384
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
385
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
386
|
+
|
|
387
|
+
expect(tab1.dataset.state).toBe("active")
|
|
388
|
+
|
|
389
|
+
// Update the mocked location to have tab2 in the URL
|
|
390
|
+
historyMock.restore()
|
|
391
|
+
restoreLocation()
|
|
392
|
+
|
|
393
|
+
const newRestoreLocation = mockLocation("http://localhost?tab=tab2")
|
|
394
|
+
const newHistoryMock = mockHistory()
|
|
395
|
+
|
|
396
|
+
// Re-get the element reference since we're in a new context
|
|
397
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
398
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--tabs")
|
|
399
|
+
|
|
400
|
+
// Simulate browser back/forward by firing popstate
|
|
401
|
+
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
402
|
+
|
|
403
|
+
await nextFrame()
|
|
404
|
+
|
|
405
|
+
// Controller should react to popstate and switch tabs
|
|
406
|
+
const newTab2 = element.querySelector('[data-value="tab2"]')
|
|
407
|
+
const newTab1 = element.querySelector('[data-value="tab1"]')
|
|
408
|
+
|
|
409
|
+
expect(newTab2.dataset.state).toBe("active")
|
|
410
|
+
expect(newTab1.dataset.state).toBe("inactive")
|
|
411
|
+
|
|
412
|
+
newHistoryMock.restore()
|
|
413
|
+
newRestoreLocation()
|
|
414
|
+
|
|
415
|
+
// Re-initialize for next tests
|
|
416
|
+
historyMock = mockHistory()
|
|
417
|
+
restoreLocation = mockLocation("http://localhost/")
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test("cleans up popstate listener on disconnect", () => {
|
|
421
|
+
let popstateRemoved = false
|
|
422
|
+
const originalRemove = window.removeEventListener
|
|
423
|
+
|
|
424
|
+
window.removeEventListener = function(event) {
|
|
425
|
+
if (event === 'popstate') {
|
|
426
|
+
popstateRemoved = true
|
|
427
|
+
}
|
|
428
|
+
return originalRemove.apply(this, arguments)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
controller.disconnect()
|
|
432
|
+
|
|
433
|
+
// Should have removed the popstate listener
|
|
434
|
+
expect(popstateRemoved).toBe(true)
|
|
435
|
+
|
|
436
|
+
window.removeEventListener = originalRemove
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test("does not add popstate listener when urlParam is not set", async () => {
|
|
440
|
+
application.stop()
|
|
441
|
+
historyMock.restore()
|
|
442
|
+
restoreLocation()
|
|
443
|
+
|
|
444
|
+
let popstateAdded = false
|
|
445
|
+
const originalAdd = window.addEventListener
|
|
446
|
+
|
|
447
|
+
window.addEventListener = function(event) {
|
|
448
|
+
if (event === 'popstate') {
|
|
449
|
+
popstateAdded = true
|
|
450
|
+
}
|
|
451
|
+
return originalAdd.apply(this, arguments)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
document.body.innerHTML = createTabsHTML() // No urlParam
|
|
455
|
+
|
|
456
|
+
application = Application.start()
|
|
457
|
+
application.register("shadcn--tabs", TabsController)
|
|
458
|
+
|
|
459
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
460
|
+
|
|
461
|
+
// Should not have added popstate listener
|
|
462
|
+
expect(popstateAdded).toBe(false)
|
|
463
|
+
|
|
464
|
+
window.addEventListener = originalAdd
|
|
465
|
+
|
|
466
|
+
// Re-initialize for potential next tests in URL sync suite
|
|
467
|
+
historyMock = mockHistory()
|
|
468
|
+
restoreLocation = mockLocation("http://localhost/")
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe("keyboard navigation", () => {
|
|
473
|
+
test("ArrowRight moves to next tab", () => {
|
|
474
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
475
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
476
|
+
|
|
477
|
+
tab1.focus()
|
|
478
|
+
keydown(tab1, 'ArrowRight')
|
|
479
|
+
|
|
480
|
+
expect(document.activeElement).toBe(tab2)
|
|
481
|
+
expect(tab2.dataset.state).toBe("active")
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test("ArrowLeft moves to previous tab", () => {
|
|
485
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
486
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
487
|
+
|
|
488
|
+
// First switch to tab2
|
|
489
|
+
click(tab2)
|
|
490
|
+
tab2.focus()
|
|
491
|
+
|
|
492
|
+
keydown(tab2, 'ArrowLeft')
|
|
493
|
+
|
|
494
|
+
expect(document.activeElement).toBe(tab1)
|
|
495
|
+
expect(tab1.dataset.state).toBe("active")
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test("ArrowDown moves to next tab", () => {
|
|
499
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
500
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
501
|
+
|
|
502
|
+
tab1.focus()
|
|
503
|
+
keydown(tab1, 'ArrowDown')
|
|
504
|
+
|
|
505
|
+
expect(document.activeElement).toBe(tab2)
|
|
506
|
+
expect(tab2.dataset.state).toBe("active")
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test("ArrowUp moves to previous tab", () => {
|
|
510
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
511
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
512
|
+
|
|
513
|
+
click(tab2)
|
|
514
|
+
tab2.focus()
|
|
515
|
+
|
|
516
|
+
keydown(tab2, 'ArrowUp')
|
|
517
|
+
|
|
518
|
+
expect(document.activeElement).toBe(tab1)
|
|
519
|
+
expect(tab1.dataset.state).toBe("active")
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test("ArrowRight wraps from last to first tab", () => {
|
|
523
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
524
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
525
|
+
|
|
526
|
+
click(tab3)
|
|
527
|
+
tab3.focus()
|
|
528
|
+
|
|
529
|
+
keydown(tab3, 'ArrowRight')
|
|
530
|
+
|
|
531
|
+
expect(document.activeElement).toBe(tab1)
|
|
532
|
+
expect(tab1.dataset.state).toBe("active")
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test("ArrowLeft wraps from first to last tab", () => {
|
|
536
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
537
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
538
|
+
|
|
539
|
+
tab1.focus()
|
|
540
|
+
|
|
541
|
+
keydown(tab1, 'ArrowLeft')
|
|
542
|
+
|
|
543
|
+
expect(document.activeElement).toBe(tab3)
|
|
544
|
+
expect(tab3.dataset.state).toBe("active")
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
test("Home key moves to first tab", () => {
|
|
548
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
549
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
550
|
+
|
|
551
|
+
click(tab3)
|
|
552
|
+
tab3.focus()
|
|
553
|
+
|
|
554
|
+
keydown(tab3, 'Home')
|
|
555
|
+
|
|
556
|
+
expect(document.activeElement).toBe(tab1)
|
|
557
|
+
expect(tab1.dataset.state).toBe("active")
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test("End key moves to last tab", () => {
|
|
561
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
562
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
563
|
+
|
|
564
|
+
tab1.focus()
|
|
565
|
+
|
|
566
|
+
keydown(tab1, 'End')
|
|
567
|
+
|
|
568
|
+
expect(document.activeElement).toBe(tab3)
|
|
569
|
+
expect(tab3.dataset.state).toBe("active")
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
test("keyboard navigation triggers click to update content", () => {
|
|
573
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
574
|
+
const content2 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab2"]')
|
|
575
|
+
|
|
576
|
+
tab1.focus()
|
|
577
|
+
keydown(tab1, 'ArrowRight')
|
|
578
|
+
|
|
579
|
+
// Content should have switched
|
|
580
|
+
expect(content2.hidden).toBe(false)
|
|
581
|
+
expect(content2.dataset.state).toBe("active")
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
test("does not handle keyboard events when no tab is focused", () => {
|
|
585
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
586
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
587
|
+
|
|
588
|
+
// Don't focus any tab
|
|
589
|
+
document.body.focus()
|
|
590
|
+
|
|
591
|
+
keydown(element, 'ArrowRight')
|
|
592
|
+
|
|
593
|
+
// Should still be on tab1
|
|
594
|
+
expect(tab1.dataset.state).toBe("active")
|
|
595
|
+
expect(tab2.dataset.state).toBe("inactive")
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test("ignores other keys", () => {
|
|
599
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
600
|
+
|
|
601
|
+
tab1.focus()
|
|
602
|
+
|
|
603
|
+
keydown(tab1, 'Enter')
|
|
604
|
+
expect(tab1.dataset.state).toBe("active")
|
|
605
|
+
|
|
606
|
+
keydown(tab1, ' ')
|
|
607
|
+
expect(tab1.dataset.state).toBe("active")
|
|
608
|
+
|
|
609
|
+
keydown(tab1, 'Tab')
|
|
610
|
+
expect(tab1.dataset.state).toBe("active")
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
test("skips disabled tabs when navigating", async () => {
|
|
614
|
+
// Create tabs with disabled trigger
|
|
615
|
+
application.stop()
|
|
616
|
+
document.body.innerHTML = `
|
|
617
|
+
<div data-controller="shadcn--tabs"
|
|
618
|
+
data-shadcn--tabs-default-value-value="tab1">
|
|
619
|
+
<div data-shadcn--tabs-target="list" role="tablist">
|
|
620
|
+
<button data-shadcn--tabs-target="trigger" data-value="tab1" role="tab" data-action="click->shadcn--tabs#selectTab keydown->shadcn--tabs#handleKeydown">Tab 1</button>
|
|
621
|
+
<button data-shadcn--tabs-target="trigger" data-value="tab2" role="tab" disabled data-action="click->shadcn--tabs#selectTab keydown->shadcn--tabs#handleKeydown">Tab 2</button>
|
|
622
|
+
<button data-shadcn--tabs-target="trigger" data-value="tab3" role="tab" data-action="click->shadcn--tabs#selectTab keydown->shadcn--tabs#handleKeydown">Tab 3</button>
|
|
623
|
+
</div>
|
|
624
|
+
<div data-shadcn--tabs-target="content" data-value="tab1" role="tabpanel">Content 1</div>
|
|
625
|
+
<div data-shadcn--tabs-target="content" data-value="tab2" role="tabpanel" hidden>Content 2</div>
|
|
626
|
+
<div data-shadcn--tabs-target="content" data-value="tab3" role="tabpanel" hidden>Content 3</div>
|
|
627
|
+
</div>
|
|
628
|
+
`
|
|
629
|
+
|
|
630
|
+
application = Application.start()
|
|
631
|
+
application.register("shadcn--tabs", TabsController)
|
|
632
|
+
|
|
633
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
634
|
+
|
|
635
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
636
|
+
|
|
637
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
638
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
639
|
+
|
|
640
|
+
tab1.focus()
|
|
641
|
+
keydown(tab1, 'ArrowRight')
|
|
642
|
+
|
|
643
|
+
// Should skip disabled tab2 and go to tab3
|
|
644
|
+
expect(document.activeElement).toBe(tab3)
|
|
645
|
+
expect(tab3.dataset.state).toBe("active")
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
test("keyboard navigation prevents default behavior", () => {
|
|
649
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
650
|
+
tab1.focus()
|
|
651
|
+
|
|
652
|
+
let preventDefaultCalled = false
|
|
653
|
+
|
|
654
|
+
const event = new KeyboardEvent('keydown', {
|
|
655
|
+
key: 'ArrowRight',
|
|
656
|
+
bubbles: true,
|
|
657
|
+
cancelable: true
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
// Override preventDefault to track if it was called
|
|
661
|
+
const originalPreventDefault = event.preventDefault
|
|
662
|
+
event.preventDefault = function() {
|
|
663
|
+
preventDefaultCalled = true
|
|
664
|
+
return originalPreventDefault.apply(this, arguments)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
tab1.dispatchEvent(event)
|
|
668
|
+
|
|
669
|
+
expect(preventDefaultCalled).toBe(true)
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
describe("ARIA attributes", () => {
|
|
674
|
+
test("triggers have correct role", () => {
|
|
675
|
+
const triggers = element.querySelectorAll('[data-shadcn--tabs-target="trigger"]')
|
|
676
|
+
|
|
677
|
+
triggers.forEach(trigger => {
|
|
678
|
+
expect(trigger.getAttribute("role")).toBe("tab")
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test("list has correct role", () => {
|
|
683
|
+
const list = element.querySelector('[data-shadcn--tabs-target="list"]')
|
|
684
|
+
expect(list.getAttribute("role")).toBe("tablist")
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test("content panels have correct role", () => {
|
|
688
|
+
const contents = element.querySelectorAll('[data-shadcn--tabs-target="content"]')
|
|
689
|
+
|
|
690
|
+
contents.forEach(content => {
|
|
691
|
+
expect(content.getAttribute("role")).toBe("tabpanel")
|
|
692
|
+
})
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
test("active tab has aria-selected=true", () => {
|
|
696
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
697
|
+
expect(tab1.getAttribute("aria-selected")).toBe("true")
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
test("inactive tabs have aria-selected=false", () => {
|
|
701
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
702
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
703
|
+
|
|
704
|
+
expect(tab2.getAttribute("aria-selected")).toBe("false")
|
|
705
|
+
expect(tab3.getAttribute("aria-selected")).toBe("false")
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
test("aria-selected updates when tab is clicked", () => {
|
|
709
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
710
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
711
|
+
|
|
712
|
+
click(tab2)
|
|
713
|
+
|
|
714
|
+
expect(tab1.getAttribute("aria-selected")).toBe("false")
|
|
715
|
+
expect(tab2.getAttribute("aria-selected")).toBe("true")
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test("active tab has tabIndex 0", () => {
|
|
719
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
720
|
+
expect(tab1.tabIndex).toBe(0)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
test("inactive tabs have tabIndex -1", () => {
|
|
724
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
725
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
726
|
+
|
|
727
|
+
expect(tab2.tabIndex).toBe(-1)
|
|
728
|
+
expect(tab3.tabIndex).toBe(-1)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test("tabIndex updates when switching tabs", () => {
|
|
732
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
733
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
734
|
+
|
|
735
|
+
click(tab2)
|
|
736
|
+
|
|
737
|
+
expect(tab1.tabIndex).toBe(-1)
|
|
738
|
+
expect(tab2.tabIndex).toBe(0)
|
|
739
|
+
})
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
describe("default value", () => {
|
|
743
|
+
test("respects custom default value", async () => {
|
|
744
|
+
application.stop()
|
|
745
|
+
document.body.innerHTML = createTabsHTML({ defaultValue: "tab2" })
|
|
746
|
+
|
|
747
|
+
application = Application.start()
|
|
748
|
+
application.register("shadcn--tabs", TabsController)
|
|
749
|
+
|
|
750
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
751
|
+
|
|
752
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
753
|
+
|
|
754
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
755
|
+
expect(tab2.dataset.state).toBe("active")
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
test("uses first tab when default value is empty", async () => {
|
|
759
|
+
application.stop()
|
|
760
|
+
document.body.innerHTML = createTabsHTML({ defaultValue: "" })
|
|
761
|
+
|
|
762
|
+
application = Application.start()
|
|
763
|
+
application.register("shadcn--tabs", TabsController)
|
|
764
|
+
|
|
765
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
766
|
+
|
|
767
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
768
|
+
|
|
769
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
770
|
+
expect(tab1.dataset.state).toBe("active")
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test("falls back to default or first tab if value is invalid", async () => {
|
|
774
|
+
application.stop()
|
|
775
|
+
document.body.innerHTML = createTabsHTML({ defaultValue: "tab2" })
|
|
776
|
+
|
|
777
|
+
application = Application.start()
|
|
778
|
+
application.register("shadcn--tabs", TabsController)
|
|
779
|
+
|
|
780
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
781
|
+
|
|
782
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
783
|
+
|
|
784
|
+
// Should use the valid default value (tab2)
|
|
785
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
786
|
+
expect(tab2.dataset.state).toBe("active")
|
|
787
|
+
})
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
describe("edge cases", () => {
|
|
791
|
+
test("handles single tab gracefully", async () => {
|
|
792
|
+
application.stop()
|
|
793
|
+
document.body.innerHTML = createTabsHTML({ tabCount: 1 })
|
|
794
|
+
|
|
795
|
+
application = Application.start()
|
|
796
|
+
application.register("shadcn--tabs", TabsController)
|
|
797
|
+
|
|
798
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
799
|
+
|
|
800
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
801
|
+
|
|
802
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
803
|
+
expect(tab1.dataset.state).toBe("active")
|
|
804
|
+
|
|
805
|
+
// Keyboard navigation should stay on same tab
|
|
806
|
+
tab1.focus()
|
|
807
|
+
keydown(tab1, 'ArrowRight')
|
|
808
|
+
expect(document.activeElement).toBe(tab1)
|
|
809
|
+
|
|
810
|
+
keydown(tab1, 'ArrowLeft')
|
|
811
|
+
expect(document.activeElement).toBe(tab1)
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test("handles many tabs", async () => {
|
|
815
|
+
application.stop()
|
|
816
|
+
document.body.innerHTML = createTabsHTML({ tabCount: 10 })
|
|
817
|
+
|
|
818
|
+
application = Application.start()
|
|
819
|
+
application.register("shadcn--tabs", TabsController)
|
|
820
|
+
|
|
821
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
822
|
+
|
|
823
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
824
|
+
|
|
825
|
+
const tab10 = element.querySelector('[data-value="tab10"]')
|
|
826
|
+
click(tab10)
|
|
827
|
+
|
|
828
|
+
expect(tab10.dataset.state).toBe("active")
|
|
829
|
+
|
|
830
|
+
// End key should work
|
|
831
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
832
|
+
click(tab1)
|
|
833
|
+
tab1.focus()
|
|
834
|
+
|
|
835
|
+
keydown(tab1, 'End')
|
|
836
|
+
expect(document.activeElement).toBe(tab10)
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
test("selectTabByValue method works correctly", () => {
|
|
840
|
+
controller.selectTabByValue("tab3")
|
|
841
|
+
|
|
842
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
843
|
+
const content3 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab3"]')
|
|
844
|
+
|
|
845
|
+
expect(tab3.dataset.state).toBe("active")
|
|
846
|
+
expect(content3.hidden).toBe(false)
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
test("selectTabByValue with updateUrl=false does not update URL", async () => {
|
|
850
|
+
application.stop()
|
|
851
|
+
|
|
852
|
+
// Set up mocks in correct order: location first, then history
|
|
853
|
+
const localRestoreLocation = mockLocation("http://localhost/")
|
|
854
|
+
const localHistoryMock = mockHistory()
|
|
855
|
+
|
|
856
|
+
document.body.innerHTML = createTabsHTML({ urlParam: "tab" })
|
|
857
|
+
|
|
858
|
+
application = Application.start()
|
|
859
|
+
application.register("shadcn--tabs", TabsController)
|
|
860
|
+
|
|
861
|
+
await new Promise(resolve => requestAnimationFrame(resolve))
|
|
862
|
+
|
|
863
|
+
element = document.querySelector('[data-controller="shadcn--tabs"]')
|
|
864
|
+
controller = application.getControllerForElementAndIdentifier(element, "shadcn--tabs")
|
|
865
|
+
|
|
866
|
+
// Clear any calls from initialization
|
|
867
|
+
localHistoryMock.calls.replaceState = []
|
|
868
|
+
|
|
869
|
+
controller.selectTabByValue("tab2", false)
|
|
870
|
+
|
|
871
|
+
expect(localHistoryMock.calls.replaceState.length).toBe(0)
|
|
872
|
+
|
|
873
|
+
localHistoryMock.restore()
|
|
874
|
+
localRestoreLocation()
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
test("handles rapid tab switching", async () => {
|
|
878
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
879
|
+
const tab3 = element.querySelector('[data-value="tab3"]')
|
|
880
|
+
const tab1 = element.querySelector('[data-value="tab1"]')
|
|
881
|
+
|
|
882
|
+
click(tab2)
|
|
883
|
+
click(tab3)
|
|
884
|
+
click(tab1)
|
|
885
|
+
click(tab2)
|
|
886
|
+
|
|
887
|
+
await nextFrame()
|
|
888
|
+
|
|
889
|
+
expect(tab2.dataset.state).toBe("active")
|
|
890
|
+
const content2 = element.querySelector('[data-shadcn--tabs-target="content"][data-value="tab2"]')
|
|
891
|
+
expect(content2.hidden).toBe(false)
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
describe("snapshots", () => {
|
|
896
|
+
test("renders default tabs correctly", () => {
|
|
897
|
+
expect(element.innerHTML).toMatchSnapshot()
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
test("renders tabs with tab2 active", () => {
|
|
901
|
+
const tab2 = element.querySelector('[data-value="tab2"]')
|
|
902
|
+
click(tab2)
|
|
903
|
+
|
|
904
|
+
expect(element.innerHTML).toMatchSnapshot()
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
})
|