import { html, css, TemplateResult, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { OmniElement } from '../core/OmniElement.js'; import { type ShowToastInit, ToastStack } from './ToastStack.js'; import '../icons/Close.icon.js'; /** * Component to visually notify a user of a message. * * @import * ```js * import '@capitec/omni-components/toast'; * ``` * * @example * ```html * * * ``` * * @element omni-toast * * Registry of all properties defined by the component. * * @slot prefix - Content to render before toast message area. * @slot - Content to render inside the component message area. * @slot close - Content to render as the close button when `closeable`. * * @fires close-click - Dispatched when the close button is clicked when `closeable`. * * @cssprop --omni-toast-min-width - Min Width. * @cssprop --omni-toast-max-width - Max Width. * @cssprop --omni-toast-width - Width. * @cssprop --omni-toast-z-index - The z-index. * @cssprop --omni-toast-border-width - Border width. * @cssprop --omni-toast-border-radius - Border radius. * @cssprop --omni-toast-box-shadow - Box shadow. * @cssprop --omni-toast-padding - Container padding. * @cssprop --omni-toast-horizontal-gap - Horizontal spacing between icon from `type` and content. * @cssprop --omni-toast-icon-size - Symmetrical size of icon from `type`. * * @cssprop --omni-toast-header-font-family - Font family for header. * @cssprop --omni-toast-header-font-size - Font size for header. * @cssprop --omni-toast-header-font-weight - Font weight for header. * @cssprop --omni-toast-header-line-height - Line height for header. * * @cssprop --omni-toast-detail-font-family - Font family for detail. * @cssprop --omni-toast-detail-font-size - Font size for detail. * @cssprop --omni-toast-detail-font-weight - Font weight for detail. * @cssprop --omni-toast-detail-line-height - Line height for detail. * @cssprop --omni-toast-vertical-gap - Vertical space between detail and header. * * @cssprop --omni-toast-background - The default background applied when no `type` is set. * @cssprop --omni-toast-default-font-color - The default font color applied when no `type` is set. * @cssprop --omni-toast-border-color - Border color. * * * @cssprop --omni-toast-success-background - The background applied when `type` is set to `success`. * @cssprop --omni-toast-success-font-color - The font color applied when `type` is set to `success`. * @cssprop --omni-toast-success-border-color - The border color applied when `type` is set to `success`. * @cssprop --omni-toast-success-icon-color - The icon color applied when `type` is set to `success`. * * @cssprop --omni-toast-warning-background - The background applied when `type` is set to `warning`. * @cssprop --omni-toast-warning-font-color - The font color applied when `type` is set to `warning`. * @cssprop --omni-toast-warning-border-color - The border color applied when `type` is set to `warning`. * @cssprop --omni-toast-warning-icon-color - The icon color applied when `type` is set to `warning`. * * @cssprop --omni-toast-error-background - The background applied when `type` is set to `error`. * @cssprop --omni-toast-error-font-color - The font color applied when `type` is set to `error`. * @cssprop --omni-toast-error-border-color - The border color applied when `type` is set to `error`. * @cssprop --omni-toast-error-icon-color - The icon color applied when `type` is set to `error`. * * @cssprop --omni-toast-info-background - The background applied when `type` is set to `info`. * @cssprop --omni-toast-info-font-color - The font color applied when `type` is set to `info`. * @cssprop --omni-toast-info-border-color - The border color applied when `type` is set to `info`. * @cssprop --omni-toast-info-icon-color - The icon color applied when `type` is set to `info`. * * @cssprop --omni-toast-close-padding - Padding applied to close button when `closeable`. * @cssprop --omni-toast-close-size - Symmetrical size applied to close button when `closeable`. * */ @customElement('omni-toast') export class Toast extends OmniElement { /** * The type of toast to display. * @attr */ @property({ type: String, reflect: true }) type: 'success' | 'warning' | 'error' | 'info' | 'none' = 'none'; /** * The toast title. * @attr */ @property({ type: String, reflect: true }) header?: string; /** * The toast detail. * @attr */ @property({ type: String, reflect: true }) detail?: string; /** * If true, will display a close button that fires a `close-click` event when clicked. * @attr */ @property({ type: Boolean, reflect: true }) closeable?: boolean; private static stack?: ToastStack & { /** * When true, will append toast to the toast stack. Otherwise when false will replace any toast(s). Defaults to true. */ stack?: boolean; /** * If true, will display a close button that fires a `close-click` event when clicked and removes the toast from the stack. */ closeable?: boolean; /** * If provided will be the time in milliseconds the toast is displayed before being automatically removed from the stack. * Defaults to 3000ms when not provided. * When set to 0 will amount to no timeout. */ duration?: number; }; /** * Global singleton {@link ToastStack} used for showing a toast, either by adding to, or replacing. * Use `Toast.show` function to add or replace toasts to this instance. */ public static get current() { if (!Toast.stack) { Toast.stack = ToastStack.create({}); if (Toast.stack) { Toast.stack.closeable = false; Toast.stack.duration = 3000; Toast.stack.stack = true; } } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return Toast.stack!; } /** * Configure the global singleton {@link ToastStack} used for showing a toast, either by adding to, or replacing. * Use `Toast.show` function to add or replace toasts to this instance. * @returns The global singleton {@link ToastStack} instance. It can also be accessed via `Toast.current` static property. */ public static configure(options: { /** * The position to stack toasts */ position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; /** * Reverse the order of toast with newest toasts showed on top of the stack. By default newest toasts are showed at the bottom of the stack. */ reverse?: boolean; /** * When true, will append toast to the toast stack. Otherwise when false will replace any toast(s). Defaults to true. */ stack?: boolean; /** * If true, will display a close button that fires a `close-click` event when clicked and removes the toast from the stack. */ closeable?: boolean; /** * If provided will be the time in millisecond the toast is displayed before being automatically removed from the stack. * Defaults to 3000ms when not provided. * When set to 0 it will never be auto removed. */ duration?: number; }) { const current = Toast.current; if (options) { if (options.position) { current.position = options.position; } if (typeof options.reverse !== 'undefined') { current.reverse = options.reverse; } if (typeof options.stack !== 'undefined') { current.stack = options.stack; } if (typeof options.closeable !== 'undefined') { current.closeable = options.closeable; } if (typeof options.duration !== 'undefined') { current.duration = options.duration === 0 ? undefined : options.duration; } } return current; } /** * Show a toast message. * @returns The {@link Toast} instance that was created. */ public static show(options: ShowToastInit) { const current = Toast.current; if (!current.stack) { current.innerHTML = ''; } // Apply default config if not specified if (typeof options.closeable === 'undefined') { options.closeable = current.closeable; } if (typeof options.duration === 'undefined') { options.duration = current.duration; } return current.showToast(options); } private _raiseCloseClick(event: MouseEvent) { // Notify any subscribers that the close button was clicked. this.dispatchEvent( new CustomEvent(`close-click`, { detail: {} }) ); // Prevent the event from bubbling up. event.stopPropagation(); } static override get styles() { return [ super.styles, css` :host { display: flex; flex-direction: row; box-sizing: border-box; min-width: var(--omni-toast-min-width); max-width: var(--omni-toast-max-width); width: var(--omni-toast-width, 100%); z-index: var(--omni-toast-z-index, 10000); background: var(--omni-toast-background, var(--omni-background-color)); border-width: var(--omni-toast-border-width, 1px); border-style: var(--omni-toast-border-style, solid); border-color: var(--omni-toast-border-color, black); border-radius: var(--omni-toast-border-radius, 10px); box-shadow: var(--omni-toast-box-shadow, 0 6px 10px 0 rgba(0,0,0,0.25)); color: var(--omni-toast-default-font-color, --omni-font-color); } .container { display: flex; flex-direction: row; justify-content: stretch; align-items: center; grid-template-columns: auto 1fr; grid-template-rows: auto auto; box-sizing: border-box; min-width: var(--omni-toast-min-width); max-width: var(--omni-toast-max-width); width: var(--omni-toast-width, 100%); padding: var(--omni-toast-padding, 10px); } /* Toast Box */ .type-icon { grid-column: 1; grid-row: 1/3; margin-right: var(--omni-toast-horizontal-gap, 10px); width: var(--omni-toast-icon-size, 24px); height: var(--omni-toast-icon-size, 24px); min-width: var(--omni-toast-icon-size, 24px); min-height: var(--omni-toast-icon-size, 24px); max-width: var(--omni-toast-icon-size, 24px); max-height: var(--omni-toast-icon-size, 24px); } .content { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; } .content .header { grid-column: 2; grid-row: 1; font-family: var(--omni-toast-header-font-family, var(--omni-font-family)); font-size: var(--omni-toast-header-font-size, 16px); font-weight: var(--omni-toast-header-font-weight, bold); line-height: var(--omni-toast-header-line-height, 1.2); } ::slotted(*) { font-family: var(--omni-toast-detail-font-family, var(--omni-font-family)); font-size: var(--omni-toast-detail-font-size, 16px); font-weight: var(--omni-toast-detail-font-weight, normal); line-height: var(--omni-toast-detail-line-height, 1.2); } .content .detail { grid-column: 2; grid-row: 2; font-family: var(--omni-toast-detail-font-family, var(--omni-font-family)); font-size: var(--omni-toast-detail-font-size, 16px); font-weight: var(--omni-toast-detail-font-weight, normal); line-height: var(--omni-toast-detail-line-height, 1.2); margin-top: var(--omni-toast-vertical-gap); } /* Info Status */ :host([type="info"]) { background: var(--omni-toast-info-background, lightcyan); border-color: var(--omni-toast-info-border-color, cyan); } :host([type="info"]) .header { color: var(--omni-toast-info-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="info"]) ::slotted(*) { color: var(--omni-toast-info-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="info"]) .detail { color: var(--omni-toast-info-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="info"]) .type-icon #shape { stroke: var(--omni-toast-info-icon-color,var(--omni-toast-info-border-color, cyan)); } :host([type="info"]) .type-icon #icon { fill: var(--omni-toast-info-icon-color,var(--omni-toast-info-border-color, cyan)); } /* Success Status */ :host([type="success"]) { background: var(--omni-toast-success-background, lightgreen); border-color: var(--omni-toast-success-border-color, darkgreen); } :host([type="success"]) .header { color: var(--omni-toast-success-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="success"]) ::slotted(*) { color: var(--omni-toast-success-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="success"]) .detail { color: var(--omni-toast-success-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="success"]) .type-icon #shape { stroke: var(--omni-toast-success-icon-color,var(--omni-toast-success-border-color, darkgreen)); } :host([type="success"]) .type-icon #icon { fill: var(--omni-toast-success-icon-color,var(--omni-toast-success-border-color, darkgreen)); } /* Error Status */ :host([type="error"]) { background: var(--omni-toast-error-background, lightcoral); border-color: var(--omni-toast-error-border-color, darkred); } :host([type="error"]) .header { color: var(--omni-toast-error-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="error"]) ::slotted(*) { color: var(--omni-toast-error-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="error"]) .detail{ color: var(--omni-toast-error-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="error"]) .type-icon #shape { stroke: var(--omni-toast-error-icon-color,var(--omni-toast-error-border-color, darkred)); } :host([type="error"]) .type-icon #icon { fill: var(--omni-toast-error-icon-color,var(--omni-toast-error-border-color, darkred)); } /* Warning Status */ :host([type="warning"]) { background: var(--omni-toast-warning-background, lightyellow); border-color: var(--omni-toast-warning-border-color, orange); } :host([type="warning"]) .header { color: var(--omni-toast-warning-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="warning"]) ::slotted(*) { color: var(--omni-toast-warning-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="warning"]) .detail { color: var(--omni-toast-warning-font-color, var(--omni-toast-default-font-color, --omni-font-color)); } :host([type="warning"]) .type-icon #shape { stroke: var(--omni-toast-warning-icon-color,var(--omni-toast-warning-border-color, orange)); } :host([type="warning"]) .type-icon #icon { fill: var(--omni-toast-warning-icon-color,var(--omni-toast-warning-border-color, orange)); } .closer { display: flex; padding: var(--omni-toast-close-padding,4px); } .close-btn { display: flex; flex-direction: column; box-sizing: border-box; padding: 0px; margin: 0px; width: var(--omni-toast-close-size,24px); height: var(--omni-toast-close-size,24px); cursor: pointer; } ` ]; } override render(): TemplateResult { return html`
${this.iconTemplate()}
${this.header ? html`` : nothing} ${this.detail ? html`` : nothing}
${ this.closeable ? html`
` : nothing } `; } private iconTemplate(): TemplateResult | typeof nothing { switch (this.type) { case 'info': return html` `; case 'success': return html` `; case 'error': return html` `; case 'warning': return html` `; default: return nothing; } } } declare global { interface HTMLElementTagNameMap { 'omni-toast': Toast; } }