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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -2
- data/README.md +21 -8
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +7 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -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/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- 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_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- 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/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/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 +6 -80
- 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 +47 -45
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -612
- data/PROGRESS.md +0 -495
- data/Rakefile +0 -95
- 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 -971
- data/__tests__/controllers/context_menu_controller.test.js +0 -905
- 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 -737
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
- data/__tests__/controllers/popover_controller.test.js +0 -982
- 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 -678
- 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/bump +0 -321
- data/bin/console +0 -11
- data/bin/release +0 -205
- data/bin/setup +0 -8
- data/bin/test +0 -75
- 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 -7438
- data/package.json +0 -71
- data/rollup.config.js +0 -29
|
@@ -1,905 +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.clickOutside({ 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.clickOutside({ 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
|
-
|
|
628
|
-
describe("scroll lock behavior", () => {
|
|
629
|
-
const scrollLockHTML = `
|
|
630
|
-
<div data-controller="shadcn--context-menu"
|
|
631
|
-
data-shadcn--context-menu-open-value="false">
|
|
632
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
633
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
634
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
635
|
-
</div>
|
|
636
|
-
</div>
|
|
637
|
-
`
|
|
638
|
-
|
|
639
|
-
beforeEach(async () => {
|
|
640
|
-
const setup = await setupController(ContextMenuController, scrollLockHTML, 'shadcn--context-menu')
|
|
641
|
-
application = setup.application
|
|
642
|
-
element = setup.element
|
|
643
|
-
controller = setup.controller
|
|
644
|
-
// Reset body overflow before each test
|
|
645
|
-
document.body.style.overflow = ""
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
afterEach(() => {
|
|
649
|
-
// Clean up body overflow after each test
|
|
650
|
-
document.body.style.overflow = ""
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
test("locks body scroll when menu opens", async () => {
|
|
654
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
655
|
-
controller.show(event)
|
|
656
|
-
await nextFrame()
|
|
657
|
-
|
|
658
|
-
expect(document.body.style.overflow).toBe("hidden")
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
test("stores original overflow value", async () => {
|
|
662
|
-
document.body.style.overflow = "auto"
|
|
663
|
-
|
|
664
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
665
|
-
controller.show(event)
|
|
666
|
-
await nextFrame()
|
|
667
|
-
|
|
668
|
-
expect(controller.originalOverflow).toBe("auto")
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
test("restores original overflow after hide animation", async () => {
|
|
672
|
-
document.body.style.overflow = "auto"
|
|
673
|
-
|
|
674
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
675
|
-
controller.show(event)
|
|
676
|
-
await nextFrame()
|
|
677
|
-
|
|
678
|
-
controller.hide()
|
|
679
|
-
// Wait for animation timeout (100ms + buffer)
|
|
680
|
-
await wait(150)
|
|
681
|
-
|
|
682
|
-
expect(document.body.style.overflow).toBe("auto")
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
test("does not lock scroll if already locked", async () => {
|
|
686
|
-
document.body.style.overflow = "hidden"
|
|
687
|
-
|
|
688
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
689
|
-
controller.show(event)
|
|
690
|
-
await nextFrame()
|
|
691
|
-
|
|
692
|
-
// originalOverflow should be null because it was already hidden
|
|
693
|
-
expect(controller.originalOverflow).toBe(null)
|
|
694
|
-
})
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
describe("double right-click handling", () => {
|
|
698
|
-
const doubleClickHTML = `
|
|
699
|
-
<div data-controller="shadcn--context-menu"
|
|
700
|
-
data-shadcn--context-menu-open-value="false">
|
|
701
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
702
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
703
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
704
|
-
</div>
|
|
705
|
-
</div>
|
|
706
|
-
`
|
|
707
|
-
|
|
708
|
-
beforeEach(async () => {
|
|
709
|
-
const setup = await setupController(ContextMenuController, doubleClickHTML, 'shadcn--context-menu')
|
|
710
|
-
application = setup.application
|
|
711
|
-
element = setup.element
|
|
712
|
-
controller = setup.controller
|
|
713
|
-
document.body.style.overflow = ""
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
afterEach(() => {
|
|
717
|
-
document.body.style.overflow = ""
|
|
718
|
-
if (controller.hideTimeoutId) {
|
|
719
|
-
clearTimeout(controller.hideTimeoutId)
|
|
720
|
-
}
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
test("calling show() while menu is already open repositions instead of closing", async () => {
|
|
724
|
-
// First right-click to open menu
|
|
725
|
-
const event1 = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
726
|
-
controller.show(event1)
|
|
727
|
-
await nextFrame()
|
|
728
|
-
|
|
729
|
-
expect(controller.openValue).toBe(true)
|
|
730
|
-
expect(controller.mouseX).toBe(100)
|
|
731
|
-
expect(controller.mouseY).toBe(100)
|
|
732
|
-
|
|
733
|
-
// Second right-click at different position while menu is open
|
|
734
|
-
// This simulates what happens when the contextmenu event is triggered again
|
|
735
|
-
const event2 = { preventDefault: jest.fn(), clientX: 250, clientY: 300 }
|
|
736
|
-
controller.show(event2)
|
|
737
|
-
await nextFrame()
|
|
738
|
-
|
|
739
|
-
// Menu should still be open at the NEW position
|
|
740
|
-
expect(controller.openValue).toBe(true)
|
|
741
|
-
expect(controller.mouseX).toBe(250)
|
|
742
|
-
expect(controller.mouseY).toBe(300)
|
|
743
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
744
|
-
expect(controller.contentTarget.dataset.state).toBe("open")
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
test("handleContextMenu should NOT close menu when contextmenu event triggers on trigger element", async () => {
|
|
748
|
-
// Open the menu first
|
|
749
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
750
|
-
controller.show(event)
|
|
751
|
-
await nextFrame()
|
|
752
|
-
await nextFrame() // Extra frame to ensure event listeners are attached
|
|
753
|
-
|
|
754
|
-
expect(controller.openValue).toBe(true)
|
|
755
|
-
|
|
756
|
-
// Simulate a contextmenu event on the trigger element
|
|
757
|
-
// This is what happens when the user right-clicks again on the trigger
|
|
758
|
-
// In the refactored code, contextmenu events are handled by handleContextMenu, not handleClickOutside
|
|
759
|
-
controller.handleContextMenu({ type: "contextmenu", target: controller.triggerTarget })
|
|
760
|
-
await nextFrame()
|
|
761
|
-
|
|
762
|
-
// Menu should still be open because it was a contextmenu event on the trigger
|
|
763
|
-
expect(controller.openValue).toBe(true)
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
test("clickOutside SHOULD close menu when regular click triggers on trigger element", async () => {
|
|
767
|
-
// Open the menu first
|
|
768
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
769
|
-
controller.show(event)
|
|
770
|
-
await nextFrame()
|
|
771
|
-
await nextFrame() // Extra frame to ensure event listeners are attached
|
|
772
|
-
|
|
773
|
-
expect(controller.openValue).toBe(true)
|
|
774
|
-
|
|
775
|
-
// Simulate a regular click event on the trigger element
|
|
776
|
-
controller.clickOutside({ type: "click", target: controller.triggerTarget })
|
|
777
|
-
await nextFrame()
|
|
778
|
-
|
|
779
|
-
// Menu should close because it was a regular click (not a contextmenu event)
|
|
780
|
-
expect(controller.openValue).toBe(false)
|
|
781
|
-
})
|
|
782
|
-
|
|
783
|
-
test("cancels pending hide timeout when showing again", async () => {
|
|
784
|
-
const event1 = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
785
|
-
controller.show(event1)
|
|
786
|
-
await nextFrame()
|
|
787
|
-
|
|
788
|
-
// Start hiding (this sets hideTimeoutId)
|
|
789
|
-
controller.hide()
|
|
790
|
-
await nextFrame()
|
|
791
|
-
|
|
792
|
-
expect(controller.hideTimeoutId).not.toBe(null)
|
|
793
|
-
|
|
794
|
-
// Immediately show again (should cancel the pending hide)
|
|
795
|
-
const event2 = { preventDefault: jest.fn(), clientX: 200, clientY: 200 }
|
|
796
|
-
controller.show(event2)
|
|
797
|
-
await nextFrame()
|
|
798
|
-
|
|
799
|
-
// The menu should be open at the new position
|
|
800
|
-
expect(controller.openValue).toBe(true)
|
|
801
|
-
expect(controller.mouseX).toBe(200)
|
|
802
|
-
expect(controller.mouseY).toBe(200)
|
|
803
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
804
|
-
})
|
|
805
|
-
|
|
806
|
-
test("menu stays open after rapid open/close/open", async () => {
|
|
807
|
-
// First open
|
|
808
|
-
const event1 = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
809
|
-
controller.show(event1)
|
|
810
|
-
await nextFrame()
|
|
811
|
-
|
|
812
|
-
// Quickly close
|
|
813
|
-
controller.hide()
|
|
814
|
-
await nextFrame()
|
|
815
|
-
|
|
816
|
-
// Immediately open again
|
|
817
|
-
const event2 = { preventDefault: jest.fn(), clientX: 150, clientY: 150 }
|
|
818
|
-
controller.show(event2)
|
|
819
|
-
await nextFrame()
|
|
820
|
-
|
|
821
|
-
// Wait longer than the animation timeout
|
|
822
|
-
await wait(150)
|
|
823
|
-
|
|
824
|
-
// Menu should still be open
|
|
825
|
-
expect(controller.openValue).toBe(true)
|
|
826
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
test("hideTimeoutId is cleared after timeout completes", async () => {
|
|
830
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
831
|
-
controller.show(event)
|
|
832
|
-
await nextFrame()
|
|
833
|
-
|
|
834
|
-
controller.hide()
|
|
835
|
-
|
|
836
|
-
// Wait for timeout to complete
|
|
837
|
-
await wait(150)
|
|
838
|
-
|
|
839
|
-
expect(controller.hideTimeoutId).toBe(null)
|
|
840
|
-
})
|
|
841
|
-
})
|
|
842
|
-
|
|
843
|
-
describe("animation delay on close", () => {
|
|
844
|
-
const animationHTML = `
|
|
845
|
-
<div data-controller="shadcn--context-menu"
|
|
846
|
-
data-shadcn--context-menu-open-value="false">
|
|
847
|
-
<div data-shadcn--context-menu-target="trigger">Trigger</div>
|
|
848
|
-
<div data-shadcn--context-menu-target="content" hidden style="position: fixed;">
|
|
849
|
-
<button data-shadcn--context-menu-target="item">Item 1</button>
|
|
850
|
-
</div>
|
|
851
|
-
</div>
|
|
852
|
-
`
|
|
853
|
-
|
|
854
|
-
beforeEach(async () => {
|
|
855
|
-
const setup = await setupController(ContextMenuController, animationHTML, 'shadcn--context-menu')
|
|
856
|
-
application = setup.application
|
|
857
|
-
element = setup.element
|
|
858
|
-
controller = setup.controller
|
|
859
|
-
})
|
|
860
|
-
|
|
861
|
-
afterEach(() => {
|
|
862
|
-
if (controller.hideTimeoutId) {
|
|
863
|
-
clearTimeout(controller.hideTimeoutId)
|
|
864
|
-
}
|
|
865
|
-
})
|
|
866
|
-
|
|
867
|
-
test("sets data-state to closed immediately", async () => {
|
|
868
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
869
|
-
controller.show(event)
|
|
870
|
-
await nextFrame()
|
|
871
|
-
|
|
872
|
-
controller.hide()
|
|
873
|
-
await nextFrame()
|
|
874
|
-
|
|
875
|
-
// data-state should be set to closed immediately for CSS animation
|
|
876
|
-
expect(controller.contentTarget.dataset.state).toBe("closed")
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
test("content remains visible during animation", async () => {
|
|
880
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
881
|
-
controller.show(event)
|
|
882
|
-
await nextFrame()
|
|
883
|
-
|
|
884
|
-
controller.hide()
|
|
885
|
-
await nextFrame()
|
|
886
|
-
|
|
887
|
-
// Content should still be visible immediately after hide() is called
|
|
888
|
-
// (hidden is set after the 100ms timeout)
|
|
889
|
-
expect(controller.contentTarget.hidden).toBe(false)
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
test("content is hidden after animation completes", async () => {
|
|
893
|
-
const event = { preventDefault: jest.fn(), clientX: 100, clientY: 100 }
|
|
894
|
-
controller.show(event)
|
|
895
|
-
await nextFrame()
|
|
896
|
-
|
|
897
|
-
controller.hide()
|
|
898
|
-
|
|
899
|
-
// Wait for animation to complete (100ms + buffer)
|
|
900
|
-
await wait(150)
|
|
901
|
-
|
|
902
|
-
expect(controller.contentTarget.hidden).toBe(true)
|
|
903
|
-
})
|
|
904
|
-
})
|
|
905
|
-
})
|