shadcn-rails 0.1.0 → 0.2.0
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 +4 -1
- data/CLAUDE.md +151 -2
- data/PROGRESS.md +30 -20
- data/README.md +89 -1398
- data/Rakefile +66 -0
- data/__tests__/controllers/combobox_controller.test.js +56 -51
- data/__tests__/controllers/context_menu_controller.test.js +280 -2
- data/__tests__/controllers/menubar_controller.test.js +5 -4
- data/__tests__/controllers/navigation_menu_controller.test.js +5 -4
- data/__tests__/controllers/popover_controller.test.js +35 -60
- data/__tests__/controllers/select_controller.test.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +13 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +61 -105
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +49 -170
- 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 +7 -7
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +12 -10
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/index.js +2 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -0
- 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_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/dropdown_menu_checkbox_item_component.rb +76 -0
- 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/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- 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/select_component.rb +23 -6
- data/bin/bump +321 -0
- data/bin/release +205 -0
- data/bin/test +75 -0
- data/jest.config.js +1 -1
- data/lib/shadcn/rails/version.rb +1 -1
- data/package-lock.json +27 -4
- data/package.json +4 -1
- metadata +11 -1
|
@@ -253,22 +253,13 @@ describe("PopoverController", () => {
|
|
|
253
253
|
expect(controller.openValue).toBe(true)
|
|
254
254
|
})
|
|
255
255
|
|
|
256
|
-
test("show
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
document.addEventListener = function(event) {
|
|
261
|
-
if (event === 'click') {
|
|
262
|
-
clickListenerAdded = true
|
|
263
|
-
}
|
|
264
|
-
return originalAddEventListener.apply(this, arguments)
|
|
265
|
-
}
|
|
266
|
-
|
|
256
|
+
test("show sets up click outside handling via stimulus-use", () => {
|
|
257
|
+
// stimulus-use useClickOutside sets up event handling internally
|
|
258
|
+
// We verify by checking the clickOutside method exists and works
|
|
267
259
|
controller.show()
|
|
268
260
|
|
|
269
|
-
expect(
|
|
270
|
-
|
|
271
|
-
document.addEventListener = originalAddEventListener
|
|
261
|
+
expect(controller.openValue).toBe(true)
|
|
262
|
+
expect(typeof controller.clickOutside).toBe("function")
|
|
272
263
|
})
|
|
273
264
|
|
|
274
265
|
test("show dispatches opened event", async () => {
|
|
@@ -327,23 +318,19 @@ describe("PopoverController", () => {
|
|
|
327
318
|
expect(controller.openValue).toBe(false)
|
|
328
319
|
})
|
|
329
320
|
|
|
330
|
-
test("hide
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
document.removeEventListener = function(event) {
|
|
335
|
-
if (event === 'click') {
|
|
336
|
-
clickListenerRemoved = true
|
|
337
|
-
}
|
|
338
|
-
return originalRemoveEventListener.apply(this, arguments)
|
|
339
|
-
}
|
|
340
|
-
|
|
321
|
+
test("hide closes the popover and clickOutside no longer has effect", () => {
|
|
322
|
+
// stimulus-use manages event listeners internally
|
|
323
|
+
// We verify hide properly closes and further clickOutside calls don't reopen
|
|
341
324
|
controller.show()
|
|
342
325
|
controller.hide()
|
|
343
326
|
|
|
344
|
-
expect(
|
|
327
|
+
expect(controller.openValue).toBe(false)
|
|
328
|
+
|
|
329
|
+
// Calling clickOutside on closed popover should not have any effect
|
|
330
|
+
const outsideElement = document.createElement("div")
|
|
331
|
+
controller.clickOutside({ target: outsideElement })
|
|
345
332
|
|
|
346
|
-
|
|
333
|
+
expect(controller.openValue).toBe(false)
|
|
347
334
|
})
|
|
348
335
|
|
|
349
336
|
test("hide dispatches closed event", async () => {
|
|
@@ -406,11 +393,14 @@ describe("PopoverController", () => {
|
|
|
406
393
|
controller.show()
|
|
407
394
|
expect(controller.openValue).toBe(true)
|
|
408
395
|
|
|
409
|
-
//
|
|
396
|
+
// Call clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
|
|
410
397
|
await nextFrame()
|
|
411
|
-
|
|
398
|
+
const outsideElement = document.createElement("div")
|
|
399
|
+
document.body.appendChild(outsideElement)
|
|
400
|
+
controller.clickOutside({ target: outsideElement })
|
|
412
401
|
|
|
413
402
|
expect(controller.openValue).toBe(false)
|
|
403
|
+
document.body.removeChild(outsideElement)
|
|
414
404
|
})
|
|
415
405
|
|
|
416
406
|
test("clicking inside popover does not close it", async () => {
|
|
@@ -419,6 +409,9 @@ describe("PopoverController", () => {
|
|
|
419
409
|
|
|
420
410
|
const content = element.querySelector('[data-shadcn--popover-target="content"]')
|
|
421
411
|
|
|
412
|
+
// Clicking inside the controller element should not close via clickOutside
|
|
413
|
+
// The clickOutside method from stimulus-use only fires for clicks outside the element
|
|
414
|
+
// So we verify the popover stays open after an internal click action
|
|
422
415
|
await nextFrame()
|
|
423
416
|
click(content)
|
|
424
417
|
|
|
@@ -438,21 +431,9 @@ describe("PopoverController", () => {
|
|
|
438
431
|
expect(controller.openValue).toBe(false)
|
|
439
432
|
})
|
|
440
433
|
|
|
441
|
-
test("
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
document.addEventListener = function(event) {
|
|
446
|
-
if (event === 'click') {
|
|
447
|
-
clickListenerAdded = true
|
|
448
|
-
}
|
|
449
|
-
return originalAddEventListener.apply(this, arguments)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Don't call show
|
|
453
|
-
expect(clickListenerAdded).toBe(false)
|
|
454
|
-
|
|
455
|
-
document.addEventListener = originalAddEventListener
|
|
434
|
+
test("clickOutside method exists for stimulus-use integration", () => {
|
|
435
|
+
// Verify the clickOutside method is defined for stimulus-use integration
|
|
436
|
+
expect(typeof controller.clickOutside).toBe("function")
|
|
456
437
|
})
|
|
457
438
|
})
|
|
458
439
|
|
|
@@ -784,23 +765,14 @@ describe("PopoverController", () => {
|
|
|
784
765
|
expect(controller.openValue).toBe(false)
|
|
785
766
|
})
|
|
786
767
|
|
|
787
|
-
test("
|
|
788
|
-
|
|
789
|
-
const originalRemoveEventListener = document.removeEventListener
|
|
790
|
-
|
|
791
|
-
document.removeEventListener = function(event) {
|
|
792
|
-
if (event === 'click') {
|
|
793
|
-
clickListenerRemoved = true
|
|
794
|
-
}
|
|
795
|
-
return originalRemoveEventListener.apply(this, arguments)
|
|
796
|
-
}
|
|
797
|
-
|
|
768
|
+
test("properly cleans up on disconnect", () => {
|
|
769
|
+
// stimulus-use handles event listener cleanup on disconnect
|
|
798
770
|
controller.show()
|
|
799
|
-
controller.
|
|
771
|
+
expect(controller.openValue).toBe(true)
|
|
800
772
|
|
|
801
|
-
|
|
773
|
+
controller.disconnect()
|
|
802
774
|
|
|
803
|
-
|
|
775
|
+
expect(controller.openValue).toBe(false)
|
|
804
776
|
})
|
|
805
777
|
|
|
806
778
|
test("restores body pointer events on disconnect when modal", async () => {
|
|
@@ -906,13 +878,16 @@ describe("PopoverController", () => {
|
|
|
906
878
|
expect(controller.openValue).toBe(true)
|
|
907
879
|
expect(content.hidden).toBe(false)
|
|
908
880
|
|
|
909
|
-
// Click outside
|
|
881
|
+
// Click outside - use clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
|
|
910
882
|
await nextFrame()
|
|
911
|
-
|
|
883
|
+
const outsideElement = document.createElement("div")
|
|
884
|
+
document.body.appendChild(outsideElement)
|
|
885
|
+
controller.clickOutside({ target: outsideElement })
|
|
912
886
|
expect(controller.openValue).toBe(false)
|
|
913
887
|
|
|
914
888
|
await wait(200)
|
|
915
889
|
expect(content.hidden).toBe(true)
|
|
890
|
+
document.body.removeChild(outsideElement)
|
|
916
891
|
|
|
917
892
|
// Reopen
|
|
918
893
|
click(trigger)
|
|
@@ -499,7 +499,8 @@ describe("SelectController", () => {
|
|
|
499
499
|
await nextFrame()
|
|
500
500
|
|
|
501
501
|
const outsideElement = document.getElementById("outside")
|
|
502
|
-
|
|
502
|
+
// Call clickOutside directly since stimulus-use doesn't trigger via DOM events in jsdom
|
|
503
|
+
controller.clickOutside({ target: outsideElement })
|
|
503
504
|
await nextFrame()
|
|
504
505
|
|
|
505
506
|
expect(controller.isOpen).toBe(false)
|
|
@@ -509,6 +510,9 @@ describe("SelectController", () => {
|
|
|
509
510
|
controller.open()
|
|
510
511
|
await nextFrame()
|
|
511
512
|
|
|
513
|
+
// Clicking inside the controller element should not close via clickOutside
|
|
514
|
+
// The clickOutside method from stimulus-use only fires for clicks outside the element
|
|
515
|
+
// So we verify the select stays open after an internal click action
|
|
512
516
|
click(controller.contentTarget)
|
|
513
517
|
await nextFrame()
|
|
514
518
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base Menu Controller
|
|
6
|
+
*
|
|
7
|
+
* A base controller for menu-like components (dropdown, context menu, select, etc.)
|
|
8
|
+
* that provides common functionality for:
|
|
9
|
+
* - Opening/closing menus
|
|
10
|
+
* - Keyboard navigation (arrow keys, home, end, enter, space, escape)
|
|
11
|
+
* - Focus management
|
|
12
|
+
* - Click outside to close (using stimulus-use)
|
|
13
|
+
* - Item selection
|
|
14
|
+
*
|
|
15
|
+
* Subclasses can override specific methods to customize behavior:
|
|
16
|
+
* - positionContent() - Custom positioning logic
|
|
17
|
+
* - showMenu() - Additional show behavior
|
|
18
|
+
* - hideMenu() - Additional hide behavior
|
|
19
|
+
* - shouldCloseOnClickOutside(event) - Custom click outside logic
|
|
20
|
+
*/
|
|
21
|
+
export default class extends Controller {
|
|
22
|
+
static targets = ["trigger", "content", "item"]
|
|
23
|
+
static values = {
|
|
24
|
+
open: { type: Boolean, default: false },
|
|
25
|
+
hideDelay: { type: Number, default: 150 }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Lifecycle hooks
|
|
29
|
+
connect() {
|
|
30
|
+
this.focusedIndex = -1
|
|
31
|
+
this.hideTimeoutId = null
|
|
32
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
33
|
+
|
|
34
|
+
// Use stimulus-use for click outside detection
|
|
35
|
+
useClickOutside(this)
|
|
36
|
+
|
|
37
|
+
if (this.openValue) {
|
|
38
|
+
this.show()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnect() {
|
|
43
|
+
this.hide()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Public API
|
|
47
|
+
toggle(event) {
|
|
48
|
+
event?.preventDefault()
|
|
49
|
+
if (this.openValue) {
|
|
50
|
+
this.hide()
|
|
51
|
+
} else {
|
|
52
|
+
this.show()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
show(event) {
|
|
57
|
+
if (this.openValue) return
|
|
58
|
+
|
|
59
|
+
// Cancel any pending hide timeout
|
|
60
|
+
this.cancelHideTimeout()
|
|
61
|
+
|
|
62
|
+
this.openValue = true
|
|
63
|
+
|
|
64
|
+
if (this.hasContentTarget) {
|
|
65
|
+
this.contentTarget.hidden = false
|
|
66
|
+
this.contentTarget.dataset.state = "open"
|
|
67
|
+
this.positionContent(event)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.hasTriggerTarget) {
|
|
71
|
+
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add event listeners
|
|
75
|
+
this.addEventListeners()
|
|
76
|
+
|
|
77
|
+
// Allow subclasses to add custom show behavior
|
|
78
|
+
this.showMenu(event)
|
|
79
|
+
|
|
80
|
+
// Focus first item
|
|
81
|
+
this.focusedIndex = -1
|
|
82
|
+
this.focusNextItem()
|
|
83
|
+
|
|
84
|
+
this.dispatch("opened")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
hide() {
|
|
88
|
+
if (!this.openValue) return
|
|
89
|
+
|
|
90
|
+
this.openValue = false
|
|
91
|
+
|
|
92
|
+
// Remove event listeners immediately to prevent double-triggering
|
|
93
|
+
this.removeEventListeners()
|
|
94
|
+
|
|
95
|
+
if (this.hasContentTarget) {
|
|
96
|
+
this.contentTarget.dataset.state = "closed"
|
|
97
|
+
// Hide after animation completes
|
|
98
|
+
this.hideTimeoutId = setTimeout(() => {
|
|
99
|
+
if (!this.openValue && this.hasContentTarget) {
|
|
100
|
+
this.contentTarget.hidden = true
|
|
101
|
+
}
|
|
102
|
+
this.hideTimeoutId = null
|
|
103
|
+
// Allow subclasses to add custom hide behavior
|
|
104
|
+
this.hideMenu()
|
|
105
|
+
}, this.hideDelayValue)
|
|
106
|
+
} else {
|
|
107
|
+
this.hideMenu()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.hasTriggerTarget) {
|
|
111
|
+
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Reset focus index
|
|
115
|
+
this.focusedIndex = -1
|
|
116
|
+
|
|
117
|
+
this.dispatch("closed")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
close() {
|
|
121
|
+
this.hide()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
selectItem(event) {
|
|
125
|
+
const item = event.currentTarget
|
|
126
|
+
if (item.dataset.disabled !== undefined) return
|
|
127
|
+
|
|
128
|
+
this.dispatch("select", { detail: { item } })
|
|
129
|
+
this.hide()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Event handling - clickOutside is called by stimulus-use
|
|
133
|
+
clickOutside(event) {
|
|
134
|
+
// Only close if menu is open and shouldCloseOnClickOutside returns true
|
|
135
|
+
if (this.openValue && this.shouldCloseOnClickOutside(event)) {
|
|
136
|
+
this.hide()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleKeydown(event) {
|
|
141
|
+
switch (event.key) {
|
|
142
|
+
case "Escape":
|
|
143
|
+
this.hide()
|
|
144
|
+
this.triggerTarget?.focus()
|
|
145
|
+
break
|
|
146
|
+
case "ArrowDown":
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
this.focusNextItem()
|
|
149
|
+
break
|
|
150
|
+
case "ArrowUp":
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
this.focusPreviousItem()
|
|
153
|
+
break
|
|
154
|
+
case "Home":
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
this.focusFirstItem()
|
|
157
|
+
break
|
|
158
|
+
case "End":
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
this.focusLastItem()
|
|
161
|
+
break
|
|
162
|
+
case "Enter":
|
|
163
|
+
case " ":
|
|
164
|
+
event.preventDefault()
|
|
165
|
+
this.selectFocusedItem()
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Focus management
|
|
171
|
+
focusNextItem() {
|
|
172
|
+
const items = this.enabledItems
|
|
173
|
+
if (items.length === 0) return
|
|
174
|
+
|
|
175
|
+
this.focusedIndex = (this.focusedIndex + 1) % items.length
|
|
176
|
+
items[this.focusedIndex].focus()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
focusPreviousItem() {
|
|
180
|
+
const items = this.enabledItems
|
|
181
|
+
if (items.length === 0) return
|
|
182
|
+
|
|
183
|
+
this.focusedIndex = this.focusedIndex <= 0 ? items.length - 1 : this.focusedIndex - 1
|
|
184
|
+
items[this.focusedIndex].focus()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
focusFirstItem() {
|
|
188
|
+
const items = this.enabledItems
|
|
189
|
+
if (items.length === 0) return
|
|
190
|
+
|
|
191
|
+
this.focusedIndex = 0
|
|
192
|
+
items[0].focus()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
focusLastItem() {
|
|
196
|
+
const items = this.enabledItems
|
|
197
|
+
if (items.length === 0) return
|
|
198
|
+
|
|
199
|
+
this.focusedIndex = items.length - 1
|
|
200
|
+
items[this.focusedIndex].focus()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
selectFocusedItem() {
|
|
204
|
+
const items = this.enabledItems
|
|
205
|
+
if (this.focusedIndex >= 0 && this.focusedIndex < items.length) {
|
|
206
|
+
items[this.focusedIndex].click()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get enabledItems() {
|
|
211
|
+
return this.itemTargets.filter(item => item.dataset.disabled === undefined)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Protected methods that subclasses can override
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Position the content element. Override in subclasses for custom positioning.
|
|
218
|
+
* @param {Event} event - The event that triggered the show (optional)
|
|
219
|
+
*/
|
|
220
|
+
positionContent(event) {
|
|
221
|
+
// Default: no positioning (subclasses should override)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Called after showing the menu. Override in subclasses for additional behavior.
|
|
226
|
+
* @param {Event} event - The event that triggered the show (optional)
|
|
227
|
+
*/
|
|
228
|
+
showMenu(event) {
|
|
229
|
+
// Default: no-op (subclasses can override)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Called after hiding the menu. Override in subclasses for additional behavior.
|
|
234
|
+
*/
|
|
235
|
+
hideMenu() {
|
|
236
|
+
// Default: no-op (subclasses can override)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Determine if the menu should close on click outside.
|
|
241
|
+
* Override in subclasses for custom behavior (e.g., context menu).
|
|
242
|
+
* @param {Event} event - The click event
|
|
243
|
+
* @returns {boolean} - True if the menu should close
|
|
244
|
+
*/
|
|
245
|
+
shouldCloseOnClickOutside(event) {
|
|
246
|
+
// Default: close if clicking outside the entire element
|
|
247
|
+
return !this.element.contains(event.target)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Private helpers
|
|
251
|
+
// Note: click outside is handled by stimulus-use's useClickOutside
|
|
252
|
+
addEventListeners() {
|
|
253
|
+
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
removeEventListeners() {
|
|
257
|
+
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
cancelHideTimeout() {
|
|
261
|
+
if (this.hideTimeoutId) {
|
|
262
|
+
clearTimeout(this.hideTimeoutId)
|
|
263
|
+
this.hideTimeoutId = null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside, useDebounce } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Combobox controller for searchable select dropdown
|
|
5
6
|
* Handles open/close, filtering, keyboard navigation, and item selection
|
|
7
|
+
* Uses stimulus-use for click outside detection and debounced filtering
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["trigger", "content", "input", "list", "item", "empty", "displayValue", "hiddenInput"]
|
|
9
11
|
static values = {
|
|
10
12
|
open: { type: Boolean, default: false },
|
|
11
13
|
value: { type: String, default: "" },
|
|
12
|
-
selectedIndex: { type: Number, default: -1 }
|
|
14
|
+
selectedIndex: { type: Number, default: -1 },
|
|
15
|
+
debounceWait: { type: Number, default: 150 }
|
|
13
16
|
}
|
|
17
|
+
static debounces = ["filter"]
|
|
14
18
|
|
|
15
19
|
connect() {
|
|
16
20
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
21
|
+
|
|
22
|
+
// Use stimulus-use for click outside detection
|
|
23
|
+
useClickOutside(this)
|
|
24
|
+
// Use stimulus-use for debounced filtering
|
|
25
|
+
useDebounce(this, { wait: this.debounceWaitValue })
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
disconnect() {
|
|
@@ -221,13 +230,9 @@ export default class extends Controller {
|
|
|
221
230
|
return this.itemTargets.filter((item) => item.style.display !== "none")
|
|
222
231
|
}
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
handleClickOutside(event) {
|
|
228
|
-
if (!this.openValue) return
|
|
229
|
-
|
|
230
|
-
if (!this.element.contains(event.target)) {
|
|
233
|
+
// Called by stimulus-use when clicking outside the element
|
|
234
|
+
clickOutside(event) {
|
|
235
|
+
if (this.openValue) {
|
|
231
236
|
this.close()
|
|
232
237
|
}
|
|
233
238
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useDebounce } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Command controller for command palette functionality
|
|
@@ -7,10 +8,13 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
7
8
|
export default class extends Controller {
|
|
8
9
|
static targets = ["input", "list", "empty", "group", "item"]
|
|
9
10
|
static values = {
|
|
10
|
-
selectedIndex: { type: Number, default: -1 }
|
|
11
|
+
selectedIndex: { type: Number, default: -1 },
|
|
12
|
+
debounceWait: { type: Number, default: 150 }
|
|
11
13
|
}
|
|
14
|
+
static debounces = ["filter"]
|
|
12
15
|
|
|
13
16
|
connect() {
|
|
17
|
+
useDebounce(this, { wait: this.debounceWaitValue })
|
|
14
18
|
this.updateSelection()
|
|
15
19
|
}
|
|
16
20
|
|