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,737 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
- import MenubarController from "../../app/assets/javascripts/shadcn/controllers/menubar_controller.js"
3
- import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
4
-
5
- describe("MenubarController", () => {
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--menubar"
17
- data-shadcn--menubar-open-index-value="-1">
18
- <div data-shadcn--menubar-target="menu">
19
- <button data-shadcn--menubar-target="trigger"
20
- data-action="click->shadcn--menubar#toggle mouseenter->shadcn--menubar#hoverOpen"
21
- aria-expanded="false">File</button>
22
- <div data-shadcn--menubar-target="content" hidden>
23
- <button data-shadcn--menubar-target="item"
24
- data-action="click->shadcn--menubar#selectItem">New</button>
25
- <button data-shadcn--menubar-target="item"
26
- data-action="click->shadcn--menubar#selectItem">Open</button>
27
- </div>
28
- </div>
29
- <div data-shadcn--menubar-target="menu">
30
- <button data-shadcn--menubar-target="trigger"
31
- data-action="click->shadcn--menubar#toggle mouseenter->shadcn--menubar#hoverOpen"
32
- aria-expanded="false">Edit</button>
33
- <div data-shadcn--menubar-target="content" hidden>
34
- <button data-shadcn--menubar-target="item"
35
- data-action="click->shadcn--menubar#selectItem">Undo</button>
36
- </div>
37
- </div>
38
- </div>
39
- `
40
-
41
- beforeEach(async () => {
42
- const setup = await setupController(MenubarController, basicHTML, 'shadcn--menubar')
43
- application = setup.application
44
- element = setup.element
45
- controller = setup.controller
46
- })
47
-
48
- test("initializes with closed state", () => {
49
- expect(controller.openIndexValue).toBe(-1)
50
- })
51
-
52
- test("initializes isMenuOpen to false", () => {
53
- expect(controller.isMenuOpen).toBe(false)
54
- })
55
-
56
- test("initializes focusedIndex to -1", () => {
57
- expect(controller.focusedIndex).toBe(-1)
58
- })
59
-
60
- test("has menu targets", () => {
61
- expect(controller.menuTargets.length).toBe(2)
62
- })
63
-
64
- test("has trigger targets", () => {
65
- expect(controller.triggerTargets.length).toBe(2)
66
- })
67
-
68
- test("has content targets", () => {
69
- expect(controller.contentTargets.length).toBe(2)
70
- })
71
-
72
- test("has item targets", () => {
73
- expect(controller.itemTargets.length).toBe(3)
74
- })
75
-
76
- test("all content is initially hidden", () => {
77
- controller.contentTargets.forEach(content => {
78
- expect(content.hidden).toBe(true)
79
- })
80
- })
81
- })
82
-
83
- describe("toggle functionality", () => {
84
- const toggleHTML = `
85
- <div data-controller="shadcn--menubar"
86
- data-shadcn--menubar-open-index-value="-1">
87
- <div data-shadcn--menubar-target="menu">
88
- <button data-shadcn--menubar-target="trigger"
89
- data-action="click->shadcn--menubar#toggle"
90
- aria-expanded="false">File</button>
91
- <div data-shadcn--menubar-target="content" hidden>
92
- <button data-shadcn--menubar-target="item">New</button>
93
- </div>
94
- </div>
95
- <div data-shadcn--menubar-target="menu">
96
- <button data-shadcn--menubar-target="trigger"
97
- data-action="click->shadcn--menubar#toggle"
98
- aria-expanded="false">Edit</button>
99
- <div data-shadcn--menubar-target="content" hidden>
100
- <button data-shadcn--menubar-target="item">Undo</button>
101
- </div>
102
- </div>
103
- </div>
104
- `
105
-
106
- beforeEach(async () => {
107
- const setup = await setupController(MenubarController, toggleHTML, 'shadcn--menubar')
108
- application = setup.application
109
- element = setup.element
110
- controller = setup.controller
111
- })
112
-
113
- test("opens menu on toggle", async () => {
114
- const trigger = controller.triggerTargets[0]
115
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
116
- await nextFrame()
117
-
118
- expect(controller.openIndexValue).toBe(0)
119
- expect(controller.isMenuOpen).toBe(true)
120
- })
121
-
122
- test("sets aria-expanded to true", async () => {
123
- const trigger = controller.triggerTargets[0]
124
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
125
- await nextFrame()
126
-
127
- expect(trigger.getAttribute("aria-expanded")).toBe("true")
128
- })
129
-
130
- test("shows content when opened", async () => {
131
- const trigger = controller.triggerTargets[0]
132
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
133
- await nextFrame()
134
-
135
- const content = controller.contentTargets[0]
136
- expect(content.hidden).toBe(false)
137
- })
138
-
139
- test("sets content data-state to open", async () => {
140
- const trigger = controller.triggerTargets[0]
141
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
142
- await nextFrame()
143
-
144
- const content = controller.contentTargets[0]
145
- expect(content.dataset.state).toBe("open")
146
- })
147
-
148
- test("closes menu on second toggle", async () => {
149
- const trigger = controller.triggerTargets[0]
150
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
151
- await nextFrame()
152
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
153
- await nextFrame()
154
-
155
- expect(controller.openIndexValue).toBe(-1)
156
- expect(controller.isMenuOpen).toBe(false)
157
- })
158
-
159
- test("switches to different menu on toggle", async () => {
160
- const trigger1 = controller.triggerTargets[0]
161
- const trigger2 = controller.triggerTargets[1]
162
-
163
- controller.toggle({ currentTarget: trigger1, preventDefault: jest.fn() })
164
- await nextFrame()
165
- expect(controller.openIndexValue).toBe(0)
166
-
167
- controller.toggle({ currentTarget: trigger2, preventDefault: jest.fn() })
168
- await nextFrame()
169
- expect(controller.openIndexValue).toBe(1)
170
- })
171
- })
172
-
173
- describe("hover functionality", () => {
174
- const hoverHTML = `
175
- <div data-controller="shadcn--menubar"
176
- data-shadcn--menubar-open-index-value="-1">
177
- <div data-shadcn--menubar-target="menu">
178
- <button data-shadcn--menubar-target="trigger"
179
- data-action="mouseenter->shadcn--menubar#hoverOpen"
180
- aria-expanded="false">File</button>
181
- <div data-shadcn--menubar-target="content" hidden>Content 1</div>
182
- </div>
183
- <div data-shadcn--menubar-target="menu">
184
- <button data-shadcn--menubar-target="trigger"
185
- data-action="mouseenter->shadcn--menubar#hoverOpen"
186
- aria-expanded="false">Edit</button>
187
- <div data-shadcn--menubar-target="content" hidden>Content 2</div>
188
- </div>
189
- </div>
190
- `
191
-
192
- beforeEach(async () => {
193
- const setup = await setupController(MenubarController, hoverHTML, 'shadcn--menubar')
194
- application = setup.application
195
- element = setup.element
196
- controller = setup.controller
197
- })
198
-
199
- test("does not open on hover when no menu is open", async () => {
200
- const trigger = controller.triggerTargets[0]
201
- controller.hoverOpen({ currentTarget: trigger })
202
- await nextFrame()
203
-
204
- expect(controller.isMenuOpen).toBe(false)
205
- })
206
-
207
- test("opens different menu on hover when one is already open", async () => {
208
- // First open a menu
209
- controller.openMenu(0)
210
- await nextFrame()
211
-
212
- const trigger2 = controller.triggerTargets[1]
213
- controller.hoverOpen({ currentTarget: trigger2 })
214
- await nextFrame()
215
-
216
- expect(controller.openIndexValue).toBe(1)
217
- })
218
- })
219
-
220
- describe("item selection", () => {
221
- const selectHTML = `
222
- <div data-controller="shadcn--menubar"
223
- data-shadcn--menubar-open-index-value="-1">
224
- <div data-shadcn--menubar-target="menu">
225
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
226
- <div data-shadcn--menubar-target="content" hidden>
227
- <button data-shadcn--menubar-target="item"
228
- data-action="click->shadcn--menubar#selectItem">New</button>
229
- <button data-shadcn--menubar-target="item"
230
- data-action="click->shadcn--menubar#selectItem"
231
- data-disabled>Disabled</button>
232
- </div>
233
- </div>
234
- </div>
235
- `
236
-
237
- beforeEach(async () => {
238
- const setup = await setupController(MenubarController, selectHTML, 'shadcn--menubar')
239
- application = setup.application
240
- element = setup.element
241
- controller = setup.controller
242
- })
243
-
244
- test("dispatches select event", async () => {
245
- controller.openMenu(0)
246
- await nextFrame()
247
-
248
- let selectedItem = null
249
- element.addEventListener("shadcn--menubar:select", (e) => {
250
- selectedItem = e.detail.item
251
- })
252
-
253
- const item = controller.itemTargets[0]
254
- controller.selectItem({ currentTarget: item })
255
- await nextFrame()
256
-
257
- expect(selectedItem).toBe(item)
258
- })
259
-
260
- test("closes menu after selection", async () => {
261
- controller.openMenu(0)
262
- await nextFrame()
263
-
264
- const item = controller.itemTargets[0]
265
- controller.selectItem({ currentTarget: item })
266
- await nextFrame()
267
-
268
- expect(controller.isMenuOpen).toBe(false)
269
- })
270
-
271
- test("does not select disabled items", async () => {
272
- controller.openMenu(0)
273
- await nextFrame()
274
-
275
- let selectFired = false
276
- element.addEventListener("shadcn--menubar:select", () => {
277
- selectFired = true
278
- })
279
-
280
- const disabledItem = controller.itemTargets[1]
281
- controller.selectItem({ currentTarget: disabledItem })
282
- await nextFrame()
283
-
284
- expect(selectFired).toBe(false)
285
- })
286
- })
287
-
288
- describe("checkbox items", () => {
289
- const checkboxHTML = `
290
- <div data-controller="shadcn--menubar"
291
- data-shadcn--menubar-open-index-value="-1">
292
- <div data-shadcn--menubar-target="menu">
293
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">View</button>
294
- <div data-shadcn--menubar-target="content" hidden>
295
- <button data-shadcn--menubar-target="item"
296
- role="menuitemcheckbox"
297
- aria-checked="false"
298
- data-state="unchecked"
299
- data-action="click->shadcn--menubar#toggleCheckbox">
300
- <span><svg style="display: none;">✓</svg></span>
301
- Show Toolbar
302
- </button>
303
- </div>
304
- </div>
305
- </div>
306
- `
307
-
308
- beforeEach(async () => {
309
- const setup = await setupController(MenubarController, checkboxHTML, 'shadcn--menubar')
310
- application = setup.application
311
- element = setup.element
312
- controller = setup.controller
313
- })
314
-
315
- test("toggles checkbox state", async () => {
316
- controller.openMenu(0)
317
- await nextFrame()
318
-
319
- const item = controller.itemTargets[0]
320
- controller.toggleCheckbox({ currentTarget: item })
321
- await nextFrame()
322
-
323
- expect(item.dataset.state).toBe("checked")
324
- expect(item.getAttribute("aria-checked")).toBe("true")
325
- })
326
-
327
- test("toggles checkbox back to unchecked", async () => {
328
- controller.openMenu(0)
329
- await nextFrame()
330
-
331
- const item = controller.itemTargets[0]
332
- controller.toggleCheckbox({ currentTarget: item })
333
- await nextFrame()
334
- controller.toggleCheckbox({ currentTarget: item })
335
- await nextFrame()
336
-
337
- expect(item.dataset.state).toBe("unchecked")
338
- expect(item.getAttribute("aria-checked")).toBe("false")
339
- })
340
-
341
- test("dispatches check event", async () => {
342
- controller.openMenu(0)
343
- await nextFrame()
344
-
345
- let checkDetail = null
346
- element.addEventListener("shadcn--menubar:check", (e) => {
347
- checkDetail = e.detail
348
- })
349
-
350
- const item = controller.itemTargets[0]
351
- controller.toggleCheckbox({ currentTarget: item })
352
- await nextFrame()
353
-
354
- expect(checkDetail.item).toBe(item)
355
- expect(checkDetail.checked).toBe(true)
356
- })
357
- })
358
-
359
- describe("radio items", () => {
360
- const radioHTML = `
361
- <div data-controller="shadcn--menubar"
362
- data-shadcn--menubar-open-index-value="-1">
363
- <div data-shadcn--menubar-target="menu">
364
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">View</button>
365
- <div data-shadcn--menubar-target="content" hidden>
366
- <div role="group">
367
- <button data-shadcn--menubar-target="item"
368
- role="menuitemradio"
369
- aria-checked="true"
370
- data-state="checked"
371
- data-value="small"
372
- data-action="click->shadcn--menubar#selectRadio">
373
- <span><svg style="display: block;">●</svg></span>
374
- Small
375
- </button>
376
- <button data-shadcn--menubar-target="item"
377
- role="menuitemradio"
378
- aria-checked="false"
379
- data-state="unchecked"
380
- data-value="medium"
381
- data-action="click->shadcn--menubar#selectRadio">
382
- <span><svg style="display: none;">●</svg></span>
383
- Medium
384
- </button>
385
- </div>
386
- </div>
387
- </div>
388
- </div>
389
- `
390
-
391
- beforeEach(async () => {
392
- const setup = await setupController(MenubarController, radioHTML, 'shadcn--menubar')
393
- application = setup.application
394
- element = setup.element
395
- controller = setup.controller
396
- })
397
-
398
- test("selects radio item", async () => {
399
- controller.openMenu(0)
400
- await nextFrame()
401
-
402
- const mediumItem = controller.itemTargets[1]
403
- controller.selectRadio({ currentTarget: mediumItem })
404
- await nextFrame()
405
-
406
- expect(mediumItem.dataset.state).toBe("checked")
407
- expect(mediumItem.getAttribute("aria-checked")).toBe("true")
408
- })
409
-
410
- test("unchecks other radio items in group", async () => {
411
- controller.openMenu(0)
412
- await nextFrame()
413
-
414
- const smallItem = controller.itemTargets[0]
415
- const mediumItem = controller.itemTargets[1]
416
-
417
- controller.selectRadio({ currentTarget: mediumItem })
418
- await nextFrame()
419
-
420
- expect(smallItem.dataset.state).toBe("unchecked")
421
- expect(smallItem.getAttribute("aria-checked")).toBe("false")
422
- })
423
-
424
- test("dispatches radioChange event", async () => {
425
- controller.openMenu(0)
426
- await nextFrame()
427
-
428
- let radioDetail = null
429
- element.addEventListener("shadcn--menubar:radioChange", (e) => {
430
- radioDetail = e.detail
431
- })
432
-
433
- const mediumItem = controller.itemTargets[1]
434
- controller.selectRadio({ currentTarget: mediumItem })
435
- await nextFrame()
436
-
437
- expect(radioDetail.item).toBe(mediumItem)
438
- expect(radioDetail.value).toBe("medium")
439
- })
440
- })
441
-
442
- describe("keyboard navigation", () => {
443
- const keyboardHTML = `
444
- <div data-controller="shadcn--menubar"
445
- data-shadcn--menubar-open-index-value="-1">
446
- <div data-shadcn--menubar-target="menu">
447
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
448
- <div data-shadcn--menubar-target="content" hidden>
449
- <button data-shadcn--menubar-target="item">New</button>
450
- <button data-shadcn--menubar-target="item" data-disabled>Disabled</button>
451
- <button data-shadcn--menubar-target="item">Save</button>
452
- </div>
453
- </div>
454
- <div data-shadcn--menubar-target="menu">
455
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">Edit</button>
456
- <div data-shadcn--menubar-target="content" hidden>
457
- <button data-shadcn--menubar-target="item">Undo</button>
458
- </div>
459
- </div>
460
- </div>
461
- `
462
-
463
- beforeEach(async () => {
464
- const setup = await setupController(MenubarController, keyboardHTML, 'shadcn--menubar')
465
- application = setup.application
466
- element = setup.element
467
- controller = setup.controller
468
-
469
- // Open the first menu
470
- controller.openMenu(0)
471
- await nextFrame()
472
- })
473
-
474
- test("ArrowDown moves to next item", async () => {
475
- // Initially focused on first item (index 0)
476
- controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
477
- await nextFrame()
478
-
479
- expect(controller.focusedIndex).toBe(1)
480
- })
481
-
482
- test("ArrowDown wraps to first item", async () => {
483
- // Move to last item
484
- controller.focusedIndex = 1 // Last enabled item in current menu
485
- controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
486
- await nextFrame()
487
-
488
- expect(controller.focusedIndex).toBe(0)
489
- })
490
-
491
- test("ArrowUp moves to previous item", async () => {
492
- controller.focusedIndex = 1
493
- controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
494
- await nextFrame()
495
-
496
- expect(controller.focusedIndex).toBe(0)
497
- })
498
-
499
- test("ArrowRight opens next menu", async () => {
500
- controller.handleKeydown({ key: "ArrowRight", preventDefault: jest.fn() })
501
- await nextFrame()
502
-
503
- expect(controller.openIndexValue).toBe(1)
504
- })
505
-
506
- test("ArrowLeft opens previous menu", async () => {
507
- controller.openMenu(1)
508
- await nextFrame()
509
-
510
- controller.handleKeydown({ key: "ArrowLeft", preventDefault: jest.fn() })
511
- await nextFrame()
512
-
513
- expect(controller.openIndexValue).toBe(0)
514
- })
515
-
516
- test("Home moves to first item", async () => {
517
- controller.focusedIndex = 1
518
- controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
519
- await nextFrame()
520
-
521
- expect(controller.focusedIndex).toBe(0)
522
- })
523
-
524
- test("End moves to last item", async () => {
525
- controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
526
- await nextFrame()
527
-
528
- expect(controller.focusedIndex).toBe(1) // Last enabled item
529
- })
530
-
531
- test("Escape closes menu", async () => {
532
- controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
533
- await nextFrame()
534
-
535
- expect(controller.isMenuOpen).toBe(false)
536
- })
537
-
538
- test("Enter/Space triggers click on focused item", async () => {
539
- const items = controller.currentMenuItems
540
- const clickSpy = jest.spyOn(items[0], 'click')
541
-
542
- controller.focusedIndex = 0
543
- controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
544
- await nextFrame()
545
-
546
- expect(clickSpy).toHaveBeenCalled()
547
- })
548
-
549
- test("prevents default on navigation keys", () => {
550
- const preventDefault = jest.fn()
551
-
552
- controller.handleKeydown({ key: "ArrowDown", preventDefault })
553
- expect(preventDefault).toHaveBeenCalled()
554
-
555
- preventDefault.mockClear()
556
- controller.handleKeydown({ key: "ArrowUp", preventDefault })
557
- expect(preventDefault).toHaveBeenCalled()
558
-
559
- preventDefault.mockClear()
560
- controller.handleKeydown({ key: "ArrowRight", preventDefault })
561
- expect(preventDefault).toHaveBeenCalled()
562
-
563
- preventDefault.mockClear()
564
- controller.handleKeydown({ key: "ArrowLeft", preventDefault })
565
- expect(preventDefault).toHaveBeenCalled()
566
- })
567
- })
568
-
569
- describe("submenu functionality", () => {
570
- const submenuHTML = `
571
- <div data-controller="shadcn--menubar"
572
- data-shadcn--menubar-open-index-value="-1">
573
- <div data-shadcn--menubar-target="menu">
574
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
575
- <div data-shadcn--menubar-target="content" hidden>
576
- <div data-shadcn--menubar-target="sub">
577
- <button data-shadcn--menubar-target="subTrigger"
578
- data-action="mouseenter->shadcn--menubar#openSub mouseleave->shadcn--menubar#startCloseSubTimer"
579
- aria-expanded="false">Share</button>
580
- <div data-shadcn--menubar-target="subContent" hidden>
581
- <button data-shadcn--menubar-target="item">Email</button>
582
- <button data-shadcn--menubar-target="item">Twitter</button>
583
- </div>
584
- </div>
585
- </div>
586
- </div>
587
- </div>
588
- `
589
-
590
- beforeEach(async () => {
591
- const setup = await setupController(MenubarController, submenuHTML, 'shadcn--menubar')
592
- application = setup.application
593
- element = setup.element
594
- controller = setup.controller
595
- })
596
-
597
- test("opens submenu", async () => {
598
- controller.openMenu(0)
599
- await nextFrame()
600
-
601
- const subTrigger = controller.subTriggerTargets[0]
602
- controller.openSub({ currentTarget: subTrigger })
603
- await nextFrame()
604
-
605
- expect(subTrigger.getAttribute("aria-expanded")).toBe("true")
606
- expect(controller.subContentTargets[0].hidden).toBe(false)
607
- })
608
-
609
- test("closeAllSubs closes all submenus", async () => {
610
- controller.openMenu(0)
611
- await nextFrame()
612
-
613
- const subTrigger = controller.subTriggerTargets[0]
614
- controller.openSub({ currentTarget: subTrigger })
615
- await nextFrame()
616
-
617
- controller.closeAllSubs()
618
- await nextFrame()
619
-
620
- expect(subTrigger.getAttribute("aria-expanded")).toBe("false")
621
- expect(controller.subContentTargets[0].hidden).toBe(true)
622
- })
623
- })
624
-
625
- describe("click outside handling", () => {
626
- const clickOutsideHTML = `
627
- <div data-controller="shadcn--menubar"
628
- data-shadcn--menubar-open-index-value="-1">
629
- <div data-shadcn--menubar-target="menu">
630
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
631
- <div data-shadcn--menubar-target="content" hidden>
632
- <button data-shadcn--menubar-target="item">New</button>
633
- </div>
634
- </div>
635
- </div>
636
- `
637
-
638
- beforeEach(async () => {
639
- const setup = await setupController(MenubarController, clickOutsideHTML, 'shadcn--menubar')
640
- application = setup.application
641
- element = setup.element
642
- controller = setup.controller
643
- })
644
-
645
- test("closes on click outside", async () => {
646
- controller.openMenu(0)
647
- await nextFrame()
648
-
649
- const outsideElement = document.createElement("div")
650
- document.body.appendChild(outsideElement)
651
-
652
- // Use clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
653
- controller.clickOutside({ target: outsideElement })
654
- await nextFrame()
655
-
656
- expect(controller.isMenuOpen).toBe(false)
657
-
658
- document.body.removeChild(outsideElement)
659
- })
660
-
661
- test("does not close on click inside", async () => {
662
- controller.openMenu(0)
663
- await nextFrame()
664
-
665
- // Clicking inside the controller element should not close via clickOutside
666
- // The clickOutside method from stimulus-use only fires for clicks outside the element
667
- // So we verify the menu stays open
668
- expect(controller.isMenuOpen).toBe(true)
669
- })
670
- })
671
-
672
- describe("currentMenuItems getter", () => {
673
- const itemsHTML = `
674
- <div data-controller="shadcn--menubar"
675
- data-shadcn--menubar-open-index-value="-1">
676
- <div data-shadcn--menubar-target="menu">
677
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
678
- <div data-shadcn--menubar-target="content" hidden>
679
- <button data-shadcn--menubar-target="item">New</button>
680
- <button data-shadcn--menubar-target="item" data-disabled>Disabled</button>
681
- <button data-shadcn--menubar-target="item">Save</button>
682
- </div>
683
- </div>
684
- </div>
685
- `
686
-
687
- beforeEach(async () => {
688
- const setup = await setupController(MenubarController, itemsHTML, 'shadcn--menubar')
689
- application = setup.application
690
- element = setup.element
691
- controller = setup.controller
692
- })
693
-
694
- test("returns empty array when no menu open", () => {
695
- expect(controller.currentMenuItems).toEqual([])
696
- })
697
-
698
- test("returns enabled items when menu is open", async () => {
699
- controller.openMenu(0)
700
- await nextFrame()
701
-
702
- const items = controller.currentMenuItems
703
- expect(items.length).toBe(2) // Only enabled items
704
- })
705
- })
706
-
707
- describe("disconnect cleanup", () => {
708
- const disconnectHTML = `
709
- <div data-controller="shadcn--menubar"
710
- data-shadcn--menubar-open-index-value="-1">
711
- <div data-shadcn--menubar-target="menu">
712
- <button data-shadcn--menubar-target="trigger" aria-expanded="false">File</button>
713
- <div data-shadcn--menubar-target="content" hidden>
714
- <button data-shadcn--menubar-target="item">New</button>
715
- </div>
716
- </div>
717
- </div>
718
- `
719
-
720
- beforeEach(async () => {
721
- const setup = await setupController(MenubarController, disconnectHTML, 'shadcn--menubar')
722
- application = setup.application
723
- element = setup.element
724
- controller = setup.controller
725
- })
726
-
727
- test("closes all on disconnect", async () => {
728
- controller.openMenu(0)
729
- await nextFrame()
730
-
731
- controller.disconnect()
732
- await nextFrame()
733
-
734
- expect(controller.openIndexValue).toBe(-1)
735
- })
736
- })
737
- })