shoelace/src/components/alert/alert.ts

245 wiersze
7.5 KiB
TypeScript
Czysty Zwykły widok Historia

2023-01-13 20:43:55 +00:00
import '../icon-button/icon-button';
2022-03-24 12:01:09 +00:00
import { animateTo, stopAnimations } from '../../internal/animate';
2023-01-13 20:43:55 +00:00
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
2022-03-24 12:01:09 +00:00
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
2023-01-13 20:43:55 +00:00
import { HasSlotController } from '../../internal/slot';
import { html } from 'lit';
2022-06-09 22:14:38 +00:00
import { LocalizeController } from '../../utilities/localize';
2023-01-13 20:43:55 +00:00
import { waitForEvent } from '../../internal/event';
import { watch } from '../../internal/watch';
import ShoelaceElement from '../../internal/shoelace-element';
2021-07-10 00:45:44 +00:00
import styles from './alert.styles';
import type { CSSResultGroup } from 'lit';
2020-07-15 21:30:37 +00:00
2020-09-18 13:40:21 +00:00
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
2020-09-16 21:00:48 +00:00
2020-07-15 21:30:37 +00:00
/**
* @summary Alerts are used to display important messages inline or as toast notifications.
2023-01-12 15:26:25 +00:00
* @documentation https://shoelace.style/components/alert
2020-07-15 21:30:37 +00:00
* @status stable
2023-01-12 15:26:25 +00:00
* @since 2.0
2020-07-15 21:30:37 +00:00
*
2021-02-26 14:09:13 +00:00
* @dependency sl-icon-button
*
2022-12-06 16:18:14 +00:00
* @slot - The alert's main content.
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
2020-07-15 21:30:37 +00:00
*
2021-06-25 20:25:46 +00:00
* @event sl-show - Emitted when the alert opens.
2021-09-27 21:58:14 +00:00
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
2021-06-25 20:25:46 +00:00
* @event sl-hide - Emitted when the alert closes.
2021-09-27 21:58:14 +00:00
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
*
2022-12-06 16:18:14 +00:00
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the optional icon.
* @csspart message - The container that wraps the alert's main content.
* @csspart close-button - The close button, an `<sl-icon-button>`.
* @csspart close-button__base - The close button's exported `base` part.
*
2021-06-25 20:25:46 +00:00
* @animation alert.show - The animation to use when showing the alert.
* @animation alert.hide - The animation to use when hiding the alert.
2020-07-15 21:30:37 +00:00
*/
2021-03-18 13:04:23 +00:00
@customElement('sl-alert')
2022-08-17 15:37:37 +00:00
export default class SlAlert extends ShoelaceElement {
static styles: CSSResultGroup = styles;
2020-07-15 21:30:37 +00:00
private autoHideTimeout: number;
2022-03-09 20:54:18 +00:00
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
2022-06-09 22:14:38 +00:00
private readonly localize = new LocalizeController(this);
2021-03-06 17:01:39 +00:00
2022-11-17 14:35:44 +00:00
@query('[part~="base"]') base: HTMLElement;
2020-10-13 16:41:57 +00:00
2022-12-06 16:18:14 +00:00
/**
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
*/
2021-07-01 00:04:46 +00:00
@property({ type: Boolean, reflect: true }) open = false;
2020-07-15 21:30:37 +00:00
2022-12-06 16:18:14 +00:00
/** Enables a close button that allows the user to dismiss the alert. */
2021-07-01 00:04:46 +00:00
@property({ type: Boolean, reflect: true }) closable = false;
2020-07-15 21:30:37 +00:00
2022-12-06 16:18:14 +00:00
/** The alert's theme variant. */
2021-12-13 22:38:40 +00:00
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
2020-07-15 21:30:37 +00:00
/**
2021-02-26 14:09:13 +00:00
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
2022-12-06 16:18:14 +00:00
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
* the alert will not close on its own.
*/
@property({ type: Number }) duration = Infinity;
2021-03-06 17:01:39 +00:00
2021-06-02 12:47:55 +00:00
firstUpdated() {
2021-05-19 17:46:18 +00:00
this.base.hidden = !this.open;
2020-07-15 21:30:37 +00:00
}
2023-01-03 20:04:07 +00:00
private restartAutoHide() {
clearTimeout(this.autoHideTimeout);
if (this.open && this.duration < Infinity) {
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
}
}
private handleCloseClick() {
this.hide();
}
private handleMouseMove() {
this.restartAutoHide();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
// Show
this.emit('sl-show');
if (this.duration < Infinity) {
this.restartAutoHide();
}
await stopAnimations(this.base);
this.base.hidden = false;
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.emit('sl-after-show');
} else {
// Hide
this.emit('sl-hide');
clearTimeout(this.autoHideTimeout);
await stopAnimations(this.base);
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.base.hidden = true;
this.emit('sl-after-hide');
}
}
@watch('duration')
handleDurationChange() {
this.restartAutoHide();
}
2020-07-15 21:30:37 +00:00
/** Shows the alert. */
2021-05-19 17:46:18 +00:00
async show() {
2021-05-27 20:29:10 +00:00
if (this.open) {
return undefined;
2020-07-15 21:30:37 +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
}
/** Hides the alert */
2021-05-19 17:46:18 +00:00
async hide() {
2021-05-27 20:29:10 +00:00
if (!this.open) {
return undefined;
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
}
2020-09-17 20:27:11 +00:00
/**
2021-03-22 15:03:24 +00:00
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
* calling this method again. The returned promise will resolve after the alert is hidden.
2020-09-17 20:27:11 +00:00
*/
async toast() {
2021-01-04 19:11:23 +00:00
return new Promise<void>(resolve => {
if (toastStack.parentElement === null) {
2020-09-18 13:40:21 +00:00
document.body.append(toastStack);
2020-09-17 20:27:11 +00:00
}
2021-02-26 14:09:13 +00:00
toastStack.appendChild(this);
2021-03-22 15:03:24 +00:00
// Wait for the toast stack to render
requestAnimationFrame(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
this.clientWidth;
this.show();
2021-03-22 15:03:24 +00:00
});
2020-09-17 20:27:11 +00:00
2021-02-26 14:09:13 +00:00
this.addEventListener(
2020-10-09 21:45:16 +00:00
'sl-after-hide',
2020-09-17 20:27:11 +00:00
() => {
2021-02-26 14:09:13 +00:00
toastStack.removeChild(this);
2020-09-17 20:27:11 +00:00
resolve();
2020-09-18 13:40:21 +00:00
// Remove the toast stack from the DOM when there are no more alerts
if (toastStack.querySelector('sl-alert') === null) {
2020-09-18 13:40:21 +00:00
toastStack.remove();
2020-09-17 20:27:11 +00:00
}
},
{ once: true }
);
});
}
2020-07-15 21:30:37 +00:00
render() {
2021-02-26 14:09:13 +00:00
return html`
2020-10-13 16:41:57 +00:00
<div
part="base"
2021-02-26 14:09:13 +00:00
class=${classMap({
2020-10-13 16:41:57 +00:00
alert: true,
'alert--open': this.open,
'alert--closable': this.closable,
2022-03-09 20:54:18 +00:00
'alert--has-icon': this.hasSlotController.test('icon'),
2021-12-13 22:38:40 +00:00
'alert--primary': this.variant === 'primary',
'alert--success': this.variant === 'success',
'alert--neutral': this.variant === 'neutral',
'alert--warning': this.variant === 'warning',
'alert--danger': this.variant === 'danger'
2021-02-26 14:09:13 +00:00
})}
2020-10-13 16:41:57 +00:00
role="alert"
2021-02-26 14:09:13 +00:00
aria-hidden=${this.open ? 'false' : 'true'}
2021-07-07 12:13:07 +00:00
@mousemove=${this.handleMouseMove}
2020-10-13 16:41:57 +00:00
>
2022-12-02 22:03:59 +00:00
<slot name="icon" part="icon" class="alert__icon"></slot>
2020-10-13 16:41:57 +00:00
2022-12-02 22:03:59 +00:00
<slot part="message" class="alert__message" aria-live="polite"></slot>
2020-10-13 16:41:57 +00:00
2021-02-26 14:09:13 +00:00
${this.closable
? html`
2022-03-09 20:54:18 +00:00
<sl-icon-button
part="close-button"
exportparts="base:close-button__base"
class="alert__close-button"
2022-11-29 16:03:58 +00:00
name="x-lg"
2022-03-09 20:54:18 +00:00
library="system"
2022-11-29 14:25:45 +00:00
label=${this.localize.term('close')}
2022-03-09 20:54:18 +00:00
@click=${this.handleCloseClick}
></sl-icon-button>
2021-02-26 14:09:13 +00:00
`
: ''}
2020-10-13 16:41:57 +00:00
</div>
2021-02-26 14:09:13 +00:00
`;
2020-07-15 21:30:37 +00:00
}
}
setDefaultAnimation('alert.show', {
keyframes: [
2022-12-01 20:38:59 +00:00
{ opacity: 0, scale: 0.8 },
{ opacity: 1, scale: 1 }
],
2021-05-26 11:29:59 +00:00
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('alert.hide', {
keyframes: [
2022-12-01 20:38:59 +00:00
{ opacity: 1, scale: 1 },
{ opacity: 0, scale: 0.8 }
],
2021-05-26 11:29:59 +00:00
options: { duration: 250, easing: 'ease' }
});
2021-03-12 14:09:08 +00:00
declare global {
interface HTMLElementTagNameMap {
'sl-alert': SlAlert;
}
}