wilday_ui 0.7.0 → 0.8.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.
@@ -0,0 +1,318 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["trigger"];
5
+ static values = {
6
+ content: String,
7
+ position: { type: String, default: "top" },
8
+ align: { type: String, default: "center" },
9
+ trigger: { type: String, default: "hover" },
10
+ showDelay: { type: Number, default: 0 },
11
+ hideDelay: { type: Number, default: 0 },
12
+ offset: { type: Number, default: 8 },
13
+ theme: { type: String, default: "light" },
14
+ size: { type: String, default: "md" },
15
+ arrow: { type: Boolean, default: false },
16
+ customStyle: String,
17
+ };
18
+
19
+ connect() {
20
+ this.tooltipElement = null;
21
+ this.showTimeoutId = null;
22
+ this.hideTimeoutId = null;
23
+ this.setupTooltip();
24
+
25
+ // Add scroll handler
26
+ this.scrollHandler = () => {
27
+ if (this.tooltipElement.style.display !== "none") {
28
+ // Check if trigger is in viewport
29
+ const triggerRect = this.triggerTarget.getBoundingClientRect();
30
+ const isInViewport =
31
+ triggerRect.top >= 0 &&
32
+ triggerRect.left >= 0 &&
33
+ triggerRect.bottom <=
34
+ (window.innerHeight || document.documentElement.clientHeight) &&
35
+ triggerRect.right <=
36
+ (window.innerWidth || document.documentElement.clientWidth);
37
+
38
+ if (isInViewport) {
39
+ // Update position if trigger is visible
40
+ this.position();
41
+ } else {
42
+ // Hide tooltip if trigger is not visible
43
+ this.hide();
44
+ }
45
+ }
46
+ };
47
+ window.addEventListener("scroll", this.scrollHandler);
48
+ }
49
+
50
+ disconnect() {
51
+ if (this.clickOutsideHandler) {
52
+ document.removeEventListener("click", this.clickOutsideHandler);
53
+ }
54
+ window.removeEventListener("scroll", this.scrollHandler);
55
+ this.removeTooltip();
56
+ }
57
+
58
+ setupTooltip() {
59
+ this.tooltipElement = this.createTooltipElement();
60
+ document.body.appendChild(this.tooltipElement);
61
+
62
+ if (this.triggerValue === "hover") {
63
+ this.triggerTarget.addEventListener("mouseenter", () => this.show());
64
+ this.triggerTarget.addEventListener("mouseleave", () => this.hide());
65
+ this.triggerTarget.addEventListener("focusin", () => this.show());
66
+ this.triggerTarget.addEventListener("focusout", () => this.hide());
67
+ } else if (this.triggerValue === "click") {
68
+ // Check if dropdown or clipboard is present
69
+ const hasDropdown =
70
+ this.element.hasAttribute("data-controller") &&
71
+ this.element.getAttribute("data-controller").includes("dropdown");
72
+ const hasClipboard =
73
+ this.element.hasAttribute("data-controller") &&
74
+ this.element.getAttribute("data-controller").includes("clipboard");
75
+
76
+ if (hasDropdown || hasClipboard) {
77
+ // Force hover behavior for dropdown/clipboard buttons
78
+ this.triggerTarget.addEventListener("mouseenter", () => this.show());
79
+ this.triggerTarget.addEventListener("mouseleave", () => this.hide());
80
+ } else {
81
+ // Normal click behavior
82
+ this.triggerTarget.addEventListener("click", (e) => {
83
+ e.stopPropagation();
84
+ this.toggle();
85
+ });
86
+
87
+ this.clickOutsideHandler = (e) => {
88
+ if (
89
+ !this.triggerTarget.contains(e.target) &&
90
+ !this.tooltipElement.contains(e.target)
91
+ ) {
92
+ this.hide();
93
+ }
94
+ };
95
+ document.addEventListener("click", this.clickOutsideHandler);
96
+ }
97
+ }
98
+ }
99
+
100
+ createTooltipElement() {
101
+ const tooltip = document.createElement("div");
102
+ tooltip.id = this.triggerTarget.getAttribute("aria-describedby");
103
+ tooltip.className = `w-tooltip w-tooltip-${this.themeValue} w-tooltip-size-${this.sizeValue}`;
104
+ if (this.arrowValue) tooltip.classList.add("w-tooltip-arrow"); // Add arrow class if enabled
105
+ tooltip.setAttribute("role", "tooltip");
106
+ tooltip.setAttribute("data-position", this.positionValue);
107
+ tooltip.setAttribute("data-align", this.alignValue);
108
+ tooltip.innerHTML = this.contentValue;
109
+
110
+ // Apply custom styles if present
111
+ if (this.hasCustomStyleValue && this.customStyleValue) {
112
+ tooltip.style.cssText += this.customStyleValue;
113
+ }
114
+
115
+ tooltip.style.display = "none";
116
+ return tooltip;
117
+ }
118
+
119
+ show() {
120
+ clearTimeout(this.hideTimeoutId);
121
+ this.showTimeoutId = setTimeout(() => {
122
+ // Reset any existing transforms and make tooltip visible but hidden
123
+ this.tooltipElement.style.transform = "none";
124
+ this.tooltipElement.style.visibility = "hidden";
125
+ this.tooltipElement.style.display = "block";
126
+
127
+ // Force a reflow to ensure dimensions are calculated correctly
128
+ this.tooltipElement.offsetHeight;
129
+
130
+ // Position the tooltip
131
+ this.position();
132
+
133
+ // Make visible with transition
134
+ this.tooltipElement.style.visibility = "visible";
135
+ requestAnimationFrame(() => {
136
+ this.tooltipElement.classList.add("w-tooltip-visible");
137
+ });
138
+ }, this.showDelayValue);
139
+ }
140
+
141
+ hide() {
142
+ clearTimeout(this.showTimeoutId);
143
+ this.hideTimeoutId = setTimeout(() => {
144
+ this.tooltipElement.classList.remove("w-tooltip-visible");
145
+ setTimeout(() => {
146
+ this.tooltipElement.style.display = "none";
147
+ }, 150); // Match transition duration
148
+ }, this.hideDelayValue);
149
+ }
150
+
151
+ toggle() {
152
+ if (this.tooltipElement.style.display === "none") {
153
+ this.show();
154
+ } else {
155
+ this.hide();
156
+ }
157
+ }
158
+
159
+ position() {
160
+ const triggerRect = this.triggerTarget.getBoundingClientRect();
161
+ const tooltipRect = this.tooltipElement.getBoundingClientRect();
162
+ const viewportHeight = window.innerHeight;
163
+ const viewportWidth = window.innerWidth;
164
+ const arrowOffset = this.arrowValue ? 2 : 0;
165
+
166
+ // Calculate scroll position
167
+ const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
168
+ const scrollY = window.pageYOffset || document.documentElement.scrollTop;
169
+
170
+ // Debug initial values
171
+ console.log("=== Initial Values ===");
172
+ console.log("Viewport Height:", viewportHeight);
173
+ console.log("Viewport Width:", viewportWidth);
174
+ console.log("Trigger Top:", triggerRect.top);
175
+ console.log("Trigger Bottom:", triggerRect.bottom);
176
+ console.log("Trigger Left:", triggerRect.left);
177
+ console.log("Trigger Right:", triggerRect.right);
178
+ console.log("Tooltip Height:", tooltipRect.height);
179
+ console.log("Tooltip Width:", tooltipRect.width);
180
+
181
+ let position = this.positionValue;
182
+ let top, left;
183
+
184
+ // Calculate available space in viewport
185
+ const spaceBelow = viewportHeight - triggerRect.bottom;
186
+ const spaceAbove = triggerRect.top;
187
+ const spaceRight = viewportWidth - triggerRect.right;
188
+ const spaceLeft = triggerRect.left;
189
+ const requiredSpaceVertical =
190
+ tooltipRect.height + this.offsetValue + arrowOffset;
191
+ const requiredSpaceHorizontal =
192
+ tooltipRect.width + this.offsetValue + arrowOffset;
193
+
194
+ // Debug space
195
+ console.log("\n=== Space Calculation ===");
196
+ console.log("Space Above:", spaceAbove);
197
+ console.log("Space Below:", spaceBelow);
198
+ console.log("Space Left:", spaceLeft);
199
+ console.log("Space Right:", spaceRight);
200
+ console.log("Required Space Vertical:", requiredSpaceVertical);
201
+ console.log("Required Space Horizontal:", requiredSpaceHorizontal);
202
+
203
+ // Determine position based on available viewport space
204
+ if (position === "right" && spaceRight >= requiredSpaceHorizontal) {
205
+ position = "right";
206
+ console.log("Using right - sufficient space");
207
+ } else if (position === "left" && spaceLeft >= requiredSpaceHorizontal) {
208
+ position = "left";
209
+ console.log("Using left - sufficient space");
210
+ } else if (position === "top" && spaceAbove >= requiredSpaceVertical) {
211
+ position = "top";
212
+ console.log("Using top - sufficient space");
213
+ } else if (position === "bottom" && spaceBelow >= requiredSpaceVertical) {
214
+ position = "bottom";
215
+ console.log("Using bottom - sufficient space");
216
+ } else if (spaceBelow >= requiredSpaceVertical) {
217
+ position = "bottom";
218
+ console.log("Fallback to bottom - sufficient space");
219
+ } else {
220
+ position = "top";
221
+ console.log("Fallback to top - insufficient space below");
222
+ }
223
+
224
+ // Calculate viewport-relative position
225
+ switch (position) {
226
+ case "top":
227
+ // Position above trigger
228
+ top =
229
+ triggerRect.top - tooltipRect.height - this.offsetValue - arrowOffset;
230
+ if (this.alignValue === "end") {
231
+ left = triggerRect.right - tooltipRect.width;
232
+ } else if (this.alignValue === "center") {
233
+ left =
234
+ triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
235
+ } else {
236
+ // start
237
+ left = triggerRect.left;
238
+ }
239
+ break;
240
+ case "bottom":
241
+ // Position below trigger
242
+ top = triggerRect.bottom + this.offsetValue + arrowOffset;
243
+ if (this.alignValue === "end") {
244
+ left = triggerRect.right - tooltipRect.width;
245
+ } else if (this.alignValue === "center") {
246
+ left =
247
+ triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
248
+ } else {
249
+ // start
250
+ left = triggerRect.left;
251
+ }
252
+ break;
253
+ case "left":
254
+ // Position to the left of trigger
255
+ left =
256
+ triggerRect.left - tooltipRect.width - this.offsetValue - arrowOffset;
257
+ if (this.alignValue === "end") {
258
+ top = triggerRect.bottom - tooltipRect.height;
259
+ } else if (this.alignValue === "center") {
260
+ top =
261
+ triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
262
+ } else {
263
+ // start
264
+ top = triggerRect.top;
265
+ }
266
+ break;
267
+ case "right":
268
+ // Position to the right of trigger
269
+ left = triggerRect.right + this.offsetValue + arrowOffset;
270
+ if (this.alignValue === "end") {
271
+ top = triggerRect.bottom - tooltipRect.height;
272
+ } else if (this.alignValue === "center") {
273
+ top =
274
+ triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
275
+ } else {
276
+ // start
277
+ top = triggerRect.top;
278
+ }
279
+ break;
280
+ }
281
+
282
+ // Ensure tooltip stays within viewport
283
+ if (top < 0) {
284
+ top = this.offsetValue; // Keep some padding from top
285
+ } else if (top + tooltipRect.height > viewportHeight) {
286
+ top = viewportHeight - tooltipRect.height - this.offsetValue;
287
+ }
288
+
289
+ if (left < 0) {
290
+ left = this.offsetValue; // Keep some padding from left
291
+ } else if (left + tooltipRect.width > viewportWidth) {
292
+ left = viewportWidth - tooltipRect.width - this.offsetValue;
293
+ }
294
+
295
+ // Debug final position
296
+ console.log("\n=== Final Position ===");
297
+ console.log("Position:", position);
298
+ console.log("Top:", top);
299
+ console.log("Left:", left);
300
+ console.log("Alignment:", this.alignValue);
301
+
302
+ // Add scroll position to final coordinates
303
+ top += scrollY;
304
+ left += scrollX;
305
+
306
+ // Update tooltip position and data attribute
307
+ this.tooltipElement.setAttribute("data-position", position);
308
+ this.tooltipElement.style.top = `${top}px`;
309
+ this.tooltipElement.style.left = `${left}px`;
310
+ }
311
+
312
+ removeTooltip() {
313
+ if (this.tooltipElement) {
314
+ this.tooltipElement.remove();
315
+ this.tooltipElement = null;
316
+ }
317
+ }
318
+ }
@@ -1,3 +1,3 @@
1
1
  module WildayUi
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.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.7.0
4
+ version: 0.8.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-21 00:00:00.000000000 Z
11
+ date: 2024-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -54,6 +54,7 @@ files:
54
54
  - app/assets/stylesheets/wilday_ui/components/button/features/old_variants.css
