shadcn-rails 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +69 -2
- data/README.md +102 -1398
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +9 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -0
- data/app/components/shadcn/accordion_component.html.erb +8 -0
- data/app/components/shadcn/accordion_component.rb +6 -15
- data/app/components/shadcn/alert_component.html.erb +6 -0
- data/app/components/shadcn/alert_component.rb +0 -18
- data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
- data/app/components/shadcn/alert_dialog_component.rb +7 -27
- data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
- data/app/components/shadcn/aspect_ratio_component.rb +4 -19
- data/app/components/shadcn/avatar_component.html.erb +20 -0
- data/app/components/shadcn/avatar_component.rb +8 -36
- data/app/components/shadcn/badge_component.html.erb +1 -0
- data/app/components/shadcn/badge_component.rb +0 -11
- data/app/components/shadcn/base_component.rb +15 -2
- data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
- data/app/components/shadcn/breadcrumb_component.rb +6 -16
- data/app/components/shadcn/button_component.html.erb +18 -0
- data/app/components/shadcn/button_component.rb +1 -41
- data/app/components/shadcn/card_component.html.erb +8 -0
- data/app/components/shadcn/card_component.rb +2 -6
- data/app/components/shadcn/checkbox_component.html.erb +32 -0
- data/app/components/shadcn/checkbox_component.rb +4 -43
- data/app/components/shadcn/collapsible_component.html.erb +8 -0
- data/app/components/shadcn/collapsible_component.rb +6 -15
- data/app/components/shadcn/command_list_component.rb +29 -14
- data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- data/app/components/shadcn/context_menu_content_component.rb +37 -14
- data/app/components/shadcn/context_menu_item_component.rb +3 -2
- data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/dialog_component.html.erb +14 -0
- data/app/components/shadcn/dialog_component.rb +8 -29
- data/app/components/shadcn/drawer_component.html.erb +12 -0
- data/app/components/shadcn/drawer_component.rb +7 -27
- data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
- data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/field_component.rb +7 -8
- data/app/components/shadcn/hover_card_component.html.erb +12 -0
- data/app/components/shadcn/hover_card_component.rb +7 -26
- data/app/components/shadcn/input_component.html.erb +18 -0
- data/app/components/shadcn/input_component.rb +2 -27
- data/app/components/shadcn/input_otp_component.rb +3 -3
- data/app/components/shadcn/kbd_component.html.erb +1 -0
- data/app/components/shadcn/kbd_component.rb +3 -10
- data/app/components/shadcn/label_component.html.erb +3 -0
- data/app/components/shadcn/label_component.rb +2 -18
- data/app/components/shadcn/menubar_component.html.erb +6 -0
- data/app/components/shadcn/menubar_component.rb +4 -15
- data/app/components/shadcn/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- data/app/components/shadcn/native_select_component.html.erb +22 -0
- data/app/components/shadcn/native_select_component.rb +9 -39
- data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
- data/app/components/shadcn/navigation_menu_component.rb +4 -15
- data/app/components/shadcn/pagination_component.html.erb +5 -0
- data/app/components/shadcn/pagination_component.rb +11 -15
- data/app/components/shadcn/popover_component.html.erb +15 -0
- data/app/components/shadcn/popover_component.rb +10 -30
- data/app/components/shadcn/progress_component.html.erb +13 -0
- data/app/components/shadcn/progress_component.rb +6 -26
- data/app/components/shadcn/radio_group_component.html.erb +8 -0
- data/app/components/shadcn/radio_group_component.rb +12 -26
- data/app/components/shadcn/radio_group_item_component.rb +32 -6
- data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
- data/app/components/shadcn/scroll_area_component.html.erb +7 -0
- data/app/components/shadcn/scroll_area_component.rb +4 -16
- data/app/components/shadcn/select_component.html.erb +46 -0
- data/app/components/shadcn/select_component.rb +29 -86
- data/app/components/shadcn/separator_component.html.erb +5 -0
- data/app/components/shadcn/separator_component.rb +6 -14
- data/app/components/shadcn/sheet_component.html.erb +12 -0
- data/app/components/shadcn/sheet_component.rb +7 -27
- data/app/components/shadcn/sidebar_component.rb +2 -2
- data/app/components/shadcn/skeleton_component.html.erb +1 -0
- data/app/components/shadcn/skeleton_component.rb +4 -2
- data/app/components/shadcn/slider_component.html.erb +12 -0
- data/app/components/shadcn/slider_component.rb +2 -21
- data/app/components/shadcn/spinner_component.html.erb +18 -0
- data/app/components/shadcn/spinner_component.rb +2 -30
- data/app/components/shadcn/switch_component.html.erb +72 -0
- data/app/components/shadcn/switch_component.rb +4 -82
- data/app/components/shadcn/table_component.html.erb +9 -0
- data/app/components/shadcn/table_component.rb +2 -10
- data/app/components/shadcn/tabs_component.html.erb +8 -0
- data/app/components/shadcn/tabs_component.rb +4 -17
- data/app/components/shadcn/textarea_component.html.erb +13 -0
- data/app/components/shadcn/textarea_component.rb +6 -22
- data/app/components/shadcn/toast_component.html.erb +36 -0
- data/app/components/shadcn/toast_component.rb +6 -54
- data/app/components/shadcn/toggle_component.html.erb +12 -0
- data/app/components/shadcn/toggle_component.rb +6 -21
- data/app/components/shadcn/toggle_group_component.html.erb +14 -0
- data/app/components/shadcn/toggle_group_component.rb +6 -29
- data/app/components/shadcn/tooltip_component.html.erb +20 -0
- data/app/components/shadcn/tooltip_component.rb +13 -38
- data/lib/generators/shadcn/add/USAGE +24 -0
- data/lib/generators/shadcn/add/add_generator.rb +279 -0
- data/lib/generators/shadcn/install/USAGE +22 -0
- data/lib/generators/shadcn/install/install_generator.rb +8 -3
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
- data/lib/shadcn/rails/version.rb +1 -1
- metadata +54 -42
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -463
- data/PROGRESS.md +0 -485
- data/Rakefile +0 -29
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
- data/__tests__/controllers/accordion_controller.test.js +0 -904
- data/__tests__/controllers/calendar_controller.test.js +0 -1370
- data/__tests__/controllers/carousel_controller.test.js +0 -912
- data/__tests__/controllers/checkbox_controller.test.js +0 -454
- data/__tests__/controllers/collapsible_controller.test.js +0 -407
- data/__tests__/controllers/combobox_controller.test.js +0 -966
- data/__tests__/controllers/context_menu_controller.test.js +0 -627
- data/__tests__/controllers/date_picker_controller.test.js +0 -636
- data/__tests__/controllers/dialog_controller.test.js +0 -878
- data/__tests__/controllers/drawer_controller.test.js +0 -995
- data/__tests__/controllers/menubar_controller.test.js +0 -736
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
- data/__tests__/controllers/popover_controller.test.js +0 -1007
- data/__tests__/controllers/radio_group_controller.test.js +0 -640
- data/__tests__/controllers/resizable_controller.test.js +0 -680
- data/__tests__/controllers/select_controller.test.js +0 -674
- data/__tests__/controllers/sheet_controller.test.js +0 -986
- data/__tests__/controllers/slider_controller.test.js +0 -1036
- data/__tests__/controllers/switch_controller.test.js +0 -424
- data/__tests__/controllers/tabs_controller.test.js +0 -907
- data/__tests__/controllers/toggle_group_controller.test.js +0 -839
- data/__tests__/controllers/tooltip_controller.test.js +0 -808
- data/__tests__/helpers/stimulus-test-helper.js +0 -203
- data/babel.config.cjs +0 -5
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/jest.config.js +0 -19
- data/jest.setup.js +0 -8
- data/lib/generators/shadcn/component/component_generator.rb +0 -188
- data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
- data/package-lock.json +0 -7415
- data/package.json +0 -68
- data/rollup.config.js +0 -29
|
@@ -1,674 +0,0 @@
|
|
|
1
|
-
import { Application } from "@hotwired/stimulus"
|
|
2
|
-
import SelectController from "../../app/assets/javascripts/shadcn/controllers/select_controller.js"
|
|
3
|
-
import { setupController, cleanupController, click, nextFrame, keydown } from '../helpers/stimulus-test-helper.js'
|
|
4
|
-
|
|
5
|
-
describe("SelectController", () => {
|
|
6
|
-
let application
|
|
7
|
-
let element
|
|
8
|
-
let controller
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
cleanupController(application)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe("basic rendering and initialization", () => {
|
|
15
|
-
const basicHTML = `
|
|
16
|
-
<div data-controller="shadcn--select"
|
|
17
|
-
data-shadcn--select-value-value="">
|
|
18
|
-
<button data-shadcn--select-target="trigger"
|
|
19
|
-
role="combobox"
|
|
20
|
-
aria-expanded="false"
|
|
21
|
-
data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
|
|
22
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
23
|
-
</button>
|
|
24
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
25
|
-
<div data-shadcn--select-target="content"
|
|
26
|
-
role="listbox"
|
|
27
|
-
hidden
|
|
28
|
-
data-state="closed">
|
|
29
|
-
<div data-shadcn--select-target="item"
|
|
30
|
-
data-value="apple"
|
|
31
|
-
role="option"
|
|
32
|
-
tabindex="-1"
|
|
33
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
34
|
-
<div data-shadcn--select-target="item"
|
|
35
|
-
data-value="banana"
|
|
36
|
-
role="option"
|
|
37
|
-
tabindex="-1"
|
|
38
|
-
data-action="click->shadcn--select#select">Banana</div>
|
|
39
|
-
<div data-shadcn--select-target="item"
|
|
40
|
-
data-value="cherry"
|
|
41
|
-
role="option"
|
|
42
|
-
tabindex="-1"
|
|
43
|
-
data-action="click->shadcn--select#select">Cherry</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
beforeEach(async () => {
|
|
49
|
-
const setup = await setupController(SelectController, basicHTML, 'shadcn--select')
|
|
50
|
-
application = setup.application
|
|
51
|
-
element = setup.element
|
|
52
|
-
controller = setup.controller
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test("initializes with closed state", () => {
|
|
56
|
-
expect(controller.isOpen).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test("initializes with empty value", () => {
|
|
60
|
-
expect(controller.valueValue).toBe("")
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test("content is hidden by default", () => {
|
|
64
|
-
expect(controller.contentTarget.hidden).toBe(true)
|
|
65
|
-
expect(controller.contentTarget.dataset.state).toBe("closed")
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test("trigger has correct aria-expanded", () => {
|
|
69
|
-
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe("opening and closing", () => {
|
|
74
|
-
const basicHTML = `
|
|
75
|
-
<div data-controller="shadcn--select">
|
|
76
|
-
<button data-shadcn--select-target="trigger"
|
|
77
|
-
role="combobox"
|
|
78
|
-
aria-expanded="false"
|
|
79
|
-
data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
|
|
80
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
81
|
-
</button>
|
|
82
|
-
<div data-shadcn--select-target="content"
|
|
83
|
-
role="listbox"
|
|
84
|
-
hidden
|
|
85
|
-
data-state="closed">
|
|
86
|
-
<div data-shadcn--select-target="item"
|
|
87
|
-
data-value="apple"
|
|
88
|
-
tabindex="-1"
|
|
89
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
beforeEach(async () => {
|
|
95
|
-
const setup = await setupController(SelectController, basicHTML, 'shadcn--select')
|
|
96
|
-
application = setup.application
|
|
97
|
-
element = setup.element
|
|
98
|
-
controller = setup.controller
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test("opens on toggle", () => {
|
|
102
|
-
controller.toggle()
|
|
103
|
-
|
|
104
|
-
expect(controller.isOpen).toBe(true)
|
|
105
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
106
|
-
expect(controller.contentTarget.dataset.state).toBe("open")
|
|
107
|
-
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("true")
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("closes when already open", () => {
|
|
111
|
-
controller.open()
|
|
112
|
-
controller.toggle()
|
|
113
|
-
|
|
114
|
-
expect(controller.isOpen).toBe(false)
|
|
115
|
-
expect(controller.triggerTarget.getAttribute("aria-expanded")).toBe("false")
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test("dispatches opened event on open", () => {
|
|
119
|
-
let eventFired = false
|
|
120
|
-
element.addEventListener("shadcn--select:opened", () => {
|
|
121
|
-
eventFired = true
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
controller.open()
|
|
125
|
-
expect(eventFired).toBe(true)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test("dispatches closed event on close", () => {
|
|
129
|
-
let eventFired = false
|
|
130
|
-
element.addEventListener("shadcn--select:closed", () => {
|
|
131
|
-
eventFired = true
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
controller.open()
|
|
135
|
-
controller.close()
|
|
136
|
-
expect(eventFired).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test("opens with trigger click", async () => {
|
|
140
|
-
click(controller.triggerTarget)
|
|
141
|
-
await nextFrame()
|
|
142
|
-
|
|
143
|
-
expect(controller.isOpen).toBe(true)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
test("does not re-open if already open", () => {
|
|
147
|
-
controller.open()
|
|
148
|
-
const dispatchSpy = jest.spyOn(controller, 'dispatch')
|
|
149
|
-
|
|
150
|
-
controller.open()
|
|
151
|
-
|
|
152
|
-
// Should not dispatch opened event again
|
|
153
|
-
expect(dispatchSpy).not.toHaveBeenCalledWith("opened")
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describe("item selection", () => {
|
|
158
|
-
const selectionHTML = `
|
|
159
|
-
<div data-controller="shadcn--select"
|
|
160
|
-
data-shadcn--select-value-value="">
|
|
161
|
-
<button data-shadcn--select-target="trigger"
|
|
162
|
-
data-action="click->shadcn--select#toggle">
|
|
163
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
164
|
-
</button>
|
|
165
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
166
|
-
<div data-shadcn--select-target="content"
|
|
167
|
-
hidden
|
|
168
|
-
data-state="closed">
|
|
169
|
-
<div data-shadcn--select-target="item"
|
|
170
|
-
data-value="apple"
|
|
171
|
-
tabindex="-1"
|
|
172
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
173
|
-
<div data-shadcn--select-target="item"
|
|
174
|
-
data-value="banana"
|
|
175
|
-
tabindex="-1"
|
|
176
|
-
data-action="click->shadcn--select#select">Banana</div>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
beforeEach(async () => {
|
|
182
|
-
const setup = await setupController(SelectController, selectionHTML, 'shadcn--select')
|
|
183
|
-
application = setup.application
|
|
184
|
-
element = setup.element
|
|
185
|
-
controller = setup.controller
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
test("selects item when clicked", async () => {
|
|
189
|
-
controller.open()
|
|
190
|
-
const appleItem = controller.itemTargets[0]
|
|
191
|
-
click(appleItem)
|
|
192
|
-
await nextFrame()
|
|
193
|
-
|
|
194
|
-
expect(controller.valueValue).toBe("apple")
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
test("updates display text on selection", async () => {
|
|
198
|
-
controller.open()
|
|
199
|
-
const appleItem = controller.itemTargets[0]
|
|
200
|
-
click(appleItem)
|
|
201
|
-
await nextFrame()
|
|
202
|
-
|
|
203
|
-
expect(controller.displayTarget.textContent).toBe("Apple")
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
test("updates hidden input value on selection", async () => {
|
|
207
|
-
controller.open()
|
|
208
|
-
const bananaItem = controller.itemTargets[1]
|
|
209
|
-
click(bananaItem)
|
|
210
|
-
await nextFrame()
|
|
211
|
-
|
|
212
|
-
expect(controller.inputTarget.value).toBe("banana")
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
test("closes dropdown after selection", async () => {
|
|
216
|
-
controller.open()
|
|
217
|
-
const appleItem = controller.itemTargets[0]
|
|
218
|
-
click(appleItem)
|
|
219
|
-
await nextFrame()
|
|
220
|
-
|
|
221
|
-
expect(controller.isOpen).toBe(false)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
test("dispatches change event on selection", async () => {
|
|
225
|
-
let eventDetail = null
|
|
226
|
-
element.addEventListener("shadcn--select:change", (e) => {
|
|
227
|
-
eventDetail = e.detail
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
controller.open()
|
|
231
|
-
const appleItem = controller.itemTargets[0]
|
|
232
|
-
click(appleItem)
|
|
233
|
-
await nextFrame()
|
|
234
|
-
|
|
235
|
-
expect(eventDetail).not.toBeNull()
|
|
236
|
-
expect(eventDetail.value).toBe("apple")
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
test("updates aria-selected on items", async () => {
|
|
240
|
-
controller.open()
|
|
241
|
-
const appleItem = controller.itemTargets[0]
|
|
242
|
-
const bananaItem = controller.itemTargets[1]
|
|
243
|
-
click(appleItem)
|
|
244
|
-
await nextFrame()
|
|
245
|
-
|
|
246
|
-
expect(appleItem.getAttribute("aria-selected")).toBe("true")
|
|
247
|
-
expect(bananaItem.getAttribute("aria-selected")).toBe("false")
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
describe("disabled items", () => {
|
|
252
|
-
const disabledHTML = `
|
|
253
|
-
<div data-controller="shadcn--select">
|
|
254
|
-
<button data-shadcn--select-target="trigger">
|
|
255
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
256
|
-
</button>
|
|
257
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
258
|
-
<div data-shadcn--select-target="content">
|
|
259
|
-
<div data-shadcn--select-target="item"
|
|
260
|
-
data-value="apple"
|
|
261
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
262
|
-
<div data-shadcn--select-target="item"
|
|
263
|
-
data-value="banana"
|
|
264
|
-
data-disabled
|
|
265
|
-
data-action="click->shadcn--select#select">Banana (Disabled)</div>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
`
|
|
269
|
-
|
|
270
|
-
beforeEach(async () => {
|
|
271
|
-
const setup = await setupController(SelectController, disabledHTML, 'shadcn--select')
|
|
272
|
-
application = setup.application
|
|
273
|
-
element = setup.element
|
|
274
|
-
controller = setup.controller
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
test("does not select disabled item", async () => {
|
|
278
|
-
controller.open()
|
|
279
|
-
const disabledItem = controller.itemTargets[1]
|
|
280
|
-
click(disabledItem)
|
|
281
|
-
await nextFrame()
|
|
282
|
-
|
|
283
|
-
expect(controller.valueValue).toBe("")
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
test("enabledItems excludes disabled items", () => {
|
|
287
|
-
const enabled = controller.enabledItems
|
|
288
|
-
expect(enabled.length).toBe(1)
|
|
289
|
-
expect(enabled[0].dataset.value).toBe("apple")
|
|
290
|
-
})
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
describe("keyboard navigation", () => {
|
|
294
|
-
const keyboardHTML = `
|
|
295
|
-
<div data-controller="shadcn--select"
|
|
296
|
-
data-action="keydown.escape->shadcn--select#close">
|
|
297
|
-
<button data-shadcn--select-target="trigger"
|
|
298
|
-
data-action="click->shadcn--select#toggle keydown->shadcn--select#handleKeydown">
|
|
299
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
300
|
-
</button>
|
|
301
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
302
|
-
<div data-shadcn--select-target="content"
|
|
303
|
-
hidden
|
|
304
|
-
data-state="closed">
|
|
305
|
-
<div data-shadcn--select-target="item"
|
|
306
|
-
data-value="apple"
|
|
307
|
-
tabindex="-1"
|
|
308
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
309
|
-
<div data-shadcn--select-target="item"
|
|
310
|
-
data-value="banana"
|
|
311
|
-
tabindex="-1"
|
|
312
|
-
data-action="click->shadcn--select#select">Banana</div>
|
|
313
|
-
<div data-shadcn--select-target="item"
|
|
314
|
-
data-value="cherry"
|
|
315
|
-
tabindex="-1"
|
|
316
|
-
data-action="click->shadcn--select#select">Cherry</div>
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
`
|
|
320
|
-
|
|
321
|
-
beforeEach(async () => {
|
|
322
|
-
const setup = await setupController(SelectController, keyboardHTML, 'shadcn--select')
|
|
323
|
-
application = setup.application
|
|
324
|
-
element = setup.element
|
|
325
|
-
controller = setup.controller
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
test("opens on Enter key when closed", () => {
|
|
329
|
-
controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
|
|
330
|
-
|
|
331
|
-
expect(controller.isOpen).toBe(true)
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
test("opens on Space key when closed", () => {
|
|
335
|
-
controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
|
|
336
|
-
|
|
337
|
-
expect(controller.isOpen).toBe(true)
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
test("opens on ArrowDown key when closed", () => {
|
|
341
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
|
|
342
|
-
|
|
343
|
-
expect(controller.isOpen).toBe(true)
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
test("closes on Escape key when open", () => {
|
|
347
|
-
controller.open()
|
|
348
|
-
controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
|
|
349
|
-
|
|
350
|
-
expect(controller.isOpen).toBe(false)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
test("navigates down with ArrowDown", () => {
|
|
354
|
-
controller.open()
|
|
355
|
-
controller.focusedIndex = 0
|
|
356
|
-
|
|
357
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
|
|
358
|
-
|
|
359
|
-
expect(controller.focusedIndex).toBe(1)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
test("navigates up with ArrowUp", () => {
|
|
363
|
-
controller.open()
|
|
364
|
-
controller.focusedIndex = 1
|
|
365
|
-
|
|
366
|
-
controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
|
|
367
|
-
|
|
368
|
-
expect(controller.focusedIndex).toBe(0)
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
test("wraps to first item from last with ArrowDown", () => {
|
|
372
|
-
controller.open()
|
|
373
|
-
controller.focusedIndex = 2
|
|
374
|
-
|
|
375
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
|
|
376
|
-
|
|
377
|
-
expect(controller.focusedIndex).toBe(0)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
test("wraps to last item from first with ArrowUp", () => {
|
|
381
|
-
controller.open()
|
|
382
|
-
controller.focusedIndex = 0
|
|
383
|
-
|
|
384
|
-
controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
|
|
385
|
-
|
|
386
|
-
expect(controller.focusedIndex).toBe(2)
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
test("jumps to first item with Home", () => {
|
|
390
|
-
controller.open()
|
|
391
|
-
controller.focusedIndex = 2
|
|
392
|
-
|
|
393
|
-
controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
|
|
394
|
-
|
|
395
|
-
expect(controller.focusedIndex).toBe(0)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test("jumps to last item with End", () => {
|
|
399
|
-
controller.open()
|
|
400
|
-
controller.focusedIndex = 0
|
|
401
|
-
|
|
402
|
-
controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
|
|
403
|
-
|
|
404
|
-
expect(controller.focusedIndex).toBe(2)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
test("selects focused item on Enter", async () => {
|
|
408
|
-
controller.open()
|
|
409
|
-
controller.focusedIndex = 1
|
|
410
|
-
|
|
411
|
-
controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
|
|
412
|
-
await nextFrame()
|
|
413
|
-
|
|
414
|
-
expect(controller.valueValue).toBe("banana")
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
test("selects focused item on Space", async () => {
|
|
418
|
-
controller.open()
|
|
419
|
-
controller.focusedIndex = 0
|
|
420
|
-
|
|
421
|
-
controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
|
|
422
|
-
await nextFrame()
|
|
423
|
-
|
|
424
|
-
expect(controller.valueValue).toBe("apple")
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
describe("initial value", () => {
|
|
429
|
-
const initialValueHTML = `
|
|
430
|
-
<div data-controller="shadcn--select"
|
|
431
|
-
data-shadcn--select-value-value="banana">
|
|
432
|
-
<button data-shadcn--select-target="trigger">
|
|
433
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
434
|
-
</button>
|
|
435
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
436
|
-
<div data-shadcn--select-target="content">
|
|
437
|
-
<div data-shadcn--select-target="item"
|
|
438
|
-
data-value="apple"
|
|
439
|
-
data-action="click->shadcn--select#select">Apple</div>
|
|
440
|
-
<div data-shadcn--select-target="item"
|
|
441
|
-
data-value="banana"
|
|
442
|
-
data-action="click->shadcn--select#select">Banana</div>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
`
|
|
446
|
-
|
|
447
|
-
beforeEach(async () => {
|
|
448
|
-
const setup = await setupController(SelectController, initialValueHTML, 'shadcn--select')
|
|
449
|
-
application = setup.application
|
|
450
|
-
element = setup.element
|
|
451
|
-
controller = setup.controller
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
test("initializes with pre-set value", () => {
|
|
455
|
-
expect(controller.valueValue).toBe("banana")
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
test("sets display text from initial value", () => {
|
|
459
|
-
expect(controller.displayTarget.textContent).toBe("Banana")
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
test("marks correct item as selected on init", () => {
|
|
463
|
-
const bananaItem = controller.itemTargets[1]
|
|
464
|
-
expect(bananaItem.getAttribute("aria-selected")).toBe("true")
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
test("focuses current value item when opening", async () => {
|
|
468
|
-
const focusSpy = jest.spyOn(controller.itemTargets[1], 'focus')
|
|
469
|
-
controller.open()
|
|
470
|
-
await nextFrame()
|
|
471
|
-
|
|
472
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
473
|
-
})
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
describe("click outside", () => {
|
|
477
|
-
const clickOutsideHTML = `
|
|
478
|
-
<div data-controller="shadcn--select">
|
|
479
|
-
<button data-shadcn--select-target="trigger"
|
|
480
|
-
data-action="click->shadcn--select#toggle">
|
|
481
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
482
|
-
</button>
|
|
483
|
-
<div data-shadcn--select-target="content" hidden>
|
|
484
|
-
<div data-shadcn--select-target="item" data-value="apple">Apple</div>
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
<div id="outside">Outside Element</div>
|
|
488
|
-
`
|
|
489
|
-
|
|
490
|
-
beforeEach(async () => {
|
|
491
|
-
const setup = await setupController(SelectController, clickOutsideHTML, 'shadcn--select')
|
|
492
|
-
application = setup.application
|
|
493
|
-
element = setup.element
|
|
494
|
-
controller = setup.controller
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
test("closes when clicking outside", async () => {
|
|
498
|
-
controller.open()
|
|
499
|
-
await nextFrame()
|
|
500
|
-
|
|
501
|
-
const outsideElement = document.getElementById("outside")
|
|
502
|
-
click(outsideElement)
|
|
503
|
-
await nextFrame()
|
|
504
|
-
|
|
505
|
-
expect(controller.isOpen).toBe(false)
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
test("does not close when clicking inside", async () => {
|
|
509
|
-
controller.open()
|
|
510
|
-
await nextFrame()
|
|
511
|
-
|
|
512
|
-
click(controller.contentTarget)
|
|
513
|
-
await nextFrame()
|
|
514
|
-
|
|
515
|
-
expect(controller.isOpen).toBe(true)
|
|
516
|
-
})
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
describe("trigger width synchronization", () => {
|
|
520
|
-
const widthSyncHTML = `
|
|
521
|
-
<div data-controller="shadcn--select">
|
|
522
|
-
<button data-shadcn--select-target="trigger"
|
|
523
|
-
style="width: 200px;"
|
|
524
|
-
data-action="click->shadcn--select#toggle">
|
|
525
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
526
|
-
</button>
|
|
527
|
-
<div data-shadcn--select-target="content" hidden>
|
|
528
|
-
<div data-shadcn--select-target="item" data-value="apple">Apple</div>
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
`
|
|
532
|
-
|
|
533
|
-
beforeEach(async () => {
|
|
534
|
-
const setup = await setupController(SelectController, widthSyncHTML, 'shadcn--select')
|
|
535
|
-
application = setup.application
|
|
536
|
-
element = setup.element
|
|
537
|
-
controller = setup.controller
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
test("sets trigger width CSS variable on open", async () => {
|
|
541
|
-
controller.open()
|
|
542
|
-
await nextFrame()
|
|
543
|
-
|
|
544
|
-
const cssVar = controller.contentTarget.style.getPropertyValue('--radix-select-trigger-width')
|
|
545
|
-
expect(cssVar).toBeTruthy()
|
|
546
|
-
})
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
describe("check icon visibility", () => {
|
|
550
|
-
const checkIconHTML = `
|
|
551
|
-
<div data-controller="shadcn--select">
|
|
552
|
-
<button data-shadcn--select-target="trigger">
|
|
553
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
554
|
-
</button>
|
|
555
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
556
|
-
<div data-shadcn--select-target="content">
|
|
557
|
-
<div data-shadcn--select-target="item"
|
|
558
|
-
data-value="apple"
|
|
559
|
-
data-action="click->shadcn--select#select">
|
|
560
|
-
Apple
|
|
561
|
-
<span data-shadcn--select-target="checkIcon" style="opacity: 0;">✓</span>
|
|
562
|
-
</div>
|
|
563
|
-
<div data-shadcn--select-target="item"
|
|
564
|
-
data-value="banana"
|
|
565
|
-
data-action="click->shadcn--select#select">
|
|
566
|
-
Banana
|
|
567
|
-
<span data-shadcn--select-target="checkIcon" style="opacity: 0;">✓</span>
|
|
568
|
-
</div>
|
|
569
|
-
</div>
|
|
570
|
-
</div>
|
|
571
|
-
`
|
|
572
|
-
|
|
573
|
-
beforeEach(async () => {
|
|
574
|
-
const setup = await setupController(SelectController, checkIconHTML, 'shadcn--select')
|
|
575
|
-
application = setup.application
|
|
576
|
-
element = setup.element
|
|
577
|
-
controller = setup.controller
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
test("shows check icon for selected item", async () => {
|
|
581
|
-
controller.open()
|
|
582
|
-
const appleItem = controller.itemTargets[0]
|
|
583
|
-
click(appleItem)
|
|
584
|
-
await nextFrame()
|
|
585
|
-
|
|
586
|
-
const checkIcon = appleItem.querySelector('[data-shadcn--select-target="checkIcon"]')
|
|
587
|
-
expect(checkIcon.style.opacity).toBe("1")
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
test("hides check icon for non-selected items", async () => {
|
|
591
|
-
controller.open()
|
|
592
|
-
const appleItem = controller.itemTargets[0]
|
|
593
|
-
click(appleItem)
|
|
594
|
-
await nextFrame()
|
|
595
|
-
|
|
596
|
-
const bananaItem = controller.itemTargets[1]
|
|
597
|
-
const checkIcon = bananaItem.querySelector('[data-shadcn--select-target="checkIcon"]')
|
|
598
|
-
expect(checkIcon.style.opacity).toBe("0")
|
|
599
|
-
})
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
describe("programmatic value change", () => {
|
|
603
|
-
const programmaticHTML = `
|
|
604
|
-
<div data-controller="shadcn--select">
|
|
605
|
-
<button data-shadcn--select-target="trigger">
|
|
606
|
-
<span data-shadcn--select-target="display">Select...</span>
|
|
607
|
-
</button>
|
|
608
|
-
<input type="hidden" data-shadcn--select-target="input" name="fruit">
|
|
609
|
-
<div data-shadcn--select-target="content">
|
|
610
|
-
<div data-shadcn--select-target="item" data-value="apple">Apple</div>
|
|
611
|
-
<div data-shadcn--select-target="item" data-value="banana">Banana</div>
|
|
612
|
-
</div>
|
|
613
|
-
</div>
|
|
614
|
-
`
|
|
615
|
-
|
|
616
|
-
beforeEach(async () => {
|
|
617
|
-
const setup = await setupController(SelectController, programmaticHTML, 'shadcn--select')
|
|
618
|
-
application = setup.application
|
|
619
|
-
element = setup.element
|
|
620
|
-
controller = setup.controller
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
test("selectByValue updates value without dispatch when specified", () => {
|
|
624
|
-
let eventFired = false
|
|
625
|
-
element.addEventListener("shadcn--select:change", () => {
|
|
626
|
-
eventFired = true
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
controller.selectByValue("apple", false)
|
|
630
|
-
|
|
631
|
-
expect(controller.valueValue).toBe("apple")
|
|
632
|
-
expect(eventFired).toBe(false)
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
test("selectByValue dispatches change when dispatch is true", () => {
|
|
636
|
-
let eventDetail = null
|
|
637
|
-
element.addEventListener("shadcn--select:change", (e) => {
|
|
638
|
-
eventDetail = e.detail
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
controller.selectByValue("banana", true)
|
|
642
|
-
|
|
643
|
-
expect(eventDetail).not.toBeNull()
|
|
644
|
-
expect(eventDetail.value).toBe("banana")
|
|
645
|
-
})
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
describe("disconnect cleanup", () => {
|
|
649
|
-
const disconnectHTML = `
|
|
650
|
-
<div data-controller="shadcn--select">
|
|
651
|
-
<button data-shadcn--select-target="trigger">Select...</button>
|
|
652
|
-
<div data-shadcn--select-target="content" hidden>
|
|
653
|
-
<div data-shadcn--select-target="item" data-value="apple">Apple</div>
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
|
-
`
|
|
657
|
-
|
|
658
|
-
beforeEach(async () => {
|
|
659
|
-
const setup = await setupController(SelectController, disconnectHTML, 'shadcn--select')
|
|
660
|
-
application = setup.application
|
|
661
|
-
element = setup.element
|
|
662
|
-
controller = setup.controller
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
test("cleans up event listeners on disconnect", () => {
|
|
666
|
-
controller.open()
|
|
667
|
-
|
|
668
|
-
const closeSpy = jest.spyOn(controller, 'close')
|
|
669
|
-
controller.disconnect()
|
|
670
|
-
|
|
671
|
-
expect(closeSpy).toHaveBeenCalled()
|
|
672
|
-
})
|
|
673
|
-
})
|
|
674
|
-
})
|