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,966 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
|
2
|
+
import ComboboxController from "../../app/assets/javascripts/shadcn/controllers/combobox_controller.js"
|
|
3
|
+
import {
|
|
4
|
+
setupController,
|
|
5
|
+
cleanupController,
|
|
6
|
+
click,
|
|
7
|
+
wait,
|
|
8
|
+
nextFrame,
|
|
9
|
+
keydown,
|
|
10
|
+
waitForEvent,
|
|
11
|
+
dispatchEvent
|
|
12
|
+
} from "../helpers/stimulus-test-helper.js"
|
|
13
|
+
|
|
14
|
+
describe("ComboboxController", () => {
|
|
15
|
+
let application, element, controller
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanupController(application)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Helper to get the HTML template for combobox tests
|
|
23
|
+
*/
|
|
24
|
+
function getComboboxHTML(options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
open = false,
|
|
27
|
+
value = "",
|
|
28
|
+
selectedIndex = -1,
|
|
29
|
+
items = [
|
|
30
|
+
{ value: "react", label: "React" },
|
|
31
|
+
{ value: "vue", label: "Vue" },
|
|
32
|
+
{ value: "angular", label: "Angular" },
|
|
33
|
+
{ value: "svelte", label: "Svelte" }
|
|
34
|
+
],
|
|
35
|
+
includeEmpty = true,
|
|
36
|
+
includeDisplayValue = true,
|
|
37
|
+
includeHiddenInput = true
|
|
38
|
+
} = options
|
|
39
|
+
|
|
40
|
+
const itemsHTML = items.map(item => `
|
|
41
|
+
<div
|
|
42
|
+
data-shadcn--combobox-target="item"
|
|
43
|
+
data-value="${item.value}"
|
|
44
|
+
data-label="${item.label}"
|
|
45
|
+
data-action="click->shadcn--combobox#select"
|
|
46
|
+
data-selected="false"
|
|
47
|
+
class="cursor-pointer"
|
|
48
|
+
>
|
|
49
|
+
<svg class="opacity-0"></svg>
|
|
50
|
+
${item.label}
|
|
51
|
+
</div>
|
|
52
|
+
`).join("")
|
|
53
|
+
|
|
54
|
+
return `
|
|
55
|
+
<div
|
|
56
|
+
data-controller="shadcn--combobox"
|
|
57
|
+
data-shadcn--combobox-open-value="${open}"
|
|
58
|
+
data-shadcn--combobox-value-value="${value}"
|
|
59
|
+
data-shadcn--combobox-selected-index-value="${selectedIndex}"
|
|
60
|
+
>
|
|
61
|
+
<button
|
|
62
|
+
data-shadcn--combobox-target="trigger"
|
|
63
|
+
data-action="click->shadcn--combobox#toggle"
|
|
64
|
+
aria-expanded="${open}"
|
|
65
|
+
>
|
|
66
|
+
${includeDisplayValue ? `<span data-shadcn--combobox-target="displayValue" class="text-muted-foreground">Select framework...</span>` : 'Select framework...'}
|
|
67
|
+
</button>
|
|
68
|
+
${includeHiddenInput ? '<input type="hidden" data-shadcn--combobox-target="hiddenInput" name="framework">' : ''}
|
|
69
|
+
<div
|
|
70
|
+
data-shadcn--combobox-target="content"
|
|
71
|
+
data-state="closed"
|
|
72
|
+
${!open ? 'hidden' : ''}
|
|
73
|
+
>
|
|
74
|
+
<input
|
|
75
|
+
data-shadcn--combobox-target="input"
|
|
76
|
+
type="text"
|
|
77
|
+
placeholder="Search..."
|
|
78
|
+
data-action="input->shadcn--combobox#filter"
|
|
79
|
+
>
|
|
80
|
+
<div data-shadcn--combobox-target="list">
|
|
81
|
+
${itemsHTML}
|
|
82
|
+
</div>
|
|
83
|
+
${includeEmpty ? '<div data-shadcn--combobox-target="empty" hidden>No results found.</div>' : ''}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("Value Initialization", () => {
|
|
90
|
+
it("initializes with default values", async () => {
|
|
91
|
+
const html = getComboboxHTML()
|
|
92
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
93
|
+
application = setup.application
|
|
94
|
+
element = setup.element
|
|
95
|
+
controller = setup.controller
|
|
96
|
+
|
|
97
|
+
expect(controller.openValue).toBe(false)
|
|
98
|
+
expect(controller.valueValue).toBe("")
|
|
99
|
+
expect(controller.selectedIndexValue).toBe(-1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("initializes with custom open value", async () => {
|
|
103
|
+
const html = getComboboxHTML({ open: true })
|
|
104
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
105
|
+
application = setup.application
|
|
106
|
+
element = setup.element
|
|
107
|
+
controller = setup.controller
|
|
108
|
+
|
|
109
|
+
expect(controller.openValue).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("initializes with custom value", async () => {
|
|
113
|
+
const html = getComboboxHTML({ value: "react" })
|
|
114
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
115
|
+
application = setup.application
|
|
116
|
+
element = setup.element
|
|
117
|
+
controller = setup.controller
|
|
118
|
+
|
|
119
|
+
expect(controller.valueValue).toBe("react")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("initializes with custom selected index", async () => {
|
|
123
|
+
const html = getComboboxHTML({ selectedIndex: 2 })
|
|
124
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
125
|
+
application = setup.application
|
|
126
|
+
element = setup.element
|
|
127
|
+
controller = setup.controller
|
|
128
|
+
|
|
129
|
+
expect(controller.selectedIndexValue).toBe(2)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("Open/Close Behavior", () => {
|
|
134
|
+
beforeEach(async () => {
|
|
135
|
+
const html = getComboboxHTML()
|
|
136
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
137
|
+
application = setup.application
|
|
138
|
+
element = setup.element
|
|
139
|
+
controller = setup.controller
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("opens the combobox when toggle is called on closed state", async () => {
|
|
143
|
+
expect(controller.openValue).toBe(false)
|
|
144
|
+
|
|
145
|
+
controller.toggle()
|
|
146
|
+
await nextFrame()
|
|
147
|
+
|
|
148
|
+
expect(controller.openValue).toBe(true)
|
|
149
|
+
expect(controller.contentTarget.hidden).toBe(false)
|
|
150
|
+
expect(controller.contentTarget.dataset.state).toBe("open")
|
|
151
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("true")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("closes the combobox when toggle is called on open state", async () => {
|
|
155
|
+
controller.open()
|
|
156
|
+
await nextFrame()
|
|
157
|
+
expect(controller.openValue).toBe(true)
|
|
158
|
+
|
|
159
|
+
controller.toggle()
|
|
160
|
+
await wait(250) // Wait for animation and cleanup
|
|
161
|
+
|
|
162
|
+
expect(controller.openValue).toBe(false)
|
|
163
|
+
expect(controller.contentTarget.dataset.state).toBe("closed")
|
|
164
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("focuses the input when opened", async () => {
|
|
168
|
+
controller.open()
|
|
169
|
+
await nextFrame()
|
|
170
|
+
await nextFrame() // requestAnimationFrame in open()
|
|
171
|
+
|
|
172
|
+
expect(document.activeElement).toBe(controller.inputTarget)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("does not open if already open", async () => {
|
|
176
|
+
controller.open()
|
|
177
|
+
await nextFrame()
|
|
178
|
+
const initialState = controller.openValue
|
|
179
|
+
|
|
180
|
+
controller.open()
|
|
181
|
+
await nextFrame()
|
|
182
|
+
|
|
183
|
+
expect(controller.openValue).toBe(initialState)
|
|
184
|
+
expect(controller.openValue).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("does not close if already closed", async () => {
|
|
188
|
+
expect(controller.openValue).toBe(false)
|
|
189
|
+
|
|
190
|
+
controller.close()
|
|
191
|
+
await wait(250)
|
|
192
|
+
|
|
193
|
+
expect(controller.openValue).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("resets selected index when opened", async () => {
|
|
197
|
+
controller.selectedIndexValue = 2
|
|
198
|
+
|
|
199
|
+
controller.open()
|
|
200
|
+
await nextFrame()
|
|
201
|
+
|
|
202
|
+
expect(controller.selectedIndexValue).toBe(-1)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("closes on Escape key", async () => {
|
|
206
|
+
controller.open()
|
|
207
|
+
await nextFrame()
|
|
208
|
+
|
|
209
|
+
keydown(document, "Escape")
|
|
210
|
+
await wait(250)
|
|
211
|
+
|
|
212
|
+
expect(controller.openValue).toBe(false)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("hides content after close animation completes", async () => {
|
|
216
|
+
controller.open()
|
|
217
|
+
await nextFrame()
|
|
218
|
+
expect(controller.contentTarget.hidden).toBe(false)
|
|
219
|
+
|
|
220
|
+
controller.close()
|
|
221
|
+
await wait(250) // Wait for animation and fallback timeout
|
|
222
|
+
|
|
223
|
+
expect(controller.contentTarget.hidden).toBe(true)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("resets input value when closed", async () => {
|
|
227
|
+
controller.open()
|
|
228
|
+
await nextFrame()
|
|
229
|
+
|
|
230
|
+
controller.inputTarget.value = "test search"
|
|
231
|
+
controller.close()
|
|
232
|
+
await wait(250)
|
|
233
|
+
|
|
234
|
+
expect(controller.inputTarget.value).toBe("")
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it("resets item visibility when closed", async () => {
|
|
238
|
+
controller.open()
|
|
239
|
+
await nextFrame()
|
|
240
|
+
|
|
241
|
+
// Hide some items
|
|
242
|
+
controller.itemTargets[0].style.display = "none"
|
|
243
|
+
controller.itemTargets[1].style.display = "none"
|
|
244
|
+
|
|
245
|
+
controller.close()
|
|
246
|
+
await wait(250)
|
|
247
|
+
|
|
248
|
+
controller.itemTargets.forEach(item => {
|
|
249
|
+
expect(item.style.display).toBe("")
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("hides empty state when closed", async () => {
|
|
254
|
+
controller.open()
|
|
255
|
+
await nextFrame()
|
|
256
|
+
|
|
257
|
+
controller.emptyTarget.hidden = false
|
|
258
|
+
controller.close()
|
|
259
|
+
await wait(250)
|
|
260
|
+
|
|
261
|
+
expect(controller.emptyTarget.hidden).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("adds keyboard listener when opened", async () => {
|
|
265
|
+
const spy = jest.spyOn(document, "addEventListener")
|
|
266
|
+
|
|
267
|
+
controller.open()
|
|
268
|
+
await nextFrame()
|
|
269
|
+
|
|
270
|
+
expect(spy).toHaveBeenCalledWith("keydown", controller.boundHandleKeydown)
|
|
271
|
+
spy.mockRestore()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it("removes keyboard listener when closed", async () => {
|
|
275
|
+
controller.open()
|
|
276
|
+
await nextFrame()
|
|
277
|
+
|
|
278
|
+
const spy = jest.spyOn(document, "removeEventListener")
|
|
279
|
+
controller.close()
|
|
280
|
+
await wait(250)
|
|
281
|
+
|
|
282
|
+
expect(spy).toHaveBeenCalledWith("keydown", controller.boundHandleKeydown)
|
|
283
|
+
spy.mockRestore()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe("Filtering", () => {
|
|
288
|
+
beforeEach(async () => {
|
|
289
|
+
const html = getComboboxHTML()
|
|
290
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
291
|
+
application = setup.application
|
|
292
|
+
element = setup.element
|
|
293
|
+
controller = setup.controller
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it("filters items based on input value", () => {
|
|
297
|
+
controller.inputTarget.value = "react"
|
|
298
|
+
controller.filter()
|
|
299
|
+
|
|
300
|
+
expect(controller.itemTargets[0].style.display).toBe("") // React - visible
|
|
301
|
+
expect(controller.itemTargets[1].style.display).toBe("none") // Vue - hidden
|
|
302
|
+
expect(controller.itemTargets[2].style.display).toBe("none") // Angular - hidden
|
|
303
|
+
expect(controller.itemTargets[3].style.display).toBe("none") // Svelte - hidden
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it("is case insensitive when filtering", () => {
|
|
307
|
+
controller.inputTarget.value = "REACT"
|
|
308
|
+
controller.filter()
|
|
309
|
+
|
|
310
|
+
expect(controller.itemTargets[0].style.display).toBe("") // React matches
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("filters by label attribute", () => {
|
|
314
|
+
controller.inputTarget.value = "Vue"
|
|
315
|
+
controller.filter()
|
|
316
|
+
|
|
317
|
+
expect(controller.itemTargets[0].style.display).toBe("none")
|
|
318
|
+
expect(controller.itemTargets[1].style.display).toBe("") // Vue visible
|
|
319
|
+
expect(controller.itemTargets[2].style.display).toBe("none")
|
|
320
|
+
expect(controller.itemTargets[3].style.display).toBe("none")
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it("filters by value attribute", () => {
|
|
324
|
+
controller.inputTarget.value = "angular"
|
|
325
|
+
controller.filter()
|
|
326
|
+
|
|
327
|
+
expect(controller.itemTargets[0].style.display).toBe("none")
|
|
328
|
+
expect(controller.itemTargets[1].style.display).toBe("none")
|
|
329
|
+
expect(controller.itemTargets[2].style.display).toBe("") // Angular visible
|
|
330
|
+
expect(controller.itemTargets[3].style.display).toBe("none")
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it("shows all items when input is empty", () => {
|
|
334
|
+
controller.inputTarget.value = "react"
|
|
335
|
+
controller.filter()
|
|
336
|
+
|
|
337
|
+
controller.inputTarget.value = ""
|
|
338
|
+
controller.filter()
|
|
339
|
+
|
|
340
|
+
controller.itemTargets.forEach(item => {
|
|
341
|
+
expect(item.style.display).toBe("")
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("shows empty state when no results match query", () => {
|
|
346
|
+
controller.inputTarget.value = "nonexistent"
|
|
347
|
+
controller.filter()
|
|
348
|
+
|
|
349
|
+
expect(controller.emptyTarget.hidden).toBe(false)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it("hides empty state when results exist", () => {
|
|
353
|
+
controller.emptyTarget.hidden = false
|
|
354
|
+
|
|
355
|
+
controller.inputTarget.value = "react"
|
|
356
|
+
controller.filter()
|
|
357
|
+
|
|
358
|
+
expect(controller.emptyTarget.hidden).toBe(true)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it("hides empty state when query is empty", () => {
|
|
362
|
+
controller.emptyTarget.hidden = false
|
|
363
|
+
|
|
364
|
+
controller.inputTarget.value = ""
|
|
365
|
+
controller.filter()
|
|
366
|
+
|
|
367
|
+
expect(controller.emptyTarget.hidden).toBe(true)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it("resets selected index after filtering", () => {
|
|
371
|
+
controller.selectedIndexValue = 2
|
|
372
|
+
|
|
373
|
+
controller.inputTarget.value = "react"
|
|
374
|
+
controller.filter()
|
|
375
|
+
|
|
376
|
+
expect(controller.selectedIndexValue).toBe(-1)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it("handles partial matches", () => {
|
|
380
|
+
controller.inputTarget.value = "vue"
|
|
381
|
+
controller.filter()
|
|
382
|
+
|
|
383
|
+
expect(controller.itemTargets[1].style.display).toBe("") // Vue
|
|
384
|
+
expect(controller.itemTargets[3].style.display).toBe("none") // Svelte (contains 'v' but not 'vue')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it("trims whitespace from query", () => {
|
|
388
|
+
controller.inputTarget.value = " react "
|
|
389
|
+
controller.filter()
|
|
390
|
+
|
|
391
|
+
expect(controller.itemTargets[0].style.display).toBe("") // React visible
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
describe("Selection", () => {
|
|
396
|
+
beforeEach(async () => {
|
|
397
|
+
const html = getComboboxHTML()
|
|
398
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
399
|
+
application = setup.application
|
|
400
|
+
element = setup.element
|
|
401
|
+
controller = setup.controller
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it("selects item on click", () => {
|
|
405
|
+
const item = controller.itemTargets[0]
|
|
406
|
+
|
|
407
|
+
click(item)
|
|
408
|
+
|
|
409
|
+
expect(controller.valueValue).toBe("react")
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it("updates hidden input value when item selected", () => {
|
|
413
|
+
const item = controller.itemTargets[1]
|
|
414
|
+
|
|
415
|
+
click(item)
|
|
416
|
+
|
|
417
|
+
expect(controller.hiddenInputTarget.value).toBe("vue")
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it("updates display value text when item selected", () => {
|
|
421
|
+
const item = controller.itemTargets[2]
|
|
422
|
+
|
|
423
|
+
click(item)
|
|
424
|
+
|
|
425
|
+
expect(controller.displayValueTarget.textContent).toBe("Angular")
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it("removes muted foreground class from display value", () => {
|
|
429
|
+
controller.displayValueTarget.classList.add("text-muted-foreground")
|
|
430
|
+
const item = controller.itemTargets[0]
|
|
431
|
+
|
|
432
|
+
click(item)
|
|
433
|
+
|
|
434
|
+
expect(controller.displayValueTarget.classList.contains("text-muted-foreground")).toBe(false)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("updates selected state on items", () => {
|
|
438
|
+
const item = controller.itemTargets[1]
|
|
439
|
+
|
|
440
|
+
click(item)
|
|
441
|
+
|
|
442
|
+
expect(controller.itemTargets[0].dataset.selected).toBe("false")
|
|
443
|
+
expect(controller.itemTargets[1].dataset.selected).toBe("true")
|
|
444
|
+
expect(controller.itemTargets[2].dataset.selected).toBe("false")
|
|
445
|
+
expect(controller.itemTargets[3].dataset.selected).toBe("false")
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it("updates check icon visibility for selected item", () => {
|
|
449
|
+
const item = controller.itemTargets[0]
|
|
450
|
+
const checkIcon = item.querySelector("svg")
|
|
451
|
+
|
|
452
|
+
click(item)
|
|
453
|
+
|
|
454
|
+
expect(checkIcon.classList.contains("opacity-100")).toBe(true)
|
|
455
|
+
expect(checkIcon.classList.contains("opacity-0")).toBe(false)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it("hides check icon for unselected items", () => {
|
|
459
|
+
click(controller.itemTargets[0])
|
|
460
|
+
|
|
461
|
+
// Select different item
|
|
462
|
+
click(controller.itemTargets[1])
|
|
463
|
+
|
|
464
|
+
const firstCheckIcon = controller.itemTargets[0].querySelector("svg")
|
|
465
|
+
expect(firstCheckIcon.classList.contains("opacity-0")).toBe(true)
|
|
466
|
+
expect(firstCheckIcon.classList.contains("opacity-100")).toBe(false)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it("dispatches change event with value and label", async () => {
|
|
470
|
+
const item = controller.itemTargets[2]
|
|
471
|
+
const eventPromise = waitForEvent(element, "shadcn--combobox:change", 1000)
|
|
472
|
+
|
|
473
|
+
click(item)
|
|
474
|
+
const event = await eventPromise
|
|
475
|
+
|
|
476
|
+
expect(event.detail.value).toBe("angular")
|
|
477
|
+
expect(event.detail.label).toBe("Angular")
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("closes combobox after selection", async () => {
|
|
481
|
+
controller.open()
|
|
482
|
+
await nextFrame()
|
|
483
|
+
expect(controller.openValue).toBe(true)
|
|
484
|
+
|
|
485
|
+
click(controller.itemTargets[0])
|
|
486
|
+
await wait(250)
|
|
487
|
+
|
|
488
|
+
expect(controller.openValue).toBe(false)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it("selects item on Enter key when item is highlighted", async () => {
|
|
492
|
+
controller.open()
|
|
493
|
+
await nextFrame()
|
|
494
|
+
|
|
495
|
+
controller.selectedIndexValue = 1
|
|
496
|
+
controller.updateSelection()
|
|
497
|
+
|
|
498
|
+
keydown(document, "Enter")
|
|
499
|
+
await wait(250)
|
|
500
|
+
|
|
501
|
+
expect(controller.valueValue).toBe("vue")
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it("does nothing on Enter if no item is highlighted", () => {
|
|
505
|
+
controller.open()
|
|
506
|
+
|
|
507
|
+
const initialValue = controller.valueValue
|
|
508
|
+
keydown(document, "Enter")
|
|
509
|
+
|
|
510
|
+
expect(controller.valueValue).toBe(initialValue)
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
describe("Value Persistence", () => {
|
|
515
|
+
beforeEach(async () => {
|
|
516
|
+
const html = getComboboxHTML()
|
|
517
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
518
|
+
application = setup.application
|
|
519
|
+
element = setup.element
|
|
520
|
+
controller = setup.controller
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it("maintains selected value after filtering", () => {
|
|
524
|
+
click(controller.itemTargets[0]) // Select React
|
|
525
|
+
|
|
526
|
+
controller.inputTarget.value = "vue"
|
|
527
|
+
controller.filter()
|
|
528
|
+
|
|
529
|
+
expect(controller.valueValue).toBe("react")
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it("maintains selected value after closing and reopening", async () => {
|
|
533
|
+
controller.open()
|
|
534
|
+
await nextFrame()
|
|
535
|
+
|
|
536
|
+
click(controller.itemTargets[1]) // Select Vue
|
|
537
|
+
await wait(250)
|
|
538
|
+
|
|
539
|
+
controller.open()
|
|
540
|
+
await nextFrame()
|
|
541
|
+
|
|
542
|
+
expect(controller.valueValue).toBe("vue")
|
|
543
|
+
expect(controller.itemTargets[1].dataset.selected).toBe("true")
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it("maintains display value after reopening", async () => {
|
|
547
|
+
controller.open()
|
|
548
|
+
await nextFrame()
|
|
549
|
+
|
|
550
|
+
click(controller.itemTargets[2]) // Select Angular
|
|
551
|
+
await wait(250)
|
|
552
|
+
|
|
553
|
+
controller.open()
|
|
554
|
+
await nextFrame()
|
|
555
|
+
|
|
556
|
+
expect(controller.displayValueTarget.textContent).toBe("Angular")
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
describe("Animation Timing", () => {
|
|
561
|
+
beforeEach(async () => {
|
|
562
|
+
const html = getComboboxHTML()
|
|
563
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
564
|
+
application = setup.application
|
|
565
|
+
element = setup.element
|
|
566
|
+
controller = setup.controller
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it("sets state to open immediately when opening", () => {
|
|
570
|
+
controller.open()
|
|
571
|
+
|
|
572
|
+
expect(controller.contentTarget.dataset.state).toBe("open")
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it("sets state to closed immediately when closing", () => {
|
|
576
|
+
controller.open()
|
|
577
|
+
controller.close()
|
|
578
|
+
|
|
579
|
+
expect(controller.contentTarget.dataset.state).toBe("closed")
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it("hides content after close animation with fallback timeout", async () => {
|
|
583
|
+
controller.open()
|
|
584
|
+
await nextFrame()
|
|
585
|
+
|
|
586
|
+
controller.close()
|
|
587
|
+
|
|
588
|
+
// Content should still be visible during animation
|
|
589
|
+
expect(controller.contentTarget.hidden).toBe(false)
|
|
590
|
+
|
|
591
|
+
// Wait for fallback timeout (200ms)
|
|
592
|
+
await wait(250)
|
|
593
|
+
|
|
594
|
+
// Content should now be hidden
|
|
595
|
+
expect(controller.contentTarget.hidden).toBe(true)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it("listens for animationend event on close", async () => {
|
|
599
|
+
controller.open()
|
|
600
|
+
await nextFrame()
|
|
601
|
+
|
|
602
|
+
const spy = jest.spyOn(controller.contentTarget, "addEventListener")
|
|
603
|
+
controller.close()
|
|
604
|
+
|
|
605
|
+
expect(spy).toHaveBeenCalledWith("animationend", expect.any(Function))
|
|
606
|
+
spy.mockRestore()
|
|
607
|
+
await wait(250)
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
describe("Keyboard Navigation", () => {
|
|
612
|
+
beforeEach(async () => {
|
|
613
|
+
const html = getComboboxHTML()
|
|
614
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
615
|
+
application = setup.application
|
|
616
|
+
element = setup.element
|
|
617
|
+
controller = setup.controller
|
|
618
|
+
controller.open()
|
|
619
|
+
await nextFrame()
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("navigates down with ArrowDown", () => {
|
|
623
|
+
expect(controller.selectedIndexValue).toBe(-1)
|
|
624
|
+
|
|
625
|
+
keydown(document, "ArrowDown")
|
|
626
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
627
|
+
|
|
628
|
+
keydown(document, "ArrowDown")
|
|
629
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it("navigates up with ArrowUp", () => {
|
|
633
|
+
controller.selectedIndexValue = 2
|
|
634
|
+
|
|
635
|
+
keydown(document, "ArrowUp")
|
|
636
|
+
expect(controller.selectedIndexValue).toBe(1)
|
|
637
|
+
|
|
638
|
+
keydown(document, "ArrowUp")
|
|
639
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it("does not go below 0 with ArrowUp", () => {
|
|
643
|
+
controller.selectedIndexValue = 0
|
|
644
|
+
|
|
645
|
+
keydown(document, "ArrowUp")
|
|
646
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it("does not go beyond last item with ArrowDown", () => {
|
|
650
|
+
const lastIndex = controller.itemTargets.length - 1
|
|
651
|
+
controller.selectedIndexValue = lastIndex
|
|
652
|
+
|
|
653
|
+
keydown(document, "ArrowDown")
|
|
654
|
+
expect(controller.selectedIndexValue).toBe(lastIndex)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it("applies highlight classes to selected item", () => {
|
|
658
|
+
controller.selectedIndexValue = 1
|
|
659
|
+
controller.updateSelection()
|
|
660
|
+
|
|
661
|
+
expect(controller.itemTargets[1].classList.contains("bg-accent")).toBe(true)
|
|
662
|
+
expect(controller.itemTargets[1].classList.contains("text-accent-foreground")).toBe(true)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it("removes highlight classes from unselected items", () => {
|
|
666
|
+
controller.selectedIndexValue = 1
|
|
667
|
+
controller.updateSelection()
|
|
668
|
+
|
|
669
|
+
expect(controller.itemTargets[0].classList.contains("bg-accent")).toBe(false)
|
|
670
|
+
expect(controller.itemTargets[2].classList.contains("bg-accent")).toBe(false)
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it("scrolls selected item into view", () => {
|
|
674
|
+
const spy = jest.spyOn(controller.itemTargets[2], "scrollIntoView")
|
|
675
|
+
|
|
676
|
+
controller.selectedIndexValue = 2
|
|
677
|
+
controller.updateSelection()
|
|
678
|
+
|
|
679
|
+
expect(spy).toHaveBeenCalledWith({ block: "nearest" })
|
|
680
|
+
spy.mockRestore()
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it("navigates only through visible items after filtering", () => {
|
|
684
|
+
controller.inputTarget.value = "react"
|
|
685
|
+
controller.filter()
|
|
686
|
+
|
|
687
|
+
keydown(document, "ArrowDown")
|
|
688
|
+
|
|
689
|
+
// Should select first (and only) visible item
|
|
690
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
691
|
+
|
|
692
|
+
keydown(document, "ArrowDown")
|
|
693
|
+
|
|
694
|
+
// Should stay at 0 since it's the last visible item
|
|
695
|
+
expect(controller.selectedIndexValue).toBe(0)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
it("prevents default on navigation keys", () => {
|
|
699
|
+
const event = new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true })
|
|
700
|
+
const spy = jest.spyOn(event, "preventDefault")
|
|
701
|
+
|
|
702
|
+
document.dispatchEvent(event)
|
|
703
|
+
|
|
704
|
+
expect(spy).toHaveBeenCalled()
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it("prevents default on Escape key", () => {
|
|
708
|
+
const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true })
|
|
709
|
+
const spy = jest.spyOn(event, "preventDefault")
|
|
710
|
+
|
|
711
|
+
document.dispatchEvent(event)
|
|
712
|
+
|
|
713
|
+
expect(spy).toHaveBeenCalled()
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it("prevents default on Enter key", () => {
|
|
717
|
+
controller.selectedIndexValue = 0
|
|
718
|
+
const event = new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true })
|
|
719
|
+
const spy = jest.spyOn(event, "preventDefault")
|
|
720
|
+
|
|
721
|
+
document.dispatchEvent(event)
|
|
722
|
+
|
|
723
|
+
expect(spy).toHaveBeenCalled()
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
describe("ARIA Attributes", () => {
|
|
728
|
+
beforeEach(async () => {
|
|
729
|
+
const html = getComboboxHTML()
|
|
730
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
731
|
+
application = setup.application
|
|
732
|
+
element = setup.element
|
|
733
|
+
controller = setup.controller
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
it("sets aria-expanded to false when closed", () => {
|
|
737
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it("sets aria-expanded to true when opened", async () => {
|
|
741
|
+
controller.open()
|
|
742
|
+
await nextFrame()
|
|
743
|
+
|
|
744
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("true")
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
it("updates aria-expanded when toggling", async () => {
|
|
748
|
+
controller.toggle()
|
|
749
|
+
await nextFrame()
|
|
750
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("true")
|
|
751
|
+
|
|
752
|
+
controller.toggle()
|
|
753
|
+
await wait(250)
|
|
754
|
+
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
describe("Helper Methods", () => {
|
|
759
|
+
beforeEach(async () => {
|
|
760
|
+
const html = getComboboxHTML()
|
|
761
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
762
|
+
application = setup.application
|
|
763
|
+
element = setup.element
|
|
764
|
+
controller = setup.controller
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it("getVisibleItems returns all items when none are filtered", () => {
|
|
768
|
+
const visibleItems = controller.getVisibleItems()
|
|
769
|
+
expect(visibleItems.length).toBe(4)
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it("getVisibleItems returns only visible items after filtering", () => {
|
|
773
|
+
controller.itemTargets[0].style.display = "none"
|
|
774
|
+
controller.itemTargets[2].style.display = "none"
|
|
775
|
+
|
|
776
|
+
const visibleItems = controller.getVisibleItems()
|
|
777
|
+
|
|
778
|
+
expect(visibleItems.length).toBe(2)
|
|
779
|
+
expect(visibleItems[0]).toBe(controller.itemTargets[1])
|
|
780
|
+
expect(visibleItems[1]).toBe(controller.itemTargets[3])
|
|
781
|
+
})
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
describe("Click Outside Handling", () => {
|
|
785
|
+
beforeEach(async () => {
|
|
786
|
+
const html = getComboboxHTML()
|
|
787
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
788
|
+
application = setup.application
|
|
789
|
+
element = setup.element
|
|
790
|
+
controller = setup.controller
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
it("closes when clicking outside the element", async () => {
|
|
794
|
+
controller.open()
|
|
795
|
+
await nextFrame()
|
|
796
|
+
|
|
797
|
+
const outsideElement = document.createElement("div")
|
|
798
|
+
document.body.appendChild(outsideElement)
|
|
799
|
+
|
|
800
|
+
const event = new MouseEvent("click", { bubbles: true })
|
|
801
|
+
Object.defineProperty(event, "target", { value: outsideElement, enumerable: true })
|
|
802
|
+
|
|
803
|
+
controller.handleClickOutside(event)
|
|
804
|
+
await wait(250)
|
|
805
|
+
|
|
806
|
+
expect(controller.openValue).toBe(false)
|
|
807
|
+
outsideElement.remove()
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
it("does not close when clicking inside the element", async () => {
|
|
811
|
+
controller.open()
|
|
812
|
+
await nextFrame()
|
|
813
|
+
|
|
814
|
+
const event = new MouseEvent("click", { bubbles: true })
|
|
815
|
+
Object.defineProperty(event, "target", { value: controller.inputTarget, enumerable: true })
|
|
816
|
+
|
|
817
|
+
controller.handleClickOutside(event)
|
|
818
|
+
await nextFrame()
|
|
819
|
+
|
|
820
|
+
expect(controller.openValue).toBe(true)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it("does nothing when already closed", () => {
|
|
824
|
+
expect(controller.openValue).toBe(false)
|
|
825
|
+
|
|
826
|
+
const outsideElement = document.createElement("div")
|
|
827
|
+
document.body.appendChild(outsideElement)
|
|
828
|
+
|
|
829
|
+
const event = new MouseEvent("click", { bubbles: true })
|
|
830
|
+
Object.defineProperty(event, "target", { value: outsideElement, enumerable: true })
|
|
831
|
+
|
|
832
|
+
controller.handleClickOutside(event)
|
|
833
|
+
|
|
834
|
+
expect(controller.openValue).toBe(false)
|
|
835
|
+
outsideElement.remove()
|
|
836
|
+
})
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
describe("Disconnect", () => {
|
|
840
|
+
beforeEach(async () => {
|
|
841
|
+
const html = getComboboxHTML()
|
|
842
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
843
|
+
application = setup.application
|
|
844
|
+
element = setup.element
|
|
845
|
+
controller = setup.controller
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it("removes keyboard event listener on disconnect", () => {
|
|
849
|
+
controller.open()
|
|
850
|
+
|
|
851
|
+
const spy = jest.spyOn(document, "removeEventListener")
|
|
852
|
+
controller.disconnect()
|
|
853
|
+
|
|
854
|
+
expect(spy).toHaveBeenCalledWith("keydown", controller.boundHandleKeydown)
|
|
855
|
+
spy.mockRestore()
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
describe("Edge Cases", () => {
|
|
860
|
+
it("handles combobox without empty target", async () => {
|
|
861
|
+
const html = getComboboxHTML({ includeEmpty: false })
|
|
862
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
863
|
+
application = setup.application
|
|
864
|
+
element = setup.element
|
|
865
|
+
controller = setup.controller
|
|
866
|
+
|
|
867
|
+
expect(controller.hasEmptyTarget).toBe(false)
|
|
868
|
+
|
|
869
|
+
controller.inputTarget.value = "nonexistent"
|
|
870
|
+
|
|
871
|
+
// Should not throw error
|
|
872
|
+
expect(() => controller.filter()).not.toThrow()
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
it("handles combobox without display value target", async () => {
|
|
876
|
+
const html = getComboboxHTML({ includeDisplayValue: false })
|
|
877
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
878
|
+
application = setup.application
|
|
879
|
+
element = setup.element
|
|
880
|
+
controller = setup.controller
|
|
881
|
+
|
|
882
|
+
expect(controller.hasDisplayValueTarget).toBe(false)
|
|
883
|
+
|
|
884
|
+
// Should not throw error when selecting
|
|
885
|
+
expect(() => click(controller.itemTargets[0])).not.toThrow()
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
it("handles combobox without hidden input target", async () => {
|
|
889
|
+
const html = getComboboxHTML({ includeHiddenInput: false })
|
|
890
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
891
|
+
application = setup.application
|
|
892
|
+
element = setup.element
|
|
893
|
+
controller = setup.controller
|
|
894
|
+
|
|
895
|
+
expect(controller.hasHiddenInputTarget).toBe(false)
|
|
896
|
+
|
|
897
|
+
// Should not throw error when selecting
|
|
898
|
+
expect(() => click(controller.itemTargets[0])).not.toThrow()
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
it("handles items without check icons", () => {
|
|
902
|
+
const html = `
|
|
903
|
+
<div data-controller="shadcn--combobox">
|
|
904
|
+
<button data-shadcn--combobox-target="trigger" data-action="click->shadcn--combobox#toggle">
|
|
905
|
+
Select
|
|
906
|
+
</button>
|
|
907
|
+
<div data-shadcn--combobox-target="content" hidden>
|
|
908
|
+
<input data-shadcn--combobox-target="input" type="text" data-action="input->shadcn--combobox#filter">
|
|
909
|
+
<div data-shadcn--combobox-target="item" data-value="item1" data-label="Item 1" data-action="click->shadcn--combobox#select">
|
|
910
|
+
Item 1
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
`
|
|
915
|
+
|
|
916
|
+
return setupController(ComboboxController, html, "shadcn--combobox").then(setup => {
|
|
917
|
+
application = setup.application
|
|
918
|
+
element = setup.element
|
|
919
|
+
controller = setup.controller
|
|
920
|
+
|
|
921
|
+
// Should not throw error when selecting item without icon
|
|
922
|
+
expect(() => click(controller.itemTargets[0])).not.toThrow()
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
it("handles empty item list", async () => {
|
|
927
|
+
const html = getComboboxHTML({ items: [] })
|
|
928
|
+
const setup = await setupController(ComboboxController, html, "shadcn--combobox")
|
|
929
|
+
application = setup.application
|
|
930
|
+
element = setup.element
|
|
931
|
+
controller = setup.controller
|
|
932
|
+
|
|
933
|
+
expect(controller.itemTargets.length).toBe(0)
|
|
934
|
+
|
|
935
|
+
// Should not throw errors
|
|
936
|
+
expect(() => controller.filter()).not.toThrow()
|
|
937
|
+
expect(() => keydown(document, "ArrowDown")).not.toThrow()
|
|
938
|
+
expect(() => controller.updateSelection()).not.toThrow()
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
it("handles item without label attribute falling back to textContent", () => {
|
|
942
|
+
const html = `
|
|
943
|
+
<div data-controller="shadcn--combobox">
|
|
944
|
+
<button data-shadcn--combobox-target="trigger"></button>
|
|
945
|
+
<div data-shadcn--combobox-target="content" hidden>
|
|
946
|
+
<input data-shadcn--combobox-target="input" type="text" data-action="input->shadcn--combobox#filter">
|
|
947
|
+
<div data-shadcn--combobox-target="item" data-value="test" data-action="click->shadcn--combobox#select">
|
|
948
|
+
Text Content Only
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
`
|
|
953
|
+
|
|
954
|
+
return setupController(ComboboxController, html, "shadcn--combobox").then(setup => {
|
|
955
|
+
application = setup.application
|
|
956
|
+
element = setup.element
|
|
957
|
+
controller = setup.controller
|
|
958
|
+
|
|
959
|
+
controller.inputTarget.value = "text"
|
|
960
|
+
controller.filter()
|
|
961
|
+
|
|
962
|
+
expect(controller.itemTargets[0].style.display).toBe("")
|
|
963
|
+
})
|
|
964
|
+
})
|
|
965
|
+
})
|
|
966
|
+
})
|