shadcn-rails 0.2.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -2
  3. data/README.md +21 -8
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
  6. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
  7. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
  8. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  9. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
  10. data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
  11. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  12. data/app/assets/javascripts/shadcn/index.js +7 -1
  13. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  14. data/app/assets/stylesheets/shadcn/base.css +32 -0
  15. data/app/components/shadcn/accordion_component.html.erb +8 -0
  16. data/app/components/shadcn/accordion_component.rb +6 -15
  17. data/app/components/shadcn/alert_component.html.erb +6 -0
  18. data/app/components/shadcn/alert_component.rb +0 -18
  19. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  20. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  21. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  22. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  23. data/app/components/shadcn/avatar_component.html.erb +20 -0
  24. data/app/components/shadcn/avatar_component.rb +8 -36
  25. data/app/components/shadcn/badge_component.html.erb +1 -0
  26. data/app/components/shadcn/badge_component.rb +0 -11
  27. data/app/components/shadcn/base_component.rb +15 -2
  28. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  29. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  30. data/app/components/shadcn/button_component.html.erb +18 -0
  31. data/app/components/shadcn/button_component.rb +1 -41
  32. data/app/components/shadcn/card_component.html.erb +8 -0
  33. data/app/components/shadcn/card_component.rb +2 -6
  34. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  35. data/app/components/shadcn/checkbox_component.rb +4 -43
  36. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  37. data/app/components/shadcn/collapsible_component.rb +6 -15
  38. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  39. data/app/components/shadcn/context_menu_component.rb +6 -26
  40. data/app/components/shadcn/dialog_component.html.erb +14 -0
  41. data/app/components/shadcn/dialog_component.rb +8 -29
  42. data/app/components/shadcn/drawer_component.html.erb +12 -0
  43. data/app/components/shadcn/drawer_component.rb +7 -27
  44. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  45. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  46. data/app/components/shadcn/field_component.rb +7 -8
  47. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  48. data/app/components/shadcn/hover_card_component.rb +7 -26
  49. data/app/components/shadcn/input_component.html.erb +18 -0
  50. data/app/components/shadcn/input_component.rb +2 -27
  51. data/app/components/shadcn/input_otp_component.rb +3 -3
  52. data/app/components/shadcn/kbd_component.html.erb +1 -0
  53. data/app/components/shadcn/kbd_component.rb +3 -10
  54. data/app/components/shadcn/label_component.html.erb +3 -0
  55. data/app/components/shadcn/label_component.rb +2 -18
  56. data/app/components/shadcn/menubar_component.html.erb +6 -0
  57. data/app/components/shadcn/menubar_component.rb +4 -15
  58. data/app/components/shadcn/native_select_component.html.erb +22 -0
  59. data/app/components/shadcn/native_select_component.rb +9 -39
  60. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  61. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  62. data/app/components/shadcn/pagination_component.html.erb +5 -0
  63. data/app/components/shadcn/pagination_component.rb +11 -15
  64. data/app/components/shadcn/popover_component.html.erb +15 -0
  65. data/app/components/shadcn/popover_component.rb +10 -30
  66. data/app/components/shadcn/progress_component.html.erb +13 -0
  67. data/app/components/shadcn/progress_component.rb +6 -26
  68. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  69. data/app/components/shadcn/radio_group_component.rb +12 -26
  70. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  71. data/app/components/shadcn/scroll_area_component.rb +4 -16
  72. data/app/components/shadcn/select_component.html.erb +46 -0
  73. data/app/components/shadcn/select_component.rb +6 -80
  74. data/app/components/shadcn/separator_component.html.erb +5 -0
  75. data/app/components/shadcn/separator_component.rb +6 -14
  76. data/app/components/shadcn/sheet_component.html.erb +12 -0
  77. data/app/components/shadcn/sheet_component.rb +7 -27
  78. data/app/components/shadcn/sidebar_component.rb +2 -2
  79. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  80. data/app/components/shadcn/skeleton_component.rb +4 -2
  81. data/app/components/shadcn/slider_component.html.erb +12 -0
  82. data/app/components/shadcn/slider_component.rb +2 -21
  83. data/app/components/shadcn/spinner_component.html.erb +18 -0
  84. data/app/components/shadcn/spinner_component.rb +2 -30
  85. data/app/components/shadcn/switch_component.html.erb +72 -0
  86. data/app/components/shadcn/switch_component.rb +4 -82
  87. data/app/components/shadcn/table_component.html.erb +9 -0
  88. data/app/components/shadcn/table_component.rb +2 -10
  89. data/app/components/shadcn/tabs_component.html.erb +8 -0
  90. data/app/components/shadcn/tabs_component.rb +4 -17
  91. data/app/components/shadcn/textarea_component.html.erb +13 -0
  92. data/app/components/shadcn/textarea_component.rb +6 -22
  93. data/app/components/shadcn/toast_component.html.erb +36 -0
  94. data/app/components/shadcn/toast_component.rb +6 -54
  95. data/app/components/shadcn/toggle_component.html.erb +12 -0
  96. data/app/components/shadcn/toggle_component.rb +6 -21
  97. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  98. data/app/components/shadcn/toggle_group_component.rb +6 -29
  99. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  100. data/app/components/shadcn/tooltip_component.rb +13 -38
  101. data/lib/generators/shadcn/add/USAGE +24 -0
  102. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  103. data/lib/generators/shadcn/install/USAGE +22 -0
  104. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  105. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  106. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  107. data/lib/shadcn/rails/version.rb +1 -1
  108. metadata +47 -45
  109. data/.dockerignore +0 -40
  110. data/CLAUDE.md +0 -612
  111. data/PROGRESS.md +0 -495
  112. data/Rakefile +0 -95
  113. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  114. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  115. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  116. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  117. data/__tests__/controllers/accordion_controller.test.js +0 -904
  118. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  119. data/__tests__/controllers/carousel_controller.test.js +0 -912
  120. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  121. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  122. data/__tests__/controllers/combobox_controller.test.js +0 -971
  123. data/__tests__/controllers/context_menu_controller.test.js +0 -905
  124. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  125. data/__tests__/controllers/dialog_controller.test.js +0 -878
  126. data/__tests__/controllers/drawer_controller.test.js +0 -995
  127. data/__tests__/controllers/menubar_controller.test.js +0 -737
  128. data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
  129. data/__tests__/controllers/popover_controller.test.js +0 -982
  130. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  131. data/__tests__/controllers/resizable_controller.test.js +0 -680
  132. data/__tests__/controllers/select_controller.test.js +0 -678
  133. data/__tests__/controllers/sheet_controller.test.js +0 -986
  134. data/__tests__/controllers/slider_controller.test.js +0 -1036
  135. data/__tests__/controllers/switch_controller.test.js +0 -424
  136. data/__tests__/controllers/tabs_controller.test.js +0 -907
  137. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  138. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  139. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  140. data/babel.config.cjs +0 -5
  141. data/bin/bump +0 -321
  142. data/bin/console +0 -11
  143. data/bin/release +0 -205
  144. data/bin/setup +0 -8
  145. data/bin/test +0 -75
  146. data/jest.config.js +0 -19
  147. data/jest.setup.js +0 -8
  148. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  149. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  150. data/package-lock.json +0 -7438
  151. data/package.json +0 -71
  152. data/rollup.config.js +0 -29
