wilday_ui 0.3.0 → 0.4.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/app/assets/builds/wilday_ui/index.js +214 -8
- data/app/assets/builds/wilday_ui/index.js.map +3 -3
- data/app/assets/stylesheets/wilday_ui/button.css +245 -0
- data/app/helpers/wilday_ui/components/button_helper.rb +219 -19
- data/app/javascript/wilday_ui/controllers/dropdown_controller.js +270 -0
- data/app/javascript/wilday_ui/controllers/index.js +7 -6
- data/app/javascript/wilday_ui/index.js +1 -1
- data/app/views/wilday_ui/_button.html.erb +16 -14
- data/app/views/wilday_ui/button/_content.html.erb +22 -0
- data/app/views/wilday_ui/button/_dropdown_menu.html.erb +26 -0
- data/lib/wilday_ui/version.rb +1 -1
- metadata +5 -2
@@ -0,0 +1,270 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["button", "menu", "submenu"];
|
5
|
+
static values = {
|
6
|
+
trigger: { type: String, default: "click" },
|
7
|
+
position: { type: String, default: "bottom" },
|
8
|
+
align: { type: String, default: "start" },
|
9
|
+
};
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
const position = this.element.dataset.dropdownPositionValue;
|
13
|
+
const align = this.element.dataset.dropdownAlignValue;
|
14
|
+
|
15
|
+
if (position) this.positionValue = position;
|
16
|
+
if (align) this.alignValue = align;
|
17
|
+
|
18
|
+
// Set up hover events if trigger is hover
|
19
|
+
if (this.triggerValue === "hover") {
|
20
|
+
this.element.addEventListener("mouseenter", () => {
|
21
|
+
console.log("Mouse enter - showing menu");
|
22
|
+
this.handleHover(true);
|
23
|
+
});
|
24
|
+
|
25
|
+
this.element.addEventListener("mouseleave", () => {
|
26
|
+
console.log("Mouse leave - hiding menu");
|
27
|
+
this.handleHover(false);
|
28
|
+
});
|
29
|
+
}
|
30
|
+
|
31
|
+
// Set up keyboard navigation
|
32
|
+
this.element.addEventListener("keydown", this.handleKeydown.bind(this));
|
33
|
+
|
34
|
+
// Close on click outside
|
35
|
+
document.addEventListener("click", this.handleClickOutside.bind(this));
|
36
|
+
|
37
|
+
// Set up parent dropdown items to handle submenus
|
38
|
+
this.setupSubmenus();
|
39
|
+
}
|
40
|
+
|
41
|
+
disconnect() {
|
42
|
+
document.removeEventListener("click", this.handleClickOutside.bind(this));
|
43
|
+
}
|
44
|
+
|
45
|
+
handleHover(show) {
|
46
|
+
if (show) {
|
47
|
+
this.menuTarget.classList.add("show");
|
48
|
+
this.buttonTarget.classList.add("active");
|
49
|
+
this.buttonTarget.setAttribute("aria-expanded", "true");
|
50
|
+
} else {
|
51
|
+
this.menuTarget.classList.remove("show");
|
52
|
+
this.buttonTarget.classList.remove("active");
|
53
|
+
this.buttonTarget.setAttribute("aria-expanded", "false");
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
toggle(event) {
|
58
|
+
event.preventDefault();
|
59
|
+
event.stopPropagation();
|
60
|
+
|
61
|
+
if (this.isOpen) {
|
62
|
+
this.menuTarget.classList.remove("show");
|
63
|
+
} else {
|
64
|
+
this.menuTarget.classList.add("show");
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
show() {
|
69
|
+
const menuElement = this.element.querySelector(".w-button-dropdown-menu");
|
70
|
+
const buttonElement = this.element.querySelector(
|
71
|
+
"[data-dropdown-target='button']"
|
72
|
+
);
|
73
|
+
|
74
|
+
this.updatePosition();
|
75
|
+
menuElement.classList.add("show");
|
76
|
+
buttonElement.classList.add("active");
|
77
|
+
buttonElement.setAttribute("aria-expanded", "true");
|
78
|
+
|
79
|
+
// Apply position and alignment
|
80
|
+
menuElement.dataset.position = this.positionValue || "bottom";
|
81
|
+
menuElement.dataset.align = this.alignValue || "start";
|
82
|
+
|
83
|
+
// Focus first menu item
|
84
|
+
const firstItem = menuElement.querySelector(".w-button-dropdown-item");
|
85
|
+
if (firstItem) firstItem.focus();
|
86
|
+
}
|
87
|
+
|
88
|
+
hide() {
|
89
|
+
const menuElement = this.element.querySelector(".w-button-dropdown-menu");
|
90
|
+
const buttonElement = this.element.querySelector(
|
91
|
+
"[data-dropdown-target='button']"
|
92
|
+
);
|
93
|
+
|
94
|
+
menuElement.classList.remove("show");
|
95
|
+
buttonElement.classList.remove("active");
|
96
|
+
buttonElement.setAttribute("aria-expanded", "false");
|
97
|
+
}
|
98
|
+
|
99
|
+
handleClickOutside(event) {
|
100
|
+
if (!this.element.contains(event.target)) {
|
101
|
+
console.log("Click outside detected. Closing all dropdowns.");
|
102
|
+
this.closeAllSubmenus();
|
103
|
+
this.hide(); // Close the main dropdown
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
handleKeydown(event) {
|
108
|
+
if (!this.isOpen && event.key === "Enter") {
|
109
|
+
this.show();
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
|
113
|
+
if (this.isOpen) {
|
114
|
+
switch (event.key) {
|
115
|
+
case "Escape":
|
116
|
+
this.hide();
|
117
|
+
this.buttonTarget.focus();
|
118
|
+
break;
|
119
|
+
case "ArrowDown":
|
120
|
+
event.preventDefault();
|
121
|
+
this.focusNextItem();
|
122
|
+
break;
|
123
|
+
case "ArrowUp":
|
124
|
+
event.preventDefault();
|
125
|
+
this.focusPreviousItem();
|
126
|
+
break;
|
127
|
+
case "ArrowRight":
|
128
|
+
this.openSubmenu(event.target);
|
129
|
+
break;
|
130
|
+
case "ArrowLeft":
|
131
|
+
this.closeSubmenu(event.target);
|
132
|
+
break;
|
133
|
+
case "Tab":
|
134
|
+
this.hide();
|
135
|
+
break;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
setupSubmenus() {
|
141
|
+
this.element
|
142
|
+
.querySelectorAll(".w-button-dropdown-parent")
|
143
|
+
.forEach((parent) => {
|
144
|
+
const submenu = parent.querySelector(".w-button-dropdown-menu");
|
145
|
+
const arrow = parent.querySelector(".w-button-dropdown-arrow");
|
146
|
+
|
147
|
+
if (!submenu) {
|
148
|
+
return;
|
149
|
+
}
|
150
|
+
|
151
|
+
// Determine the trigger (hover or click)
|
152
|
+
const trigger =
|
153
|
+
parent.closest(".w-button-wrapper")?.dataset.dropdownTriggerValue ||
|
154
|
+
"click";
|
155
|
+
|
156
|
+
if (trigger === "hover") {
|
157
|
+
parent.addEventListener("mouseenter", () => {
|
158
|
+
this.showSubmenu(submenu, arrow);
|
159
|
+
});
|
160
|
+
parent.addEventListener("mouseleave", () => {
|
161
|
+
this.hideSubmenu(submenu, arrow);
|
162
|
+
});
|
163
|
+
} else if (trigger === "click") {
|
164
|
+
parent.addEventListener("click", (event) => {
|
165
|
+
event.stopPropagation(); // Prevent closing parent dropdown
|
166
|
+
this.toggleSubmenu(submenu, arrow);
|
167
|
+
});
|
168
|
+
|
169
|
+
// Close the submenu when clicking outside
|
170
|
+
document.addEventListener("click", (event) => {
|
171
|
+
if (!parent.contains(event.target)) {
|
172
|
+
this.hideSubmenu(submenu, arrow);
|
173
|
+
}
|
174
|
+
});
|
175
|
+
}
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
toggleSubmenu(submenu, arrow) {
|
180
|
+
if (submenu.classList.contains("show")) {
|
181
|
+
this.hideSubmenu(submenu, arrow);
|
182
|
+
} else {
|
183
|
+
this.showSubmenu(submenu, arrow);
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
showSubmenu(submenu, arrow) {
|
188
|
+
submenu.classList.add("show");
|
189
|
+
submenu.setAttribute("aria-expanded", "true");
|
190
|
+
|
191
|
+
// Rotate arrow if present
|
192
|
+
if (arrow) {
|
193
|
+
arrow.classList.add("active");
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
hideSubmenu(submenu, arrow) {
|
198
|
+
submenu.classList.remove("show");
|
199
|
+
submenu.setAttribute("aria-expanded", "false");
|
200
|
+
|
201
|
+
// Reset arrow rotation if present
|
202
|
+
if (arrow) {
|
203
|
+
arrow.classList.remove("active");
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
isParentOpen(parent) {
|
208
|
+
const parentMenu = parent.closest(".w-button-dropdown-menu");
|
209
|
+
return parentMenu && parentMenu.classList.contains("show");
|
210
|
+
}
|
211
|
+
|
212
|
+
closeAllSubmenus() {
|
213
|
+
this.element
|
214
|
+
.querySelectorAll(".w-button-dropdown-menu.show")
|
215
|
+
.forEach((menu) => {
|
216
|
+
menu.classList.remove("show");
|
217
|
+
menu.setAttribute("aria-expanded", "false");
|
218
|
+
});
|
219
|
+
}
|
220
|
+
|
221
|
+
openSubmenu(parentItem) {
|
222
|
+
const submenu = parentItem.querySelector(".w-button-dropdown-menu");
|
223
|
+
if (submenu) {
|
224
|
+
this.showSubmenu(submenu);
|
225
|
+
submenu.querySelector(".w-button-dropdown-item").focus();
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
closeSubmenu(parentItem) {
|
230
|
+
const submenu = parentItem.closest(".w-button-dropdown-menu");
|
231
|
+
if (submenu) {
|
232
|
+
this.hideSubmenu(submenu);
|
233
|
+
parentItem.closest(".w-button-dropdown-parent").focus();
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
focusNextItem() {
|
238
|
+
const items = this.getMenuItems();
|
239
|
+
const currentIndex = items.indexOf(document.activeElement);
|
240
|
+
const nextIndex = currentIndex + 1 < items.length ? currentIndex + 1 : 0;
|
241
|
+
items[nextIndex].focus();
|
242
|
+
}
|
243
|
+
|
244
|
+
focusPreviousItem() {
|
245
|
+
const items = this.getMenuItems();
|
246
|
+
const currentIndex = items.indexOf(document.activeElement);
|
247
|
+
const previousIndex =
|
248
|
+
currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
249
|
+
items[previousIndex].focus();
|
250
|
+
}
|
251
|
+
|
252
|
+
getMenuItems() {
|
253
|
+
return Array.from(
|
254
|
+
this.menuTarget.querySelectorAll(".w-button-dropdown-item")
|
255
|
+
);
|
256
|
+
}
|
257
|
+
|
258
|
+
updatePosition() {
|
259
|
+
const menuElement = this.element.querySelector(".w-button-dropdown-menu");
|
260
|
+
const position = this.hasPositionValue ? this.positionValue : "bottom";
|
261
|
+
const align = this.hasAlignValue ? this.alignValue : "start";
|
262
|
+
|
263
|
+
menuElement.setAttribute("data-position", position);
|
264
|
+
menuElement.setAttribute("data-align", align);
|
265
|
+
}
|
266
|
+
|
267
|
+
get isOpen() {
|
268
|
+
return this.menuTarget.classList.contains("show");
|
269
|
+
}
|
270
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Application } from "@hotwired/stimulus";
|
2
2
|
import ButtonController from "./button_controller";
|
3
|
+
import DropdownController from "./dropdown_controller";
|
3
4
|
|
4
5
|
// Initialize Stimulus
|
5
6
|
const application = Application.start();
|
@@ -7,10 +8,10 @@ window.Stimulus = application;
|
|
7
8
|
|
8
9
|
// Register the button controller
|
9
10
|
application.register("button", ButtonController);
|
10
|
-
|
11
|
+
application.register("dropdown", DropdownController);
|
11
12
|
// Debug check to ensure Stimulus is loaded
|
12
|
-
if (window.Stimulus) {
|
13
|
-
|
14
|
-
} else {
|
15
|
-
|
16
|
-
}
|
13
|
+
// if (window.Stimulus) {
|
14
|
+
// console.log("✅ Stimulus is loaded and initialized.");
|
15
|
+
// } else {
|
16
|
+
// console.error("❌ Stimulus failed to load.");
|
17
|
+
// }
|
@@ -4,7 +4,9 @@ html_options = {
|
|
4
4
|
class: ["w-button", variant_class, size_class, radius_class, additional_classes, ("w-button-loading" if loading)].compact.join(" "),
|
5
5
|
disabled: disabled || loading,
|
6
6
|
"aria-busy": loading,
|
7
|
-
"aria-disabled": disabled || loading
|
7
|
+
"aria-disabled": disabled || loading,
|
8
|
+
"aria-expanded": dropdown ? "false" : nil,
|
9
|
+
"aria-haspopup": dropdown ? "true" : nil
|
8
10
|
}
|
9
11
|
|
10
12
|
# Only add Stimulus data attributes if they're in the passed options
|
@@ -16,21 +18,21 @@ end
|
|
16
18
|
html_options.merge!(local_assigns[:html_options] || {})
|
17
19
|
|
18
20
|
tag_type = href.present? ? :a : :button
|
19
|
-
%>
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
<i class="w-button-icon-left <%= icon %>"></i>
|
22
|
+
# Conditionally render wrapper for dropdowns or advanced features
|
23
|
+
if local_assigns[:wrapper_options].present?
|
24
|
+
%>
|
25
|
+
<div <%= tag.attributes(wrapper_options) %>>
|
26
|
+
<%= content_tag tag_type, html_options do %>
|
27
|
+
<%= render partial: "wilday_ui/button/content", locals: local_assigns %>
|
28
28
|
<% end %>
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<% if icon_position == :right && icon.present? %>
|
33
|
-
<i class="w-button-icon-right <%= icon %>"></i>
|
29
|
+
|
30
|
+
<% if dropdown && dropdown_items %>
|
31
|
+
<%= render partial: "wilday_ui/button/dropdown_menu", locals: { items: dropdown_items, dropdown: dropdown } %>
|
34
32
|
<% end %>
|
33
|
+
</div>
|
34
|
+
<% else %>
|
35
|
+
<%= content_tag tag_type, html_options do %>
|
36
|
+
<%= render partial: "wilday_ui/button/content", locals: local_assigns %>
|
35
37
|
<% end %>
|
36
38
|
<% end %>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% if loading %>
|
2
|
+
<span class="w-button-spinner"></span>
|
3
|
+
<%= loading_text %>
|
4
|
+
<% else %>
|
5
|
+
<% if icon_position == :left && icon.present? %>
|
6
|
+
<i class="w-button-icon-left <%= icon %>"></i>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= content %>
|
10
|
+
|
11
|
+
<% if dropdown %>
|
12
|
+
<% if dropdown_icon.present? %>
|
13
|
+
<i class="w-button-dropdown-arrow <%= dropdown_icon %>"></i>
|
14
|
+
<% else %>
|
15
|
+
<span class="w-button-dropdown-arrow"></span>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<% if icon_position == :right && icon.present? %>
|
20
|
+
<i class="w-button-icon-right <%= icon %>"></i>
|
21
|
+
<% end %>
|
22
|
+
<% end %>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% if items.present? %>
|
2
|
+
<div class="w-button-dropdown-menu" data-dropdown-target="menu" role="menu">
|
3
|
+
<% items.each do |item| %>
|
4
|
+
<% if item[:divider] %>
|
5
|
+
<div class="w-button-dropdown-divider" role="separator"></div>
|
6
|
+
<% elsif item[:children].present? %>
|
7
|
+
<div class="w-button-dropdown-item w-button-dropdown-parent" role="menuitem">
|
8
|
+
<%= item[:text] %>
|
9
|
+
<span class="w-button-dropdown-arrow"></span>
|
10
|
+
<%= render partial: "wilday_ui/button/dropdown_menu",
|
11
|
+
locals: {
|
12
|
+
items: item[:children],
|
13
|
+
parent_id: item[:id]
|
14
|
+
} %>
|
15
|
+
</div>
|
16
|
+
<% else %>
|
17
|
+
<%= link_to item[:text],
|
18
|
+
item[:href],
|
19
|
+
class: "w-button-dropdown-item",
|
20
|
+
role: "menuitem",
|
21
|
+
tabindex: "-1"
|
22
|
+
%>
|
23
|
+
<% end %>
|
24
|
+
<% end %>
|
25
|
+
</div>
|
26
|
+
<% end %>
|
data/lib/wilday_ui/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wilday_ui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- davidwinalda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -49,6 +49,7 @@ files:
|
|
49
49
|
- app/helpers/wilday_ui/components/button_helper.rb
|
50
50
|
- app/javascript/wilday_ui/components/button.js
|
51
51
|
- app/javascript/wilday_ui/controllers/button_controller.js
|
52
|
+
- app/javascript/wilday_ui/controllers/dropdown_controller.js
|
52
53
|
- app/javascript/wilday_ui/controllers/index.js
|
53
54
|
- app/javascript/wilday_ui/index.js
|
54
55
|
- app/jobs/wilday_ui/application_job.rb
|
@@ -56,6 +57,8 @@ files:
|
|
56
57
|
- app/models/wilday_ui/application_record.rb
|
57
58
|
- app/views/layouts/wilday_ui/application.html.erb
|
58
59
|
- app/views/wilday_ui/_button.html.erb
|
60
|
+
- app/views/wilday_ui/button/_content.html.erb
|
61
|
+
- app/views/wilday_ui/button/_dropdown_menu.html.erb
|
59
62
|
- config/routes.rb
|
60
63
|
- lib/tasks/wilday_ui_tasks.rake
|
61
64
|
- lib/wilday_ui.rb
|