import { html } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../internal/animate'; import { waitForEvent } from '../../internal/event'; import ShoelaceElement from '../../internal/shoelace-element'; import { watch } from '../../internal/watch'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry'; import { LocalizeController } from '../../utilities/localize'; import '../icon/icon'; import styles from './details.styles'; import type { CSSResultGroup } from 'lit'; /** * @summary Details show a brief summary and expand to show additional content. * * @since 2.0 * @status stable * * @dependency sl-icon * * @slot - The details' main content. * @slot summary - The details' summary. Alternatively, you can use the `summary` attribute. * @slot expand-icon - Optional expand icon to use instead of the default. Works best with ``. * @slot collapse-icon - Optional collapse icon to use instead of the default. Works best with ``. * * @event sl-show - Emitted when the details opens. * @event sl-after-show - Emitted after the details opens and all animations are complete. * @event sl-hide - Emitted when the details closes. * @event sl-after-hide - Emitted after the details closes and all animations are complete. * * @csspart base - The component's base wrapper. * @csspart header - The header that wraps both the summary and the expand/collapse icon. * @csspart summary - The container that wraps the summary. * @csspart summary-icon - The container that wraps the expand/collapse icons. * @csspart content - The details content. * * @animation details.show - The animation to use when showing details. You can use `height: auto` with this animation. * @animation details.hide - The animation to use when hiding details. You can use `height: auto` with this animation. */ @customElement('sl-details') export default class SlDetails extends ShoelaceElement { static styles: CSSResultGroup = styles; private readonly localize = new LocalizeController(this); @query('.details') details: HTMLElement; @query('.details__header') header: HTMLElement; @query('.details__body') body: HTMLElement; @query('.details__expand-icon-slot') expandIconSlot: HTMLSlotElement; /** * Indicates whether or not the details is open. You can toggle this attribute to show and hide the details, or you * can use the `show()` and `hide()` methods and this attribute will reflect the details' open state. */ @property({ type: Boolean, reflect: true }) open = false; /** The summary to show in the header. If you need to display HTML, use the `summary` slot instead. */ @property() summary: string; /** Disables the details so it can't be toggled. */ @property({ type: Boolean, reflect: true }) disabled = false; firstUpdated() { this.body.hidden = !this.open; this.body.style.height = this.open ? 'auto' : '0'; } private handleSummaryClick() { if (!this.disabled) { if (this.open) { this.hide(); } else { this.show(); } this.header.focus(); } } private handleSummaryKeyDown(event: KeyboardEvent) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (this.open) { this.hide(); } else { this.show(); } } if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { event.preventDefault(); this.hide(); } if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { event.preventDefault(); this.show(); } } @watch('open', { waitUntilFirstUpdate: true }) async handleOpenChange() { if (this.open) { // Show const slShow = this.emit('sl-show', { cancelable: true }); if (slShow.defaultPrevented) { this.open = false; return; } await stopAnimations(this.body); this.body.hidden = false; const { keyframes, options } = getAnimation(this, 'details.show', { dir: this.localize.dir() }); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); this.body.style.height = 'auto'; this.emit('sl-after-show'); } else { // Hide const slHide = this.emit('sl-hide', { cancelable: true }); if (slHide.defaultPrevented) { this.open = true; return; } await stopAnimations(this.body); const { keyframes, options } = getAnimation(this, 'details.hide', { dir: this.localize.dir() }); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); this.body.hidden = true; this.body.style.height = 'auto'; this.emit('sl-after-hide'); } } /** Shows the details. */ async show() { if (this.open || this.disabled) { return undefined; } this.open = true; return waitForEvent(this, 'sl-after-show'); } /** Hides the details */ async hide() { if (!this.open || this.disabled) { return undefined; } this.open = false; return waitForEvent(this, 'sl-after-hide'); } render() { const isRtl = this.localize.dir() === 'rtl'; return html`
`; } } setDefaultAnimation('details.show', { keyframes: [ { height: '0', opacity: '0' }, { height: 'auto', opacity: '1' } ], options: { duration: 250, easing: 'linear' } }); setDefaultAnimation('details.hide', { keyframes: [ { height: 'auto', opacity: '1' }, { height: '0', opacity: '0' } ], options: { duration: 250, easing: 'linear' } }); declare global { interface HTMLElementTagNameMap { 'sl-details': SlDetails; } }