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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -2
- data/README.md +102 -1398
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +9 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -0
- data/app/components/shadcn/accordion_component.html.erb +8 -0
- data/app/components/shadcn/accordion_component.rb +6 -15
- data/app/components/shadcn/alert_component.html.erb +6 -0
- data/app/components/shadcn/alert_component.rb +0 -18
- data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
- data/app/components/shadcn/alert_dialog_component.rb +7 -27
- data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
- data/app/components/shadcn/aspect_ratio_component.rb +4 -19
- data/app/components/shadcn/avatar_component.html.erb +20 -0
- data/app/components/shadcn/avatar_component.rb +8 -36
- data/app/components/shadcn/badge_component.html.erb +1 -0
- data/app/components/shadcn/badge_component.rb +0 -11
- data/app/components/shadcn/base_component.rb +15 -2
- data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
- data/app/components/shadcn/breadcrumb_component.rb +6 -16
- data/app/components/shadcn/button_component.html.erb +18 -0
- data/app/components/shadcn/button_component.rb +1 -41
- data/app/components/shadcn/card_component.html.erb +8 -0
- data/app/components/shadcn/card_component.rb +2 -6
- data/app/components/shadcn/checkbox_component.html.erb +32 -0
- data/app/components/shadcn/checkbox_component.rb +4 -43
- data/app/components/shadcn/collapsible_component.html.erb +8 -0
- data/app/components/shadcn/collapsible_component.rb +6 -15
- data/app/components/shadcn/command_list_component.rb +29 -14
- data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- data/app/components/shadcn/context_menu_content_component.rb +37 -14
- data/app/components/shadcn/context_menu_item_component.rb +3 -2
- data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/dialog_component.html.erb +14 -0
- data/app/components/shadcn/dialog_component.rb +8 -29
- data/app/components/shadcn/drawer_component.html.erb +12 -0
- data/app/components/shadcn/drawer_component.rb +7 -27
- data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
- data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/field_component.rb +7 -8
- data/app/components/shadcn/hover_card_component.html.erb +12 -0
- data/app/components/shadcn/hover_card_component.rb +7 -26
- data/app/components/shadcn/input_component.html.erb +18 -0
- data/app/components/shadcn/input_component.rb +2 -27
- data/app/components/shadcn/input_otp_component.rb +3 -3
- data/app/components/shadcn/kbd_component.html.erb +1 -0
- data/app/components/shadcn/kbd_component.rb +3 -10
- data/app/components/shadcn/label_component.html.erb +3 -0
- data/app/components/shadcn/label_component.rb +2 -18
- data/app/components/shadcn/menubar_component.html.erb +6 -0
- data/app/components/shadcn/menubar_component.rb +4 -15
- data/app/components/shadcn/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- data/app/components/shadcn/native_select_component.html.erb +22 -0
- data/app/components/shadcn/native_select_component.rb +9 -39
- data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
- data/app/components/shadcn/navigation_menu_component.rb +4 -15
- data/app/components/shadcn/pagination_component.html.erb +5 -0
- data/app/components/shadcn/pagination_component.rb +11 -15
- data/app/components/shadcn/popover_component.html.erb +15 -0
- data/app/components/shadcn/popover_component.rb +10 -30
- data/app/components/shadcn/progress_component.html.erb +13 -0
- data/app/components/shadcn/progress_component.rb +6 -26
- data/app/components/shadcn/radio_group_component.html.erb +8 -0
- data/app/components/shadcn/radio_group_component.rb +12 -26
- data/app/components/shadcn/radio_group_item_component.rb +32 -6
- data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
- data/app/components/shadcn/scroll_area_component.html.erb +7 -0
- data/app/components/shadcn/scroll_area_component.rb +4 -16
- data/app/components/shadcn/select_component.html.erb +46 -0
- data/app/components/shadcn/select_component.rb +29 -86
- data/app/components/shadcn/separator_component.html.erb +5 -0
- data/app/components/shadcn/separator_component.rb +6 -14
- data/app/components/shadcn/sheet_component.html.erb +12 -0
- data/app/components/shadcn/sheet_component.rb +7 -27
- data/app/components/shadcn/sidebar_component.rb +2 -2
- data/app/components/shadcn/skeleton_component.html.erb +1 -0
- data/app/components/shadcn/skeleton_component.rb +4 -2
- data/app/components/shadcn/slider_component.html.erb +12 -0
- data/app/components/shadcn/slider_component.rb +2 -21
- data/app/components/shadcn/spinner_component.html.erb +18 -0
- data/app/components/shadcn/spinner_component.rb +2 -30
- data/app/components/shadcn/switch_component.html.erb +72 -0
- data/app/components/shadcn/switch_component.rb +4 -82
- data/app/components/shadcn/table_component.html.erb +9 -0
- data/app/components/shadcn/table_component.rb +2 -10
- data/app/components/shadcn/tabs_component.html.erb +8 -0
- data/app/components/shadcn/tabs_component.rb +4 -17
- data/app/components/shadcn/textarea_component.html.erb +13 -0
- data/app/components/shadcn/textarea_component.rb +6 -22
- data/app/components/shadcn/toast_component.html.erb +36 -0
- data/app/components/shadcn/toast_component.rb +6 -54
- data/app/components/shadcn/toggle_component.html.erb +12 -0
- data/app/components/shadcn/toggle_component.rb +6 -21
- data/app/components/shadcn/toggle_group_component.html.erb +14 -0
- data/app/components/shadcn/toggle_group_component.rb +6 -29
- data/app/components/shadcn/tooltip_component.html.erb +20 -0
- data/app/components/shadcn/tooltip_component.rb +13 -38
- data/lib/generators/shadcn/add/USAGE +24 -0
- data/lib/generators/shadcn/add/add_generator.rb +279 -0
- data/lib/generators/shadcn/install/USAGE +22 -0
- data/lib/generators/shadcn/install/install_generator.rb +8 -3
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
- data/lib/shadcn/rails/version.rb +1 -1
- metadata +54 -42
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -463
- data/PROGRESS.md +0 -485
- data/Rakefile +0 -29
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
- data/__tests__/controllers/accordion_controller.test.js +0 -904
- data/__tests__/controllers/calendar_controller.test.js +0 -1370
- data/__tests__/controllers/carousel_controller.test.js +0 -912
- data/__tests__/controllers/checkbox_controller.test.js +0 -454
- data/__tests__/controllers/collapsible_controller.test.js +0 -407
- data/__tests__/controllers/combobox_controller.test.js +0 -966
- data/__tests__/controllers/context_menu_controller.test.js +0 -627
- data/__tests__/controllers/date_picker_controller.test.js +0 -636
- data/__tests__/controllers/dialog_controller.test.js +0 -878
- data/__tests__/controllers/drawer_controller.test.js +0 -995
- data/__tests__/controllers/menubar_controller.test.js +0 -736
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
- data/__tests__/controllers/popover_controller.test.js +0 -1007
- data/__tests__/controllers/radio_group_controller.test.js +0 -640
- data/__tests__/controllers/resizable_controller.test.js +0 -680
- data/__tests__/controllers/select_controller.test.js +0 -674
- data/__tests__/controllers/sheet_controller.test.js +0 -986
- data/__tests__/controllers/slider_controller.test.js +0 -1036
- data/__tests__/controllers/switch_controller.test.js +0 -424
- data/__tests__/controllers/tabs_controller.test.js +0 -907
- data/__tests__/controllers/toggle_group_controller.test.js +0 -839
- data/__tests__/controllers/tooltip_controller.test.js +0 -808
- data/__tests__/helpers/stimulus-test-helper.js +0 -203
- data/babel.config.cjs +0 -5
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/jest.config.js +0 -19
- data/jest.setup.js +0 -8
- data/lib/generators/shadcn/component/component_generator.rb +0 -188
- data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
- data/package-lock.json +0 -7415
- data/package.json +0 -68
- data/rollup.config.js +0 -29
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
import { Application } from "@hotwired/stimulus"
|
|
2
|
-
import ContextMenuController from "../../app/assets/javascripts/shadcn/controllers/context_menu_controller.js"
|
|
3
|
-
import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
|
|
4
|
-
|
|
5
|
-
describe("ContextMenuController", () => {
|
|
6
|
-
let application
|
|
7
|
-
let element
|
|
8
|
-
let controller
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
cleanupController(application)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe("basic rendering and initialization", () => {
|
|
15
|
-
const basicHTML = `
|
|
16
|
-
<div data-controller="shadcn--context-menu"
|
|
17
|
-
data-shadcn--context-menu-open-value="false">
|
|
18
|
-
<div data-shadcn--context-menu-target="trigger"
|
|
19
|
-
data-action="contextmenu->shadcn--context-menu#show">
|
|
20
|
-
Right click here
|
|
21
|
-
</div>
|
|
22
|
-
<div data-shadcn--context-menu-target="content" hidden>
|
|
23
|
-
<button data-shadcn--context-menu-target="item"
|
|
24
|
-
data-action="click->shadcn--context-menu#selectItem">Item 1</button>
|
|
25
|
-
<button data-shadcn--context-menu-target="item"
|
|
26
|
-
data-action="click->shadcn--context-menu#selectItem">Item 2</button>
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
const setup = await setupController(ContextMenuController, basicHTML, 'shadcn--context-menu')
|
|
33
|
-
application = setup.application
|
|
34
|
-
element = setup.element
|
|
35
|
-
controller = setup.controller
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test("initializes with closed state", () => {
|
|
39
|
-
expect(controller.openValue).toBe(false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test("initializes focusedIndex to -1", () => {
|
|
43
|
-
expect(controller.focusedIndex).toBe(-1)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("content is hidden initially", () => {
|
|
47
|
-
expect(controller.contentTarget.hidden).toBe(true)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test("has trigger target", () => {
|
|
51
|
-
expect(controller.hasTriggerTarget).toBe(true)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test("has content target", () => {
|
|
55
|
-
expect(controller.hasContentTarget).toBe(true)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("has item targets", () => {
|
|
59
|
-
expect(controller.itemTargets.length).toBe(2)
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe("show functionality", () => {
|
|
64
|
-
const showHTML = `
|
|
65
|
-
<div data-controller="shadcn--context-menu"
|
|
66
|
-
data-shadcn--context-menu-open-value="false">
|
|
67
|
-
<div data-shadcn--context-menu-target="trigger"
|
|
68
|
-
data-action="contextmenu->shadcn--context-menu#show">
|
|
69
|
-
Right click here
|
|
70
|
-
</div>
|
|
71
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
72
|
-
<button data-shadcn--context-menu-target="item"
|
|
73
|
-
data-action="click->shadcn--context-menu#selectItem">Item 1</button>
|
|
74
|
-
<button data-shadcn--context-menu-target="item"
|
|
75
|
-
data-action="click->shadcn--context-menu#selectItem">Item 2</button>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
`
|
|
79
|
-
|
|
80
|
-
beforeEach(async () => {
|
|
81
|
-
const setup = await setupController(ContextMenuController, showHTML, 'shadcn--context-menu')
|
|
82
|
-
application = setup.application
|
|
83
|
-
element = setup.element
|
|
84
|
-
controller = setup.controller
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("sets openValue to true", async () => {
|
|
88
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
89
|
-
controller.show(event)
|
|
90
|
-
await nextFrame()
|
|
91
|
-
|
|
92
|
-
expect(controller.openValue).toBe(true)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test("prevents default on event", async () => {
|
|
96
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
97
|
-
controller.show(event)
|
|
98
|
-
|
|
99
|
-
expect(event.preventDefault).toHaveBeenCalled()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("stores mouse position", async () => {
|
|
103
|
-
const event = { preventDefault: jest.fn(), clientX: 150, clientY: 200 }
|
|
104
|
-
controller.show(event)
|
|
105
|
-
|
|
106
|
-
expect(controller.mouseX).toBe(150)
|
|
107
|
-
expect(controller.mouseY).toBe(200)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("shows content", async () => {
|
|
111
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
112
|
-
controller.show(event)
|
|
113
|
-
await nextFrame()
|
|
114
|
-
|
|
115
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test("sets content data-state to open", async () => {
|
|
119
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
120
|
-
controller.show(event)
|
|
121
|
-
await nextFrame()
|
|
122
|
-
|
|
123
|
-
expect(controller.contentTarget.dataset.state).toBe("open")
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test("dispatches opened event", async () => {
|
|
127
|
-
let eventFired = false
|
|
128
|
-
element.addEventListener("shadcn--context-menu:opened", () => {
|
|
129
|
-
eventFired = true
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
133
|
-
controller.show(event)
|
|
134
|
-
await nextFrame()
|
|
135
|
-
|
|
136
|
-
expect(eventFired).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test("focuses first item on show", async () => {
|
|
140
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
141
|
-
controller.show(event)
|
|
142
|
-
await nextFrame()
|
|
143
|
-
|
|
144
|
-
expect(controller.focusedIndex).toBe(0)
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe("hide functionality", () => {
|
|
149
|
-
const hideHTML = `
|
|
150
|
-
<div data-controller="shadcn--context-menu"
|
|
151
|
-
data-shadcn--context-menu-open-value="false">
|
|
152
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
153
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
154
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
beforeEach(async () => {
|
|
160
|
-
const setup = await setupController(ContextMenuController, hideHTML, 'shadcn--context-menu')
|
|
161
|
-
application = setup.application
|
|
162
|
-
element = setup.element
|
|
163
|
-
controller = setup.controller
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test("sets openValue to false", async () => {
|
|
167
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
168
|
-
controller.show(event)
|
|
169
|
-
await nextFrame()
|
|
170
|
-
|
|
171
|
-
controller.hide()
|
|
172
|
-
await nextFrame()
|
|
173
|
-
|
|
174
|
-
expect(controller.openValue).toBe(false)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
test("sets content data-state to closed", async () => {
|
|
178
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
179
|
-
controller.show(event)
|
|
180
|
-
await nextFrame()
|
|
181
|
-
|
|
182
|
-
controller.hide()
|
|
183
|
-
await nextFrame()
|
|
184
|
-
|
|
185
|
-
expect(controller.contentTarget.dataset.state).toBe("closed")
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
test("dispatches closed event", async () => {
|
|
189
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
190
|
-
controller.show(event)
|
|
191
|
-
await nextFrame()
|
|
192
|
-
|
|
193
|
-
let eventFired = false
|
|
194
|
-
element.addEventListener("shadcn--context-menu:closed", () => {
|
|
195
|
-
eventFired = true
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
controller.hide()
|
|
199
|
-
await nextFrame()
|
|
200
|
-
|
|
201
|
-
expect(eventFired).toBe(true)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test("resets focusedIndex to -1", async () => {
|
|
205
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
206
|
-
controller.show(event)
|
|
207
|
-
await nextFrame()
|
|
208
|
-
|
|
209
|
-
controller.hide()
|
|
210
|
-
await nextFrame()
|
|
211
|
-
|
|
212
|
-
expect(controller.focusedIndex).toBe(-1)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
test("does nothing if already closed", async () => {
|
|
216
|
-
let eventFired = false
|
|
217
|
-
element.addEventListener("shadcn--context-menu:closed", () => {
|
|
218
|
-
eventFired = true
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
controller.hide()
|
|
222
|
-
await nextFrame()
|
|
223
|
-
|
|
224
|
-
expect(eventFired).toBe(false)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
test("close() is an alias for hide()", async () => {
|
|
228
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
229
|
-
controller.show(event)
|
|
230
|
-
await nextFrame()
|
|
231
|
-
|
|
232
|
-
controller.close()
|
|
233
|
-
await nextFrame()
|
|
234
|
-
|
|
235
|
-
expect(controller.openValue).toBe(false)
|
|
236
|
-
})
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
describe("item selection", () => {
|
|
240
|
-
const selectHTML = `
|
|
241
|
-
<div data-controller="shadcn--context-menu"
|
|
242
|
-
data-shadcn--context-menu-open-value="false">
|
|
243
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
244
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
245
|
-
<button data-shadcn--context-menu-target="item"
|
|
246
|
-
data-action="click->shadcn--context-menu#selectItem">Item 1</button>
|
|
247
|
-
<button data-shadcn--context-menu-target="item"
|
|
248
|
-
data-action="click->shadcn--context-menu#selectItem"
|
|
249
|
-
data-disabled>Disabled Item</button>
|
|
250
|
-
<button data-shadcn--context-menu-target="item"
|
|
251
|
-
data-action="click->shadcn--context-menu#selectItem">Item 3</button>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
`
|
|
255
|
-
|
|
256
|
-
beforeEach(async () => {
|
|
257
|
-
const setup = await setupController(ContextMenuController, selectHTML, 'shadcn--context-menu')
|
|
258
|
-
application = setup.application
|
|
259
|
-
element = setup.element
|
|
260
|
-
controller = setup.controller
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
test("dispatches select event with item", async () => {
|
|
264
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
265
|
-
controller.show(event)
|
|
266
|
-
await nextFrame()
|
|
267
|
-
|
|
268
|
-
let selectedItem = null
|
|
269
|
-
element.addEventListener("shadcn--context-menu:select", (e) => {
|
|
270
|
-
selectedItem = e.detail.item
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const item = controller.itemTargets[0]
|
|
274
|
-
controller.selectItem({ currentTarget: item })
|
|
275
|
-
await nextFrame()
|
|
276
|
-
|
|
277
|
-
expect(selectedItem).toBe(item)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
test("closes menu after selection", async () => {
|
|
281
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
282
|
-
controller.show(event)
|
|
283
|
-
await nextFrame()
|
|
284
|
-
|
|
285
|
-
const item = controller.itemTargets[0]
|
|
286
|
-
controller.selectItem({ currentTarget: item })
|
|
287
|
-
await nextFrame()
|
|
288
|
-
|
|
289
|
-
expect(controller.openValue).toBe(false)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
test("does not select disabled items", async () => {
|
|
293
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
294
|
-
controller.show(event)
|
|
295
|
-
await nextFrame()
|
|
296
|
-
|
|
297
|
-
let selectFired = false
|
|
298
|
-
element.addEventListener("shadcn--context-menu:select", () => {
|
|
299
|
-
selectFired = true
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
const disabledItem = controller.itemTargets[1]
|
|
303
|
-
controller.selectItem({ currentTarget: disabledItem })
|
|
304
|
-
await nextFrame()
|
|
305
|
-
|
|
306
|
-
expect(selectFired).toBe(false)
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
test("enabled items getter filters disabled items", () => {
|
|
310
|
-
const enabledItems = controller.enabledItems
|
|
311
|
-
expect(enabledItems.length).toBe(2)
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
describe("keyboard navigation", () => {
|
|
316
|
-
const keyboardHTML = `
|
|
317
|
-
<div data-controller="shadcn--context-menu"
|
|
318
|
-
data-shadcn--context-menu-open-value="false">
|
|
319
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
320
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
321
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
322
|
-
<button data-shadcn--context-menu-target="item" data-disabled>Disabled</button>
|
|
323
|
-
<button data-shadcn--context-menu-target="item">Item 3</button>
|
|
324
|
-
<button data-shadcn--context-menu-target="item">Item 4</button>
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
327
|
-
`
|
|
328
|
-
|
|
329
|
-
beforeEach(async () => {
|
|
330
|
-
const setup = await setupController(ContextMenuController, keyboardHTML, 'shadcn--context-menu')
|
|
331
|
-
application = setup.application
|
|
332
|
-
element = setup.element
|
|
333
|
-
controller = setup.controller
|
|
334
|
-
|
|
335
|
-
// Open the menu first
|
|
336
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
337
|
-
controller.show(event)
|
|
338
|
-
await nextFrame()
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
test("ArrowDown moves to next item", async () => {
|
|
342
|
-
// Already at first item (index 0) from show()
|
|
343
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
|
|
344
|
-
await nextFrame()
|
|
345
|
-
|
|
346
|
-
expect(controller.focusedIndex).toBe(1)
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
test("ArrowDown wraps to first item", async () => {
|
|
350
|
-
// Move to last enabled item
|
|
351
|
-
controller.focusedIndex = 2 // Last enabled item (index 2 in enabledItems)
|
|
352
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault: jest.fn() })
|
|
353
|
-
await nextFrame()
|
|
354
|
-
|
|
355
|
-
expect(controller.focusedIndex).toBe(0)
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test("ArrowUp moves to previous item", async () => {
|
|
359
|
-
controller.focusedIndex = 1
|
|
360
|
-
controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
|
|
361
|
-
await nextFrame()
|
|
362
|
-
|
|
363
|
-
expect(controller.focusedIndex).toBe(0)
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
test("ArrowUp wraps to last item from first", async () => {
|
|
367
|
-
controller.focusedIndex = 0
|
|
368
|
-
controller.handleKeydown({ key: "ArrowUp", preventDefault: jest.fn() })
|
|
369
|
-
await nextFrame()
|
|
370
|
-
|
|
371
|
-
expect(controller.focusedIndex).toBe(2) // Last enabled item
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
test("Home moves to first item", async () => {
|
|
375
|
-
controller.focusedIndex = 2
|
|
376
|
-
controller.handleKeydown({ key: "Home", preventDefault: jest.fn() })
|
|
377
|
-
await nextFrame()
|
|
378
|
-
|
|
379
|
-
expect(controller.focusedIndex).toBe(0)
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
test("End moves to last item", async () => {
|
|
383
|
-
controller.focusedIndex = 0
|
|
384
|
-
controller.handleKeydown({ key: "End", preventDefault: jest.fn() })
|
|
385
|
-
await nextFrame()
|
|
386
|
-
|
|
387
|
-
expect(controller.focusedIndex).toBe(2) // Last enabled item
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
test("Escape closes the menu", async () => {
|
|
391
|
-
controller.handleKeydown({ key: "Escape", preventDefault: jest.fn() })
|
|
392
|
-
await nextFrame()
|
|
393
|
-
|
|
394
|
-
expect(controller.openValue).toBe(false)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
test("Enter triggers click on focused item", async () => {
|
|
398
|
-
const enabledItems = controller.enabledItems
|
|
399
|
-
const clickSpy = jest.spyOn(enabledItems[0], 'click')
|
|
400
|
-
|
|
401
|
-
controller.focusedIndex = 0
|
|
402
|
-
controller.handleKeydown({ key: "Enter", preventDefault: jest.fn() })
|
|
403
|
-
await nextFrame()
|
|
404
|
-
|
|
405
|
-
expect(clickSpy).toHaveBeenCalled()
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
test("Space triggers click on focused item", async () => {
|
|
409
|
-
const enabledItems = controller.enabledItems
|
|
410
|
-
const clickSpy = jest.spyOn(enabledItems[0], 'click')
|
|
411
|
-
|
|
412
|
-
controller.focusedIndex = 0
|
|
413
|
-
controller.handleKeydown({ key: " ", preventDefault: jest.fn() })
|
|
414
|
-
await nextFrame()
|
|
415
|
-
|
|
416
|
-
expect(clickSpy).toHaveBeenCalled()
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
test("prevents default on navigation keys", () => {
|
|
420
|
-
const preventDefault = jest.fn()
|
|
421
|
-
|
|
422
|
-
controller.handleKeydown({ key: "ArrowDown", preventDefault })
|
|
423
|
-
expect(preventDefault).toHaveBeenCalled()
|
|
424
|
-
|
|
425
|
-
preventDefault.mockClear()
|
|
426
|
-
controller.handleKeydown({ key: "ArrowUp", preventDefault })
|
|
427
|
-
expect(preventDefault).toHaveBeenCalled()
|
|
428
|
-
|
|
429
|
-
preventDefault.mockClear()
|
|
430
|
-
controller.handleKeydown({ key: "Home", preventDefault })
|
|
431
|
-
expect(preventDefault).toHaveBeenCalled()
|
|
432
|
-
|
|
433
|
-
preventDefault.mockClear()
|
|
434
|
-
controller.handleKeydown({ key: "End", preventDefault })
|
|
435
|
-
expect(preventDefault).toHaveBeenCalled()
|
|
436
|
-
})
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
describe("click outside handling", () => {
|
|
440
|
-
const clickOutsideHTML = `
|
|
441
|
-
<div data-controller="shadcn--context-menu"
|
|
442
|
-
data-shadcn--context-menu-open-value="false">
|
|
443
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
444
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
445
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
448
|
-
`
|
|
449
|
-
|
|
450
|
-
beforeEach(async () => {
|
|
451
|
-
const setup = await setupController(ContextMenuController, clickOutsideHTML, 'shadcn--context-menu')
|
|
452
|
-
application = setup.application
|
|
453
|
-
element = setup.element
|
|
454
|
-
controller = setup.controller
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
test("closes on click outside", async () => {
|
|
458
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
459
|
-
controller.show(event)
|
|
460
|
-
await nextFrame()
|
|
461
|
-
|
|
462
|
-
// Simulate click outside
|
|
463
|
-
const outsideElement = document.createElement("div")
|
|
464
|
-
document.body.appendChild(outsideElement)
|
|
465
|
-
controller.handleClickOutside({ target: outsideElement })
|
|
466
|
-
await nextFrame()
|
|
467
|
-
|
|
468
|
-
expect(controller.openValue).toBe(false)
|
|
469
|
-
|
|
470
|
-
document.body.removeChild(outsideElement)
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
test("does not close on click inside content", async () => {
|
|
474
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
475
|
-
controller.show(event)
|
|
476
|
-
await nextFrame()
|
|
477
|
-
|
|
478
|
-
// Simulate click inside content
|
|
479
|
-
controller.handleClickOutside({ target: controller.contentTarget })
|
|
480
|
-
await nextFrame()
|
|
481
|
-
|
|
482
|
-
expect(controller.openValue).toBe(true)
|
|
483
|
-
})
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
describe("positioning", () => {
|
|
487
|
-
const positionHTML = `
|
|
488
|
-
<div data-controller="shadcn--context-menu"
|
|
489
|
-
data-shadcn--context-menu-open-value="false">
|
|
490
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
491
|
-
<div data-shadcn--context-menu-target="content" hidden
|
|
492
|
-
style="position: fixed; width: 200px; height: 150px;">
|
|
493
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
496
|
-
`
|
|
497
|
-
|
|
498
|
-
beforeEach(async () => {
|
|
499
|
-
const setup = await setupController(ContextMenuController, positionHTML, 'shadcn--context-menu')
|
|
500
|
-
application = setup.application
|
|
501
|
-
element = setup.element
|
|
502
|
-
controller = setup.controller
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
test("positions content at mouse location", async () => {
|
|
506
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 150 }
|
|
507
|
-
controller.show(event)
|
|
508
|
-
await nextFrame()
|
|
509
|
-
|
|
510
|
-
const content = controller.contentTarget
|
|
511
|
-
expect(content.style.left).toBe("100px")
|
|
512
|
-
expect(content.style.top).toBe("150px")
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
test("positions content with minimum offset from edges", async () => {
|
|
516
|
-
const event = { preventDefault: jest.fn(), clientX: 5, clientY: 5 }
|
|
517
|
-
controller.show(event)
|
|
518
|
-
await nextFrame()
|
|
519
|
-
|
|
520
|
-
const content = controller.contentTarget
|
|
521
|
-
// Should be at least 8px from edge
|
|
522
|
-
expect(parseInt(content.style.left)).toBeGreaterThanOrEqual(8)
|
|
523
|
-
expect(parseInt(content.style.top)).toBeGreaterThanOrEqual(8)
|
|
524
|
-
})
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
describe("disconnect cleanup", () => {
|
|
528
|
-
const disconnectHTML = `
|
|
529
|
-
<div data-controller="shadcn--context-menu"
|
|
530
|
-
data-shadcn--context-menu-open-value="false">
|
|
531
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
532
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
533
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
534
|
-
</div>
|
|
535
|
-
</div>
|
|
536
|
-
`
|
|
537
|
-
|
|
538
|
-
beforeEach(async () => {
|
|
539
|
-
const setup = await setupController(ContextMenuController, disconnectHTML, 'shadcn--context-menu')
|
|
540
|
-
application = setup.application
|
|
541
|
-
element = setup.element
|
|
542
|
-
controller = setup.controller
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
test("hides menu on disconnect", async () => {
|
|
546
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
547
|
-
controller.show(event)
|
|
548
|
-
await nextFrame()
|
|
549
|
-
|
|
550
|
-
controller.disconnect()
|
|
551
|
-
await nextFrame()
|
|
552
|
-
|
|
553
|
-
expect(controller.openValue).toBe(false)
|
|
554
|
-
})
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
describe("without items", () => {
|
|
558
|
-
const noItemsHTML = `
|
|
559
|
-
<div data-controller="shadcn--context-menu"
|
|
560
|
-
data-shadcn--context-menu-open-value="false">
|
|
561
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
562
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
563
|
-
<p>No items here</p>
|
|
564
|
-
</div>
|
|
565
|
-
</div>
|
|
566
|
-
`
|
|
567
|
-
|
|
568
|
-
beforeEach(async () => {
|
|
569
|
-
const setup = await setupController(ContextMenuController, noItemsHTML, 'shadcn--context-menu')
|
|
570
|
-
application = setup.application
|
|
571
|
-
element = setup.element
|
|
572
|
-
controller = setup.controller
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
test("handles empty items gracefully", async () => {
|
|
576
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
577
|
-
|
|
578
|
-
expect(() => {
|
|
579
|
-
controller.show(event)
|
|
580
|
-
}).not.toThrow()
|
|
581
|
-
|
|
582
|
-
expect(controller.openValue).toBe(true)
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
test("navigation does nothing with no items", async () => {
|
|
586
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
587
|
-
controller.show(event)
|
|
588
|
-
await nextFrame()
|
|
589
|
-
|
|
590
|
-
expect(() => {
|
|
591
|
-
controller.focusNextItem()
|
|
592
|
-
controller.focusPreviousItem()
|
|
593
|
-
controller.focusFirstItem()
|
|
594
|
-
controller.focusLastItem()
|
|
595
|
-
}).not.toThrow()
|
|
596
|
-
})
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
describe("show without event", () => {
|
|
600
|
-
const noEventHTML = `
|
|
601
|
-
<div data-controller="shadcn--context-menu"
|
|
602
|
-
data-shadcn--context-menu-open-value="false">
|
|
603
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
604
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
605
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
606
|
-
</div>
|
|
607
|
-
</div>
|
|
608
|
-
`
|
|
609
|
-
|
|
610
|
-
beforeEach(async () => {
|
|
611
|
-
const setup = await setupController(ContextMenuController, noEventHTML, 'shadcn--context-menu')
|
|
612
|
-
application = setup.application
|
|
613
|
-
element = setup.element
|
|
614
|
-
controller = setup.controller
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
test("handles show called without event", async () => {
|
|
618
|
-
expect(() => {
|
|
619
|
-
controller.show()
|
|
620
|
-
}).not.toThrow()
|
|
621
|
-
|
|
622
|
-
expect(controller.openValue).toBe(true)
|
|
623
|
-
expect(controller.mouseX).toBe(0)
|
|
624
|
-
expect(controller.mouseY).toBe(0)
|
|
625
|
-
})
|
|
626
|
-
})
|
|
627
|
-
})
|