import * as colors from '../../00colors.js'; import * as plugins from '../../00plugins.js'; import { zIndexLayers, zIndexRegistry } from '../../00zindex.js'; import { cssGeistFontFamily } from '../../00fonts.js'; import { demoFunc } from './dees-modal.demo.js'; import { customElement, html, DeesElement, property, type TemplateResult, cssManager, css, type CSSResult, unsafeCSS, unsafeHTML, state, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js'; import '../../00group-utility/dees-icon/dees-icon.js'; import '../../00group-layout/dees-tile/dees-tile.js'; import { themeDefaultStyles } from '../../00theme.js'; declare global { interface HTMLElementTagNameMap { 'dees-modal': DeesModal; } } @customElement('dees-modal') export class DeesModal extends DeesElement { // STATIC public static demo = demoFunc; public static demoGroups = ['Overlay']; public static async createAndShow(optionsArg: { heading: string; content: TemplateResult; menuOptions: plugins.tsclass.website.IMenuItem[]; width?: 'small' | 'medium' | 'large' | 'fullscreen' | number; maxWidth?: number; minWidth?: number; showCloseButton?: boolean; showHelpButton?: boolean; onHelp?: () => void | Promise; mobileFullscreen?: boolean; contentPadding?: number; }) { const body = document.body; const modal = new DeesModal(); modal.heading = optionsArg.heading; modal.content = optionsArg.content; modal.menuOptions = optionsArg.menuOptions; if (optionsArg.width) modal.width = optionsArg.width; if (optionsArg.maxWidth) modal.maxWidth = optionsArg.maxWidth; if (optionsArg.minWidth) modal.minWidth = optionsArg.minWidth; if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton; if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton; if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp; if (optionsArg.mobileFullscreen !== undefined) modal.mobileFullscreen = optionsArg.mobileFullscreen; if (optionsArg.contentPadding !== undefined) modal.contentPadding = optionsArg.contentPadding; modal.windowLayer = await DeesWindowLayer.createAndShow({ blur: true, }); modal.windowLayer.addEventListener('click', async () => { await modal.destroy(); }); body.append(modal.windowLayer); body.append(modal); // Get z-index for modal (should be above window layer) modal.modalZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(modal, modal.modalZIndex); return modal; } // INSTANCE @property({ type: String, }) accessor heading = ''; @state({}) accessor content!: TemplateResult; @state({}) accessor menuOptions: plugins.tsclass.website.IMenuItem[] = []; @property({ type: String }) accessor width: 'small' | 'medium' | 'large' | 'fullscreen' | number = 'medium'; @property({ type: Number }) accessor maxWidth!: number; @property({ type: Number }) accessor minWidth!: number; @property({ type: Boolean }) accessor showCloseButton: boolean = true; @property({ type: Boolean }) accessor showHelpButton: boolean = false; @property({ attribute: false }) accessor onHelp!: () => void | Promise; @property({ type: Boolean }) accessor mobileFullscreen: boolean = false; @property({ type: Number }) accessor contentPadding: number = 16; @state() accessor modalZIndex: number = 1000; constructor() { super(); } public static styles = [ themeDefaultStyles, cssManager.defaultStyles, css` /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host { font-family: ${cssGeistFontFamily}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; } .modalContainer { display: flex; position: fixed; top: 0px; left: 0px; width: 100vw; height: 100vh; box-sizing: border-box; align-items: center; justify-content: center; } dees-tile { will-change: transform, opacity; transform: translateY(8px) scale(0.98); opacity: 0; min-height: 120px; max-height: calc(100vh - 80px); transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease; margin: 40px; overscroll-behavior: contain; } dees-tile::part(outer) { box-shadow: 0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')}, 0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')}, 0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')}; } /* Width variations */ dees-tile.width-small { width: 380px; } dees-tile.width-medium { width: 560px; } dees-tile.width-large { width: 800px; } dees-tile.width-fullscreen { width: calc(100vw - 40px); height: calc(100vh - 40px); max-height: calc(100vh - 40px); } @media (max-width: 768px) { dees-tile { width: calc(100vw - 40px) !important; max-width: none !important; } .modalContainer { padding: 10px; } dees-tile { margin: 10px; max-height: calc(100vh - 20px); } dees-tile.mobile-fullscreen { width: 100vw !important; height: 100vh !important; max-height: 100vh !important; margin: 0; } dees-tile.mobile-fullscreen::part(outer) { border-radius: 0; border: none; } } dees-tile.show { opacity: 1; transform: translateY(0) scale(1); } dees-tile.show.predestroy { opacity: 0; transform: translateY(6px) scale(0.98); transition: transform 0.15s ease-in, opacity 0.15s ease-in; } .heading { height: 36px; display: flex; align-items: center; padding: 0 8px 0 16px; position: relative; } .heading .heading-text { flex: 1; font-weight: 500; font-size: 13px; letter-spacing: -0.01em; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 63.9%)')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .heading .header-buttons { display: flex; align-items: center; gap: 2px; flex-shrink: 0; margin-left: 8px; } .heading .header-button { width: 24px; height: 24px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s ease; background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 45%)')}; } .heading .header-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 93%)', 'hsl(0 0% 12%)')}; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .heading .header-button:active { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')}; } .heading .header-button dees-icon { width: 14px; height: 14px; display: block; } .content { overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')} transparent; } .bottomButtons { display: flex; flex-direction: row; justify-content: flex-end; align-items: center; gap: 0; height: 36px; width: 100%; box-sizing: border-box; } .bottomButtons .bottomButton { padding: 0 16px; height: 100%; text-align: center; font-size: 12px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.15s ease; background: transparent; border: none; border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 93%)', 'hsl(0 0% 11%)')}; color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')}; white-space: nowrap; display: flex; align-items: center; } .bottomButtons .bottomButton:first-child { border-left: none; } .bottomButtons .bottomButton:hover { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 10%)')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; } .bottomButtons .bottomButton:active { background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 13%)')}; } .bottomButtons .bottomButton.primary { color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; font-weight: 600; } .bottomButtons .bottomButton.primary:hover { background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')}; color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')}; } .bottomButtons .bottomButton.primary:active { background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.12)', 'hsl(213.1 93.9% 67.8% / 0.12)')}; } `, ]; public render(): TemplateResult { const widthClass = typeof this.width === 'string' ? `width-${this.width}` : ''; const customWidth = typeof this.width === 'number' ? `${this.width}px` : ''; const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : ''; const minWidthStyle = this.minWidth ? `${this.minWidth}px` : ''; const mobileFullscreenClass = this.mobileFullscreen ? 'mobile-fullscreen' : ''; return html`
${this.heading}
${this.showHelpButton ? html`
` : ''} ${this.showCloseButton ? html`
this.destroy()} title="Close">
` : ''}
${this.content}
${this.menuOptions.length > 0 ? html`
${this.menuOptions.map( (actionArg, index) => html`
{ actionArg.action(this); }}>${actionArg.name}
` )}
` : ''}
`; } private windowLayer!: DeesWindowLayer; public async firstUpdated(_changedProperties: Map) { super.firstUpdated(_changedProperties); const domtools = await this.domtoolsPromise; await domtools.convenience.smartdelay.delayFor(30); const tile = this.shadowRoot!.querySelector('dees-tile'); tile!.classList.add('show'); } public async handleOutsideClick(eventArg: MouseEvent) { eventArg.stopPropagation(); const modalContainer = this.shadowRoot!.querySelector('.modalContainer'); if (eventArg.target === modalContainer) { await this.destroy(); } } public async destroy() { const domtools = await this.domtoolsPromise; const tile = this.shadowRoot!.querySelector('dees-tile'); tile!.classList.add('predestroy'); await domtools.convenience.smartdelay.delayFor(200); document.body.removeChild(this); await this.windowLayer.destroy(); // Unregister from z-index registry zIndexRegistry.unregister(this); } private async handleHelp() { if (this.onHelp) { await this.onHelp(); } } }