131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
package webui
|
|
|
|
import "kjol/vdom"
|
|
|
|
// Port of web/kit/Accordion.tsx.
|
|
//
|
|
// NOTE: The TSX AccordionItem kept its open state in an internal signal (seeded
|
|
// by startOpen, synced from an optional isOpen prop via createEffect). Per the
|
|
// porting guide, selection state collapses to a plain value + callback: IsOpen
|
|
// is a plain bool and OnToggle fires on click (the caller flips its own state).
|
|
// The TSX startOpen prop collapses into the caller-supplied initial IsOpen /
|
|
// open[] / openIndex.
|
|
|
|
const accordionRootCls = "ui-accordion border border-neutral-200 rounded-default overflow-hidden"
|
|
const accordionItemCls = "ui-accordion-item border-b border-neutral-200 last:border-b-0"
|
|
const accordionTriggerCls = "ui-accordion-trigger flex items-center justify-between w-full py-3 px-4 text-left font-medium text-neutral-900 bg-neutral-50 cursor-pointer border-none transition-colors hover:bg-neutral-100 active:bg-neutral-200 focus:outline-hidden disabled:text-neutral-400 disabled:cursor-not-allowed disabled:bg-neutral-50"
|
|
const accordionTitleCls = "ui-accordion-title flex-1"
|
|
const accordionContentCls = "ui-accordion-content px-4 pb-4 text-neutral-700"
|
|
|
|
func accordionIconCls(open bool) string {
|
|
c := "ui-accordion-icon text-neutral-500 leading-none transition-transform duration-200"
|
|
if open {
|
|
c += " rotate-180"
|
|
}
|
|
return c
|
|
}
|
|
|
|
// AccordionItemProps configures a single AccordionItem. IsOpen is the current
|
|
// open state; OnToggle fires when the (enabled) trigger is clicked.
|
|
type AccordionItemProps struct {
|
|
IsOpen bool
|
|
Disabled bool
|
|
Title string
|
|
OnToggle func()
|
|
}
|
|
|
|
// AccordionItem is one collapsible row: a trigger button plus its content
|
|
// (children), shown when open and not disabled.
|
|
func AccordionItem(p AccordionItemProps, children ...*vdom.VNode) *vdom.VNode {
|
|
ariaExpanded := "false"
|
|
if p.IsOpen {
|
|
ariaExpanded = "true"
|
|
}
|
|
trigger := []vdom.Mod{
|
|
vdom.Attr("type", "button"),
|
|
vdom.Attr("class", accordionTriggerCls),
|
|
vdom.Attr("aria-expanded", ariaExpanded),
|
|
vdom.On(vdom.EVENT_CLICK, func() {
|
|
if p.Disabled {
|
|
return
|
|
}
|
|
if p.OnToggle != nil {
|
|
p.OnToggle()
|
|
}
|
|
}),
|
|
vdom.Span(vdom.Attr("class", accordionTitleCls), vdom.Text(p.Title)),
|
|
}
|
|
if p.Disabled {
|
|
trigger = append(trigger, vdom.Attr("disabled", "disabled"))
|
|
} else {
|
|
chev := "chevron-down"
|
|
if p.IsOpen {
|
|
chev = "chevron-up"
|
|
}
|
|
trigger = append(trigger, vdom.Span(vdom.Attr("class", accordionIconCls(p.IsOpen)),
|
|
Icon(chev, 18, ""),
|
|
))
|
|
}
|
|
|
|
mods := []vdom.Mod{vdom.Attr("class", accordionItemCls), vdom.Button(trigger...)}
|
|
if p.IsOpen && !p.Disabled {
|
|
mods = append(mods, vdom.Div(kids([]vdom.Mod{vdom.Attr("class", accordionContentCls)}, children)...))
|
|
}
|
|
return vdom.Div(mods...)
|
|
}
|
|
|
|
// AccordionItemData is one entry for Accordion / SingleAccordion.
|
|
type AccordionItemData struct {
|
|
Title string
|
|
Content *vdom.VNode
|
|
Disabled bool
|
|
}
|
|
|
|
// Accordion renders independently-collapsible items. open[i] is item i's open
|
|
// state (missing entries are closed); clicking item i calls onToggle(i) — the
|
|
// caller flips open[i].
|
|
func Accordion(items []AccordionItemData, open []bool, onToggle func(int)) *vdom.VNode {
|
|
mods := []vdom.Mod{vdom.Attr("class", accordionRootCls)}
|
|
for i, item := range items {
|
|
idx := i
|
|
isOpen := i < len(open) && open[i]
|
|
mods = append(mods, AccordionItem(AccordionItemProps{
|
|
IsOpen: isOpen,
|
|
Disabled: item.Disabled,
|
|
Title: item.Title,
|
|
OnToggle: func() {
|
|
if onToggle != nil {
|
|
onToggle(idx)
|
|
}
|
|
},
|
|
}, item.Content))
|
|
}
|
|
return vdom.Div(mods...)
|
|
}
|
|
|
|
// SingleAccordion renders items where at most one is open. openIndex is the open
|
|
// item (-1 = none); clicking an item calls onChange with the new open index —
|
|
// the clicked index, or -1 if that item was already open.
|
|
func SingleAccordion(items []AccordionItemData, openIndex int, onChange func(int)) *vdom.VNode {
|
|
mods := []vdom.Mod{vdom.Attr("class", accordionRootCls)}
|
|
for i, item := range items {
|
|
idx := i
|
|
mods = append(mods, AccordionItem(AccordionItemProps{
|
|
IsOpen: openIndex == i,
|
|
Disabled: item.Disabled,
|
|
Title: item.Title,
|
|
OnToggle: func() {
|
|
if onChange == nil {
|
|
return
|
|
}
|
|
if openIndex == idx {
|
|
onChange(-1)
|
|
} else {
|
|
onChange(idx)
|
|
}
|
|
},
|
|
}, item.Content))
|
|
}
|
|
return vdom.Div(mods...)
|
|
}
|