@@ -1,640 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
- import RadioGroupController from "../../app/assets/javascripts/shadcn/controllers/radio_group_controller.js"
3
- import { setupController, cleanupController, click, nextFrame, keydown } from '../helpers/stimulus-test-helper.js'
4
-
5
- describe("RadioGroupController", () => {
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--radio-group"
17
- data-shadcn--radio-group-name-value="size"
18
- data-shadcn--radio-group-value-value=""
19
- role="radiogroup">
20
- <button data-shadcn--radio-group-target="item"
21
- data-value="small"
22
- role="radio"
23
- type="button"
24
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
25
- Small
26
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
27
- </button>
28
- <button data-shadcn--radio-group-target="item"
29
- data-value="medium"
30
- role="radio"
31
- type="button"
32
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
33
- Medium
34
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
35
- </button>
36
- <button data-shadcn--radio-group-target="item"
37
- data-value="large"
38
- role="radio"
39
- type="button"
40
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
41
- Large
42
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
43
- </button>
44
- </div>
45
- `
46
-
47
- beforeEach(async () => {
48
- const setup = await setupController(RadioGroupController, basicHTML, 'shadcn--radio-group')
49
- application = setup.application
50
- element = setup.element
51
- controller = setup.controller
52
- })
53
-
54
- test("initializes with empty value", () => {
55
- expect(controller.valueValue).toBe("")
56
- })
57
-
58
- test("initializes with name value", () => {
59
- expect(controller.nameValue).toBe("size")
60
- })
61
-
62
- test("initializes all items with aria-checked false", () => {
63
- controller.itemTargets.forEach(item => {
64
- expect(item.getAttribute("aria-checked")).toBe("false")
65
- })
66
- })
67
-
68
- test("initializes all items with data-state unchecked", () => {
69
- controller.itemTargets.forEach(item => {
70
- expect(item.dataset.state).toBe("unchecked")
71
- })
72
- })
73
-
74
- test("first enabled item is focusable when no value selected", () => {
75
- const firstItem = controller.itemTargets[0]
76
- expect(firstItem.getAttribute("tabindex")).toBe("0")
77
- })
78
-
79
- test("other items are not focusable initially", () => {
80
- const secondItem = controller.itemTargets[1]
81
- const thirdItem = controller.itemTargets[2]
82
- expect(secondItem.getAttribute("tabindex")).toBe("-1")
83
- expect(thirdItem.getAttribute("tabindex")).toBe("-1")
84
- })
85
- })
86
-
87
- describe("selection", () => {
88
- const selectionHTML = `
89
- <div data-controller="shadcn--radio-group"
90
- data-shadcn--radio-group-name-value="option"
91
- data-shadcn--radio-group-value-value="">
92
- <button data-shadcn--radio-group-target="item"
93
- data-value="one"
94
- role="radio"
95
- data-action="click->shadcn--radio-group#select">
96
- One
97
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
98
- </button>
99
- <button data-shadcn--radio-group-target="item"
100
- data-value="two"
101
- role="radio"
102
- data-action="click->shadcn--radio-group#select">
103
- Two
104
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
105
- </button>
106
- </div>
107
- `
108
-
109
- beforeEach(async () => {
110
- const setup = await setupController(RadioGroupController, selectionHTML, 'shadcn--radio-group')
111
- application = setup.application
112
- element = setup.element
113
- controller = setup.controller
114
- })
115
-
116
- test("selects item when clicked", async () => {
117
- const firstItem = controller.itemTargets[0]
118
- click(firstItem)
119
- await nextFrame()
120
-
121
- expect(controller.valueValue).toBe("one")
122
- })
123
-
124
- test("updates aria-checked on selected item", async () => {
125
- const firstItem = controller.itemTargets[0]
126
- click(firstItem)
127
- await nextFrame()
128
-
129
- expect(firstItem.getAttribute("aria-checked")).toBe("true")
130
- })
131
-
132
- test("updates data-state on selected item", async () => {
133
- const firstItem = controller.itemTargets[0]
134
- click(firstItem)
135
- await nextFrame()
136
-
137
- expect(firstItem.dataset.state).toBe("checked")
138
- })
139
-
140
- test("makes selected item focusable", async () => {
141
- const secondItem = controller.itemTargets[1]
142
- click(secondItem)
143
- await nextFrame()
144
-
145
- expect(secondItem.getAttribute("tabindex")).toBe("0")
146
- })
147
-
148
- test("makes non-selected items non-focusable", async () => {
149
- const firstItem = controller.itemTargets[0]
150
- const secondItem = controller.itemTargets[1]
151
- click(secondItem)
152
- await nextFrame()
153
-
154
- expect(firstItem.getAttribute("tabindex")).toBe("-1")
155
- })
156
-
157
- test("deselects previous selection when new item selected", async () => {
158
- const firstItem = controller.itemTargets[0]
159
- const secondItem = controller.itemTargets[1]
160
-
161
- click(firstItem)
162
- await nextFrame()
163
- expect(firstItem.getAttribute("aria-checked")).toBe("true")
164
-
165
- click(secondItem)
166
- await nextFrame()
167
- expect(firstItem.getAttribute("aria-checked")).toBe("false")
168
- expect(secondItem.getAttribute("aria-checked")).toBe("true")
169
- })
170
-
171
- test("dispatches change event on selection", async () => {
172
- let eventDetail = null
173
- element.addEventListener("shadcn--radio-group:change", (e) => {
174
- eventDetail = e.detail
175
- })
176
-
177
- const firstItem = controller.itemTargets[0]
178
- click(firstItem)
179
- await nextFrame()
180
-
181
- expect(eventDetail).not.toBeNull()
182
- expect(eventDetail.value).toBe("one")
183
- expect(eventDetail.name).toBe("option")
184
- })
185
-
186
- test("dispatches native input event on selection", async () => {
187
- let inputEventFired = false
188
- element.addEventListener("input", () => {
189
- inputEventFired = true
190
- })
191
-
192
- const firstItem = controller.itemTargets[0]
193
- click(firstItem)
194
- await nextFrame()
195
-
196
- expect(inputEventFired).toBe(true)
197
- })
198
- })
199
-
200
- describe("indicator visibility", () => {
201
- const indicatorHTML = `
202
- <div data-controller="shadcn--radio-group"
203
- data-shadcn--radio-group-value-value="">
204
- <button data-shadcn--radio-group-target="item"
205
- data-value="a"
206
- data-action="click->shadcn--radio-group#select">
207
- A
208
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
209
- </button>
210
- <button data-shadcn--radio-group-target="item"
211
- data-value="b"
212
- data-action="click->shadcn--radio-group#select">
213
- B
214
- <span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
215
- </button>
216
- </div>
217
- `
218
-
219
- beforeEach(async () => {
220
- const setup = await setupController(RadioGroupController, indicatorHTML, 'shadcn--radio-group')
221
- application = setup.application
222
- element = setup.element
223
- controller = setup.controller
224
- })
225
-
226
- test("shows indicator for selected item", async () => {
227
- const firstItem = controller.itemTargets[0]
228
- click(firstItem)
229
- await nextFrame()
230
-
231
- const indicator = firstItem.querySelector('[data-shadcn--radio-group-target="indicator"]')
232
- expect(indicator.classList.contains("opacity-0")).toBe(false)
233
- })
234
-
235
- test("hides indicator for non-selected items", async () => {
236
- const firstItem = controller.itemTargets[0]
237
- const secondItem = controller.itemTargets[1]
238
- click(firstItem)
239
- await nextFrame()
240
-
241
- const indicator = secondItem.querySelector('[data-shadcn--radio-group-target="indicator"]')
242
- expect(indicator.classList.contains("opacity-0")).toBe(true)
243
- })
244
- })
245
-
246
- describe("keyboard navigation", () => {
247
- const keyboardHTML = `
248
- <div data-controller="shadcn--radio-group"
249
- data-shadcn--radio-group-value-value="">
250
- <button data-shadcn--radio-group-target="item"
251
- data-value="first"
252
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
253
- First
254
- </button>
255
- <button data-shadcn--radio-group-target="item"
256
- data-value="second"
257
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
258
- Second
259
- </button>
260
- <button data-shadcn--radio-group-target="item"
261
- data-value="third"
262
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
263
- Third
264
- </button>
265
- </div>
266
- `
267
-
268
- beforeEach(async () => {
269
- const setup = await setupController(RadioGroupController, keyboardHTML, 'shadcn--radio-group')
270
- application = setup.application
271
- element = setup.element
272
- controller = setup.controller
273
- })
274
-
275
- test("navigates forward with ArrowDown", async () => {
276
- const firstItem = controller.itemTargets[0]
277
- const secondItem = controller.itemTargets[1]
278
- const focusSpy = jest.spyOn(secondItem, 'focus')
279
-
280
- controller.handleKeydown({
281
- key: "ArrowDown",
282
- preventDefault: jest.fn(),
283
- currentTarget: firstItem
284
- })
285
- await nextFrame()
286
-
287
- expect(focusSpy).toHaveBeenCalled()
288
- expect(controller.valueValue).toBe("second")
289
- })
290
-
291
- test("navigates forward with ArrowRight", async () => {
292
- const firstItem = controller.itemTargets[0]
293
- const secondItem = controller.itemTargets[1]
294
- const focusSpy = jest.spyOn(secondItem, 'focus')
295
-
296
- controller.handleKeydown({
297
- key: "ArrowRight",
298
- preventDefault: jest.fn(),
299
- currentTarget: firstItem
300
- })
301
- await nextFrame()
302
-
303
- expect(focusSpy).toHaveBeenCalled()
304
- })
305
-
306
- test("navigates backward with ArrowUp", async () => {
307
- const firstItem = controller.itemTargets[0]
308
- const secondItem = controller.itemTargets[1]
309
- const focusSpy = jest.spyOn(firstItem, 'focus')
310
-
311
- controller.handleKeydown({
312
- key: "ArrowUp",
313
- preventDefault: jest.fn(),
314
- currentTarget: secondItem
315
- })
316
- await nextFrame()
317
-
318
- expect(focusSpy).toHaveBeenCalled()
319
- expect(controller.valueValue).toBe("first")
320
- })
321
-
322
- test("navigates backward with ArrowLeft", async () => {
323
- const firstItem = controller.itemTargets[0]
324
- const secondItem = controller.itemTargets[1]
325
- const focusSpy = jest.spyOn(firstItem, 'focus')
326
-
327
- controller.handleKeydown({
328
- key: "ArrowLeft",
329
- preventDefault: jest.fn(),
330
- currentTarget: secondItem
331
- })
332
- await nextFrame()
333
-
334
- expect(focusSpy).toHaveBeenCalled()
335
- })
336
-
337
- test("wraps from last to first with ArrowDown", async () => {
338
- const firstItem = controller.itemTargets[0]
339
- const thirdItem = controller.itemTargets[2]
340
- const focusSpy = jest.spyOn(firstItem, 'focus')
341
-
342
- controller.handleKeydown({
343
- key: "ArrowDown",
344
- preventDefault: jest.fn(),
345
- currentTarget: thirdItem
346
- })
347
- await nextFrame()
348
-
349
- expect(focusSpy).toHaveBeenCalled()
350
- })
351
-
352
- test("wraps from first to last with ArrowUp", async () => {
353
- const firstItem = controller.itemTargets[0]
354
- const thirdItem = controller.itemTargets[2]
355
- const focusSpy = jest.spyOn(thirdItem, 'focus')
356
-
357
- controller.handleKeydown({
358
- key: "ArrowUp",
359
- preventDefault: jest.fn(),
360
- currentTarget: firstItem
361
- })
362
- await nextFrame()
363
-
364
- expect(focusSpy).toHaveBeenCalled()
365
- })
366
-
367
- test("selects current item with Space", async () => {
368
- const secondItem = controller.itemTargets[1]
369
-
370
- controller.handleKeydown({
371
- key: " ",
372
- preventDefault: jest.fn(),
373
- currentTarget: secondItem
374
- })
375
- await nextFrame()
376
-
377
- expect(controller.valueValue).toBe("second")
378
- })
379
-
380
- test("selects current item with Enter", async () => {
381
- const secondItem = controller.itemTargets[1]
382
-
383
- controller.handleKeydown({
384
- key: "Enter",
385
- preventDefault: jest.fn(),
386
- currentTarget: secondItem
387
- })
388
- await nextFrame()
389
-
390
- expect(controller.valueValue).toBe("second")
391
- })
392
-
393
- test("dispatches change event on keyboard navigation", async () => {
394
- let eventDetail = null
395
- element.addEventListener("shadcn--radio-group:change", (e) => {
396
- eventDetail = e.detail
397
- })
398
-
399
- const firstItem = controller.itemTargets[0]
400
- controller.handleKeydown({
401
- key: "ArrowDown",
402
- preventDefault: jest.fn(),
403
- currentTarget: firstItem
404
- })
405
- await nextFrame()
406
-
407
- expect(eventDetail).not.toBeNull()
408
- expect(eventDetail.value).toBe("second")
409
- })
410
- })
411
-
412
- describe("disabled items", () => {
413
- const disabledHTML = `
414
- <div data-controller="shadcn--radio-group"
415
- data-shadcn--radio-group-value-value="">
416
- <button data-shadcn--radio-group-target="item"
417
- data-value="enabled"
418
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
419
- Enabled
420
- </button>
421
- <button data-shadcn--radio-group-target="item"
422
- data-value="disabled"
423
- disabled
424
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
425
- Disabled
426
- </button>
427
- <button data-shadcn--radio-group-target="item"
428
- data-value="also-enabled"
429
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
430
- Also Enabled
431
- </button>
432
- </div>
433
- `
434
-
435
- beforeEach(async () => {
436
- const setup = await setupController(RadioGroupController, disabledHTML, 'shadcn--radio-group')
437
- application = setup.application
438
- element = setup.element
439
- controller = setup.controller
440
- })
441
-
442
- test("does not select disabled item on click", async () => {
443
- const disabledItem = controller.itemTargets[1]
444
- click(disabledItem)
445
- await nextFrame()
446
-
447
- expect(controller.valueValue).toBe("")
448
- })
449
-
450
- test("enabledItems excludes disabled items", () => {
451
- const enabled = controller.enabledItems
452
- expect(enabled.length).toBe(2)
453
- expect(enabled.map(item => item.dataset.value)).toEqual(["enabled", "also-enabled"])
454
- })
455
-
456
- test("keyboard navigation skips disabled items", async () => {
457
- const firstItem = controller.itemTargets[0]
458
- const thirdItem = controller.itemTargets[2]
459
- const focusSpy = jest.spyOn(thirdItem, 'focus')
460
-
461
- controller.handleKeydown({
462
- key: "ArrowDown",
463
- preventDefault: jest.fn(),
464
- currentTarget: firstItem
465
- })
466
- await nextFrame()
467
-
468
- expect(focusSpy).toHaveBeenCalled()
469
- expect(controller.valueValue).toBe("also-enabled")
470
- })
471
- })
472
-
473
- describe("initial value", () => {
474
- const initialValueHTML = `
475
- <div data-controller="shadcn--radio-group"
476
- data-shadcn--radio-group-value-value="medium">
477
- <button data-shadcn--radio-group-target="item"
478
- data-value="small">Small</button>
479
- <button data-shadcn--radio-group-target="item"
480
- data-value="medium">Medium</button>
481
- <button data-shadcn--radio-group-target="item"
482
- data-value="large">Large</button>
483
- </div>
484
- `
485
-
486
- beforeEach(async () => {
487
- const setup = await setupController(RadioGroupController, initialValueHTML, 'shadcn--radio-group')
488
- application = setup.application
489
- element = setup.element
490
- controller = setup.controller
491
- })
492
-
493
- test("initializes with pre-set value", () => {
494
- expect(controller.valueValue).toBe("medium")
495
- })
496
-
497
- test("marks correct item as checked on init", () => {
498
- const mediumItem = controller.itemTargets[1]
499
- expect(mediumItem.getAttribute("aria-checked")).toBe("true")
500
- expect(mediumItem.dataset.state).toBe("checked")
501
- })
502
-
503
- test("selected item is focusable on init", () => {
504
- const mediumItem = controller.itemTargets[1]
505
- expect(mediumItem.getAttribute("tabindex")).toBe("0")
506
- })
507
-
508
- test("non-selected items are not focusable on init", () => {
509
- const smallItem = controller.itemTargets[0]
510
- const largeItem = controller.itemTargets[2]
511
- expect(smallItem.getAttribute("tabindex")).toBe("-1")
512
- expect(largeItem.getAttribute("tabindex")).toBe("-1")
513
- })
514
- })
515
-
516
- describe("programmatic value change", () => {
517
- const programmaticHTML = `
518
- <div data-controller="shadcn--radio-group"
519
- data-shadcn--radio-group-value-value="">
520
- <button data-shadcn--radio-group-target="item" data-value="x">X</button>
521
- <button data-shadcn--radio-group-target="item" data-value="y">Y</button>
522
- </div>
523
- `
524
-
525
- beforeEach(async () => {
526
- const setup = await setupController(RadioGroupController, programmaticHTML, 'shadcn--radio-group')
527
- application = setup.application
528
- element = setup.element
529
- controller = setup.controller
530
- })
531
-
532
- test("updates selection when valueValue changes", async () => {
533
- controller.valueValue = "y"
534
- await nextFrame()
535
-
536
- const yItem = controller.itemTargets[1]
537
- expect(yItem.getAttribute("aria-checked")).toBe("true")
538
- })
539
-
540
- test("valueValueChanged callback updates UI", async () => {
541
- controller.valueValue = "x"
542
- await nextFrame()
543
-
544
- const xItem = controller.itemTargets[0]
545
- const yItem = controller.itemTargets[1]
546
- expect(xItem.getAttribute("aria-checked")).toBe("true")
547
- expect(yItem.getAttribute("aria-checked")).toBe("false")
548
- })
549
- })
550
-
551
- describe("edge cases", () => {
552
- const edgeCaseHTML = `
553
- <div data-controller="shadcn--radio-group"
554
- data-shadcn--radio-group-value-value="">
555
- <button data-shadcn--radio-group-target="item"
556
- data-value="only"
557
- data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">Only Option</button>
558
- </div>
559
- `
560
-
561
- beforeEach(async () => {
562
- const setup = await setupController(RadioGroupController, edgeCaseHTML, 'shadcn--radio-group')
563
- application = setup.application
564
- element = setup.element
565
- controller = setup.controller
566
- })
567
-
568
- test("handles single item gracefully", async () => {
569
- const onlyItem = controller.itemTargets[0]
570
- click(onlyItem)
571
- await nextFrame()
572
-
573
- expect(controller.valueValue).toBe("only")
574
- })
575
-
576
- test("keyboard navigation with single item stays on that item", async () => {
577
- const onlyItem = controller.itemTargets[0]
578
- const focusSpy = jest.spyOn(onlyItem, 'focus')
579
-
580
- controller.handleKeydown({
581
- key: "ArrowDown",
582
- preventDefault: jest.fn(),
583
- currentTarget: onlyItem
584
- })
585
- await nextFrame()
586
-
587
- expect(focusSpy).toHaveBeenCalled()
588
- })
589
- })
590
-
591
- describe("non-matching key handling", () => {
592
- const keyHandlingHTML = `
593
- <div data-controller="shadcn--radio-group"
594
- data-shadcn--radio-group-value-value="">
595
- <button data-shadcn--radio-group-target="item"
596
- data-value="a"
597
- data-action="keydown->shadcn--radio-group#handleKeydown">A</button>
598
- <button data-shadcn--radio-group-target="item"
599
- data-value="b"
600
- data-action="keydown->shadcn--radio-group#handleKeydown">B</button>
601
- </div>
602
- `
603
-
604
- beforeEach(async () => {
605
- const setup = await setupController(RadioGroupController, keyHandlingHTML, 'shadcn--radio-group')
606
- application = setup.application
607
- element = setup.element
608
- controller = setup.controller
609
- })
610
-
611
- test("ignores non-navigation keys", async () => {
612
- const firstItem = controller.itemTargets[0]
613
- const preventDefault = jest.fn()
614
-
615
- controller.handleKeydown({
616
- key: "Tab",
617
- preventDefault,
618
- currentTarget: firstItem
619
- })
620
- await nextFrame()
621
-
622
- expect(preventDefault).not.toHaveBeenCalled()
623
- expect(controller.valueValue).toBe("")
624
- })
625
-
626
- test("ignores letter keys", async () => {
627
- const firstItem = controller.itemTargets[0]
628
- const preventDefault = jest.fn()
629
-
630
- controller.handleKeydown({
631
- key: "a",
632
- preventDefault,
633
- currentTarget: firstItem
634
- })
635
- await nextFrame()
636
-
637
- expect(preventDefault).not.toHaveBeenCalled()
638
- })
639
- })
640
- })