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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -2
  3. data/README.md +102 -1398
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
  6. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
  7. data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
  8. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
  9. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
  10. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  11. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
  12. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
  13. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
  14. data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
  15. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
  16. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  17. data/app/assets/javascripts/shadcn/index.js +9 -1
  18. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  19. data/app/assets/stylesheets/shadcn/base.css +32 -0
  20. data/app/assets/stylesheets/shadcn/components.css +12 -0
  21. data/app/components/shadcn/accordion_component.html.erb +8 -0
  22. data/app/components/shadcn/accordion_component.rb +6 -15
  23. data/app/components/shadcn/alert_component.html.erb +6 -0
  24. data/app/components/shadcn/alert_component.rb +0 -18
  25. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  26. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  27. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  28. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  29. data/app/components/shadcn/avatar_component.html.erb +20 -0
  30. data/app/components/shadcn/avatar_component.rb +8 -36
  31. data/app/components/shadcn/badge_component.html.erb +1 -0
  32. data/app/components/shadcn/badge_component.rb +0 -11
  33. data/app/components/shadcn/base_component.rb +15 -2
  34. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  35. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  36. data/app/components/shadcn/button_component.html.erb +18 -0
  37. data/app/components/shadcn/button_component.rb +1 -41
  38. data/app/components/shadcn/card_component.html.erb +8 -0
  39. data/app/components/shadcn/card_component.rb +2 -6
  40. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  41. data/app/components/shadcn/checkbox_component.rb +4 -43
  42. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  43. data/app/components/shadcn/collapsible_component.rb +6 -15
  44. data/app/components/shadcn/command_list_component.rb +29 -14
  45. data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
  46. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  47. data/app/components/shadcn/context_menu_component.rb +6 -26
  48. data/app/components/shadcn/context_menu_content_component.rb +37 -14
  49. data/app/components/shadcn/context_menu_item_component.rb +3 -2
  50. data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
  51. data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
  52. data/app/components/shadcn/dialog_component.html.erb +14 -0
  53. data/app/components/shadcn/dialog_component.rb +8 -29
  54. data/app/components/shadcn/drawer_component.html.erb +12 -0
  55. data/app/components/shadcn/drawer_component.rb +7 -27
  56. data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
  57. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  58. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  59. data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
  60. data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
  61. data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
  62. data/app/components/shadcn/field_component.rb +7 -8
  63. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  64. data/app/components/shadcn/hover_card_component.rb +7 -26
  65. data/app/components/shadcn/input_component.html.erb +18 -0
  66. data/app/components/shadcn/input_component.rb +2 -27
  67. data/app/components/shadcn/input_otp_component.rb +3 -3
  68. data/app/components/shadcn/kbd_component.html.erb +1 -0
  69. data/app/components/shadcn/kbd_component.rb +3 -10
  70. data/app/components/shadcn/label_component.html.erb +3 -0
  71. data/app/components/shadcn/label_component.rb +2 -18
  72. data/app/components/shadcn/menubar_component.html.erb +6 -0
  73. data/app/components/shadcn/menubar_component.rb +4 -15
  74. data/app/components/shadcn/menubar_content_component.rb +45 -20
  75. data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
  76. data/app/components/shadcn/native_select_component.html.erb +22 -0
  77. data/app/components/shadcn/native_select_component.rb +9 -39
  78. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  79. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  80. data/app/components/shadcn/pagination_component.html.erb +5 -0
  81. data/app/components/shadcn/pagination_component.rb +11 -15
  82. data/app/components/shadcn/popover_component.html.erb +15 -0
  83. data/app/components/shadcn/popover_component.rb +10 -30
  84. data/app/components/shadcn/progress_component.html.erb +13 -0
  85. data/app/components/shadcn/progress_component.rb +6 -26
  86. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  87. data/app/components/shadcn/radio_group_component.rb +12 -26
  88. data/app/components/shadcn/radio_group_item_component.rb +32 -6
  89. data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
  90. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  91. data/app/components/shadcn/scroll_area_component.rb +4 -16
  92. data/app/components/shadcn/select_component.html.erb +46 -0
  93. data/app/components/shadcn/select_component.rb +29 -86
  94. data/app/components/shadcn/separator_component.html.erb +5 -0
  95. data/app/components/shadcn/separator_component.rb +6 -14
  96. data/app/components/shadcn/sheet_component.html.erb +12 -0
  97. data/app/components/shadcn/sheet_component.rb +7 -27
  98. data/app/components/shadcn/sidebar_component.rb +2 -2
  99. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  100. data/app/components/shadcn/skeleton_component.rb +4 -2
  101. data/app/components/shadcn/slider_component.html.erb +12 -0
  102. data/app/components/shadcn/slider_component.rb +2 -21
  103. data/app/components/shadcn/spinner_component.html.erb +18 -0
  104. data/app/components/shadcn/spinner_component.rb +2 -30
  105. data/app/components/shadcn/switch_component.html.erb +72 -0
  106. data/app/components/shadcn/switch_component.rb +4 -82
  107. data/app/components/shadcn/table_component.html.erb +9 -0
  108. data/app/components/shadcn/table_component.rb +2 -10
  109. data/app/components/shadcn/tabs_component.html.erb +8 -0
  110. data/app/components/shadcn/tabs_component.rb +4 -17
  111. data/app/components/shadcn/textarea_component.html.erb +13 -0
  112. data/app/components/shadcn/textarea_component.rb +6 -22
  113. data/app/components/shadcn/toast_component.html.erb +36 -0
  114. data/app/components/shadcn/toast_component.rb +6 -54
  115. data/app/components/shadcn/toggle_component.html.erb +12 -0
  116. data/app/components/shadcn/toggle_component.rb +6 -21
  117. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  118. data/app/components/shadcn/toggle_group_component.rb +6 -29
  119. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  120. data/app/components/shadcn/tooltip_component.rb +13 -38
  121. data/lib/generators/shadcn/add/USAGE +24 -0
  122. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  123. data/lib/generators/shadcn/install/USAGE +22 -0
  124. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  125. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  126. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  127. data/lib/shadcn/rails/version.rb +1 -1
  128. metadata +54 -42
  129. data/.dockerignore +0 -40
  130. data/CLAUDE.md +0 -463
  131. data/PROGRESS.md +0 -485
  132. data/Rakefile +0 -29
  133. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  134. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  135. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  136. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  137. data/__tests__/controllers/accordion_controller.test.js +0 -904
  138. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  139. data/__tests__/controllers/carousel_controller.test.js +0 -912
  140. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  141. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  142. data/__tests__/controllers/combobox_controller.test.js +0 -966
  143. data/__tests__/controllers/context_menu_controller.test.js +0 -627
  144. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  145. data/__tests__/controllers/dialog_controller.test.js +0 -878
  146. data/__tests__/controllers/drawer_controller.test.js +0 -995
  147. data/__tests__/controllers/menubar_controller.test.js +0 -736
  148. data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
  149. data/__tests__/controllers/popover_controller.test.js +0 -1007
  150. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  151. data/__tests__/controllers/resizable_controller.test.js +0 -680
  152. data/__tests__/controllers/select_controller.test.js +0 -674
  153. data/__tests__/controllers/sheet_controller.test.js +0 -986
  154. data/__tests__/controllers/slider_controller.test.js +0 -1036
  155. data/__tests__/controllers/switch_controller.test.js +0 -424
  156. data/__tests__/controllers/tabs_controller.test.js +0 -907
  157. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  158. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  159. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  160. data/babel.config.cjs +0 -5
  161. data/bin/console +0 -11
  162. data/bin/setup +0 -8
  163. data/jest.config.js +0 -19
  164. data/jest.setup.js +0 -8
  165. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  166. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  167. data/package-lock.json +0 -7415
  168. data/package.json +0 -68
  169. data/rollup.config.js +0 -29
