import { css, html } from "lit";
import { BaseElement } from "../../core/base-element.ts";
/**
* CTAccordion - Collapsible content panels
*
* @element ct-accordion
*
* @attr {string} type - "single" | "multiple" - Whether one or multiple panels can be open
* @attr {string|string[]} value - Currently open panel(s)
* @attr {boolean} collapsible - Allow closing all panels (for single type)
*
* @slot - Default slot for ct-accordion-item elements
*
* @fires ct-change - Fired on expand/collapse with detail: { value }
*
* @example
*
*
* Section 1
* Content 1
*
*
*/
export type AccordionType = "single" | "multiple";
export class CTAccordion extends BaseElement {
static override properties = {
type: { type: String },
value: { type: String },
collapsible: { type: Boolean },
};
static override styles = css`
:host {
display: block;
width: 100%;
}
.accordion {
display: flex;
flex-direction: column;
gap: var(--accordion-gap, 0);
}
/* Allow custom styling via CSS custom properties */
:host {
--accordion-gap: 0;
}
`;
declare type: AccordionType;
declare value: string | string[];
declare collapsible: boolean;
constructor() {
super();
this.type = "single";
this.value = [];
this.collapsible = false;
}
override connectedCallback() {
super.connectedCallback();
// Listen for item toggle events
this.addEventListener(
"ct-accordion-toggle",
this.handleItemToggle as EventListener,
);
// Parse value from attribute if it's a string
const valueAttr = this.getAttribute("value");
if (valueAttr) {
try {
this.value = JSON.parse(valueAttr);
} catch {
this.value = valueAttr;
}
}
}
override disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener(
"ct-accordion-toggle",
this.handleItemToggle as EventListener,
);
}
override updated(changedProperties: Map) {
super.updated(changedProperties);
if (changedProperties.has("value") || changedProperties.has("type")) {
// Ensure value type matches accordion type
if (this.type === "single" && Array.isArray(this.value)) {
this.value = this.value[0] || "";
}
// Update item states
this.updateItemStates();
}
}
override render() {
return html`
`;
}
private handleItemToggle = (
event: CustomEvent<{ value: string; expanded: boolean }>,
) => {
const { value: itemValue, expanded } = event.detail;
event.stopPropagation();
let newValue: string | string[];
if (this.type === "single") {
if (expanded) {
newValue = itemValue;
} else {
// Only allow closing if collapsible is true
newValue = this.collapsible ? "" : itemValue;
}
} else {
const currentValues = Array.isArray(this.value)
? this.value
: [this.value].filter(Boolean);
if (expanded) {
newValue = [...currentValues, itemValue];
} else {
newValue = currentValues.filter((v) => v !== itemValue);
}
}
this.value = newValue;
// Emit change event
this.emit("ct-change", {
value: this.value,
type: this.type,
});
};
private updateItemStates(): void {
const items = this.querySelectorAll("ct-accordion-item");
const values = Array.isArray(this.value)
? this.value
: [this.value].filter(Boolean);
items.forEach((item) => {
const itemValue = item.getAttribute("value");
if (itemValue) {
const isExpanded = values.includes(itemValue);
if (isExpanded) {
item.setAttribute("expanded", "");
} else {
item.removeAttribute("expanded");
}
}
});
}
/**
* Get all accordion items
*/
get items(): NodeListOf {
return this.querySelectorAll("ct-accordion-item");
}
/**
* Expand all items (only works with type="multiple")
*/
expandAll(): void {
if (this.type === "multiple") {
const items = this.querySelectorAll("ct-accordion-item");
const values: string[] = [];
items.forEach((item) => {
const value = item.getAttribute("value");
if (value && !item.hasAttribute("disabled")) {
values.push(value);
}
});
this.value = values;
}
}
/**
* Collapse all items
*/
collapseAll(): void {
this.value = this.type === "single" ? "" : [];
}
}
globalThis.customElements.define("ct-accordion", CTAccordion);