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()}
`; } }