1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-06 05:55:21 +02:00

Initial tooltip component

This commit is contained in:
Zach Gollwitzer 2025-07-18 14:41:19 -04:00
parent 40ae3126ac
commit d932ac84a7
5 changed files with 147 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<div data-controller="ds-tooltip" data-ds-tooltip-placement-value="<%= placement %>" data-ds-tooltip-offset-value="<%= offset %>" data-ds-tooltip-cross-axis-value="<%= cross_axis %>">
<%= helpers.icon icon_name, size: size, color: color %>
<div role="tooltip" data-ds-tooltip-target="tooltip" class="hidden absolute z-50 bg-gray-700 text-sm px-1.5 py-1 rounded-md">
<div class="fg-inverse font-normal max-w-[200px]">
<%= tooltip_content %>
</div>
</div>
</div>

View file

@ -0,0 +1,17 @@
class DS::Tooltip < ApplicationComponent
attr_reader :placement, :offset, :cross_axis, :icon_name, :size, :color
def initialize(text: nil, placement: "top", offset: 10, cross_axis: 0, icon: "info", size: "sm", color: "default")
@text = text
@placement = placement
@offset = offset
@cross_axis = cross_axis
@icon_name = icon
@size = size
@color = color
end
def tooltip_content
content? ? content : @text
end
end

View file

@ -16,6 +16,10 @@
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<span class="font-medium"><%= balance_trend.current.format %></span>
<%= render DS::Tooltip.new(text: "The end of day balance, after all transactions and adjustments") %>
</div>
<%= helpers.icon "chevron-down", class: "group-open:rotate-180" %>
</div>
</div>

View file

@ -0,0 +1,85 @@
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["tooltip"];
static values = {
placement: { type: String, default: "top" },
offset: { type: Number, default: 10 },
crossAxis: { type: Number, default: 0 },
};
connect() {
this._cleanup = null;
this.boundUpdate = this.update.bind(this);
this.addEventListeners();
}
disconnect() {
this.removeEventListeners();
this.stopAutoUpdate();
}
addEventListeners() {
this.element.addEventListener("mouseenter", this.show);
this.element.addEventListener("mouseleave", this.hide);
}
removeEventListeners() {
this.element.removeEventListener("mouseenter", this.show);
this.element.removeEventListener("mouseleave", this.hide);
}
show = () => {
this.tooltipTarget.style.display = "block";
this.startAutoUpdate();
this.update();
};
hide = () => {
this.tooltipTarget.style.display = "none";
this.stopAutoUpdate();
};
startAutoUpdate() {
if (!this._cleanup) {
this._cleanup = autoUpdate(
this.element.firstElementChild, // Use the icon as the reference element
this.tooltipTarget,
this.boundUpdate
);
}
}
stopAutoUpdate() {
if (this._cleanup) {
this._cleanup();
this._cleanup = null;
}
}
update() {
computePosition(this.element.firstElementChild, this.tooltipTarget, {
placement: this.placementValue,
middleware: [
offset({
mainAxis: this.offsetValue,
crossAxis: this.crossAxisValue,
}),
flip(),
shift({ padding: 5 }),
],
}).then(({ x, y }) => {
Object.assign(this.tooltipTarget.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
}

View file

@ -0,0 +1,32 @@
class TooltipComponentPreview < ViewComponent::Preview
# @param text text
# @param placement select [top, right, bottom, left]
# @param offset number
# @param cross_axis number
# @param icon text
# @param size select [xs, sm, md, lg, xl, 2xl]
# @param color select [default, white, success, warning, destructive, current]
def default(text: "This is helpful information", placement: "top", offset: 10, cross_axis: 0, icon: "info", size: "sm", color: "default")
render DS::Tooltip.new(
text: text,
placement: placement,
offset: offset,
cross_axis: cross_axis,
icon: icon,
size: size,
color: color
)
end
def with_block_content
render DS::Tooltip.new(icon: "help-circle", color: "warning") do
tag.div do
tag.p("Custom content with formatting:", class: "font-medium mb-1") +
tag.ul(class: "list-disc list-inside text-xs") do
tag.li("First item") +
tag.li("Second item")
end
end
end
end
end