shoelace/src/components/details/details.ts

226 wiersze
6.3 KiB
TypeScript
Czysty Zwykły widok Historia

import { LitElement, html, unsafeCSS } from 'lit';
2021-05-27 21:00:43 +00:00
import { customElement, property, query } from 'lit/decorators.js';
2021-03-06 17:01:39 +00:00
import { classMap } from 'lit-html/directives/class-map';
2021-05-26 11:31:26 +00:00
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
2021-03-18 13:04:23 +00:00
import { event, EventEmitter, watch } from '../../internal/decorators';
2021-05-27 20:29:10 +00:00
import { waitForEvent } from '../../internal/event';
2021-02-26 14:09:13 +00:00
import { focusVisible } from '../../internal/focus-visible';
2021-05-26 11:31:26 +00:00
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
2021-05-03 19:08:17 +00:00
import styles from 'sass:./details.scss';
2020-07-15 21:30:37 +00:00
let id = 0;
/**
2020-07-17 10:09:10 +00:00
* @since 2.0
2020-07-15 21:30:37 +00:00
* @status stable
*
2021-02-26 14:09:13 +00:00
* @dependency sl-icon
*
2020-07-15 21:30:37 +00:00
* @slot - The details' content.
* @slot summary - The details' summary. Alternatively, you can use the summary prop.
*
* @part base - The component's base wrapper.
2020-08-02 10:54:17 +00:00
* @part header - The summary header.
2020-07-15 21:30:37 +00:00
* @part summary - The details summary.
* @part summary-icon - The expand/collapse summary icon.
* @part content - The details content.
*
2021-05-26 11:31:26 +00:00
* @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.
2020-07-15 21:30:37 +00:00
*/
2021-03-18 13:04:23 +00:00
@customElement('sl-details')
2021-03-09 00:14:32 +00:00
export default class SlDetails extends LitElement {
2021-03-06 17:01:39 +00:00
static styles = unsafeCSS(styles);
@query('.details') details: HTMLElement;
@query('.details__header') header: HTMLElement;
@query('.details__body') body: HTMLElement;
2021-02-26 14:09:13 +00:00
private componentId = `details-${++id}`;
2021-05-26 11:31:26 +00:00
private hasInitialized = false;
2020-07-15 21:30:37 +00:00
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */
2021-03-06 20:34:33 +00:00
@property({ type: Boolean, reflect: true }) open = false;
2020-07-15 21:30:37 +00:00
2020-08-03 16:54:47 +00:00
/** The summary to show in the details header. If you need to display HTML, use the `summary` slot instead. */
2021-04-02 11:47:25 +00:00
@property() summary: string;
2020-07-15 21:30:37 +00:00
2021-02-26 14:09:13 +00:00
/** Disables the details so it can't be toggled. */
2021-03-06 20:34:33 +00:00
@property({ type: Boolean, reflect: true }) disabled = false;
2021-03-06 17:01:39 +00:00
2021-05-27 20:29:10 +00:00
/** Emitted when the details opens. */
2021-03-06 17:01:39 +00:00
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the details opens and all transitions are complete. */
2021-04-01 12:50:32 +00:00
@event('sl-after-show') slAfterShow: EventEmitter<void>;
2020-07-15 21:30:37 +00:00
2021-05-27 20:29:10 +00:00
/** Emitted when the details closes. */
2021-03-06 17:01:39 +00:00
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the details closes and all transitions are complete. */
2021-04-01 12:50:32 +00:00
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
2021-03-06 17:01:39 +00:00
2021-06-02 12:47:55 +00:00
connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => focusVisible.observe(this.details));
}
2020-07-15 21:30:37 +00:00
2021-06-02 12:47:55 +00:00
firstUpdated() {
2021-02-05 21:09:05 +00:00
this.body.hidden = !this.open;
2021-03-30 13:30:00 +00:00
this.body.style.height = this.open ? 'auto' : '0';
2021-05-26 11:31:26 +00:00
2021-06-02 12:47:55 +00:00
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
2020-07-15 21:30:37 +00:00
}
2021-03-06 17:01:39 +00:00
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.details);
2020-07-15 21:30:37 +00:00
}
2021-05-27 20:29:10 +00:00
/** Shows the details. */
2021-05-26 11:31:26 +00:00
async show() {
2021-05-27 20:29:10 +00:00
if (this.open) {
2020-08-13 14:29:31 +00:00
return;
}
2020-07-15 21:30:37 +00:00
2021-05-26 11:31:26 +00:00
this.open = true;
2021-05-27 20:29:10 +00:00
return waitForEvent(this, 'sl-after-show');
2020-07-15 21:30:37 +00:00
}
2021-05-27 20:29:10 +00:00
/** Hides the details */
2021-05-26 11:31:26 +00:00
async hide() {
2021-05-27 20:29:10 +00:00
if (!this.open) {
2020-08-13 14:29:31 +00:00
return;
}
2020-07-15 21:30:37 +00:00
this.open = false;
2021-05-27 20:29:10 +00:00
return waitForEvent(this, 'sl-after-hide');
2020-07-15 21:30:37 +00:00
}
handleSummaryClick() {
if (!this.disabled) {
this.open ? this.hide() : this.show();
this.header.focus();
}
}
handleSummaryKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.open ? this.hide() : this.show();
}
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.preventDefault();
this.hide();
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault();
this.show();
}
}
2021-03-06 20:09:12 +00:00
@watch('open')
2021-05-27 20:29:10 +00:00
async handleOpenChange() {
if (!this.hasInitialized) {
return;
}
if (this.open) {
// Show
this.slShow.emit();
await stopAnimations(this);
this.body.hidden = false;
const { keyframes, options } = getAnimation(this, 'details.show');
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
this.body.style.height = 'auto';
this.slAfterShow.emit();
} else {
// Hide
this.slHide.emit();
await stopAnimations(this);
const { keyframes, options } = getAnimation(this, 'details.hide');
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
this.body.hidden = true;
this.body.style.height = 'auto';
this.slAfterHide.emit();
}
2021-02-26 14:09:13 +00:00
}
2020-07-15 21:30:37 +00:00
render() {
2021-02-26 14:09:13 +00:00
return html`
2020-07-15 21:30:37 +00:00
<div
part="base"
2021-02-26 14:09:13 +00:00
class=${classMap({
2020-07-15 21:30:37 +00:00
details: true,
'details--open': this.open,
'details--disabled': this.disabled
2021-02-26 14:09:13 +00:00
})}
2020-07-15 21:30:37 +00:00
>
<header
2020-08-02 10:54:17 +00:00
part="header"
2021-02-26 14:09:13 +00:00
id=${`${this.componentId}-header`}
2020-07-15 21:30:37 +00:00
class="details__header"
role="button"
2021-02-26 14:09:13 +00:00
aria-expanded=${this.open ? 'true' : 'false'}
aria-controls=${`${this.componentId}-content`}
aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
2021-03-06 17:01:39 +00:00
@click=${this.handleSummaryClick}
@keydown=${this.handleSummaryKeyDown}
2020-07-15 21:30:37 +00:00
>
<div part="summary" class="details__summary">
2021-02-26 14:09:13 +00:00
<slot name="summary">${this.summary}</slot>
2020-07-15 21:30:37 +00:00
</div>
<span part="summary-icon" class="details__summary-icon">
<sl-icon name="chevron-right" library="system"></sl-icon>
2020-07-15 21:30:37 +00:00
</span>
</header>
2021-05-26 11:31:26 +00:00
<div class="details__body">
2020-07-15 21:30:37 +00:00
<div
part="content"
2021-02-26 14:09:13 +00:00
id=${`${this.componentId}-content`}
2020-07-15 21:30:37 +00:00
class="details__content"
role="region"
2021-02-26 14:09:13 +00:00
aria-labelledby=${`${this.componentId}-header`}
2020-07-15 21:30:37 +00:00
>
2021-03-06 17:01:39 +00:00
<slot></slot>
2020-07-15 21:30:37 +00:00
</div>
</div>
</div>
2021-02-26 14:09:13 +00:00
`;
2020-07-15 21:30:37 +00:00
}
}
2021-05-26 11:31:26 +00:00
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' }
});
2021-03-12 14:09:08 +00:00
declare global {
interface HTMLElementTagNameMap {
'sl-details': SlDetails;
}
}