import { html } from 'lit'; import { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { BootstrapElement, defineElement } from '@bootstrap-wc/core'; /** * `` — Bootstrap collapse (height-animated show/hide). * * @fires bs-show - before opening. * @fires bs-shown - after open animation. * @fires bs-hide - before closing. * @fires bs-hidden - after close animation. */ export class BsCollapse extends BootstrapElement { @property({ type: Boolean, reflect: true }) open = false; @property({ type: Boolean }) horizontal = false; @query('.collapse-inner') private _inner!: HTMLElement; private _busy = false; override updated(changed: Map) { if (changed.has('open') && !this._busy) { void this._animate(this.open, changed.get('open') === undefined); } } /** Toggle open state. */ toggle() { this.open = !this.open; } /** Show (open). */ show() { this.open = true; } /** Hide (close). */ hide() { this.open = false; } private async _animate(opening: boolean, isFirst: boolean) { if (!this._inner) return; if (isFirst) { // Skip animation on initial render. this._inner.style.removeProperty(this.horizontal ? 'width' : 'height'); return; } this._busy = true; const el = this._inner; const dimension = this.horizontal ? 'width' : 'height'; const scrollDim = this.horizontal ? el.scrollWidth : el.scrollHeight; this.dispatchEvent( new CustomEvent(opening ? 'bs-show' : 'bs-hide', { bubbles: true, composed: true }), ); el.classList.remove('collapse', 'show'); el.classList.add('collapsing'); if (this.horizontal) el.classList.add('collapse-horizontal'); if (opening) { el.style[dimension] = '0px'; // eslint-disable-next-line @typescript-eslint/no-unused-expressions el.offsetHeight; // reflow el.style[dimension] = `${scrollDim}px`; } else { el.style[dimension] = `${scrollDim}px`; // eslint-disable-next-line @typescript-eslint/no-unused-expressions el.offsetHeight; el.style[dimension] = '0px'; } await new Promise((resolve) => { const done = () => { el.removeEventListener('transitionend', done); resolve(); }; el.addEventListener('transitionend', done); setTimeout(done, 400); }); el.classList.remove('collapsing'); el.classList.add('collapse'); if (opening) el.classList.add('show'); el.style[dimension] = ''; this._busy = false; this.dispatchEvent( new CustomEvent(opening ? 'bs-shown' : 'bs-hidden', { bubbles: true, composed: true }), ); } override render() { const classes = classMap({ 'collapse-inner': true, collapse: !this._busy, 'collapse-horizontal': this.horizontal, show: this.open && !this._busy, }); return html`
`; } } defineElement('bs-collapse', BsCollapse); declare global { interface HTMLElementTagNameMap { 'bs-collapse': BsCollapse; } }