initial port of the UI kit
This commit is contained in:
189
go/webui/floating.go
Normal file
189
go/webui/floating.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package webui
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
// Port of web/kit/Floating.tsx.
|
||||
//
|
||||
// The TSX is a floating-ui-style popover kit built on Solid context, refs,
|
||||
// getBoundingClientRect, requestAnimationFrame, window scroll/resize listeners,
|
||||
// document-level mousedown/keydown handlers, a Portal to document.body, and a
|
||||
// global single-open FloatingManager. None of that has an equivalent in the
|
||||
// neutral vdom runtime, so this port keeps the API shape (Root / Trigger /
|
||||
// Content) plus the Tailwind, and models open state as a plain value + callback.
|
||||
//
|
||||
// NOTE: All computed positioning (calculatePosition, flip, shift, offset px,
|
||||
// getBoundingClientRect, the position() signal, window scroll/resize reflow) is
|
||||
// dropped. FloatingContent is approximated with a statically-positioned
|
||||
// `absolute` element anchored to FloatingRoot's `relative` container, placed via
|
||||
// Tailwind utilities chosen from the Placement value. The original rendered the
|
||||
// content through a Portal with `position: fixed`; here it stays in-flow.
|
||||
// NOTE: The global single-open FloatingManager, the open-order stack
|
||||
// (openFloatings), outside-click dismissal, Escape-to-close, and hover-open /
|
||||
// hover-close timers are dropped — callers own open state and decide when to
|
||||
// toggle it. useFloatingContext / FloatingContextValue and the useFloatingHover
|
||||
// hook are dropped (no Solid context; nothing to share through).
|
||||
|
||||
// Placement values (subset of CSS anchor placements) understood by
|
||||
// FloatingContent's static positioning approximation.
|
||||
const (
|
||||
PlacementTop = "top"
|
||||
PlacementTopStart = "top-start"
|
||||
PlacementTopEnd = "top-end"
|
||||
PlacementBottom = "bottom"
|
||||
PlacementBottomStart = "bottom-start"
|
||||
PlacementBottomEnd = "bottom-end"
|
||||
PlacementLeft = "left"
|
||||
PlacementLeftStart = "left-start"
|
||||
PlacementLeftEnd = "left-end"
|
||||
PlacementRight = "right"
|
||||
PlacementRightStart = "right-start"
|
||||
PlacementRightEnd = "right-end"
|
||||
)
|
||||
|
||||
// PositionOptions mirrors the TSX PositionOptions. Retained for API parity; in
|
||||
// this port only Placement influences rendering (see the file-level NOTE) — the
|
||||
// numeric/flip/shift fields are not consumed because there is no measurement.
|
||||
type PositionOptions struct {
|
||||
Placement string
|
||||
Offset int
|
||||
Flip bool
|
||||
Shift bool
|
||||
ShiftPadding int
|
||||
}
|
||||
|
||||
// floatingPlacementClass maps a Placement to Tailwind utilities that position an
|
||||
// `absolute` child relative to its `relative` FloatingRoot container. The ~4px
|
||||
// default offset is approximated with the mt-1/mb-1/ml-1/mr-1 gap classes.
|
||||
func floatingPlacementClass(placement string) string {
|
||||
switch placement {
|
||||
case PlacementTop:
|
||||
return "bottom-full left-1/2 -translate-x-1/2 mb-1"
|
||||
case PlacementTopStart:
|
||||
return "bottom-full left-0 mb-1"
|
||||
case PlacementTopEnd:
|
||||
return "bottom-full right-0 mb-1"
|
||||
case PlacementBottom:
|
||||
return "top-full left-1/2 -translate-x-1/2 mt-1"
|
||||
case PlacementBottomEnd:
|
||||
return "top-full right-0 mt-1"
|
||||
case PlacementLeft:
|
||||
return "right-full top-1/2 -translate-y-1/2 mr-1"
|
||||
case PlacementLeftStart:
|
||||
return "right-full top-0 mr-1"
|
||||
case PlacementLeftEnd:
|
||||
return "right-full bottom-0 mr-1"
|
||||
case PlacementRight:
|
||||
return "left-full top-1/2 -translate-y-1/2 ml-1"
|
||||
case PlacementRightStart:
|
||||
return "left-full top-0 ml-1"
|
||||
case PlacementRightEnd:
|
||||
return "left-full bottom-0 ml-1"
|
||||
default: // PlacementBottomStart and unknown values
|
||||
return "top-full left-0 mt-1"
|
||||
}
|
||||
}
|
||||
|
||||
// FloatingRootProps configures FloatingRoot. Open is the current open state
|
||||
// (caller-held); OnOpenChange is invoked by children that toggle it. The
|
||||
// numeric/flip/shift/standalone fields are retained for API parity but are not
|
||||
// used by the static-positioning approximation (see the file-level NOTE).
|
||||
type FloatingRootProps struct {
|
||||
Open bool
|
||||
OnOpenChange func(bool)
|
||||
Placement string
|
||||
Offset int
|
||||
Flip bool
|
||||
Shift bool
|
||||
ShiftPadding int
|
||||
Standalone bool
|
||||
Class string
|
||||
}
|
||||
|
||||
// FloatingRoot wraps a Trigger + Content pair. The TSX component rendered no DOM
|
||||
// node (only a context provider); this port emits a `relative inline-block`
|
||||
// container so the absolutely-positioned FloatingContent has an anchor.
|
||||
func FloatingRoot(p FloatingRootProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
return vdom.El("div", kids([]vdom.Mod{
|
||||
vdom.Attr("class", cx("relative inline-block", p.Class)),
|
||||
}, children)...)
|
||||
}
|
||||
|
||||
// FloatingTriggerProps configures FloatingTrigger. Open feeds aria-expanded;
|
||||
// OnToggle fires on click. OpenOnHover/HoverDelay/HoverCloseDelay are retained
|
||||
// for API parity but are inert here (hover timers dropped).
|
||||
type FloatingTriggerProps struct {
|
||||
Open bool
|
||||
OnToggle func()
|
||||
OpenOnHover bool
|
||||
HoverDelay int
|
||||
HoverCloseDelay int
|
||||
Class string
|
||||
Title string
|
||||
}
|
||||
|
||||
// FloatingTrigger renders the <button> that toggles the floating content.
|
||||
//
|
||||
// NOTE: keyboard activation (Enter/Space to toggle, Escape to close) and
|
||||
// hover-open/hover-close behavior are dropped — the vdom Event exposes no key,
|
||||
// and there are no timers. Click toggling via OnToggle is preserved.
|
||||
func FloatingTrigger(p FloatingTriggerProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
ariaExpanded := "false"
|
||||
if p.Open {
|
||||
ariaExpanded = "true"
|
||||
}
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("type", "button"),
|
||||
vdom.Attr("class", p.Class),
|
||||
vdom.Attr("aria-expanded", ariaExpanded),
|
||||
vdom.Attr("aria-haspopup", "menu"),
|
||||
}
|
||||
if p.Title != "" {
|
||||
mods = append(mods, vdom.Attr("title", p.Title))
|
||||
}
|
||||
if p.OnToggle != nil {
|
||||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, p.OnToggle))
|
||||
}
|
||||
return vdom.El("button", kids(mods, children)...)
|
||||
}
|
||||
|
||||
// FloatingContentProps configures FloatingContent. Open toggles visibility;
|
||||
// Placement picks the static position (see floatingPlacementClass). Style is an
|
||||
// optional extra inline-style passthrough. OnMouseEnter/OnMouseLeave are wired
|
||||
// (used by hover popovers to keep themselves open), though the close timer they
|
||||
// fed in the TSX is gone.
|
||||
type FloatingContentProps struct {
|
||||
Open bool
|
||||
Placement string
|
||||
Class string
|
||||
Style string
|
||||
OnMouseEnter func()
|
||||
OnMouseLeave func()
|
||||
}
|
||||
|
||||
// FloatingContent renders the popover panel (role="menu"). It is hidden via the
|
||||
// `hidden` utility while closed rather than unmounted.
|
||||
//
|
||||
// NOTE: rendered in-flow as an `absolute` element instead of portaled to
|
||||
// document.body with computed `position: fixed` coordinates; z-[110] preserves
|
||||
// the TSX's stacking intent (above a z-[100] modal container).
|
||||
func FloatingContent(p FloatingContentProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
vis := "hidden"
|
||||
if p.Open {
|
||||
vis = "block"
|
||||
}
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("role", "menu"),
|
||||
vdom.Attr("data-floating-content", "true"),
|
||||
vdom.Attr("class", cx("absolute z-[110]", floatingPlacementClass(p.Placement), vis, p.Class)),
|
||||
}
|
||||
if p.Style != "" {
|
||||
mods = append(mods, vdom.Attr("style", p.Style))
|
||||
}
|
||||
if p.OnMouseEnter != nil {
|
||||
mods = append(mods, vdom.On("mouseenter", p.OnMouseEnter))
|
||||
}
|
||||
if p.OnMouseLeave != nil {
|
||||
mods = append(mods, vdom.On("mouseleave", p.OnMouseLeave))
|
||||
}
|
||||
return vdom.El("div", kids(mods, children)...)
|
||||
}
|
||||
Reference in New Issue
Block a user