import { html, css, type TemplateResult, nothing, type PropertyValueMap, render as renderToElement } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { type Ref, ref, createRef } from 'lit/directives/ref.js'; import { OmniElement } from '../core/OmniElement.js'; import type { RenderFunction, RenderResult } from '../render-element/RenderElement.js'; export type { RenderFunction, RenderResult } from '../render-element/RenderElement.js'; // Considered doing this dynamically on `Modal.show` but due to reasonable tradeoffs, this is still satisfactory in terms of optimization. import '../render-element/RenderElement.js'; /** * Control to display modal content with optional header and footer content. * * @import * ```js * import '@capitec/omni-components/modal'; * ``` * * @example * ```html * * Rich Header Content * Body Content * Footer Content * * ``` * * @element omni-modal * * Registry of all properties defined by the component. * * @fires click-outside - Dispatched when a click or touch occurs outside the modal container. * * @slot header - Content to render inside the component header. * @slot - Content to render inside the component body. * @slot footer - Content to render inside the component footer. * * @csspart dialog - Internal `HTMLDialogElement` instance. * @csspart backdrop - Internal `HTMLDivElement` instance for backdrop. * @csspart container - Internal `HTMLDivElement` instance for container. * @csspart header - Internal `HTMLDivElement` instance for header. * @csspart body - Internal `HTMLDivElement` instance for body. * @csspart footer - Internal `HTMLDivElement` instance for footer. * * @cssprop --omni-modal-dialog-top - Top position for wrapping `HTMLDialogElement`. * @cssprop --omni-modal-dialog-left - Left position for wrapping `HTMLDialogElement`. * @cssprop --omni-modal-dialog-right - Right position for wrapping `HTMLDialogElement`. * @cssprop --omni-modal-dialog-bottom - Bottom position for wrapping `HTMLDialogElement`. * @cssprop --omni-modal-dialog-background - Background for wrapping `HTMLDialogElement` backdrop. * * @cssprop --omni-modal-container-padding - Padding for modal content container. * @cssprop --omni-modal-container-box-shadow - Box shadow for modal content container. * @cssprop --omni-modal-max-width - Max width for modal content container. * @cssprop --omni-modal-min-width - Min width for modal content container. * @cssprop --omni-modal-max-height - Max height for modal content container. * * @cssprop --omni-modal-header-font-color - Font color for modal header. * @cssprop --omni-modal-header-font-family - Font family for modal header. * @cssprop --omni-modal-header-font-size - Font size for modal header. * @cssprop --omni-modal-header-font-weight - Font weight for modal header. * @cssprop --omni-modal-header-background - Background for modal header. * @cssprop --omni-modal-header-padding-left - Left padding for modal header. * @cssprop --omni-modal-header-padding-top - Top padding for modal header. * @cssprop --omni-modal-header-padding-right - Right padding for modal header. * @cssprop --omni-modal-header-padding-bottom - Bottom padding for modal header. * @cssprop --omni-modal-header-border-radius - Border radius for modal header. * * @cssprop --omni-modal-body-font-color - Font color for modal body. * @cssprop --omni-modal-body-font-size - Font size for modal body. * @cssprop --omni-modal-body-font-family - Font family for modal body. * @cssprop --omni-modal-body-font-weight - Font weight for modal body. * @cssprop --omni-modal-body-padding - Padding for modal body. * @cssprop --omni-modal-body-background - Background for modal body. * @cssprop --omni-modal-body-overflow - Overflow for modal body. * * @cssprop --omni-modal-no-header-body-border-top-radius - Top border radius for modal body when there is no header. * @cssprop --omni-modal-no-footer-body-border-bottom-radius - Bottom border radius for modal body when there is no footer. * * @cssprop --omni-modal-footer-text-align - Text align for modal footer. * @cssprop --omni-modal-footer-padding - Padding for modal footer. * @cssprop --omni-modal-footer-font-color - Font color for modal footer. * @cssprop --omni-modal-footer-font-family - Font family for modal footer. * @cssprop --omni-modal-footer-font-size - Font size for modal footer. * @cssprop --omni-modal-footer-font-weight - Font weight for modal footer. * @cssprop --omni-modal-footer-background - Background for modal footer. * */ @customElement('omni-modal') export class Modal extends OmniElement { /** * Title text to be displayed in header area. * @attr [header-label] */ @property({ type: String, attribute: 'header-label', reflect: true }) headerLabel: string = ''; /** * Header text horizontal alignment: * - `left` Align header to the left. * - `center` Align header to the center. * - `right` Align header to the right. * @attr [header-align] */ @property({ type: String, attribute: 'header-align', reflect: true }) headerAlign?: 'left' | 'center' | 'right'; /** * If true, will hide the modal. * @attr */ @property({ type: Boolean, reflect: true }) hide?: boolean; /** * If true, will not display the header section of the modal * @attr [no-header] */ @property({ type: Boolean, attribute: 'no-header', reflect: true }) noHeader?: boolean; /** * If true, will not display the footer section of the modal * @attr [no-footer] */ @property({ type: Boolean, attribute: 'no-footer', reflect: true }) noFooter?: boolean; /** * If true, will not apply the modal as fullscreen on mobile viewports. * @attr [no-fullscreen] */ @property({ type: Boolean, attribute: 'no-fullscreen', reflect: true }) noFullscreen?: boolean; /** * Internal `HTMLDialogElement` instance. * @no_attribute * @ignore */ @query('dialog') dialog!: HTMLDialogElement; /** * Creates a new Modal element with the provided context and appends it to the DOM (either to document body or to provided target parent element). * @param init Initialisation context for Modal element that will be created. * @returns Modal element that was created. */ public static show(init: ModalInit): Modal | undefined { if (!init.parent) { // If no parent element is specified, the Modal will be appended to an empty div directly on the document body. init.parent = document.createElement('div'); document.body.appendChild(init.parent); } if (typeof init.parent === 'string') { // If a parent element is specified as a string, find the actual parent element instance using the provided string as an id. init.parent = document.getElementById(init.parent); if (!init.parent) { return undefined; } } const refToModal: Ref = createRef(); renderToElement( html` ${ init.header ? html`` : nothing } ${ init.footer ? html`` : nothing } `, init.parent ); return refToModal.value; } private notifyClickOutside(e: Event) { const containerElement = this.dialog.querySelector(`.container`); if (containerElement && !e.composedPath().includes(containerElement)) { this.dispatchEvent(new CustomEvent('click-outside')); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected override updated(_changedProperties: PropertyValueMap | Map): void { super.updated(_changedProperties); // `HTMLDialogElement` needs to programmatically be opened and closed. This would open on first render unless the `hide` attribute is set. if (!this.hide && !this.dialog.open) { this.dialog.showModal(); } if (this.hide && this.dialog.open) { this.dialog.close(); } } static override get styles() { return [ super.styles, css` :host([hide]){ display: none; } .modal { display: flex; position: fixed; align-items: center; justify-content: center; flex-direction: column; left: 0; top: 0; max-width: 100%; max-height: 100%; width: 100%; height: 100%; background: transparent; cursor: default; margin: unset; border-style: none; padding: unset; top: var(--omni-modal-dialog-top, inherit); left: var(--omni-modal-dialog-left, 0px); right: var(--omni-modal-dialog-right, 0px); bottom: var(--omni-modal-dialog-bottom, 0px); opacity: inherit; } ::backdrop { background: transparent; } .backdrop { background: var(--omni-modal-dialog-background, rgba(0, 0, 0, 0.3)); } .container { display: flex; flex-direction: column; justify-content: flex-start; align-items: stretch; padding: var(--omni-modal-container-padding, 0px); background: transparent; box-shadow: var(--omni-modal-container-box-shadow); max-width: var(--omni-modal-max-width,100%); min-width: var(--omni-modal-min-width, auto); max-height: var(--omni-modal-max-height, 100%); } .header { display: inline-flex; align-items: center; color: var(--omni-modal-header-font-color,var(--omni-font-color)); background: var(--omni-modal-header-background, var(--omni-background-active-color)); font-family: var(--omni-modal-header-font-family, var(--omni-font-family)); font-size: var(--omni-modal-header-font-size, var(--omni-font-size)); font-weight: var(--omni-modal-header-font-weight, var(--omni-font-weight)); padding-left: var(--omni-modal-header-padding-left, 24px); padding-top: var(--omni-modal-header-padding-top, 24px); padding-right: var(--omni-modal-header-padding-right, 24px); padding-bottom: var(--omni-modal-header-padding-bottom, 24px); position: relative; } .header.center { justify-content: center; text-align: center; } .header.right { justify-content: right; text-align: right; } .body { margin-top:0px; padding: var(--omni-modal-body-padding, 24px 24px 24px 24px); color: var(--omni-modal-body-font-color, var(--omni-font-color)); font-size: var(--omni-modal-body-font-size, var(--omni-font-size)); font-family: var(--omni-modal-body-font-family, var(--omni-font-family)); font-weight: var(--omni-modal-body-font-weight, var(--omni-font-weight)); background: var(--omni-modal-body-background, var(--omni-background-color)); line-height: 24px; overflow: var(--omni-modal-body-overflow, auto); } .body[no-header] { border-top-left-radius: var(--omni-modal-no-header-body-border-top-radius, 4px); border-top-right-radius: var(--omni-modal-no-header-body-border-top-radius, 4px); } .body[no-footer] { border-bottom-left-radius: var(--omni-modal-no-footer-body-border-bottom-radius, 4px); border-bottom-right-radius: var(--omni-modal-no-footer-body-border-bottom-radius, 4px); } .footer { align-self: stretch; text-align: var(--omni-modal-footer-text-align, right); padding: var(--omni-modal-footer-padding, 12px 12px 12px 0px); color: var(--omni-modal-footer-font-color,var(--omni-font-color)); font-size: var(--omni-modal-footer-font-size, var(--omni-font-size)); font-family: var(--omni-modal-footer-font-family, var(--omni-font-family)); font-weight: var(--omni-modal-footer-font-weight, var(--omni-font-weight)); background: var(--omni-modal-footer-background, var(--omni-background-active-color)); } ::slotted(omni-render-element) { width: 100%; } @media screen and (min-width: 767px) { .header { border-radius: var(--omni-modal-header-border-radius, 4px 4px 0px 0px); } } @media screen and (max-width: 767px) { .container:not([no-fullscreen]) { height: 100%; } .body:not([no-fullscreen]) { height: inherit; } } ` ]; } override render(): TemplateResult { return html` `; } _renderHeader() { if (this.noHeader) { return nothing; } return html`
${this.headerLabel}
`; } _renderFooter() { if (this.noFooter) { return nothing; } return html` `; } } /** * Context for `Modal.show` function to programmatically render a new `` instance. */ export type ModalInit = { /** * The id to apply to the Modal element. */ id?: string; /** * The container to append the Modal as child. If not provided will append to a new div element on the document body. */ parent?: string | HTMLElement | DocumentFragment | null; /** * A function that returns, or an instance of content to render as modal body */ body: RenderFunction | RenderResult; /** * A function that returns, or an instance of content to render in the modal header */ header?: RenderFunction | RenderResult; /** * A function that returns, or an instance of content to render in the modal footer */ footer?: RenderFunction | RenderResult; /** * Header text alignment: * - `left` Align header to the left. * - `center` Align header to the center. * - `right` Align header to the right. */ headerAlign?: 'left' | 'center' | 'right'; /** * If true, will not display the header section of the modal */ noHeader?: boolean; /** * If true, will not display the footer section of the modal */ noFooter?: boolean; /** * If true will not apply the modal as fullscreen on mobile viewports. */ noFullscreen?: boolean; }; declare global { interface HTMLElementTagNameMap { 'omni-modal': Modal; } }