/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import {LitElement, html, CSSResultArray, TemplateResult} from 'lit'; import {styles} from './nile-accordion.css'; import NileElement from '../internal/nile-element'; import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../internal/animate'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property, query } from 'lit/decorators.js'; import { getAnimation, setDefaultAnimation } from '../utilities/animation-registry'; import { waitForEvent } from '../internal/event'; import { watch } from '../internal/watch'; import type { CSSResultGroup } from 'lit'; /** * Nile detail component. * * @tag nile-accordion * */ @customElement('nile-accordion') export class NileAccordion extends NileElement { /** * @summary Accordian show a brief summary and expand to show additional content. * * @dependency nile-icon * * @slot - The accordian' main content. * @slot summary - The accordian' 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 nile-show - Emitted when the accordian opens. * @event nile-after-show - Emitted after the accordian opens and all animations are complete. * @event nile-hide - Emitted when the accordian closes. * @event nile-after-hide - Emitted after the accordian 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 accordian content. * * @animation accordian.show - The animation to use when showing accordian. You can use `height: auto` with this animation. * @animation accordian.hide - The animation to use when hiding accordian. You can use `height: auto` with this animation. */ static styles: CSSResultGroup = styles; @query('.accordian') accordian: HTMLElement; @query('.accordian__header') header: HTMLElement; @query('.accordian__body') body: HTMLElement; @query('.accordian__expand-icon-slot') expandIconSlot: HTMLSlotElement; /** * Indicates whether or not the accordian is open. You can toggle this attribute to show and hide the accordian, or you * can use the `show()` and `hide()` methods and this attribute will reflect the accordian' open state. */ @property({ type: Boolean, reflect: true }) open = false; /** * Indicates the visual style of the accordian component. Accepted values are `'dark'` or `'light'`. * Defaults to `'light'`. */ @property({ reflect: true }) variant: 'dark' | 'light' = 'light'; /** * Specifies the direction of the arrow indicator. Accepted values are `'left'` or `'right'`. * Defaults to `'right'`. */ @property({ reflect: true }) expandIconPlacement: 'left' | 'right' = 'right'; /** * Specifies the size of the accordian component. Accepted values are `'sm'`, `'md'`, or `'lg'`. * Defaults to `'md'`. */ @property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md'; /** The summary to show in the header. If you need to display HTML, use the `summary` slot instead. */ @property() summary: string; /** Disables the accordian 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 nileShow = this.emit('nile-show', { cancelable: true }); if (nileShow.defaultPrevented) { this.open = false; return; } await stopAnimations(this.body); this.body.hidden = false; const { keyframes, options } = getAnimation(this, 'accordian.show', { dir: 'ltr' }); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); this.body.style.height = 'auto'; this.emit('nile-after-show'); } else { // Hide const nileHide = this.emit('nile-hide', { cancelable: true }); if (nileHide.defaultPrevented) { this.open = true; return; } await stopAnimations(this.body); const { keyframes, options } = getAnimation(this, 'accordian.hide', { dir: 'ltr' }); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); this.body.hidden = true; this.body.style.height = 'auto'; this.emit('nile-after-hide'); } } /** Shows the accordian. */ async show() { if (this.open || this.disabled) { return undefined; } this.open = true; return waitForEvent(this, 'nile-after-show'); } /** Hides the accordian */ async hide() { if (!this.open || this.disabled) { return undefined; } this.open = false; return waitForEvent(this, 'nile-after-hide'); } render() { const isRtl = true; return html`
`; } } setDefaultAnimation('accordian.show', { keyframes: [ { height: '0', opacity: '0' }, { height: 'auto', opacity: '1' } ], options: { duration: 250, easing: 'linear' } }); setDefaultAnimation('accordian.hide', { keyframes: [ { height: 'auto', opacity: '1' }, { height: '0', opacity: '0' } ], options: { duration: 250, easing: 'linear' } }); export default NileAccordion; declare global { interface HTMLElementTagNameMap { 'nile-accordion': NileAccordion; } }