import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';
// Event types
export type CollapsibleToggleEvent = CustomEvent<{ open: boolean }>;
// Props interface
export interface CollapsibleProps {
open?: boolean;
bordered?: boolean;
shadow?: boolean;
// Indicator variants (mutually exclusive, priority: noIndicator > useX > useMinus > useChevron)
useChevron?: boolean;
useX?: boolean;
useMinus?: boolean;
noIndicator?: boolean;
onToggle?: (event: CollapsibleToggleEvent) => void;
}
export class AgCollapsible extends LitElement implements CollapsibleProps {
@property({ type: Boolean, reflect: true })
declare open: boolean;
@property({ type: Boolean, reflect: true })
declare bordered: boolean;
@property({ type: Boolean, reflect: true })
declare shadow: boolean;
@property({ type: Boolean, reflect: true, attribute: 'use-chevron' })
declare useChevron: boolean;
@property({ type: Boolean, reflect: true, attribute: 'use-x' })
declare useX: boolean;
@property({ type: Boolean, reflect: true, attribute: 'use-minus' })
declare useMinus: boolean;
@property({ type: Boolean, reflect: true, attribute: 'no-indicator' })
declare noIndicator: boolean;
@property({ attribute: false })
declare onToggle?: (event: CollapsibleToggleEvent) => void;
constructor() {
super();
this.open = false;
this.bordered = false;
this.shadow = false;
this.useChevron = true; // Default indicator
this.useX = false;
this.useMinus = false;
this.noIndicator = false;
}
static styles = css`
:host {
display: block;
}
/* Base details element */
details {
border: none;
border-radius: var(--ag-radius-md);
}
:host([bordered]) details {
border: var(--ag-border-width-1) solid var(--ag-border);
}
:host([shadow]) details {
box-shadow: var(--ag-shadow-md);
}
summary {
cursor: pointer;
padding: var(--ag-space-4);
display: flex;
justify-content: space-between;
align-items: center;
list-style: none; /* Remove default marker */
background: none;
color: var(--ag-text-primary);
}
summary::-webkit-details-marker {
display: none; /* Chrome */
}
summary:focus-visible {
outline: var(--ag-focus-width) solid rgba(var(--ag-focus), 0.5);
outline-offset: var(--ag-focus-offset);
transition: outline var(--ag-motion-medium) ease;
}
.collapsible-content {
padding: var(--ag-space-4);
margin-block-end: var(--ag-space-2);
}
/* Indicator wrapper - visible by default unless noIndicator is set */
.indicator {
display: inline-flex;
align-items: center;
flex-shrink: 0;
transition: transform var(--ag-motion-slow) ease;
}
:host([no-indicator]) .indicator {
display: none;
}
/* Chevron indicator (default): starts pointing down, rotates 180deg to point up when open */
:host([use-chevron]) details[open] > summary .indicator {
transform: rotate(180deg);
}
/* X indicator: starts rotated 180deg (upside-down plus), rotates to 45deg (X) when open */
:host([use-x]) .indicator {
transform: rotate(180deg);
}
:host([use-x]) details[open] > summary .indicator {
transform: rotate(45deg);
}
/* Minus indicator: Plus swaps to minus icon when open - no rotation needed */
:host([use-minus]) .indicator {
/* No rotation - the icon swap from plus to minus provides the visual feedback */
transform: none;
}
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
summary:focus-visible,
.indicator {
transition: none;
}
}
`;
private _renderChevronIndicator() {
return html`
`;
}
private _renderPlusIndicator() {
return html`
`;
}
private _renderMinusIndicator() {
return html`
`;
}
private _renderIndicator() {
// Priority: noIndicator > useX > useMinus > useChevron (default)
if (this.noIndicator) {
return null;
}
if (this.useX) {
return this._renderPlusIndicator();
}
if (this.useMinus) {
// Render plus when closed, minus when open
return this.open ? this._renderMinusIndicator() : this._renderPlusIndicator();
}
// Default: chevron
return this._renderChevronIndicator();
}
private _onToggle(e: Event) {
const details = e.target as HTMLDetailsElement;
this.open = details.open;
const toggleEvent = new CustomEvent<{ open: boolean }>('toggle', {
detail: { open: this.open },
bubbles: true,
composed: true,
});
this.dispatchEvent(toggleEvent);
if (this.onToggle) {
this.onToggle(toggleEvent);
}
}
render() {
return html`
${this._renderIndicator()}
`;
}
}