wilday_ui 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- console.log("✅ Stimulus is loaded and initialized.");
14
- } else {
15
- console.error("❌ Stimulus failed to load.");
16
- }
13
+ // if (window.Stimulus) {
14
+ // console.log("✅ Stimulus is loaded and initialized.");
15
+ // } else {
16
+ // console.error("❌ Stimulus failed to load.");
17
+ // }
@@ -1,4 +1,4 @@
1
1
  import "./controllers";
2
2
  import "./components/button";
3
3
 
4
- console.log("JavaScript loaded");
4
+ // console.log("JavaScript loaded");
@@ -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
- <%= content_tag tag_type, html_options do %>
22
- <% if loading %>
23
- <span class="w-button-spinner"></span>
24
- <%= loading_text %>
25
- <% else %>
26
- <% if icon_position == :left && icon.present? %>
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
- <%= content %>
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 %>
@@ -1,3 +1,3 @@
1
1
  module WildayUi
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
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.3.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-12 00:00:00.000000000 Z
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