Files
kjol/go/webui/tooltips.go

102 lines
4.3 KiB
Go

// Port of web/kit/Tooltips.tsx.
//
// NOTE: the TSX builds tooltips on Floating.tsx (floating-ui-style measured
// positioning with flip/shift, a Portal, a FloatingContext, and hover/focus
// open-delay timers). The neutral runtime has none of that, so this port keeps
// the API + Tailwind + arrow markup and instead reveals the bubble purely in CSS
// via the Tailwind `group` marker (group-hover / group-focus-within). Placement
// is approximated with static absolute utility classes (no flip/shift and no
// measured coordinates), the numeric offset collapses to the fixed ~8px m*-2 gap
// (the TSX default), and the open delay is dropped.
package webui
import "kjol/vdom"
const tooltipCls = "bg-neutral-800 text-white text-sm px-2.5 py-1.5 rounded-default shadow-lg max-w-80 relative"
// tooltipArrowCls returns the arrow classes for a tooltip on the given base side
// (the arrow points back toward the trigger). Ported verbatim from arrowCls.
func tooltipArrowCls(base string) string {
const common = "absolute w-0 h-0"
switch base {
case "top":
return common + " -bottom-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-neutral-800"
case "bottom":
return common + " -top-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[6px] border-b-neutral-800"
case "left":
return common + " -right-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-l-[6px] border-l-neutral-800"
case "right":
return common + " -left-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-r-[6px] border-r-neutral-800"
default:
return common
}
}
// tooltipPlacementCls positions the bubble relative to the trigger wrapper.
func tooltipPlacementCls(placement string) string {
switch placement {
case "bottom":
return "top-full left-1/2 -translate-x-1/2 mt-2"
case "left":
return "right-full top-1/2 -translate-y-1/2 mr-2"
case "right":
return "left-full top-1/2 -translate-y-1/2 ml-2"
default: // "top"
return "bottom-full left-1/2 -translate-x-1/2 mb-2"
}
}
// tooltipRevealCls returns the CSS reveal classes for the given trigger mode.
func tooltipRevealCls(trigger string) string {
if trigger == "focus" {
return "invisible opacity-0 transition-opacity group-focus-within:visible group-focus-within:opacity-100"
}
return "invisible opacity-0 transition-opacity group-hover:visible group-hover:opacity-100"
}
// TooltipProps configures Tooltip. Trigger is "hover" (default) or "focus";
// Placement is "top" (default), "bottom", "left" or "right".
type TooltipProps struct {
Content *vdom.VNode
Trigger string
Placement string
Class string
}
// Tooltip wraps children (the trigger) and shows Content in a floating bubble on
// hover (or keyboard focus when Trigger is "focus").
func Tooltip(p TooltipProps, children ...*vdom.VNode) *vdom.VNode {
placement := pick(p.Placement, "top")
bubble := vdom.El("div",
vdom.Attr("role", "tooltip"),
vdom.Attr("data-floating-content", "true"),
vdom.Attr("class", cx("absolute z-[110]", tooltipPlacementCls(placement), tooltipCls, tooltipRevealCls(p.Trigger))),
)
if p.Content != nil {
bubble.Children = append(bubble.Children, p.Content)
}
bubble.Children = append(bubble.Children, vdom.El("div", vdom.Attr("class", tooltipArrowCls(placement))))
// The wrapper carries `group` + `tabindex` so both hover and keyboard-focus
// reveal work in CSS. inline-block mirrors the TSX trigger's display.
mods := []vdom.Mod{
vdom.Attr("class", cx("group relative inline-block", p.Class)),
vdom.Attr("tabindex", "0"),
}
mods = kids(mods, children)
mods = append(mods, bubble)
return vdom.El("span", mods...)
}
// HoverTooltip is Tooltip with hover reveal (the TSX default variant).
func HoverTooltip(content *vdom.VNode, placement, class string, children ...*vdom.VNode) *vdom.VNode {
return Tooltip(TooltipProps{Content: content, Trigger: "hover", Placement: placement, Class: class}, children...)
}
// FocusTooltip is Tooltip revealed on keyboard focus.
func FocusTooltip(content *vdom.VNode, placement, class string, children ...*vdom.VNode) *vdom.VNode {
return Tooltip(TooltipProps{Content: content, Trigger: "focus", Placement: placement, Class: class}, children...)
}