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,599 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
- import NavigationMenuController from "../../app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js"
3
- import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
4
-
5
- describe("NavigationMenuController", () => {
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
- <nav data-controller="shadcn--navigation-menu"
17
- data-shadcn--navigation-menu-open-index-value="-1"
18
- data-shadcn--navigation-menu-delay-duration-value="200"
19
- data-shadcn--navigation-menu-skip-delay-duration-value="300">
20
- <ul data-shadcn--navigation-menu-target="list">
21
- <li data-shadcn--navigation-menu-target="item">
22
- <button data-shadcn--navigation-menu-target="trigger"
23
- data-action="click->shadcn--navigation-menu#toggle mouseenter->shadcn--navigation-menu#hoverOpen mouseleave->shadcn--navigation-menu#hoverClose"
24
- aria-expanded="false">Products</button>
25
- <div data-shadcn--navigation-menu-target="content" hidden>
26
- <a href="/product1">Product 1</a>
27
- </div>
28
- </li>
29
- <li data-shadcn--navigation-menu-target="item">
30
- <button data-shadcn--navigation-menu-target="trigger"
31
- data-action="click->shadcn--navigation-menu#toggle mouseenter->shadcn--navigation-menu#hoverOpen mouseleave->shadcn--navigation-menu#hoverClose"
32
- aria-expanded="false">Services</button>
33
- <div data-shadcn--navigation-menu-target="content" hidden>
34
- <a href="/service1">Service 1</a>
35
- </div>
36
- </li>
37
- </ul>
38
- </nav>
39
- `
40
-
41
- beforeEach(async () => {
42
- const setup = await setupController(NavigationMenuController, basicHTML, 'shadcn--navigation-menu')
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 isOpen to false", () => {
53
- expect(controller.isOpen).toBe(false)
54
- })
55
-
56
- test("initializes with default delay values", () => {
57
- expect(controller.delayDurationValue).toBe(200)
58
- expect(controller.skipDelayDurationValue).toBe(300)
59
- })
60
-
61
- test("has list target", () => {
62
- expect(controller.hasListTarget).toBe(true)
63
- })
64
-
65
- test("has item targets", () => {
66
- expect(controller.itemTargets.length).toBe(2)
67
- })
68
-
69
- test("has trigger targets", () => {
70
- expect(controller.triggerTargets.length).toBe(2)
71
- })
72
-
73
- test("has content targets", () => {
74
- expect(controller.contentTargets.length).toBe(2)
75
- })
76
-
77
- test("all content is initially hidden", () => {
78
- controller.contentTargets.forEach(content => {
79
- expect(content.hidden).toBe(true)
80
- })
81
- })
82
- })
83
-
84
- describe("toggle functionality", () => {
85
- const toggleHTML = `
86
- <nav data-controller="shadcn--navigation-menu"
87
- data-shadcn--navigation-menu-open-index-value="-1">
88
- <ul data-shadcn--navigation-menu-target="list">
89
- <li data-shadcn--navigation-menu-target="item">
90
- <button data-shadcn--navigation-menu-target="trigger"
91
- data-action="click->shadcn--navigation-menu#toggle"
92
- aria-expanded="false">Products</button>
93
- <div data-shadcn--navigation-menu-target="content" hidden>Content 1</div>
94
- </li>
95
- <li data-shadcn--navigation-menu-target="item">
96
- <button data-shadcn--navigation-menu-target="trigger"
97
- data-action="click->shadcn--navigation-menu#toggle"
98
- aria-expanded="false">Services</button>
99
- <div data-shadcn--navigation-menu-target="content" hidden>Content 2</div>
100
- </li>
101
- </ul>
102
- </nav>
103
- `
104
-
105
- beforeEach(async () => {
106
- const setup = await setupController(NavigationMenuController, toggleHTML, 'shadcn--navigation-menu')
107
- application = setup.application
108
- element = setup.element
109
- controller = setup.controller
110
- })
111
-
112
- test("opens item on toggle", async () => {
113
- const trigger = controller.triggerTargets[0]
114
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
115
- await nextFrame()
116
-
117
- expect(controller.openIndexValue).toBe(0)
118
- expect(controller.isOpen).toBe(true)
119
- })
120
-
121
- test("sets aria-expanded to true", async () => {
122
- const trigger = controller.triggerTargets[0]
123
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
124
- await nextFrame()
125
-
126
- expect(trigger.getAttribute("aria-expanded")).toBe("true")
127
- })
128
-
129
- test("shows content when opened", async () => {
130
- const trigger = controller.triggerTargets[0]
131
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
132
- await nextFrame()
133
-
134
- const content = controller.contentTargets[0]
135
- expect(content.hidden).toBe(false)
136
- })
137
-
138
- test("sets content data-state to open", async () => {
139
- const trigger = controller.triggerTargets[0]
140
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
141
- await nextFrame()
142
-
143
- const content = controller.contentTargets[0]
144
- expect(content.dataset.state).toBe("open")
145
- })
146
-
147
- test("closes item on second toggle", async () => {
148
- const trigger = controller.triggerTargets[0]
149
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
150
- await nextFrame()
151
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
152
- await nextFrame()
153
-
154
- expect(controller.openIndexValue).toBe(-1)
155
- expect(controller.isOpen).toBe(false)
156
- })
157
-
158
- test("switches to different item on toggle", async () => {
159
- const trigger1 = controller.triggerTargets[0]
160
- const trigger2 = controller.triggerTargets[1]
161
-
162
- controller.toggle({ currentTarget: trigger1, preventDefault: jest.fn() })
163
- await nextFrame()
164
-
165
- expect(controller.openIndexValue).toBe(0)
166
-
167
- controller.toggle({ currentTarget: trigger2, preventDefault: jest.fn() })
168
- await nextFrame()
169
-
170
- expect(controller.openIndexValue).toBe(1)
171
- })
172
-
173
- test("sets wasClickOpened flag on toggle", async () => {
174
- const trigger = controller.triggerTargets[0]
175
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
176
- await nextFrame()
177
-
178
- expect(controller.wasClickOpened).toBe(true)
179
- })
180
- })
181
-
182
- describe("openItem and closeItem", () => {
183
- const itemHTML = `
184
- <nav data-controller="shadcn--navigation-menu"
185
- data-shadcn--navigation-menu-open-index-value="-1">
186
- <ul data-shadcn--navigation-menu-target="list">
187
- <li data-shadcn--navigation-menu-target="item">
188
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu 1</button>
189
- <div data-shadcn--navigation-menu-target="content" hidden>Content 1</div>
190
- </li>
191
- <li data-shadcn--navigation-menu-target="item">
192
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu 2</button>
193
- <div data-shadcn--navigation-menu-target="content" hidden>Content 2</div>
194
- </li>
195
- </ul>
196
- </nav>
197
- `
198
-
199
- beforeEach(async () => {
200
- const setup = await setupController(NavigationMenuController, itemHTML, 'shadcn--navigation-menu')
201
- application = setup.application
202
- element = setup.element
203
- controller = setup.controller
204
- })
205
-
206
- test("openItem opens specified index", async () => {
207
- controller.openItem(0)
208
- await nextFrame()
209
-
210
- expect(controller.openIndexValue).toBe(0)
211
- })
212
-
213
- test("openItem ignores invalid index (negative)", async () => {
214
- controller.openItem(-1)
215
- await nextFrame()
216
-
217
- expect(controller.openIndexValue).toBe(-1)
218
- })
219
-
220
- test("openItem ignores invalid index (too high)", async () => {
221
- controller.openItem(99)
222
- await nextFrame()
223
-
224
- expect(controller.openIndexValue).toBe(-1)
225
- })
226
-
227
- test("openItem closes previous item when opening new one", async () => {
228
- controller.openItem(0)
229
- await nextFrame()
230
-
231
- controller.openItem(1)
232
- await nextFrame()
233
-
234
- const trigger0 = controller.triggerTargets[0]
235
- const content0 = controller.contentTargets[0]
236
-
237
- expect(trigger0.getAttribute("aria-expanded")).toBe("false")
238
- expect(content0.dataset.state).toBe("closed")
239
- })
240
-
241
- test("closeItem closes specified index", async () => {
242
- controller.openItem(0)
243
- await nextFrame()
244
-
245
- controller.closeItem(0)
246
- await nextFrame()
247
-
248
- const trigger = controller.triggerTargets[0]
249
- expect(trigger.getAttribute("aria-expanded")).toBe("false")
250
- })
251
-
252
- test("sets motion direction when switching items", async () => {
253
- controller.openItem(0)
254
- await nextFrame()
255
-
256
- controller.openItem(1)
257
- await nextFrame()
258
-
259
- const content1 = controller.contentTargets[1]
260
- expect(content1.dataset.motion).toBe("from-end")
261
- })
262
-
263
- test("sets opposite motion direction", async () => {
264
- controller.openItem(1)
265
- await nextFrame()
266
-
267
- controller.openItem(0)
268
- await nextFrame()
269
-
270
- const content0 = controller.contentTargets[0]
271
- expect(content0.dataset.motion).toBe("from-start")
272
- })
273
- })
274
-
275
- describe("closeAll", () => {
276
- const closeAllHTML = `
277
- <nav data-controller="shadcn--navigation-menu"
278
- data-shadcn--navigation-menu-open-index-value="-1">
279
- <ul data-shadcn--navigation-menu-target="list">
280
- <li data-shadcn--navigation-menu-target="item">
281
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
282
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
283
- </li>
284
- </ul>
285
- </nav>
286
- `
287
-
288
- beforeEach(async () => {
289
- const setup = await setupController(NavigationMenuController, closeAllHTML, 'shadcn--navigation-menu')
290
- application = setup.application
291
- element = setup.element
292
- controller = setup.controller
293
- })
294
-
295
- test("resets openIndexValue to -1", async () => {
296
- controller.openItem(0)
297
- await nextFrame()
298
-
299
- controller.closeAll()
300
- await nextFrame()
301
-
302
- expect(controller.openIndexValue).toBe(-1)
303
- })
304
-
305
- test("resets isOpen to false", async () => {
306
- controller.openItem(0)
307
- await nextFrame()
308
-
309
- controller.closeAll()
310
- await nextFrame()
311
-
312
- expect(controller.isOpen).toBe(false)
313
- })
314
-
315
- test("resets wasClickOpened to false", async () => {
316
- const trigger = controller.triggerTargets[0]
317
- controller.toggle({ currentTarget: trigger, preventDefault: jest.fn() })
318
- await nextFrame()
319
-
320
- controller.closeAll()
321
- await nextFrame()
322
-
323
- expect(controller.wasClickOpened).toBe(false)
324
- })
325
-
326
- test("sets all triggers to aria-expanded false", async () => {
327
- controller.openItem(0)
328
- await nextFrame()
329
-
330
- controller.closeAll()
331
- await nextFrame()
332
-
333
- controller.triggerTargets.forEach(trigger => {
334
- expect(trigger.getAttribute("aria-expanded")).toBe("false")
335
- })
336
- })
337
- })
338
-
339
- describe("keyboard navigation", () => {
340
- const keyboardHTML = `
341
- <nav data-controller="shadcn--navigation-menu"
342
- data-shadcn--navigation-menu-open-index-value="-1">
343
- <ul data-shadcn--navigation-menu-target="list">
344
- <li data-shadcn--navigation-menu-target="item">
345
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu 1</button>
346
- <div data-shadcn--navigation-menu-target="content" hidden>Content 1</div>
347
- </li>
348
- <li data-shadcn--navigation-menu-target="item">
349
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu 2</button>
350
- <div data-shadcn--navigation-menu-target="content" hidden>Content 2</div>
351
- </li>
352
- <li data-shadcn--navigation-menu-target="item">
353
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu 3</button>
354
- <div data-shadcn--navigation-menu-target="content" hidden>Content 3</div>
355
- </li>
356
- </ul>
357
- </nav>
358
- `
359
-
360
- beforeEach(async () => {
361
- const setup = await setupController(NavigationMenuController, keyboardHTML, 'shadcn--navigation-menu')
362
- application = setup.application
363
- element = setup.element
364
- controller = setup.controller
365
-
366
- // Open the first menu to enable keyboard navigation
367
- controller.openItem(0)
368
- await nextFrame()
369
- })
370
-
371
- test("ArrowRight navigates to next item", async () => {
372
- controller.handleKeydown({ key: "ArrowRight", preventDefault: jest.fn() })
373
- await nextFrame()
374
-
375
- expect(controller.openIndexValue).toBe(1)
376
- })
377
-
378
- test("ArrowRight wraps to first item", async () => {
379
- controller.openItem(2)
380
- await nextFrame()
381
-
382
- controller.handleKeydown({ key: "ArrowRight", preventDefault: jest.fn() })
383
- await nextFrame()
384
-
385
- expect(controller.openIndexValue).toBe(0)
386
- })
387
-
388
- test("ArrowLeft navigates to previous item", async () => {
389
- controller.openItem(1)
390
- await nextFrame()
391
-
392
- controller.handleKeydown({ key: "ArrowLeft", preventDefault: jest.fn() })
393
- await nextFrame()
394
-
395
- expect(controller.openIndexValue).toBe(0)
396
- })
397
-
398
- test("ArrowLeft wraps to last item", async () => {
399
- controller.handleKeydown({ key: "ArrowLeft", preventDefault: jest.fn() })
400
- await nextFrame()
401
-
402
- expect(controller.openIndexValue).toBe(2)
403
- })
404
-
405
- test("Escape closes all menus", async () => {
406
- controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
407
- await nextFrame()
408
-
409
- expect(controller.isOpen).toBe(false)
410
- expect(controller.openIndexValue).toBe(-1)
411
- })
412
-
413
- test("prevents default on navigation keys", () => {
414
- const preventDefault = jest.fn()
415
- controller.handleKeydown({ key: "ArrowRight", preventDefault })
416
- expect(preventDefault).toHaveBeenCalled()
417
-
418
- preventDefault.mockClear()
419
- controller.handleKeydown({ key: "ArrowLeft", preventDefault })
420
- expect(preventDefault).toHaveBeenCalled()
421
- })
422
- })
423
-
424
- describe("click outside handling", () => {
425
- const clickOutsideHTML = `
426
- <nav data-controller="shadcn--navigation-menu"
427
- data-shadcn--navigation-menu-open-index-value="-1">
428
- <ul data-shadcn--navigation-menu-target="list">
429
- <li data-shadcn--navigation-menu-target="item">
430
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
431
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
432
- </li>
433
- </ul>
434
- </nav>
435
- `
436
-
437
- beforeEach(async () => {
438
- const setup = await setupController(NavigationMenuController, clickOutsideHTML, 'shadcn--navigation-menu')
439
- application = setup.application
440
- element = setup.element
441
- controller = setup.controller
442
- })
443
-
444
- test("closes on click outside", async () => {
445
- controller.openItem(0)
446
- await nextFrame()
447
-
448
- const outsideElement = document.createElement("div")
449
- document.body.appendChild(outsideElement)
450
-
451
- // Use clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
452
- controller.clickOutside({ target: outsideElement })
453
- await nextFrame()
454
-
455
- expect(controller.isOpen).toBe(false)
456
-
457
- document.body.removeChild(outsideElement)
458
- })
459
-
460
- test("does not close on click inside", async () => {
461
- controller.openItem(0)
462
- await nextFrame()
463
-
464
- // Clicking inside the controller element should not close via clickOutside
465
- // The clickOutside method from stimulus-use only fires for clicks outside the element
466
- // So we verify the menu stays open (clickOutside isn't even called for inside clicks)
467
- expect(controller.isOpen).toBe(true)
468
- })
469
- })
470
-
471
- describe("timer management", () => {
472
- const timerHTML = `
473
- <nav data-controller="shadcn--navigation-menu"
474
- data-shadcn--navigation-menu-open-index-value="-1"
475
- data-shadcn--navigation-menu-delay-duration-value="50"
476
- data-shadcn--navigation-menu-skip-delay-duration-value="50">
477
- <ul data-shadcn--navigation-menu-target="list">
478
- <li data-shadcn--navigation-menu-target="item">
479
- <button data-shadcn--navigation-menu-target="trigger"
480
- data-action="mouseenter->shadcn--navigation-menu#hoverOpen mouseleave->shadcn--navigation-menu#hoverClose"
481
- aria-expanded="false">Menu</button>
482
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
483
- </li>
484
- </ul>
485
- </nav>
486
- `
487
-
488
- beforeEach(async () => {
489
- const setup = await setupController(NavigationMenuController, timerHTML, 'shadcn--navigation-menu')
490
- application = setup.application
491
- element = setup.element
492
- controller = setup.controller
493
- })
494
-
495
- test("clearTimers clears open timer", () => {
496
- controller.openTimer = setTimeout(() => {}, 1000)
497
- controller.clearTimers()
498
-
499
- expect(controller.openTimer).toBeNull()
500
- })
501
-
502
- test("clearTimers clears close timer", () => {
503
- controller.closeTimer = setTimeout(() => {}, 1000)
504
- controller.clearTimers()
505
-
506
- expect(controller.closeTimer).toBeNull()
507
- })
508
- })
509
-
510
- describe("disconnect cleanup", () => {
511
- const disconnectHTML = `
512
- <nav data-controller="shadcn--navigation-menu"
513
- data-shadcn--navigation-menu-open-index-value="-1">
514
- <ul data-shadcn--navigation-menu-target="list">
515
- <li data-shadcn--navigation-menu-target="item">
516
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
517
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
518
- </li>
519
- </ul>
520
- </nav>
521
- `
522
-
523
- beforeEach(async () => {
524
- const setup = await setupController(NavigationMenuController, disconnectHTML, 'shadcn--navigation-menu')
525
- application = setup.application
526
- element = setup.element
527
- controller = setup.controller
528
- })
529
-
530
- test("closes all on disconnect", async () => {
531
- controller.openItem(0)
532
- await nextFrame()
533
-
534
- controller.disconnect()
535
- await nextFrame()
536
-
537
- expect(controller.openIndexValue).toBe(-1)
538
- })
539
-
540
- test("clears timers on disconnect", () => {
541
- controller.openTimer = setTimeout(() => {}, 1000)
542
- controller.closeTimer = setTimeout(() => {}, 1000)
543
-
544
- controller.disconnect()
545
-
546
- expect(controller.openTimer).toBeNull()
547
- expect(controller.closeTimer).toBeNull()
548
- })
549
- })
550
-
551
- describe("viewport functionality", () => {
552
- const viewportHTML = `
553
- <nav data-controller="shadcn--navigation-menu"
554
- data-shadcn--navigation-menu-open-index-value="-1">
555
- <ul data-shadcn--navigation-menu-target="list">
556
- <li data-shadcn--navigation-menu-target="item">
557
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
558
- <div data-shadcn--navigation-menu-target="content" hidden style="width: 200px; height: 100px;">
559
- <p>Content here</p>
560
- </div>
561
- </li>
562
- </ul>
563
- <div data-shadcn--navigation-menu-target="viewport" hidden></div>
564
- </nav>
565
- `
566
-
567
- beforeEach(async () => {
568
- const setup = await setupController(NavigationMenuController, viewportHTML, 'shadcn--navigation-menu')
569
- application = setup.application
570
- element = setup.element
571
- controller = setup.controller
572
- })
573
-
574
- test("has viewport target", () => {
575
- expect(controller.hasViewportTarget).toBe(true)
576
- })
577
-
578
- test("shows viewport when item opened", async () => {
579
- controller.openItem(0)
580
- await nextFrame()
581
-
582
- expect(controller.viewportTarget.hidden).toBe(false)
583
- })
584
-
585
- test("sets viewport data-state to open", async () => {
586
- controller.openItem(0)
587
- await nextFrame()
588
-
589
- expect(controller.viewportTarget.dataset.state).toBe("open")
590
- })
591
-
592
- test("copies content to viewport", async () => {
593
- controller.openItem(0)
594
- await nextFrame()
595
-
596
- expect(controller.viewportTarget.innerHTML).toContain("Content here")
597
- })
598
- })
599
- })