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,982 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
- import PopoverController from "../../app/assets/javascripts/shadcn/controllers/popover_controller.js"
3
- import { setupController, cleanupController, click, wait, nextFrame, keydown, waitForEvent } from '../helpers/stimulus-test-helper.js'
4
-
5
- describe("PopoverController", () => {
6
- let application
7
- let element
8
- let controller
9
-
10
- const createPopoverHTML = (options = {}) => {
11
- const {
12
- open = false,
13
- side = "bottom",
14
- align = "center",
15
- modal = false,
16
- includeContent = true,
17
- includeTrigger = true
18
- } = options
19
-
20
- const openAttr = open ? 'data-shadcn--popover-open-value="true"' : ''
21
- const sideAttr = side !== "bottom" ? `data-shadcn--popover-side-value="${side}"` : ''
22
- const alignAttr = align !== "center" ? `data-shadcn--popover-align-value="${align}"` : ''
23
- const modalAttr = modal ? 'data-shadcn--popover-modal-value="true"' : ''
24
-
25
- const triggerHTML = includeTrigger ? `
26
- <button data-shadcn--popover-target="trigger" data-action="click->shadcn--popover#toggle">
27
- Open
28
- </button>
29
- ` : ''
30
-
31
- const contentHTML = includeContent ? `
32
- <div data-shadcn--popover-target="content" hidden>
33
- Popover content
34
- </div>
35
- ` : ''
36
-
37
- return `
38
- <div data-controller="shadcn--popover"
39
- ${openAttr}
40
- ${sideAttr}
41
- ${alignAttr}
42
- ${modalAttr}>
43
- ${triggerHTML}
44
- ${contentHTML}
45
- </div>
46
- `
47
- }
48
-
49
- beforeEach(async () => {
50
- application = Application.start()
51
- application.register("shadcn--popover", PopoverController)
52
- document.body.innerHTML = createPopoverHTML()
53
-
54
- await new Promise(resolve => requestAnimationFrame(resolve))
55
-
56
- element = document.querySelector('[data-controller="shadcn--popover"]')
57
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
58
- })
59
-
60
- afterEach(() => {
61
- if (application) {
62
- application.stop()
63
- }
64
- document.body.innerHTML = ""
65
- // Reset body styles
66
- document.body.style.pointerEvents = ""
67
- })
68
-
69
- describe("initialization", () => {
70
- test("connects successfully", () => {
71
- expect(controller).not.toBeNull()
72
- expect(controller).toBeDefined()
73
- })
74
-
75
- test("initializes with default values", () => {
76
- expect(controller.openValue).toBe(false)
77
- expect(controller.sideValue).toBe("bottom")
78
- expect(controller.alignValue).toBe("center")
79
- expect(controller.modalValue).toBe(false)
80
- })
81
-
82
- test("initializes with custom values", async () => {
83
- application.stop()
84
- document.body.innerHTML = createPopoverHTML({
85
- open: true,
86
- side: "top",
87
- align: "start",
88
- modal: true
89
- })
90
-
91
- application = Application.start()
92
- application.register("shadcn--popover", PopoverController)
93
-
94
- await new Promise(resolve => requestAnimationFrame(resolve))
95
-
96
- element = document.querySelector('[data-controller="shadcn--popover"]')
97
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
98
-
99
- expect(controller.openValue).toBe(true)
100
- expect(controller.sideValue).toBe("top")
101
- expect(controller.alignValue).toBe("start")
102
- expect(controller.modalValue).toBe(true)
103
- })
104
-
105
- test("respects open=true value on initialization", async () => {
106
- application.stop()
107
- document.body.innerHTML = createPopoverHTML({ open: true })
108
-
109
- application = Application.start()
110
- application.register("shadcn--popover", PopoverController)
111
-
112
- await new Promise(resolve => requestAnimationFrame(resolve))
113
-
114
- element = document.querySelector('[data-controller="shadcn--popover"]')
115
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
116
-
117
- // The openValue should be set to true from the data attribute
118
- expect(controller.openValue).toBe(true)
119
-
120
- // Note: Due to the guard clause in show(), when openValue is already true,
121
- // the connect() method calls show() but it returns early, so the content
122
- // state is not set. This is actual controller behavior.
123
- // To properly open on init, the value would need to be set after connect.
124
- })
125
-
126
- test("keeps popover hidden when initialized with open=false", () => {
127
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
128
- expect(content.hidden).toBe(true)
129
- expect(content.dataset.state).toBeUndefined()
130
- })
131
-
132
- test("handles missing trigger target gracefully", async () => {
133
- application.stop()
134
- document.body.innerHTML = createPopoverHTML({ includeTrigger: false })
135
-
136
- application = Application.start()
137
- application.register("shadcn--popover", PopoverController)
138
-
139
- await new Promise(resolve => requestAnimationFrame(resolve))
140
-
141
- element = document.querySelector('[data-controller="shadcn--popover"]')
142
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
143
-
144
- expect(controller).toBeDefined()
145
- expect(controller.hasTriggerTarget).toBe(false)
146
- })
147
-
148
- test("handles missing content target gracefully", async () => {
149
- application.stop()
150
- document.body.innerHTML = createPopoverHTML({ includeContent: false })
151
-
152
- application = Application.start()
153
- application.register("shadcn--popover", PopoverController)
154
-
155
- await new Promise(resolve => requestAnimationFrame(resolve))
156
-
157
- element = document.querySelector('[data-controller="shadcn--popover"]')
158
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
159
-
160
- expect(controller).toBeDefined()
161
- expect(controller.hasContentTarget).toBe(false)
162
- })
163
- })
164
-
165
- describe("toggle behavior", () => {
166
- test("toggle opens closed popover", () => {
167
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
168
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
169
-
170
- expect(controller.openValue).toBe(false)
171
-
172
- click(trigger)
173
-
174
- expect(controller.openValue).toBe(true)
175
- expect(content.hidden).toBe(false)
176
- expect(content.dataset.state).toBe("open")
177
- })
178
-
179
- test("toggle closes open popover", async () => {
180
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
181
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
182
-
183
- // Open first
184
- click(trigger)
185
- expect(controller.openValue).toBe(true)
186
-
187
- // Close
188
- click(trigger)
189
- expect(controller.openValue).toBe(false)
190
- expect(content.dataset.state).toBe("closed")
191
-
192
- // Content should be hidden after animation delay
193
- await wait(200)
194
- expect(content.hidden).toBe(true)
195
- })
196
-
197
- test("toggle prevents default event behavior", () => {
198
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
199
-
200
- let defaultPrevented = false
201
- const event = new MouseEvent('click', {
202
- bubbles: true,
203
- cancelable: true,
204
- view: window
205
- })
206
-
207
- const originalPreventDefault = event.preventDefault
208
- event.preventDefault = function() {
209
- defaultPrevented = true
210
- return originalPreventDefault.apply(this, arguments)
211
- }
212
-
213
- trigger.dispatchEvent(event)
214
-
215
- expect(defaultPrevented).toBe(true)
216
- })
217
-
218
- test("multiple toggles work correctly", async () => {
219
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
220
-
221
- click(trigger) // Open
222
- expect(controller.openValue).toBe(true)
223
-
224
- click(trigger) // Close
225
- expect(controller.openValue).toBe(false)
226
-
227
- await wait(200)
228
-
229
- click(trigger) // Open again
230
- expect(controller.openValue).toBe(true)
231
-
232
- click(trigger) // Close again
233
- expect(controller.openValue).toBe(false)
234
- })
235
- })
236
-
237
- describe("show method", () => {
238
- test("show opens popover", () => {
239
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
240
-
241
- controller.show()
242
-
243
- expect(controller.openValue).toBe(true)
244
- expect(content.hidden).toBe(false)
245
- expect(content.dataset.state).toBe("open")
246
- })
247
-
248
- test("show is idempotent when already open", () => {
249
- controller.show()
250
- expect(controller.openValue).toBe(true)
251
-
252
- controller.show()
253
- expect(controller.openValue).toBe(true)
254
- })
255
-
256
- test("show sets up click outside handling via stimulus-use", () => {
257
- // stimulus-use useClickOutside sets up event handling internally
258
- // We verify by checking the clickOutside method exists and works
259
- controller.show()
260
-
261
- expect(controller.openValue).toBe(true)
262
- expect(typeof controller.clickOutside).toBe("function")
263
- })
264
-
265
- test("show dispatches opened event", async () => {
266
- const eventPromise = waitForEvent(element, "shadcn--popover:opened")
267
-
268
- controller.show()
269
-
270
- const event = await eventPromise
271
- expect(event).toBeDefined()
272
- })
273
-
274
- test("show sets side data attribute on content", () => {
275
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
276
-
277
- controller.show()
278
-
279
- expect(content.dataset.side).toBe("bottom")
280
- })
281
-
282
- test("show calls positionContent", () => {
283
- let positionContentCalled = false
284
- const originalPositionContent = controller.positionContent.bind(controller)
285
-
286
- controller.positionContent = function() {
287
- positionContentCalled = true
288
- return originalPositionContent()
289
- }
290
-
291
- controller.show()
292
-
293
- expect(positionContentCalled).toBe(true)
294
- })
295
- })
296
-
297
- describe("hide method", () => {
298
- test("hide closes open popover", async () => {
299
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
300
-
301
- controller.show()
302
- expect(controller.openValue).toBe(true)
303
-
304
- controller.hide()
305
-
306
- expect(controller.openValue).toBe(false)
307
- expect(content.dataset.state).toBe("closed")
308
-
309
- await wait(200)
310
- expect(content.hidden).toBe(true)
311
- })
312
-
313
- test("hide is idempotent when already closed", () => {
314
- expect(controller.openValue).toBe(false)
315
-
316
- controller.hide()
317
-
318
- expect(controller.openValue).toBe(false)
319
- })
320
-
321
- test("hide closes the popover and clickOutside no longer has effect", () => {
322
- // stimulus-use manages event listeners internally
323
- // We verify hide properly closes and further clickOutside calls don't reopen
324
- controller.show()
325
- controller.hide()
326
-
327
- expect(controller.openValue).toBe(false)
328
-
329
- // Calling clickOutside on closed popover should not have any effect
330
- const outsideElement = document.createElement("div")
331
- controller.clickOutside({ target: outsideElement })
332
-
333
- expect(controller.openValue).toBe(false)
334
- })
335
-
336
- test("hide dispatches closed event", async () => {
337
- controller.show()
338
-
339
- const eventPromise = waitForEvent(element, "shadcn--popover:closed")
340
-
341
- controller.hide()
342
-
343
- const event = await eventPromise
344
- expect(event).toBeDefined()
345
- })
346
-
347
- test("hide delays hiding content for animation", async () => {
348
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
349
-
350
- controller.show()
351
- controller.hide()
352
-
353
- // Should be marked as closed but not hidden yet
354
- expect(content.dataset.state).toBe("closed")
355
- expect(content.hidden).toBe(false)
356
-
357
- // After delay, should be hidden
358
- await wait(200)
359
- expect(content.hidden).toBe(true)
360
- })
361
-
362
- test("hide animation is cancelled if reopened", async () => {
363
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
364
-
365
- controller.show()
366
- controller.hide()
367
-
368
- // Reopen before animation completes
369
- await wait(50)
370
- controller.show()
371
-
372
- await wait(150)
373
-
374
- // Should still be visible
375
- expect(content.hidden).toBe(false)
376
- expect(content.dataset.state).toBe("open")
377
- })
378
- })
379
-
380
- describe("close method", () => {
381
- test("close is an alias for hide", () => {
382
- controller.show()
383
- expect(controller.openValue).toBe(true)
384
-
385
- controller.close()
386
-
387
- expect(controller.openValue).toBe(false)
388
- })
389
- })
390
-
391
- describe("click outside behavior", () => {
392
- test("clicking outside closes popover", async () => {
393
- controller.show()
394
- expect(controller.openValue).toBe(true)
395
-
396
- // Call clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
397
- await nextFrame()
398
- const outsideElement = document.createElement("div")
399
- document.body.appendChild(outsideElement)
400
- controller.clickOutside({ target: outsideElement })
401
-
402
- expect(controller.openValue).toBe(false)
403
- document.body.removeChild(outsideElement)
404
- })
405
-
406
- test("clicking inside popover does not close it", async () => {
407
- controller.show()
408
- expect(controller.openValue).toBe(true)
409
-
410
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
411
-
412
- // Clicking inside the controller element should not close via clickOutside
413
- // The clickOutside method from stimulus-use only fires for clicks outside the element
414
- // So we verify the popover stays open after an internal click action
415
- await nextFrame()
416
- click(content)
417
-
418
- expect(controller.openValue).toBe(true)
419
- })
420
-
421
- test("clicking trigger does not trigger click outside", async () => {
422
- controller.show()
423
- expect(controller.openValue).toBe(true)
424
-
425
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
426
-
427
- await nextFrame()
428
- click(trigger)
429
-
430
- // Should toggle to closed via toggle action, not click outside
431
- expect(controller.openValue).toBe(false)
432
- })
433
-
434
- test("clickOutside method exists for stimulus-use integration", () => {
435
- // Verify the clickOutside method is defined for stimulus-use integration
436
- expect(typeof controller.clickOutside).toBe("function")
437
- })
438
- })
439
-
440
- describe("modal behavior", () => {
441
- test("modal=true disables pointer events on body when open", async () => {
442
- application.stop()
443
- document.body.innerHTML = createPopoverHTML({ modal: true })
444
-
445
- application = Application.start()
446
- application.register("shadcn--popover", PopoverController)
447
-
448
- await new Promise(resolve => requestAnimationFrame(resolve))
449
-
450
- element = document.querySelector('[data-controller="shadcn--popover"]')
451
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
452
-
453
- controller.show()
454
-
455
- expect(document.body.style.pointerEvents).toBe("none")
456
- })
457
-
458
- test("modal=true enables pointer events on content when open", async () => {
459
- application.stop()
460
- document.body.innerHTML = createPopoverHTML({ modal: true })
461
-
462
- application = Application.start()
463
- application.register("shadcn--popover", PopoverController)
464
-
465
- await new Promise(resolve => requestAnimationFrame(resolve))
466
-
467
- element = document.querySelector('[data-controller="shadcn--popover"]')
468
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
469
-
470
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
471
-
472
- controller.show()
473
-
474
- expect(content.style.pointerEvents).toBe("auto")
475
- })
476
-
477
- test("modal=true restores pointer events on body when closed", async () => {
478
- application.stop()
479
- document.body.innerHTML = createPopoverHTML({ modal: true })
480
-
481
- application = Application.start()
482
- application.register("shadcn--popover", PopoverController)
483
-
484
- await new Promise(resolve => requestAnimationFrame(resolve))
485
-
486
- element = document.querySelector('[data-controller="shadcn--popover"]')
487
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
488
-
489
- controller.show()
490
- expect(document.body.style.pointerEvents).toBe("none")
491
-
492
- controller.hide()
493
- expect(document.body.style.pointerEvents).toBe("")
494
- })
495
-
496
- test("modal=false does not affect pointer events", () => {
497
- controller.show()
498
-
499
- expect(document.body.style.pointerEvents).toBe("")
500
- })
501
- })
502
-
503
- describe("positioning - side", () => {
504
- test("positions content on bottom by default", () => {
505
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
506
-
507
- controller.show()
508
-
509
- expect(content.style.position).toBe("absolute")
510
- expect(content.style.top).toBe("100%")
511
- expect(content.style.bottom).toBe("auto")
512
- expect(content.style.marginTop).toBe("8px")
513
- })
514
-
515
- test("positions content on top", async () => {
516
- application.stop()
517
- document.body.innerHTML = createPopoverHTML({ side: "top" })
518
-
519
- application = Application.start()
520
- application.register("shadcn--popover", PopoverController)
521
-
522
- await new Promise(resolve => requestAnimationFrame(resolve))
523
-
524
- element = document.querySelector('[data-controller="shadcn--popover"]')
525
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
526
-
527
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
528
-
529
- controller.show()
530
-
531
- expect(content.style.position).toBe("absolute")
532
- expect(content.style.bottom).toBe("100%")
533
- expect(content.style.top).toBe("auto")
534
- expect(content.style.marginBottom).toBe("8px")
535
- })
536
-
537
- test("positions content on left", async () => {
538
- application.stop()
539
- document.body.innerHTML = createPopoverHTML({ side: "left" })
540
-
541
- application = Application.start()
542
- application.register("shadcn--popover", PopoverController)
543
-
544
- await new Promise(resolve => requestAnimationFrame(resolve))
545
-
546
- element = document.querySelector('[data-controller="shadcn--popover"]')
547
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
548
-
549
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
550
-
551
- controller.show()
552
-
553
- expect(content.style.position).toBe("absolute")
554
- expect(content.style.right).toBe("100%")
555
- expect(content.style.left).toBe("auto")
556
- expect(content.style.marginRight).toBe("8px")
557
- })
558
-
559
- test("positions content on right", async () => {
560
- application.stop()
561
- document.body.innerHTML = createPopoverHTML({ side: "right" })
562
-
563
- application = Application.start()
564
- application.register("shadcn--popover", PopoverController)
565
-
566
- await new Promise(resolve => requestAnimationFrame(resolve))
567
-
568
- element = document.querySelector('[data-controller="shadcn--popover"]')
569
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
570
-
571
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
572
-
573
- controller.show()
574
-
575
- expect(content.style.position).toBe("absolute")
576
- expect(content.style.left).toBe("100%")
577
- expect(content.style.right).toBe("auto")
578
- expect(content.style.marginLeft).toBe("8px")
579
- })
580
-
581
- test("sets data-side attribute on content", async () => {
582
- application.stop()
583
- document.body.innerHTML = createPopoverHTML({ side: "right" })
584
-
585
- application = Application.start()
586
- application.register("shadcn--popover", PopoverController)
587
-
588
- await new Promise(resolve => requestAnimationFrame(resolve))
589
-
590
- element = document.querySelector('[data-controller="shadcn--popover"]')
591
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
592
-
593
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
594
-
595
- controller.show()
596
-
597
- expect(content.dataset.side).toBe("right")
598
- })
599
- })
600
-
601
- describe("positioning - align", () => {
602
- test("aligns content to center by default on bottom side", () => {
603
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
604
-
605
- controller.show()
606
-
607
- expect(content.style.left).toBe("50%")
608
- expect(content.style.transform).toBe("translateX(-50%)")
609
- })
610
-
611
- test("aligns content to start", async () => {
612
- application.stop()
613
- document.body.innerHTML = createPopoverHTML({ align: "start" })
614
-
615
- application = Application.start()
616
- application.register("shadcn--popover", PopoverController)
617
-
618
- await new Promise(resolve => requestAnimationFrame(resolve))
619
-
620
- element = document.querySelector('[data-controller="shadcn--popover"]')
621
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
622
-
623
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
624
-
625
- controller.show()
626
-
627
- expect(content.style.left).toBe("0px")
628
- expect(content.style.right).toBe("auto")
629
- })
630
-
631
- test("aligns content to end", async () => {
632
- application.stop()
633
- document.body.innerHTML = createPopoverHTML({ align: "end" })
634
-
635
- application = Application.start()
636
- application.register("shadcn--popover", PopoverController)
637
-
638
- await new Promise(resolve => requestAnimationFrame(resolve))
639
-
640
- element = document.querySelector('[data-controller="shadcn--popover"]')
641
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
642
-
643
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
644
-
645
- controller.show()
646
-
647
- expect(content.style.right).toBe("0px")
648
- expect(content.style.left).toBe("auto")
649
- })
650
-
651
- test("center alignment only applies transform on top/bottom sides", async () => {
652
- application.stop()
653
- document.body.innerHTML = createPopoverHTML({ side: "left", align: "center" })
654
-
655
- application = Application.start()
656
- application.register("shadcn--popover", PopoverController)
657
-
658
- await new Promise(resolve => requestAnimationFrame(resolve))
659
-
660
- element = document.querySelector('[data-controller="shadcn--popover"]')
661
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
662
-
663
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
664
-
665
- controller.show()
666
-
667
- // Should not have transform for left/right sides
668
- expect(content.style.transform).toBe("")
669
- })
670
- })
671
-
672
- describe("positionContent edge cases", () => {
673
- test("does not position if content target missing", async () => {
674
- application.stop()
675
- document.body.innerHTML = createPopoverHTML({ includeContent: false })
676
-
677
- application = Application.start()
678
- application.register("shadcn--popover", PopoverController)
679
-
680
- await new Promise(resolve => requestAnimationFrame(resolve))
681
-
682
- element = document.querySelector('[data-controller="shadcn--popover"]')
683
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
684
-
685
- // Should not throw
686
- expect(() => controller.positionContent()).not.toThrow()
687
- })
688
-
689
- test("does not position if trigger target missing", async () => {
690
- application.stop()
691
- document.body.innerHTML = createPopoverHTML({ includeTrigger: false })
692
-
693
- application = Application.start()
694
- application.register("shadcn--popover", PopoverController)
695
-
696
- await new Promise(resolve => requestAnimationFrame(resolve))
697
-
698
- element = document.querySelector('[data-controller="shadcn--popover"]')
699
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
700
-
701
- // Should not throw
702
- expect(() => controller.positionContent()).not.toThrow()
703
- })
704
- })
705
-
706
- describe("event dispatching", () => {
707
- test("dispatches opened event when opening", async () => {
708
- const eventPromise = waitForEvent(element, "shadcn--popover:opened")
709
-
710
- controller.show()
711
-
712
- const event = await eventPromise
713
- expect(event.type).toBe("shadcn--popover:opened")
714
- })
715
-
716
- test("dispatches closed event when closing", async () => {
717
- controller.show()
718
-
719
- const eventPromise = waitForEvent(element, "shadcn--popover:closed")
720
-
721
- controller.hide()
722
-
723
- const event = await eventPromise
724
- expect(event.type).toBe("shadcn--popover:closed")
725
- })
726
-
727
- test("opened event bubbles", async () => {
728
- let eventCaught = false
729
-
730
- document.body.addEventListener("shadcn--popover:opened", () => {
731
- eventCaught = true
732
- })
733
-
734
- controller.show()
735
-
736
- await nextFrame()
737
-
738
- expect(eventCaught).toBe(true)
739
- })
740
-
741
- test("closed event bubbles", async () => {
742
- controller.show()
743
-
744
- let eventCaught = false
745
-
746
- document.body.addEventListener("shadcn--popover:closed", () => {
747
- eventCaught = true
748
- })
749
-
750
- controller.hide()
751
-
752
- await nextFrame()
753
-
754
- expect(eventCaught).toBe(true)
755
- })
756
- })
757
-
758
- describe("disconnect cleanup", () => {
759
- test("closes popover on disconnect", () => {
760
- controller.show()
761
- expect(controller.openValue).toBe(true)
762
-
763
- controller.disconnect()
764
-
765
- expect(controller.openValue).toBe(false)
766
- })
767
-
768
- test("properly cleans up on disconnect", () => {
769
- // stimulus-use handles event listener cleanup on disconnect
770
- controller.show()
771
- expect(controller.openValue).toBe(true)
772
-
773
- controller.disconnect()
774
-
775
- expect(controller.openValue).toBe(false)
776
- })
777
-
778
- test("restores body pointer events on disconnect when modal", async () => {
779
- application.stop()
780
- document.body.innerHTML = createPopoverHTML({ modal: true })
781
-
782
- application = Application.start()
783
- application.register("shadcn--popover", PopoverController)
784
-
785
- await new Promise(resolve => requestAnimationFrame(resolve))
786
-
787
- element = document.querySelector('[data-controller="shadcn--popover"]')
788
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
789
-
790
- controller.show()
791
- expect(document.body.style.pointerEvents).toBe("none")
792
-
793
- controller.disconnect()
794
-
795
- expect(document.body.style.pointerEvents).toBe("")
796
- })
797
- })
798
-
799
- describe("ARIA attributes", () => {
800
- test("trigger can have aria-haspopup", () => {
801
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
802
-
803
- // Set manually as this would typically be in the HTML
804
- trigger.setAttribute('aria-haspopup', 'dialog')
805
-
806
- expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
807
- })
808
-
809
- test("trigger can have aria-expanded", () => {
810
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
811
-
812
- // Set manually as this would typically be managed in the HTML/component
813
- trigger.setAttribute('aria-expanded', 'false')
814
- expect(trigger.getAttribute('aria-expanded')).toBe('false')
815
-
816
- controller.show()
817
- trigger.setAttribute('aria-expanded', 'true')
818
- expect(trigger.getAttribute('aria-expanded')).toBe('true')
819
- })
820
-
821
- test("content can have role dialog", () => {
822
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
823
-
824
- // Set manually as this would typically be in the HTML
825
- content.setAttribute('role', 'dialog')
826
-
827
- expect(content.getAttribute('role')).toBe('dialog')
828
- })
829
- })
830
-
831
- describe("edge cases", () => {
832
- test("rapid open/close transitions", async () => {
833
- controller.show()
834
- controller.hide()
835
- controller.show()
836
- controller.hide()
837
- controller.show()
838
-
839
- expect(controller.openValue).toBe(true)
840
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
841
- expect(content.hidden).toBe(false)
842
- })
843
-
844
- test("handles getBoundingClientRect on trigger", () => {
845
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
846
-
847
- let getBoundingClientRectCalled = false
848
-
849
- // Mock getBoundingClientRect
850
- const originalGetBoundingClientRect = trigger.getBoundingClientRect.bind(trigger)
851
- trigger.getBoundingClientRect = function() {
852
- getBoundingClientRectCalled = true
853
- return originalGetBoundingClientRect()
854
- }
855
-
856
- controller.show()
857
-
858
- expect(getBoundingClientRectCalled).toBe(true)
859
- })
860
-
861
- test("handles null event in toggle", () => {
862
- // Should not throw when called without event
863
- expect(() => controller.toggle()).not.toThrow()
864
- })
865
-
866
- test("handles undefined event in toggle", () => {
867
- expect(() => controller.toggle(undefined)).not.toThrow()
868
- })
869
- })
870
-
871
- describe("integration scenarios", () => {
872
- test("complete interaction flow: open, click outside, reopen", async () => {
873
- const trigger = element.querySelector('[data-shadcn--popover-target="trigger"]')
874
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
875
-
876
- // Open
877
- click(trigger)
878
- expect(controller.openValue).toBe(true)
879
- expect(content.hidden).toBe(false)
880
-
881
- // Click outside - use clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
882
- await nextFrame()
883
- const outsideElement = document.createElement("div")
884
- document.body.appendChild(outsideElement)
885
- controller.clickOutside({ target: outsideElement })
886
- expect(controller.openValue).toBe(false)
887
-
888
- await wait(200)
889
- expect(content.hidden).toBe(true)
890
- document.body.removeChild(outsideElement)
891
-
892
- // Reopen
893
- click(trigger)
894
- expect(controller.openValue).toBe(true)
895
- expect(content.hidden).toBe(false)
896
- })
897
-
898
- test("modal popover prevents background interaction", async () => {
899
- application.stop()
900
- document.body.innerHTML = `
901
- <div>
902
- <button id="background-button">Background</button>
903
- ${createPopoverHTML({ modal: true })}
904
- </div>
905
- `
906
-
907
- application = Application.start()
908
- application.register("shadcn--popover", PopoverController)
909
-
910
- await new Promise(resolve => requestAnimationFrame(resolve))
911
-
912
- element = document.querySelector('[data-controller="shadcn--popover"]')
913
- controller = application.getControllerForElementAndIdentifier(element, "shadcn--popover")
914
-
915
- controller.show()
916
-
917
- // Body should block pointer events
918
- expect(document.body.style.pointerEvents).toBe("none")
919
-
920
- // Content should allow pointer events
921
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
922
- expect(content.style.pointerEvents).toBe("auto")
923
- })
924
-
925
- test("changing side value while open repositions content", async () => {
926
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
927
-
928
- controller.show()
929
- expect(content.style.top).toBe("100%")
930
- expect(content.dataset.side).toBe("bottom")
931
-
932
- // Change side value
933
- controller.sideValue = "top"
934
- // Need to update data attribute manually since positionContent doesn't do it
935
- content.dataset.side = controller.sideValue
936
- controller.positionContent()
937
-
938
- expect(content.style.bottom).toBe("100%")
939
- expect(content.dataset.side).toBe("top")
940
- })
941
-
942
- test("changing align value while open repositions content", () => {
943
- const content = element.querySelector('[data-shadcn--popover-target="content"]')
944
-
945
- controller.show()
946
- expect(content.style.left).toBe("50%")
947
- expect(content.style.transform).toBe("translateX(-50%)")
948
-
949
- // Change align value
950
- controller.alignValue = "start"
951
- controller.positionContent()
952
-
953
- expect(content.style.left).toBe("0px")
954
- expect(content.style.right).toBe("auto")
955
- })
956
- })
957
-
958
- describe("snapshots", () => {
959
- test("renders closed popover correctly", () => {
960
- expect(element.innerHTML).toMatchSnapshot()
961
- })
962
-
963
- test("renders open popover correctly", () => {
964
- controller.show()
965
- expect(element.innerHTML).toMatchSnapshot()
966
- })
967
-
968
- test("renders modal popover correctly", async () => {
969
- application.stop()
970
- document.body.innerHTML = createPopoverHTML({ modal: true, open: true })
971
-
972
- application = Application.start()
973
- application.register("shadcn--popover", PopoverController)
974
-
975
- await new Promise(resolve => requestAnimationFrame(resolve))
976
-
977
- element = document.querySelector('[data-controller="shadcn--popover"]')
978
-
979
- expect(element.innerHTML).toMatchSnapshot()
980
- })
981
- })
982
- })