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
|
@@ -1,43 +1,65 @@
|
|
|
1
|
-
import
|
|
1
|
+
import BaseMenuController from "./base_menu_controller"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Context Menu controller for right-click menus
|
|
5
|
-
*
|
|
5
|
+
* Extends BaseMenuController with context menu-specific positioning and event handling
|
|
6
6
|
*/
|
|
7
|
-
export default class extends
|
|
8
|
-
static targets = [
|
|
7
|
+
export default class extends BaseMenuController {
|
|
8
|
+
static targets = [...BaseMenuController.targets]
|
|
9
9
|
static values = {
|
|
10
|
-
|
|
10
|
+
...BaseMenuController.values,
|
|
11
|
+
hideDelay: { type: Number, default: 100 }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
connect() {
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
15
|
+
super.connect()
|
|
16
|
+
this.boundHandleContextMenu = this.handleContextMenu.bind(this)
|
|
17
|
+
this.originalOverflow = null
|
|
18
|
+
this.mouseX = 0
|
|
19
|
+
this.mouseY = 0
|
|
20
|
+
this._ignoreClickOutside = false
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
// Override clickOutside to handle the deferred close behavior
|
|
24
|
+
// Context menus need to ignore clicks in the same frame as the right-click
|
|
25
|
+
clickOutside(event) {
|
|
26
|
+
if (this._ignoreClickOutside) return
|
|
27
|
+
super.clickOutside(event)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
show(event) {
|
|
24
31
|
event?.preventDefault()
|
|
25
32
|
|
|
33
|
+
// Cancel any pending hide timeout from a previous close
|
|
34
|
+
this.cancelHideTimeout()
|
|
35
|
+
|
|
26
36
|
// Store mouse position for positioning
|
|
27
37
|
this.mouseX = event?.clientX || 0
|
|
28
38
|
this.mouseY = event?.clientY || 0
|
|
29
39
|
|
|
30
40
|
this.openValue = true
|
|
31
41
|
|
|
42
|
+
// Lock scroll (only if not already locked)
|
|
43
|
+
if (document.body.style.overflow !== "hidden") {
|
|
44
|
+
this.originalOverflow = document.body.style.overflow
|
|
45
|
+
document.body.style.overflow = "hidden"
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
if (this.hasContentTarget) {
|
|
33
49
|
this.contentTarget.hidden = false
|
|
34
50
|
this.contentTarget.dataset.state = "open"
|
|
35
51
|
this.positionContent()
|
|
36
52
|
}
|
|
37
53
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
// Defer click outside detection to prevent immediate close from right-click
|
|
55
|
+
// The contextmenu event can sometimes trigger a click in the same event cycle
|
|
56
|
+
this._ignoreClickOutside = true
|
|
57
|
+
requestAnimationFrame(() => {
|
|
58
|
+
this._ignoreClickOutside = false
|
|
59
|
+
if (this.openValue) {
|
|
60
|
+
document.addEventListener("contextmenu", this.boundHandleContextMenu)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
41
63
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
42
64
|
|
|
43
65
|
// Focus first item
|
|
@@ -52,117 +74,51 @@ export default class extends Controller {
|
|
|
52
74
|
|
|
53
75
|
this.openValue = false
|
|
54
76
|
|
|
77
|
+
// Remove event listeners immediately to prevent double-triggering
|
|
78
|
+
document.removeEventListener("contextmenu", this.boundHandleContextMenu)
|
|
79
|
+
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
80
|
+
|
|
55
81
|
if (this.hasContentTarget) {
|
|
56
82
|
this.contentTarget.dataset.state = "closed"
|
|
57
|
-
//
|
|
58
|
-
|
|
83
|
+
// Wait for animation to complete before hiding and restoring scroll
|
|
84
|
+
// Animation duration is 100ms, add buffer for smooth transition
|
|
85
|
+
this.hideTimeoutId = setTimeout(() => {
|
|
59
86
|
if (!this.openValue) {
|
|
60
87
|
this.contentTarget.hidden = true
|
|
88
|
+
// Restore scroll only after menu is fully hidden
|
|
89
|
+
document.body.style.overflow = this.originalOverflow || ""
|
|
61
90
|
}
|
|
62
|
-
|
|
91
|
+
this.hideTimeoutId = null
|
|
92
|
+
}, this.hideDelayValue)
|
|
93
|
+
} else {
|
|
94
|
+
// No content target, restore scroll immediately
|
|
95
|
+
document.body.style.overflow = this.originalOverflow || ""
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
// Remove event listeners
|
|
66
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
67
|
-
document.removeEventListener("contextmenu", this.boundHandleClickOutside)
|
|
68
|
-
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
69
|
-
|
|
70
98
|
// Reset focus index
|
|
71
99
|
this.focusedIndex = -1
|
|
72
100
|
|
|
73
101
|
this.dispatch("closed")
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
selectItem(event) {
|
|
81
|
-
const item = event.currentTarget
|
|
82
|
-
if (item.dataset.disabled !== undefined) return
|
|
83
|
-
|
|
84
|
-
this.dispatch("select", { detail: { item } })
|
|
85
|
-
this.hide()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
handleClickOutside(event) {
|
|
89
|
-
// Don't close if clicking inside the content
|
|
90
|
-
if (this.hasContentTarget && this.contentTarget.contains(event.target)) {
|
|
104
|
+
handleContextMenu(event) {
|
|
105
|
+
// Don't close if right-clicking on the trigger element
|
|
106
|
+
// This allows show() to be called again to reposition the menu
|
|
107
|
+
if (this.hasTriggerTarget && this.triggerTarget.contains(event.target)) {
|
|
91
108
|
return
|
|
92
109
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
handleKeydown(event) {
|
|
97
|
-
switch (event.key) {
|
|
98
|
-
case "Escape":
|
|
99
|
-
this.hide()
|
|
100
|
-
break
|
|
101
|
-
case "ArrowDown":
|
|
102
|
-
event.preventDefault()
|
|
103
|
-
this.focusNextItem()
|
|
104
|
-
break
|
|
105
|
-
case "ArrowUp":
|
|
106
|
-
event.preventDefault()
|
|
107
|
-
this.focusPreviousItem()
|
|
108
|
-
break
|
|
109
|
-
case "Home":
|
|
110
|
-
event.preventDefault()
|
|
111
|
-
this.focusFirstItem()
|
|
112
|
-
break
|
|
113
|
-
case "End":
|
|
114
|
-
event.preventDefault()
|
|
115
|
-
this.focusLastItem()
|
|
116
|
-
break
|
|
117
|
-
case "Enter":
|
|
118
|
-
case " ":
|
|
119
|
-
event.preventDefault()
|
|
120
|
-
this.selectFocusedItem()
|
|
121
|
-
break
|
|
110
|
+
// Close if right-clicking outside the content
|
|
111
|
+
if (this.hasContentTarget && !this.contentTarget.contains(event.target)) {
|
|
112
|
+
this.hide()
|
|
122
113
|
}
|
|
123
114
|
}
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
this.focusedIndex = (this.focusedIndex + 1) % items.length
|
|
130
|
-
items[this.focusedIndex].focus()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
focusPreviousItem() {
|
|
134
|
-
const items = this.enabledItems
|
|
135
|
-
if (items.length === 0) return
|
|
136
|
-
|
|
137
|
-
this.focusedIndex = this.focusedIndex <= 0 ? items.length - 1 : this.focusedIndex - 1
|
|
138
|
-
items[this.focusedIndex].focus()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
focusFirstItem() {
|
|
142
|
-
const items = this.enabledItems
|
|
143
|
-
if (items.length === 0) return
|
|
144
|
-
|
|
145
|
-
this.focusedIndex = 0
|
|
146
|
-
items[0].focus()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
focusLastItem() {
|
|
150
|
-
const items = this.enabledItems
|
|
151
|
-
if (items.length === 0) return
|
|
152
|
-
|
|
153
|
-
this.focusedIndex = items.length - 1
|
|
154
|
-
items[this.focusedIndex].focus()
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
selectFocusedItem() {
|
|
158
|
-
const items = this.enabledItems
|
|
159
|
-
if (this.focusedIndex >= 0 && this.focusedIndex < items.length) {
|
|
160
|
-
items[this.focusedIndex].click()
|
|
116
|
+
shouldCloseOnClickOutside(event) {
|
|
117
|
+
// Don't close if clicking inside the content
|
|
118
|
+
if (this.hasContentTarget && this.contentTarget.contains(event.target)) {
|
|
119
|
+
return false
|
|
161
120
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
get enabledItems() {
|
|
165
|
-
return this.itemTargets.filter(item => item.dataset.disabled === undefined)
|
|
121
|
+
return true
|
|
166
122
|
}
|
|
167
123
|
|
|
168
124
|
positionContent() {
|
|
@@ -1,185 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import BaseMenuController from "./base_menu_controller"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Dropdown controller for dropdown menus
|
|
5
|
-
*
|
|
5
|
+
* Extends BaseMenuController with dropdown-specific positioning
|
|
6
6
|
*/
|
|
7
|
-
export default class extends
|
|
8
|
-
static targets = [
|
|
7
|
+
export default class extends BaseMenuController {
|
|
8
|
+
static targets = [...BaseMenuController.targets]
|
|
9
9
|
static values = {
|
|
10
|
-
|
|
10
|
+
...BaseMenuController.values,
|
|
11
11
|
align: { type: String, default: "end" },
|
|
12
12
|
side: { type: String, default: "bottom" }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
18
|
-
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
19
|
-
|
|
20
|
-
if (this.openValue) {
|
|
21
|
-
this.show()
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
disconnect() {
|
|
26
|
-
this.hide()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
toggle(event) {
|
|
30
|
-
event?.preventDefault()
|
|
31
|
-
if (this.openValue) {
|
|
32
|
-
this.hide()
|
|
33
|
-
} else {
|
|
34
|
-
this.show()
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
show() {
|
|
39
|
-
if (this.openValue) return
|
|
40
|
-
|
|
41
|
-
this.openValue = true
|
|
42
|
-
|
|
15
|
+
show(event) {
|
|
16
|
+
// Store side value for positioning before showing
|
|
43
17
|
if (this.hasContentTarget) {
|
|
44
|
-
this.contentTarget.hidden = false
|
|
45
|
-
this.contentTarget.dataset.state = "open"
|
|
46
18
|
this.contentTarget.dataset.side = this.sideValue
|
|
47
|
-
this.positionContent()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (this.hasTriggerTarget) {
|
|
51
|
-
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Add event listeners
|
|
55
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
56
|
-
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
57
|
-
|
|
58
|
-
// Focus first item
|
|
59
|
-
this.focusedIndex = -1
|
|
60
|
-
this.focusNextItem()
|
|
61
|
-
|
|
62
|
-
this.dispatch("opened")
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
hide() {
|
|
66
|
-
if (!this.openValue) return
|
|
67
|
-
|
|
68
|
-
this.openValue = false
|
|
69
|
-
|
|
70
|
-
if (this.hasContentTarget) {
|
|
71
|
-
this.contentTarget.dataset.state = "closed"
|
|
72
|
-
// Hide after animation
|
|
73
|
-
setTimeout(() => {
|
|
74
|
-
if (!this.openValue) {
|
|
75
|
-
this.contentTarget.hidden = true
|
|
76
|
-
}
|
|
77
|
-
}, 150)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (this.hasTriggerTarget) {
|
|
81
|
-
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Remove event listeners
|
|
85
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
86
|
-
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
87
|
-
|
|
88
|
-
// Reset focus index
|
|
89
|
-
this.focusedIndex = -1
|
|
90
|
-
|
|
91
|
-
this.dispatch("closed")
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
close() {
|
|
95
|
-
this.hide()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
selectItem(event) {
|
|
99
|
-
const item = event.currentTarget
|
|
100
|
-
if (item.dataset.disabled !== undefined) return
|
|
101
|
-
|
|
102
|
-
this.dispatch("select", { detail: { item } })
|
|
103
|
-
this.hide()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
handleClickOutside(event) {
|
|
107
|
-
if (!this.element.contains(event.target)) {
|
|
108
|
-
this.hide()
|
|
109
19
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
handleKeydown(event) {
|
|
113
|
-
switch (event.key) {
|
|
114
|
-
case "Escape":
|
|
115
|
-
this.hide()
|
|
116
|
-
this.triggerTarget?.focus()
|
|
117
|
-
break
|
|
118
|
-
case "ArrowDown":
|
|
119
|
-
event.preventDefault()
|
|
120
|
-
this.focusNextItem()
|
|
121
|
-
break
|
|
122
|
-
case "ArrowUp":
|
|
123
|
-
event.preventDefault()
|
|
124
|
-
this.focusPreviousItem()
|
|
125
|
-
break
|
|
126
|
-
case "Home":
|
|
127
|
-
event.preventDefault()
|
|
128
|
-
this.focusFirstItem()
|
|
129
|
-
break
|
|
130
|
-
case "End":
|
|
131
|
-
event.preventDefault()
|
|
132
|
-
this.focusLastItem()
|
|
133
|
-
break
|
|
134
|
-
case "Enter":
|
|
135
|
-
case " ":
|
|
136
|
-
event.preventDefault()
|
|
137
|
-
this.selectFocusedItem()
|
|
138
|
-
break
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
focusNextItem() {
|
|
143
|
-
const items = this.enabledItems
|
|
144
|
-
if (items.length === 0) return
|
|
145
|
-
|
|
146
|
-
this.focusedIndex = (this.focusedIndex + 1) % items.length
|
|
147
|
-
items[this.focusedIndex].focus()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
focusPreviousItem() {
|
|
151
|
-
const items = this.enabledItems
|
|
152
|
-
if (items.length === 0) return
|
|
153
|
-
|
|
154
|
-
this.focusedIndex = this.focusedIndex <= 0 ? items.length - 1 : this.focusedIndex - 1
|
|
155
|
-
items[this.focusedIndex].focus()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
focusFirstItem() {
|
|
159
|
-
const items = this.enabledItems
|
|
160
|
-
if (items.length === 0) return
|
|
161
|
-
|
|
162
|
-
this.focusedIndex = 0
|
|
163
|
-
items[0].focus()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
focusLastItem() {
|
|
167
|
-
const items = this.enabledItems
|
|
168
|
-
if (items.length === 0) return
|
|
169
|
-
|
|
170
|
-
this.focusedIndex = items.length - 1
|
|
171
|
-
items[this.focusedIndex].focus()
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
selectFocusedItem() {
|
|
175
|
-
const items = this.enabledItems
|
|
176
|
-
if (this.focusedIndex >= 0 && this.focusedIndex < items.length) {
|
|
177
|
-
items[this.focusedIndex].click()
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
get enabledItems() {
|
|
182
|
-
return this.itemTargets.filter(item => item.dataset.disabled === undefined)
|
|
20
|
+
super.show(event)
|
|
183
21
|
}
|
|
184
22
|
|
|
185
23
|
positionContent() {
|
|
@@ -222,4 +60,45 @@ export default class extends Controller {
|
|
|
222
60
|
break
|
|
223
61
|
}
|
|
224
62
|
}
|
|
63
|
+
|
|
64
|
+
toggleCheckbox(event) {
|
|
65
|
+
const item = event.currentTarget
|
|
66
|
+
if (item.dataset.disabled !== undefined) return
|
|
67
|
+
|
|
68
|
+
const isChecked = item.dataset.state === "checked"
|
|
69
|
+
item.dataset.state = isChecked ? "unchecked" : "checked"
|
|
70
|
+
item.setAttribute("aria-checked", (!isChecked).toString())
|
|
71
|
+
|
|
72
|
+
// Toggle the check icon visibility
|
|
73
|
+
const indicator = item.querySelector("span svg")
|
|
74
|
+
if (indicator) {
|
|
75
|
+
indicator.style.display = isChecked ? "none" : "block"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.dispatch("check", { detail: { item, checked: !isChecked } })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
selectRadio(event) {
|
|
82
|
+
const item = event.currentTarget
|
|
83
|
+
if (item.dataset.disabled !== undefined) return
|
|
84
|
+
|
|
85
|
+
const group = item.closest("[role='group']")
|
|
86
|
+
if (group) {
|
|
87
|
+
// Uncheck all radio items in the group
|
|
88
|
+
group.querySelectorAll("[role='menuitemradio']").forEach(radio => {
|
|
89
|
+
radio.dataset.state = "unchecked"
|
|
90
|
+
radio.setAttribute("aria-checked", "false")
|
|
91
|
+
const indicator = radio.querySelector("span svg")
|
|
92
|
+
if (indicator) indicator.style.display = "none"
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check this item
|
|
97
|
+
item.dataset.state = "checked"
|
|
98
|
+
item.setAttribute("aria-checked", "true")
|
|
99
|
+
const indicator = item.querySelector("span svg")
|
|
100
|
+
if (indicator) indicator.style.display = "block"
|
|
101
|
+
|
|
102
|
+
this.dispatch("radioChange", { detail: { item, value: item.dataset.value } })
|
|
103
|
+
}
|
|
225
104
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Menubar controller
|
|
5
6
|
* Handles menu opening/closing, keyboard navigation, hover behavior
|
|
7
|
+
* Uses stimulus-use for click outside detection
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["menu", "trigger", "content", "item", "sub", "subTrigger", "subContent"]
|
|
@@ -13,14 +15,15 @@ export default class extends Controller {
|
|
|
13
15
|
connect() {
|
|
14
16
|
this.focusedIndex = -1
|
|
15
17
|
this.isMenuOpen = false
|
|
16
|
-
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
17
18
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
18
19
|
this.closeSubTimer = null
|
|
20
|
+
|
|
21
|
+
// Use stimulus-use for click outside detection
|
|
22
|
+
useClickOutside(this)
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
disconnect() {
|
|
22
26
|
this.closeAll()
|
|
23
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
24
27
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -72,8 +75,7 @@ export default class extends Controller {
|
|
|
72
75
|
this.isMenuOpen = true
|
|
73
76
|
this.focusedIndex = -1
|
|
74
77
|
|
|
75
|
-
// Add event
|
|
76
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
78
|
+
// Add keydown event listener (click outside is handled by stimulus-use)
|
|
77
79
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
78
80
|
|
|
79
81
|
// Focus first item
|
|
@@ -100,7 +102,7 @@ export default class extends Controller {
|
|
|
100
102
|
this.isMenuOpen = false
|
|
101
103
|
this.focusedIndex = -1
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
// Remove keydown listener (click outside is handled by stimulus-use)
|
|
104
106
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -198,8 +200,9 @@ export default class extends Controller {
|
|
|
198
200
|
})
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// Called by stimulus-use when clicking outside the element
|
|
204
|
+
clickOutside(event) {
|
|
205
|
+
if (this.isMenuOpen) {
|
|
203
206
|
this.closeAll()
|
|
204
207
|
}
|
|
205
208
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Navigation Menu Controller
|
|
5
6
|
* Handles navigation menu interactions with dropdown content areas
|
|
7
|
+
* Uses stimulus-use for click outside detection
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["list", "item", "trigger", "content", "viewport"]
|
|
@@ -19,8 +21,10 @@ export default class extends Controller {
|
|
|
19
21
|
this.closeTimer = null
|
|
20
22
|
this.wasClickOpened = false
|
|
21
23
|
|
|
22
|
-
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
23
24
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
25
|
+
|
|
26
|
+
// Use stimulus-use for click outside detection
|
|
27
|
+
useClickOutside(this)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
disconnect() {
|
|
@@ -125,8 +129,7 @@ export default class extends Controller {
|
|
|
125
129
|
|
|
126
130
|
this.isOpen = true
|
|
127
131
|
|
|
128
|
-
// Add event
|
|
129
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
132
|
+
// Add keydown event listener (click outside is handled by stimulus-use)
|
|
130
133
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -183,12 +186,13 @@ export default class extends Controller {
|
|
|
183
186
|
this.isOpen = false
|
|
184
187
|
this.wasClickOpened = false
|
|
185
188
|
|
|
186
|
-
|
|
189
|
+
// Remove keydown listener (click outside is handled by stimulus-use)
|
|
187
190
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
// Called by stimulus-use when clicking outside the element
|
|
194
|
+
clickOutside(event) {
|
|
195
|
+
if (this.isOpen) {
|
|
192
196
|
this.closeAll()
|
|
193
197
|
}
|
|
194
198
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Popover controller for rich content overlays
|
|
6
|
+
* Uses stimulus-use for click outside detection
|
|
5
7
|
*/
|
|
6
8
|
export default class extends Controller {
|
|
7
9
|
static targets = ["trigger", "content"]
|
|
@@ -13,7 +15,8 @@ export default class extends Controller {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
connect() {
|
|
16
|
-
|
|
18
|
+
// Use stimulus-use for click outside detection
|
|
19
|
+
useClickOutside(this)
|
|
17
20
|
|
|
18
21
|
if (this.openValue) {
|
|
19
22
|
this.show()
|
|
@@ -45,8 +48,6 @@ export default class extends Controller {
|
|
|
45
48
|
this.positionContent()
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
49
|
-
|
|
50
51
|
if (this.modalValue) {
|
|
51
52
|
document.body.style.pointerEvents = "none"
|
|
52
53
|
this.contentTarget.style.pointerEvents = "auto"
|
|
@@ -69,8 +70,6 @@ export default class extends Controller {
|
|
|
69
70
|
}, 150)
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
73
|
-
|
|
74
73
|
if (this.modalValue) {
|
|
75
74
|
document.body.style.pointerEvents = ""
|
|
76
75
|
}
|
|
@@ -82,8 +81,9 @@ export default class extends Controller {
|
|
|
82
81
|
this.hide()
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
// Called by stimulus-use when clicking outside the element
|
|
85
|
+
clickOutside(event) {
|
|
86
|
+
if (this.openValue) {
|
|
87
87
|
this.hide()
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Select controller for custom select dropdowns
|
|
6
|
+
* Uses stimulus-use for click outside detection
|
|
5
7
|
*/
|
|
6
8
|
export default class extends Controller {
|
|
7
9
|
static targets = ["trigger", "content", "input", "item", "display", "checkIcon"]
|
|
@@ -12,7 +14,9 @@ export default class extends Controller {
|
|
|
12
14
|
connect() {
|
|
13
15
|
this.isOpen = false
|
|
14
16
|
this.focusedIndex = -1
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
// Use stimulus-use for click outside detection
|
|
19
|
+
useClickOutside(this)
|
|
16
20
|
|
|
17
21
|
// Set initial value display
|
|
18
22
|
if (this.valueValue) {
|
|
@@ -53,8 +57,6 @@ export default class extends Controller {
|
|
|
53
57
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
57
|
-
|
|
58
60
|
// Focus current value or first item
|
|
59
61
|
this.focusedIndex = -1
|
|
60
62
|
const currentItem = this.itemTargets.find(item => item.dataset.value === this.valueValue)
|
|
@@ -86,12 +88,18 @@ export default class extends Controller {
|
|
|
86
88
|
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
90
91
|
this.focusedIndex = -1
|
|
91
92
|
|
|
92
93
|
this.dispatch("closed")
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// Called by stimulus-use when clicking outside the element
|
|
97
|
+
clickOutside(event) {
|
|
98
|
+
if (this.isOpen) {
|
|
99
|
+
this.close()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
select(event) {
|
|
96
104
|
const item = event.currentTarget
|
|
97
105
|
if (item.dataset.disabled !== undefined) return
|
|
@@ -132,12 +140,6 @@ export default class extends Controller {
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
handleClickOutside(event) {
|
|
136
|
-
if (!this.element.contains(event.target)) {
|
|
137
|
-
this.close()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
143
|
handleKeydown(event) {
|
|
142
144
|
if (!this.isOpen) {
|
|
143
145
|
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
|