shadcn-rails 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -2
  3. data/README.md +102 -1398
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
  6. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
  7. data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
  8. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
  9. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
  10. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  11. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
  12. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
  13. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
  14. data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
  15. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
  16. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  17. data/app/assets/javascripts/shadcn/index.js +9 -1
  18. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  19. data/app/assets/stylesheets/shadcn/base.css +32 -0
  20. data/app/assets/stylesheets/shadcn/components.css +12 -0
  21. data/app/components/shadcn/accordion_component.html.erb +8 -0
  22. data/app/components/shadcn/accordion_component.rb +6 -15
  23. data/app/components/shadcn/alert_component.html.erb +6 -0
  24. data/app/components/shadcn/alert_component.rb +0 -18
  25. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  26. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  27. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  28. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  29. data/app/components/shadcn/avatar_component.html.erb +20 -0
  30. data/app/components/shadcn/avatar_component.rb +8 -36
  31. data/app/components/shadcn/badge_component.html.erb +1 -0
  32. data/app/components/shadcn/badge_component.rb +0 -11
  33. data/app/components/shadcn/base_component.rb +15 -2
  34. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  35. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  36. data/app/components/shadcn/button_component.html.erb +18 -0
  37. data/app/components/shadcn/button_component.rb +1 -41
  38. data/app/components/shadcn/card_component.html.erb +8 -0
  39. data/app/components/shadcn/card_component.rb +2 -6
  40. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  41. data/app/components/shadcn/checkbox_component.rb +4 -43
  42. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  43. data/app/components/shadcn/collapsible_component.rb +6 -15
  44. data/app/components/shadcn/command_list_component.rb +29 -14
  45. data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
  46. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  47. data/app/components/shadcn/context_menu_component.rb +6 -26
  48. data/app/components/shadcn/context_menu_content_component.rb +37 -14
  49. data/app/components/shadcn/context_menu_item_component.rb +3 -2
  50. data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
  51. data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
  52. data/app/components/shadcn/dialog_component.html.erb +14 -0
  53. data/app/components/shadcn/dialog_component.rb +8 -29
  54. data/app/components/shadcn/drawer_component.html.erb +12 -0
  55. data/app/components/shadcn/drawer_component.rb +7 -27
  56. data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
  57. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  58. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  59. data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
  60. data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
  61. data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
  62. data/app/components/shadcn/field_component.rb +7 -8
  63. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  64. data/app/components/shadcn/hover_card_component.rb +7 -26
  65. data/app/components/shadcn/input_component.html.erb +18 -0
  66. data/app/components/shadcn/input_component.rb +2 -27
  67. data/app/components/shadcn/input_otp_component.rb +3 -3
  68. data/app/components/shadcn/kbd_component.html.erb +1 -0
  69. data/app/components/shadcn/kbd_component.rb +3 -10
  70. data/app/components/shadcn/label_component.html.erb +3 -0
  71. data/app/components/shadcn/label_component.rb +2 -18
  72. data/app/components/shadcn/menubar_component.html.erb +6 -0
  73. data/app/components/shadcn/menubar_component.rb +4 -15
  74. data/app/components/shadcn/menubar_content_component.rb +45 -20
  75. data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
  76. data/app/components/shadcn/native_select_component.html.erb +22 -0
  77. data/app/components/shadcn/native_select_component.rb +9 -39
  78. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  79. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  80. data/app/components/shadcn/pagination_component.html.erb +5 -0
  81. data/app/components/shadcn/pagination_component.rb +11 -15
  82. data/app/components/shadcn/popover_component.html.erb +15 -0
  83. data/app/components/shadcn/popover_component.rb +10 -30
  84. data/app/components/shadcn/progress_component.html.erb +13 -0
  85. data/app/components/shadcn/progress_component.rb +6 -26
  86. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  87. data/app/components/shadcn/radio_group_component.rb +12 -26
  88. data/app/components/shadcn/radio_group_item_component.rb +32 -6
  89. data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
  90. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  91. data/app/components/shadcn/scroll_area_component.rb +4 -16
  92. data/app/components/shadcn/select_component.html.erb +46 -0
  93. data/app/components/shadcn/select_component.rb +29 -86
  94. data/app/components/shadcn/separator_component.html.erb +5 -0
  95. data/app/components/shadcn/separator_component.rb +6 -14
  96. data/app/components/shadcn/sheet_component.html.erb +12 -0
  97. data/app/components/shadcn/sheet_component.rb +7 -27
  98. data/app/components/shadcn/sidebar_component.rb +2 -2
  99. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  100. data/app/components/shadcn/skeleton_component.rb +4 -2
  101. data/app/components/shadcn/slider_component.html.erb +12 -0
  102. data/app/components/shadcn/slider_component.rb +2 -21
  103. data/app/components/shadcn/spinner_component.html.erb +18 -0
  104. data/app/components/shadcn/spinner_component.rb +2 -30
  105. data/app/components/shadcn/switch_component.html.erb +72 -0
  106. data/app/components/shadcn/switch_component.rb +4 -82
  107. data/app/components/shadcn/table_component.html.erb +9 -0
  108. data/app/components/shadcn/table_component.rb +2 -10
  109. data/app/components/shadcn/tabs_component.html.erb +8 -0
  110. data/app/components/shadcn/tabs_component.rb +4 -17
  111. data/app/components/shadcn/textarea_component.html.erb +13 -0
  112. data/app/components/shadcn/textarea_component.rb +6 -22
  113. data/app/components/shadcn/toast_component.html.erb +36 -0
  114. data/app/components/shadcn/toast_component.rb +6 -54
  115. data/app/components/shadcn/toggle_component.html.erb +12 -0
  116. data/app/components/shadcn/toggle_component.rb +6 -21
  117. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  118. data/app/components/shadcn/toggle_group_component.rb +6 -29
  119. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  120. data/app/components/shadcn/tooltip_component.rb +13 -38
  121. data/lib/generators/shadcn/add/USAGE +24 -0
  122. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  123. data/lib/generators/shadcn/install/USAGE +22 -0
  124. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  125. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  126. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  127. data/lib/shadcn/rails/version.rb +1 -1
  128. metadata +54 -42
  129. data/.dockerignore +0 -40
  130. data/CLAUDE.md +0 -463
  131. data/PROGRESS.md +0 -485
  132. data/Rakefile +0 -29
  133. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  134. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  135. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  136. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  137. data/__tests__/controllers/accordion_controller.test.js +0 -904
  138. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  139. data/__tests__/controllers/carousel_controller.test.js +0 -912
  140. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  141. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  142. data/__tests__/controllers/combobox_controller.test.js +0 -966
  143. data/__tests__/controllers/context_menu_controller.test.js +0 -627
  144. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  145. data/__tests__/controllers/dialog_controller.test.js +0 -878
  146. data/__tests__/controllers/drawer_controller.test.js +0 -995
  147. data/__tests__/controllers/menubar_controller.test.js +0 -736
  148. data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
  149. data/__tests__/controllers/popover_controller.test.js +0 -1007
  150. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  151. data/__tests__/controllers/resizable_controller.test.js +0 -680
  152. data/__tests__/controllers/select_controller.test.js +0 -674
  153. data/__tests__/controllers/sheet_controller.test.js +0 -986
  154. data/__tests__/controllers/slider_controller.test.js +0 -1036
  155. data/__tests__/controllers/switch_controller.test.js +0 -424
  156. data/__tests__/controllers/tabs_controller.test.js +0 -907
  157. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  158. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  159. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  160. data/babel.config.cjs +0 -5
  161. data/bin/console +0 -11
  162. data/bin/setup +0 -8
  163. data/jest.config.js +0 -19
  164. data/jest.setup.js +0 -8
  165. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  166. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  167. data/package-lock.json +0 -7415
  168. data/package.json +0 -68
  169. data/rollup.config.js +0 -29
