128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
import { createSignal, createEffect, For, Show, JSXElement } from "solid-js";
|
|
import { Icon } from "./Icons.tsx";
|
|
|
|
// Baseline Tailwind for the scoped .ui-accordion stack. The ui-* class
|
|
// names are kept so page-specific CSS overrides (e.g. sale.css,
|
|
// licensing.css) can continue to layer on top.
|
|
|
|
const ROOT = "ui-accordion border border-line rounded-default overflow-hidden";
|
|
const ITEM = "ui-accordion-item border-b border-line last:border-b-0";
|
|
const TRIGGER = "ui-accordion-trigger flex items-center justify-between w-full py-3 px-4 text-left font-medium text-ink bg-surface-muted cursor-pointer border-none transition-colors hover:bg-surface-raised active:bg-surface-strong focus:outline-hidden disabled:text-ink-faint disabled:cursor-not-allowed disabled:bg-surface-muted";
|
|
const TITLE = "ui-accordion-title flex-1";
|
|
const CONTENT = "ui-accordion-content px-4 pb-4 text-ink";
|
|
|
|
function iconCls(open: boolean): string {
|
|
return "ui-accordion-icon text-ink-muted leading-none transition-transform duration-200" +
|
|
(open ? " rotate-180" : "");
|
|
}
|
|
|
|
interface AccordionItemProps {
|
|
startOpen?: boolean;
|
|
isOpen?: boolean;
|
|
disabled?: boolean;
|
|
title?: string;
|
|
children?: JSXElement;
|
|
}
|
|
|
|
export function AccordionItem(props: AccordionItemProps) {
|
|
const getStartOpen = () => typeof props.startOpen === "function" ? (props.startOpen as () => boolean)() : !!props.startOpen;
|
|
const [isOpen, setIsOpen] = createSignal(getStartOpen());
|
|
|
|
createEffect(() => {
|
|
if (props.isOpen === undefined) return;
|
|
const v = typeof props.isOpen === "function" ? (props.isOpen as () => boolean)() : !!props.isOpen;
|
|
setIsOpen(v);
|
|
});
|
|
|
|
const isDisabled = () => typeof props.disabled === "function" ? (props.disabled as () => boolean)() : !!props.disabled;
|
|
const title = () => typeof props.title === "function" ? (props.title as () => string)() : props.title;
|
|
|
|
const toggle = () => {
|
|
if (isDisabled()) return;
|
|
setIsOpen(!isOpen());
|
|
};
|
|
|
|
return (
|
|
<div class={ITEM}>
|
|
<button type="button" class={TRIGGER} disabled={isDisabled()} onclick={toggle} aria-expanded={isOpen()}>
|
|
<span class={TITLE}>{title()}</span>
|
|
<Show when={!isDisabled()}>
|
|
<span class={iconCls(isOpen())}>
|
|
<Icon icon={isOpen() ? "chevron-up" : "chevron-down"} size={18}/>
|
|
</span>
|
|
</Show>
|
|
</button>
|
|
<Show when={isOpen() && !isDisabled()}>
|
|
<div class={CONTENT}>
|
|
{props.children}
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface AccordionItemData {
|
|
title: string;
|
|
content: JSXElement;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
interface AccordionProps {
|
|
items?: AccordionItemData[];
|
|
}
|
|
|
|
export function Accordion(props: AccordionProps) {
|
|
const items = () => props.items || [];
|
|
return (
|
|
<div class={ROOT}>
|
|
<For each={items()}>{(item) => (
|
|
<AccordionItem title={(() => {
|
|
const t = item.title;
|
|
return typeof t === "function" ? (t as () => string)() : t;
|
|
})()} disabled={(() => {
|
|
const d = item.disabled;
|
|
return typeof d === "function" ? (d as () => boolean)() : !!d;
|
|
})()}>
|
|
{item.content}
|
|
</AccordionItem>
|
|
)}</For>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface SingleAccordionProps {
|
|
items?: AccordionItemData[];
|
|
startOpen?: number;
|
|
}
|
|
|
|
export function SingleAccordion(props: SingleAccordionProps) {
|
|
const items = () => props.items || [];
|
|
const [openIndex, setOpenIndex] = createSignal(props.startOpen ?? 0);
|
|
|
|
const toggleItem = (index: number) => {
|
|
setOpenIndex(openIndex() === index ? -1 : index);
|
|
};
|
|
|
|
return (
|
|
<div class={ROOT}>
|
|
<For each={items()}>{(item, index) => (
|
|
<div class={ITEM}>
|
|
<button type="button" class={TRIGGER} disabled={!!item.disabled} onclick={() => toggleItem(index())} aria-expanded={openIndex() === index()}>
|
|
<span class={TITLE}>{item.title}</span>
|
|
<Show when={!item.disabled}>
|
|
<span class={iconCls(openIndex() === index())}>
|
|
<Icon icon={openIndex() === index() ? "chevron-up" : "chevron-down"} size={18}/>
|
|
</span>
|
|
</Show>
|
|
</button>
|
|
<Show when={openIndex() === index() && !item.disabled}>
|
|
<div class={CONTENT}>
|
|
{item.content}
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
)}</For>
|
|
</div>
|
|
);
|
|
}
|