55
55
  - app/assets/stylesheets/wilday_ui/components/button/features/radius.css
56
56
  - app/assets/stylesheets/wilday_ui/components/button/features/sizes.css
57
+ - app/assets/stylesheets/wilday_ui/components/button/features/tooltip.css
57
58
  - app/assets/stylesheets/wilday_ui/components/button/features/variants.css
58
59
  - app/assets/stylesheets/wilday_ui/components/button/index.css
59
60
  - app/assets/stylesheets/wilday_ui/tokens/colors.css
@@ -61,13 +62,13 @@ files:
61
62
  - app/helpers/wilday_ui/application_helper.rb
62
63
  - app/helpers/wilday_ui/components/button_helper.rb
63
64
  - app/helpers/wilday_ui/javascript_helper.rb
64
- - app/helpers/wilday_ui/theme_helper.rb
65
65
  - app/javascript/wilday_ui/components/button.js
66
66
  - app/javascript/wilday_ui/controllers/button_controller.js
67
67
  - app/javascript/wilday_ui/controllers/clipboard_controller.js
68
68
  - app/javascript/wilday_ui/controllers/confirmation_controller.js
69
69
  - app/javascript/wilday_ui/controllers/dropdown_controller.js
70
70
  - app/javascript/wilday_ui/controllers/index.js
71
+ - app/javascript/wilday_ui/controllers/tooltip_controller.js
71
72
  - app/javascript/wilday_ui/index.js
72
73
  - app/jobs/wilday_ui/application_job.rb
73
74
  - app/mailers/wilday_ui/application_mailer.rb
@@ -1,19 +0,0 @@
1
- module WildayUi
2
- module ThemeHelper
3
- def generate_theme_css_variables
4
- colors = WildayUi::Config::Theme.configuration.colors
5
- css_vars = colors.map do |color_name, shades|
6
- shades.map do |shade, value|
7
- "--w-color-#{color_name}-#{shade}: #{value};"
8
- end
9
- end.flatten.join("\n")
10
-
11
- "<style>:root { #{css_vars} }</style>".html_safe
12
- end
13
-
14
- # Helper to get specific color value
15
- def theme_color(name, shade = "500")
16
- WildayUi::Config::Theme.configuration.colors.dig(name.to_s, shade.to_s)
17
- end
18
- end
19
- end