165 lines
4.6 KiB
TypeScript
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>
|
|
`;
|
|
}
|
|
}
|