Eve/src/components/General/Dialog.ts

165 lines
4.6 KiB
TypeScript

import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@customElement('arx-dialog')
export class EveDialog extends LitElement {
@property({ type: String }) override title = '';
@property({ type: Boolean }) open = false;
@property({ type: Boolean }) closeOnOverlayClick = true;
@property({ type: Boolean }) closeOnEscape = true;
@property({ type: String }) width = '420px';
@property({ type: String }) maxWidth = '90%';
@state() private _previousFocus: HTMLElement | null = null;
static override styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: oklch(from var(--color-base-content) l c h / 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
backdrop-filter: blur(4px);
}
.overlay.active {
opacity: 1;
pointer-events: all;
}
.dialog-container {
background-color: var(--color-base-100);
border-radius: var(--radius-box);
border: var(--border) solid var(--color-base-300);
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
calc(var(--depth) * 8px)
oklch(from var(--color-base-content) l c h / 0.2),
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.4);
width: var(--dialog-width, 420px);
max-width: var(--dialog-max-width, 90%);
padding: 28px;
transform: scale(0.95) translateY(10px);
transition: transform 0.25s cubic-bezier(0.1, 1, 0.2, 1);
color: var(--color-base-content);
}
.overlay.active .dialog-container {
transform: scale(1) translateY(0);
}
.dialog-header {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
line-height: 1.4;
color: var(--color-base-content);
}
.dialog-content {
margin-bottom: 24px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-container:focus-within {
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
calc(var(--depth) * 8px)
oklch(from var(--color-base-content) l c h / 0.2),
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.4),
0 0 0 2px oklch(from var(--color-accent) l c h / 0.2);
}
`;
constructor() {
super();
this._handleKeyDown = this._handleKeyDown.bind(this);
}
override connectedCallback() {
super.connectedCallback();
document.addEventListener('keydown', this._handleKeyDown);
}
override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('keydown', this._handleKeyDown);
}
override updated(changedProps: Map<string, unknown>) {
if (changedProps.has('open') && this.open) {
this._previousFocus = document.activeElement as HTMLElement;
// Focus the dialog container after rendering
setTimeout(() => {
const container = this.shadowRoot?.querySelector('.dialog-container') as HTMLElement;
if (container) container.focus();
}, 50);
} else if (changedProps.has('open') && !this.open && this._previousFocus) {
this._previousFocus.focus();
}
}
private _handleKeyDown(e: KeyboardEvent) {
if (!this.open || !this.closeOnEscape) return;
if (e.key === 'Escape') this.close();
}
private _handleOverlayClick(e: MouseEvent) {
if (this.closeOnOverlayClick && e.target === e.currentTarget) {
this.close();
}
}
show() {
this.open = true;
}
close() {
this.open = false;
this.dispatchEvent(new CustomEvent('close'));
}
override render() {
return html`
<div
class="${classMap({ overlay: true, active: this.open })}"
@click=${this._handleOverlayClick}
style="--dialog-width: ${this.width}; --dialog-max-width: ${this.maxWidth};"
>
<div class="dialog-container" tabindex="-1">
${this.title ? html`<div class="dialog-header">${this.title}</div>` : ''}
<div class="dialog-content">
<slot></slot>
</div>
<div class="dialog-footer">
<slot name="footer"></slot>
</div>
</div>
</div>
`;
}
}