@@ -1,839 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
- import ToggleGroupController from "../../app/assets/javascripts/shadcn/controllers/toggle_group_controller.js"
3
- import { setupController, cleanupController, click, wait, nextFrame, keydown, waitForEvent } from '../helpers/stimulus-test-helper.js'
4
-
5
- describe("ToggleGroupController", () => {
6
- let application
7
- let element
8
- let controller
9
-
10
- afterEach(() => {
11
- cleanupController(application)
12
- })
13
-
14
- describe("single mode", () => {
15
- const singleModeHTML = `
16
- <div data-controller="shadcn--toggle-group"
17
- data-shadcn--toggle-group-type-value="single">
18
- <button data-shadcn--toggle-group-target="item"
19
- data-value="bold"
20
- data-action="click->shadcn--toggle-group#toggle">Bold</button>
21
- <button data-shadcn--toggle-group-target="item"
22
- data-value="italic"
23
- data-action="click->shadcn--toggle-group#toggle">Italic</button>
24
- <button data-shadcn--toggle-group-target="item"
25
- data-value="underline"
26
- data-action="click->shadcn--toggle-group#toggle">Underline</button>
27
- </div>
28
- `
29
-
30
- beforeEach(async () => {
31
- const setup = await setupController(ToggleGroupController, singleModeHTML, 'shadcn--toggle-group')
32
- application = setup.application
33
- element = setup.element
34
- controller = setup.controller
35
- })
36
-
37
- test("initializes with type 'single' by default", () => {
38
- expect(controller.typeValue).toBe("single")
39
- })
40
-
41
- test("initializes with empty value", () => {
42
- expect(controller.valueValue).toBe("")
43
- })
44
-
45
- test("initializes all items with data-state='off'", () => {
46
- controller.itemTargets.forEach(item => {
47
- expect(item.getAttribute("data-state")).toBe("off")
48
- expect(item.getAttribute("aria-pressed")).toBe("false")
49
- })
50
- })
51
-
52
- test("selects an item when clicked", async () => {
53
- const boldButton = controller.itemTargets[0]
54
- click(boldButton)
55
- await nextFrame()
56
-
57
- expect(controller.valueValue).toBe("bold")
58
- expect(boldButton.getAttribute("data-state")).toBe("on")
59
- expect(boldButton.getAttribute("aria-pressed")).toBe("true")
60
- })
61
-
62
- test("deselects other items when selecting new item", async () => {
63
- const boldButton = controller.itemTargets[0]
64
- const italicButton = controller.itemTargets[1]
65
-
66
- // Select bold
67
- click(boldButton)
68
- await nextFrame()
69
- expect(controller.valueValue).toBe("bold")
70
- expect(boldButton.getAttribute("data-state")).toBe("on")
71
-
72
- // Select italic - bold should be deselected
73
- click(italicButton)
74
- await nextFrame()
75
- expect(controller.valueValue).toBe("italic")
76
- expect(italicButton.getAttribute("data-state")).toBe("on")
77
- expect(boldButton.getAttribute("data-state")).toBe("off")
78
- expect(boldButton.getAttribute("aria-pressed")).toBe("false")
79
- })
80
-
81
- test("toggles off when clicking selected item", async () => {
82
- const boldButton = controller.itemTargets[0]
83
-
84
- // Select bold
85
- click(boldButton)
86
- await nextFrame()
87
- expect(controller.valueValue).toBe("bold")
88
- expect(boldButton.getAttribute("data-state")).toBe("on")
89
-
90
- // Click again to deselect
91
- click(boldButton)
92
- await nextFrame()
93
- expect(controller.valueValue).toBe("")
94
- expect(boldButton.getAttribute("data-state")).toBe("off")
95
- expect(boldButton.getAttribute("aria-pressed")).toBe("false")
96
- })
97
-
98
- test("dispatches change event when selection changes", async () => {
99
- const boldButton = controller.itemTargets[0]
100
- let eventDetail = null
101
-
102
- element.addEventListener("shadcn--toggle-group:change", (e) => {
103
- eventDetail = e.detail
104
- })
105
-
106
- click(boldButton)
107
- await nextFrame()
108
-
109
- expect(eventDetail).not.toBeNull()
110
- expect(eventDetail.value).toEqual(["bold"])
111
- })
112
-
113
- test("dispatches change event on deselection", async () => {
114
- const boldButton = controller.itemTargets[0]
115
- let changeCount = 0
116
- let lastEventDetail = null
117
-
118
- element.addEventListener("shadcn--toggle-group:change", (e) => {
119
- changeCount++
120
- lastEventDetail = e.detail
121
- })
122
-
123
- // Select
124
- click(boldButton)
125
- await nextFrame()
126
- expect(changeCount).toBe(1)
127
- expect(lastEventDetail.value).toEqual(["bold"])
128
-
129
- // Deselect
130
- click(boldButton)
131
- await nextFrame()
132
- expect(changeCount).toBe(2)
133
- expect(lastEventDetail.value).toEqual([])
134
- })
135
-
136
- test("only one item can be selected at a time", async () => {
137
- const boldButton = controller.itemTargets[0]
138
- const italicButton = controller.itemTargets[1]
139
- const underlineButton = controller.itemTargets[2]
140
-
141
- click(boldButton)
142
- await nextFrame()
143
- expect(controller.getValues()).toEqual(["bold"])
144
-
145
- click(italicButton)
146
- await nextFrame()
147
- expect(controller.getValues()).toEqual(["italic"])
148
-
149
- click(underlineButton)
150
- await nextFrame()
151
- expect(controller.getValues()).toEqual(["underline"])
152
-
153
- // Verify only underline is on
154
- expect(boldButton.getAttribute("data-state")).toBe("off")
155
- expect(italicButton.getAttribute("data-state")).toBe("off")
156
- expect(underlineButton.getAttribute("data-state")).toBe("on")
157
- })
158
- })
159
-
160
- describe("multiple mode", () => {
161
- const multipleModeHTML = `
162
- <div data-controller="shadcn--toggle-group"
163
- data-shadcn--toggle-group-type-value="multiple">
164
- <button data-shadcn--toggle-group-target="item"
165
- data-value="bold"
166
- data-action="click->shadcn--toggle-group#toggle">Bold</button>
167
- <button data-shadcn--toggle-group-target="item"
168
- data-value="italic"
169
- data-action="click->shadcn--toggle-group#toggle">Italic</button>
170
- <button data-shadcn--toggle-group-target="item"
171
- data-value="underline"
172
- data-action="click->shadcn--toggle-group#toggle">Underline</button>
173
- </div>
174
- `
175
-
176
- beforeEach(async () => {
177
- const setup = await setupController(ToggleGroupController, multipleModeHTML, 'shadcn--toggle-group')
178
- application = setup.application
179
- element = setup.element
180
- controller = setup.controller
181
- })
182
-
183
- test("initializes with type 'multiple'", () => {
184
- expect(controller.typeValue).toBe("multiple")
185
- })
186
-
187
- test("can select multiple items", async () => {
188
- const boldButton = controller.itemTargets[0]
189
- const italicButton = controller.itemTargets[1]
190
-
191
- click(boldButton)
192
- await nextFrame()
193
- expect(controller.valueValue).toBe("bold")
194
- expect(boldButton.getAttribute("data-state")).toBe("on")
195
-
196
- click(italicButton)
197
- await nextFrame()
198
- expect(controller.valueValue).toBe("bold,italic")
199
- expect(boldButton.getAttribute("data-state")).toBe("on")
200
- expect(italicButton.getAttribute("data-state")).toBe("on")
201
- })
202
-
203
- test("can select all items", async () => {
204
- const boldButton = controller.itemTargets[0]
205
- const italicButton = controller.itemTargets[1]
206
- const underlineButton = controller.itemTargets[2]
207
-
208
- click(boldButton)
209
- await nextFrame()
210
- click(italicButton)
211
- await nextFrame()
212
- click(underlineButton)
213
- await nextFrame()
214
-
215
- expect(controller.valueValue).toBe("bold,italic,underline")
216
- expect(controller.getValues()).toEqual(["bold", "italic", "underline"])
217
-
218
- // All should be on
219
- controller.itemTargets.forEach(item => {
220
- expect(item.getAttribute("data-state")).toBe("on")
221
- expect(item.getAttribute("aria-pressed")).toBe("true")
222
- })
223
- })
224
-
225
- test("can deselect individual items", async () => {
226
- const boldButton = controller.itemTargets[0]
227
- const italicButton = controller.itemTargets[1]
228
-
229
- // Select both
230
- click(boldButton)
231
- await nextFrame()
232
- click(italicButton)
233
- await nextFrame()
234
- expect(controller.valueValue).toBe("bold,italic")
235
-
236
- // Deselect bold
237
- click(boldButton)
238
- await nextFrame()
239
- expect(controller.valueValue).toBe("italic")
240
- expect(boldButton.getAttribute("data-state")).toBe("off")
241
- expect(italicButton.getAttribute("data-state")).toBe("on")
242
- })
243
-
244
- test("maintains order when toggling items", async () => {
245
- const boldButton = controller.itemTargets[0]
246
- const italicButton = controller.itemTargets[1]
247
- const underlineButton = controller.itemTargets[2]
248
-
249
- // Select in specific order: italic, bold, underline
250
- click(italicButton)
251
- await nextFrame()
252
- click(boldButton)
253
- await nextFrame()
254
- click(underlineButton)
255
- await nextFrame()
256
-
257
- // Should maintain selection order
258
- expect(controller.valueValue).toBe("italic,bold,underline")
259
- })
260
-
261
- test("removes item from middle of selection", async () => {
262
- const boldButton = controller.itemTargets[0]
263
- const italicButton = controller.itemTargets[1]
264
- const underlineButton = controller.itemTargets[2]
265
-
266
- // Select all three
267
- click(boldButton)
268
- await nextFrame()
269
- click(italicButton)
270
- await nextFrame()
271
- click(underlineButton)
272
- await nextFrame()
273
- expect(controller.valueValue).toBe("bold,italic,underline")
274
-
275
- // Deselect middle item
276
- click(italicButton)
277
- await nextFrame()
278
- expect(controller.valueValue).toBe("bold,underline")
279
- })
280
-
281
- test("handles deselecting all items", async () => {
282
- const boldButton = controller.itemTargets[0]
283
- const italicButton = controller.itemTargets[1]
284
-
285
- // Select two items
286
- click(boldButton)
287
- await nextFrame()
288
- click(italicButton)
289
- await nextFrame()
290
- expect(controller.valueValue).toBe("bold,italic")
291
-
292
- // Deselect both
293
- click(boldButton)
294
- await nextFrame()
295
- click(italicButton)
296
- await nextFrame()
297
-
298
- expect(controller.valueValue).toBe("")
299
- expect(controller.getValues()).toEqual([])
300
- controller.itemTargets.forEach(item => {
301
- expect(item.getAttribute("data-state")).toBe("off")
302
- })
303
- })
304
-
305
- test("dispatches change event with array of selected values", async () => {
306
- const boldButton = controller.itemTargets[0]
307
- const italicButton = controller.itemTargets[1]
308
- let eventDetail = null
309
-
310
- element.addEventListener("shadcn--toggle-group:change", (e) => {
311
- eventDetail = e.detail
312
- })
313
-
314
- click(boldButton)
315
- await nextFrame()
316
- expect(eventDetail.value).toEqual(["bold"])
317
-
318
- click(italicButton)
319
- await nextFrame()
320
- expect(eventDetail.value).toEqual(["bold", "italic"])
321
- })
322
-
323
- test("comma-separated values are parsed correctly", async () => {
324
- // Manually set value
325
- controller.valueValue = "bold,italic,underline"
326
- await nextFrame()
327
-
328
- expect(controller.getValues()).toEqual(["bold", "italic", "underline"])
329
-
330
- // All items should be marked as on
331
- controller.itemTargets.forEach(item => {
332
- expect(item.getAttribute("data-state")).toBe("on")
333
- })
334
- })
335
- })
336
-
337
- describe("value initialization", () => {
338
- test("initializes with pre-selected value in single mode", async () => {
339
- const html = `
340
- <div data-controller="shadcn--toggle-group"
341
- data-shadcn--toggle-group-type-value="single"
342
- data-shadcn--toggle-group-value-value="italic">
343
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
344
- <button data-shadcn--toggle-group-target="item" data-value="italic">Italic</button>
345
- </div>
346
- `
347
-
348
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
349
- application = setup.application
350
- element = setup.element
351
- controller = setup.controller
352
-
353
- expect(controller.valueValue).toBe("italic")
354
- const italicButton = controller.itemTargets[1]
355
- expect(italicButton.getAttribute("data-state")).toBe("on")
356
- expect(italicButton.getAttribute("aria-pressed")).toBe("true")
357
- })
358
-
359
- test("initializes with pre-selected values in multiple mode", async () => {
360
- const html = `
361
- <div data-controller="shadcn--toggle-group"
362
- data-shadcn--toggle-group-type-value="multiple"
363
- data-shadcn--toggle-group-value-value="bold,underline">
364
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
365
- <button data-shadcn--toggle-group-target="item" data-value="italic">Italic</button>
366
- <button data-shadcn--toggle-group-target="item" data-value="underline">Underline</button>
367
- </div>
368
- `
369
-
370
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
371
- application = setup.application
372
- element = setup.element
373
- controller = setup.controller
374
-
375
- expect(controller.valueValue).toBe("bold,underline")
376
- expect(controller.getValues()).toEqual(["bold", "underline"])
377
-
378
- const boldButton = controller.itemTargets[0]
379
- const italicButton = controller.itemTargets[1]
380
- const underlineButton = controller.itemTargets[2]
381
-
382
- expect(boldButton.getAttribute("data-state")).toBe("on")
383
- expect(italicButton.getAttribute("data-state")).toBe("off")
384
- expect(underlineButton.getAttribute("data-state")).toBe("on")
385
- })
386
- })
387
-
388
- describe("hidden input synchronization", () => {
389
- test("updates hidden input value in single mode", async () => {
390
- const html = `
391
- <div data-controller="shadcn--toggle-group"
392
- data-shadcn--toggle-group-type-value="single">
393
- <button data-shadcn--toggle-group-target="item" data-value="bold"
394
- data-action="click->shadcn--toggle-group#toggle">Bold</button>
395
- <input type="hidden" data-shadcn--toggle-group-target="input" name="format">
396
- </div>
397
- `
398
-
399
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
400
- application = setup.application
401
- element = setup.element
402
- controller = setup.controller
403
-
404
- const input = element.querySelector('input[type="hidden"]')
405
- expect(input.value).toBe("")
406
-
407
- const boldButton = controller.itemTargets[0]
408
- click(boldButton)
409
- await nextFrame()
410
-
411
- expect(input.value).toBe("bold")
412
- })
413
-
414
- test("updates hidden input value in multiple mode", async () => {
415
- const html = `
416
- <div data-controller="shadcn--toggle-group"
417
- data-shadcn--toggle-group-type-value="multiple">
418
- <button data-shadcn--toggle-group-target="item" data-value="bold"
419
- data-action="click->shadcn--toggle-group#toggle">Bold</button>
420
- <button data-shadcn--toggle-group-target="item" data-value="italic"
421
- data-action="click->shadcn--toggle-group#toggle">Italic</button>
422
- <input type="hidden" data-shadcn--toggle-group-target="input" name="formats">
423
- </div>
424
- `
425
-
426
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
427
- application = setup.application
428
- element = setup.element
429
- controller = setup.controller
430
-
431
- const input = element.querySelector('input[type="hidden"]')
432
- const boldButton = controller.itemTargets[0]
433
- const italicButton = controller.itemTargets[1]
434
-
435
- click(boldButton)
436
- await nextFrame()
437
- expect(input.value).toBe("bold")
438
-
439
- click(italicButton)
440
- await nextFrame()
441
- expect(input.value).toBe("bold,italic")
442
- })
443
-
444
- test("initializes hidden input with pre-selected value", async () => {
445
- const html = `
446
- <div data-controller="shadcn--toggle-group"
447
- data-shadcn--toggle-group-type-value="single"
448
- data-shadcn--toggle-group-value-value="bold">
449
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
450
- <input type="hidden" data-shadcn--toggle-group-target="input">
451
- </div>
452
- `
453
-
454
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
455
- application = setup.application
456
- element = setup.element
457
- controller = setup.controller
458
-
459
- const input = element.querySelector('input[type="hidden"]')
460
- expect(input.value).toBe("bold")
461
- })
462
- })
463
-
464
- describe("valueValueChanged callback", () => {
465
- const html = `
466
- <div data-controller="shadcn--toggle-group"
467
- data-shadcn--toggle-group-type-value="single">
468
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
469
- <button data-shadcn--toggle-group-target="item" data-value="italic">Italic</button>
470
- </div>
471
- `
472
-
473
- beforeEach(async () => {
474
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
475
- application = setup.application
476
- element = setup.element
477
- controller = setup.controller
478
- })
479
-
480
- test("updates item states when value changes programmatically", async () => {
481
- controller.valueValue = "bold"
482
- await nextFrame()
483
-
484
- const boldButton = controller.itemTargets[0]
485
- expect(boldButton.getAttribute("data-state")).toBe("on")
486
- expect(boldButton.getAttribute("aria-pressed")).toBe("true")
487
- })
488
-
489
- test("updates states when value changes from external source", async () => {
490
- // Simulate external value change
491
- controller.valueValue = "italic"
492
- await nextFrame()
493
-
494
- const italicButton = controller.itemTargets[1]
495
- expect(italicButton.getAttribute("data-state")).toBe("on")
496
-
497
- const boldButton = controller.itemTargets[0]
498
- expect(boldButton.getAttribute("data-state")).toBe("off")
499
- })
500
- })
501
-
502
- describe("edge cases and error handling", () => {
503
- const html = `
504
- <div data-controller="shadcn--toggle-group"
505
- data-shadcn--toggle-group-type-value="single">
506
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
507
- <button data-shadcn--toggle-group-target="item" data-value="italic">Italic</button>
508
- </div>
509
- `
510
-
511
- beforeEach(async () => {
512
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
513
- application = setup.application
514
- element = setup.element
515
- controller = setup.controller
516
- })
517
-
518
- test("handles empty value gracefully", async () => {
519
- controller.valueValue = ""
520
- await nextFrame()
521
-
522
- expect(controller.getValues()).toEqual([])
523
- controller.itemTargets.forEach(item => {
524
- expect(item.getAttribute("data-state")).toBe("off")
525
- })
526
- })
527
-
528
- test("handles value with only commas", async () => {
529
- controller.valueValue = ",,,"
530
- await nextFrame()
531
-
532
- // Should filter out empty strings
533
- expect(controller.getValues()).toEqual([])
534
- })
535
-
536
- test("handles value with trailing comma", async () => {
537
- controller.valueValue = "bold,"
538
- await nextFrame()
539
-
540
- expect(controller.getValues()).toEqual(["bold"])
541
- const boldButton = controller.itemTargets[0]
542
- expect(boldButton.getAttribute("data-state")).toBe("on")
543
- })
544
-
545
- test("handles value with leading comma", async () => {
546
- controller.valueValue = ",bold"
547
- await nextFrame()
548
-
549
- expect(controller.getValues()).toEqual(["bold"])
550
- const boldButton = controller.itemTargets[0]
551
- expect(boldButton.getAttribute("data-state")).toBe("on")
552
- })
553
-
554
- test("handles non-existent value gracefully", async () => {
555
- controller.valueValue = "strikethrough"
556
- await nextFrame()
557
-
558
- // Should not error, just no items will be marked as on
559
- controller.itemTargets.forEach(item => {
560
- expect(item.getAttribute("data-state")).toBe("off")
561
- })
562
- })
563
-
564
- test("handles multiple commas between values", async () => {
565
- controller.typeValue = "multiple"
566
- controller.valueValue = "bold,,,italic"
567
- await nextFrame()
568
-
569
- expect(controller.getValues()).toEqual(["bold", "italic"])
570
- })
571
- })
572
-
573
- describe("connect lifecycle", () => {
574
- test("updates states on connect", async () => {
575
- const html = `
576
- <div data-controller="shadcn--toggle-group"
577
- data-shadcn--toggle-group-type-value="single"
578
- data-shadcn--toggle-group-value-value="bold">
579
- <button data-shadcn--toggle-group-target="item" data-value="bold">Bold</button>
580
- <button data-shadcn--toggle-group-target="item" data-value="italic">Italic</button>
581
- </div>
582
- `
583
-
584
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
585
- application = setup.application
586
- element = setup.element
587
- controller = setup.controller
588
-
589
- // After connect, states should be updated
590
- const boldButton = controller.itemTargets[0]
591
- expect(boldButton.getAttribute("data-state")).toBe("on")
592
- expect(boldButton.getAttribute("aria-pressed")).toBe("true")
593
- })
594
-
595
- test("initializes all aria-pressed attributes on connect", async () => {
596
- const html = `
597
- <div data-controller="shadcn--toggle-group">
598
- <button data-shadcn--toggle-group-target="item" data-value="a">A</button>
599
- <button data-shadcn--toggle-group-target="item" data-value="b">B</button>
600
- <button data-shadcn--toggle-group-target="item" data-value="c">C</button>
601
- </div>
602
- `
603
-
604
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
605
- application = setup.application
606
- element = setup.element
607
- controller = setup.controller
608
-
609
- controller.itemTargets.forEach(item => {
610
- expect(item.hasAttribute("aria-pressed")).toBe(true)
611
- expect(item.hasAttribute("data-state")).toBe(true)
612
- })
613
- })
614
- })
615
-
616
- describe("type safety and filtering", () => {
617
- const html = `
618
- <div data-controller="shadcn--toggle-group"
619
- data-shadcn--toggle-group-type-value="multiple">
620
- <button data-shadcn--toggle-group-target="item" data-value="a">A</button>
621
- <button data-shadcn--toggle-group-target="item" data-value="b">B</button>
622
- </div>
623
- `
624
-
625
- beforeEach(async () => {
626
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
627
- application = setup.application
628
- element = setup.element
629
- controller = setup.controller
630
- })
631
-
632
- test("getValues filters out empty strings", () => {
633
- controller.valueValue = "a,,b,,"
634
- expect(controller.getValues()).toEqual(["a", "b"])
635
- })
636
-
637
- test("getValues handles whitespace-only values", () => {
638
- controller.valueValue = "a, ,b"
639
- // Note: This will include the space as a value
640
- // If trimming is desired, the controller would need to implement it
641
- const values = controller.getValues()
642
- expect(values.length).toBe(3)
643
- expect(values).toEqual(["a", " ", "b"])
644
- })
645
- })
646
-
647
- describe("rapid toggling", () => {
648
- const html = `
649
- <div data-controller="shadcn--toggle-group"
650
- data-shadcn--toggle-group-type-value="single">
651
- <button data-shadcn--toggle-group-target="item" data-value="a"
652
- data-action="click->shadcn--toggle-group#toggle">A</button>
653
- <button data-shadcn--toggle-group-target="item" data-value="b"
654
- data-action="click->shadcn--toggle-group#toggle">B</button>
655
- </div>
656
- `
657
-
658
- beforeEach(async () => {
659
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
660
- application = setup.application
661
- element = setup.element
662
- controller = setup.controller
663
- })
664
-
665
- test("handles rapid clicks correctly", async () => {
666
- const buttonA = controller.itemTargets[0]
667
- const buttonB = controller.itemTargets[1]
668
-
669
- // Rapid fire clicks
670
- click(buttonA)
671
- click(buttonB)
672
- click(buttonA)
673
- await nextFrame()
674
-
675
- // Final state should be 'a'
676
- expect(controller.valueValue).toBe("a")
677
- expect(buttonA.getAttribute("data-state")).toBe("on")
678
- expect(buttonB.getAttribute("data-state")).toBe("off")
679
- })
680
-
681
- test("handles rapid toggle on/off in single mode", async () => {
682
- const buttonA = controller.itemTargets[0]
683
-
684
- click(buttonA) // on
685
- click(buttonA) // off
686
- click(buttonA) // on
687
- click(buttonA) // off
688
- await nextFrame()
689
-
690
- expect(controller.valueValue).toBe("")
691
- expect(buttonA.getAttribute("data-state")).toBe("off")
692
- })
693
-
694
- test("handles rapid multiple selections in multiple mode", async () => {
695
- controller.typeValue = "multiple"
696
- const buttonA = controller.itemTargets[0]
697
- const buttonB = controller.itemTargets[1]
698
-
699
- click(buttonA)
700
- click(buttonB)
701
- click(buttonA) // deselect
702
- click(buttonA) // reselect
703
- await nextFrame()
704
-
705
- expect(controller.valueValue).toBe("b,a")
706
- })
707
- })
708
-
709
- describe("event detail validation", () => {
710
- const html = `
711
- <div data-controller="shadcn--toggle-group"
712
- data-shadcn--toggle-group-type-value="multiple">
713
- <button data-shadcn--toggle-group-target="item" data-value="x"
714
- data-action="click->shadcn--toggle-group#toggle">X</button>
715
- <button data-shadcn--toggle-group-target="item" data-value="y"
716
- data-action="click->shadcn--toggle-group#toggle">Y</button>
717
- </div>
718
- `
719
-
720
- beforeEach(async () => {
721
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
722
- application = setup.application
723
- element = setup.element
724
- controller = setup.controller
725
- })
726
-
727
- test("change event detail contains array of values", async () => {
728
- let receivedDetail = null
729
- element.addEventListener("shadcn--toggle-group:change", (e) => {
730
- receivedDetail = e.detail
731
- })
732
-
733
- const buttonX = controller.itemTargets[0]
734
- click(buttonX)
735
- await nextFrame()
736
-
737
- expect(receivedDetail).toBeTruthy()
738
- expect(receivedDetail.value).toBeInstanceOf(Array)
739
- expect(receivedDetail.value).toEqual(["x"])
740
- })
741
-
742
- test("change event bubbles", async () => {
743
- let eventCaught = false
744
- document.body.addEventListener("shadcn--toggle-group:change", () => {
745
- eventCaught = true
746
- })
747
-
748
- const buttonX = controller.itemTargets[0]
749
- click(buttonX)
750
- await nextFrame()
751
-
752
- expect(eventCaught).toBe(true)
753
- })
754
- })
755
-
756
- describe("type switching (edge case)", () => {
757
- const html = `
758
- <div data-controller="shadcn--toggle-group"
759
- data-shadcn--toggle-group-type-value="single"
760
- data-shadcn--toggle-group-value-value="a,b">
761
- <button data-shadcn--toggle-group-target="item" data-value="a">A</button>
762
- <button data-shadcn--toggle-group-target="item" data-value="b">B</button>
763
- </div>
764
- `
765
-
766
- beforeEach(async () => {
767
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
768
- application = setup.application
769
- element = setup.element
770
- controller = setup.controller
771
- })
772
-
773
- test("handles multiple values in single mode gracefully", async () => {
774
- // Even though we're in single mode with multiple values, it should parse them
775
- const values = controller.getValues()
776
- expect(values).toEqual(["a", "b"])
777
-
778
- // Both should be marked as on (edge case behavior)
779
- const buttonA = controller.itemTargets[0]
780
- const buttonB = controller.itemTargets[1]
781
- expect(buttonA.getAttribute("data-state")).toBe("on")
782
- expect(buttonB.getAttribute("data-state")).toBe("on")
783
- })
784
-
785
- test("clicking selected item in single mode with multiple values clears all", async () => {
786
- // Start with multiple values selected (edge case - shouldn't happen in normal use)
787
- controller.valueValue = "a,b"
788
- await nextFrame()
789
-
790
- // Manually call toggle like a click would
791
- const buttonA = controller.itemTargets[0]
792
- controller.toggle({ currentTarget: buttonA })
793
- await nextFrame()
794
-
795
- // Since currentValues ["a", "b"] includes "a", it sets to empty string
796
- expect(controller.valueValue).toBe("")
797
- })
798
- })
799
-
800
- describe("item without data-value attribute", () => {
801
- const html = `
802
- <div data-controller="shadcn--toggle-group"
803
- data-shadcn--toggle-group-type-value="single">
804
- <button data-shadcn--toggle-group-target="item" data-value="a"
805
- data-action="click->shadcn--toggle-group#toggle">A</button>
806
- <button data-shadcn--toggle-group-target="item"
807
- data-action="click->shadcn--toggle-group#toggle">No Value</button>
808
- </div>
809
- `
810
-
811
- beforeEach(async () => {
812
- const setup = await setupController(ToggleGroupController, html, 'shadcn--toggle-group')
813
- application = setup.application
814
- element = setup.element
815
- controller = setup.controller
816
- })
817
-
818
- test("handles item without data-value gracefully", async () => {
819
- const noValueButton = controller.itemTargets[1]
820
-
821
- // Should not error when clicking
822
- expect(() => {
823
- click(noValueButton)
824
- }).not.toThrow()
825
- })
826
-
827
- test("treats undefined value as empty string or undefined", async () => {
828
- const noValueButton = controller.itemTargets[1]
829
- click(noValueButton)
830
- await nextFrame()
831
-
832
- // Behavior depends on implementation - typically would add undefined
833
- // The filter(Boolean) in the controller would filter it out
834
- const values = controller.getValues()
835
- // undefined gets converted to string "undefined" or filtered
836
- expect(values.length).toBeLessThanOrEqual(1)
837
- })
838
- })
839
- })