@@ -1,598 +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
- controller.handleClickOutside({ target: outsideElement })
452
- await nextFrame()
453
-
454
- expect(controller.isOpen).toBe(false)
455
-
456
- document.body.removeChild(outsideElement)
457
- })
458
-
459
- test("does not close on click inside", async () => {
460
- controller.openItem(0)
461
- await nextFrame()
462
-
463
- controller.handleClickOutside({ target: element })
464
- await nextFrame()
465
-
466
- expect(controller.isOpen).toBe(true)
467
- })
468
- })
469
-
470
- describe("timer management", () => {
471
- const timerHTML = `
472
- <nav data-controller="shadcn--navigation-menu"
473
- data-shadcn--navigation-menu-open-index-value="-1"
474
- data-shadcn--navigation-menu-delay-duration-value="50"
475
- data-shadcn--navigation-menu-skip-delay-duration-value="50">
476
- <ul data-shadcn--navigation-menu-target="list">
477
- <li data-shadcn--navigation-menu-target="item">
478
- <button data-shadcn--navigation-menu-target="trigger"
479
- data-action="mouseenter->shadcn--navigation-menu#hoverOpen mouseleave->shadcn--navigation-menu#hoverClose"
480
- aria-expanded="false">Menu</button>
481
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
482
- </li>
483
- </ul>
484
- </nav>
485
- `
486
-
487
- beforeEach(async () => {
488
- const setup = await setupController(NavigationMenuController, timerHTML, 'shadcn--navigation-menu')
489
- application = setup.application
490
- element = setup.element
491
- controller = setup.controller
492
- })
493
-
494
- test("clearTimers clears open timer", () => {
495
- controller.openTimer = setTimeout(() => {}, 1000)
496
- controller.clearTimers()
497
-
498
- expect(controller.openTimer).toBeNull()
499
- })
500
-
501
- test("clearTimers clears close timer", () => {
502
- controller.closeTimer = setTimeout(() => {}, 1000)
503
- controller.clearTimers()
504
-
505
- expect(controller.closeTimer).toBeNull()
506
- })
507
- })
508
-
509
- describe("disconnect cleanup", () => {
510
- const disconnectHTML = `
511
- <nav data-controller="shadcn--navigation-menu"
512
- data-shadcn--navigation-menu-open-index-value="-1">
513
- <ul data-shadcn--navigation-menu-target="list">
514
- <li data-shadcn--navigation-menu-target="item">
515
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
516
- <div data-shadcn--navigation-menu-target="content" hidden>Content</div>
517
- </li>
518
- </ul>
519
- </nav>
520
- `
521
-
522
- beforeEach(async () => {
523
- const setup = await setupController(NavigationMenuController, disconnectHTML, 'shadcn--navigation-menu')
524
- application = setup.application
525
- element = setup.element
526
- controller = setup.controller
527
- })
528
-
529
- test("closes all on disconnect", async () => {
530
- controller.openItem(0)
531
- await nextFrame()
532
-
533
- controller.disconnect()
534
- await nextFrame()
535
-
536
- expect(controller.openIndexValue).toBe(-1)
537
- })
538
-
539
- test("clears timers on disconnect", () => {
540
- controller.openTimer = setTimeout(() => {}, 1000)
541
- controller.closeTimer = setTimeout(() => {}, 1000)
542
-
543
- controller.disconnect()
544
-
545
- expect(controller.openTimer).toBeNull()
546
- expect(controller.closeTimer).toBeNull()
547
- })
548
- })
549
-
550
- describe("viewport functionality", () => {
551
- const viewportHTML = `
552
- <nav data-controller="shadcn--navigation-menu"
553
- data-shadcn--navigation-menu-open-index-value="-1">
554
- <ul data-shadcn--navigation-menu-target="list">
555
- <li data-shadcn--navigation-menu-target="item">
556
- <button data-shadcn--navigation-menu-target="trigger" aria-expanded="false">Menu</button>
557
- <div data-shadcn--navigation-menu-target="content" hidden style="width: 200px; height: 100px;">
558
- <p>Content here</p>
559
- </div>
560
- </li>
561
- </ul>
562
- <div data-shadcn--navigation-menu-target="viewport" hidden></div>
563
- </nav>
564
- `
565
-
566
- beforeEach(async () => {
567
- const setup = await setupController(NavigationMenuController, viewportHTML, 'shadcn--navigation-menu')
568
- application = setup.application
569
- element = setup.element
570
- controller = setup.controller
571
- })
572
-
573
- test("has viewport target", () => {
574
- expect(controller.hasViewportTarget).toBe(true)
575
- })
576
-
577
- test("shows viewport when item opened", async () => {
578
- controller.openItem(0)
579
- await nextFrame()
580
-
581
- expect(controller.viewportTarget.hidden).toBe(false)
582
- })
583
-
584
- test("sets viewport data-state to open", async () => {
585
- controller.openItem(0)
586
- await nextFrame()
587
-
588
- expect(controller.viewportTarget.dataset.state).toBe("open")
589
- })
590
-
591
- test("copies content to viewport", async () => {
592
- controller.openItem(0)
593
- await nextFrame()
594
-
595
- expect(controller.viewportTarget.innerHTML).toContain("Content here")
596
- })
597
- })
598
- })