Files
HomeAssistantVS/custom_components/maintenance_supporter/frontend-src/components/confirm-dialog.ts
T
2026-07-08 10:43:39 -04:00

171 lines
5.2 KiB
TypeScript

/** Reusable confirmation dialog wrapping <ha-dialog>. */
import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { HomeAssistant } from "../types";
import { t } from "../styles";
export interface ConfirmOptions {
title: string;
message: string;
confirmText?: string;
danger?: boolean;
}
export interface PromptOptions extends ConfirmOptions {
inputLabel?: string;
inputType?: string; // "text" | "date" etc.
inputValue?: string;
}
export interface PromptResult {
confirmed: boolean;
value: string;
}
export class MaintenanceConfirmDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _title = "";
@state() private _message = "";
@state() private _confirmText = "";
@state() private _danger = false;
@state() private _inputLabel = "";
@state() private _inputType = "";
@state() private _inputValue = "";
private _resolve: ((value: boolean) => void) | null = null;
private _promptResolve: ((value: PromptResult) => void) | null = null;
public confirm(opts: ConfirmOptions): Promise<boolean> {
this._title = opts.title;
this._message = opts.message;
this._confirmText = opts.confirmText || "OK";
this._danger = opts.danger || false;
this._inputLabel = "";
this._inputType = "";
this._inputValue = "";
this._open = true;
return new Promise<boolean>((resolve) => {
this._resolve = resolve;
this._promptResolve = null;
});
}
public prompt(opts: PromptOptions): Promise<PromptResult> {
this._title = opts.title;
this._message = opts.message;
this._confirmText = opts.confirmText || "OK";
this._danger = opts.danger || false;
this._inputLabel = opts.inputLabel || "";
this._inputType = opts.inputType || "text";
this._inputValue = opts.inputValue || "";
this._open = true;
return new Promise<PromptResult>((resolve) => {
this._promptResolve = resolve;
this._resolve = null;
});
}
private _cancel(): void {
this._open = false;
if (this._promptResolve) {
this._promptResolve({ confirmed: false, value: "" });
this._promptResolve = null;
}
this._resolve?.(false);
this._resolve = null;
}
private _confirmAction(): void {
this._open = false;
if (this._promptResolve) {
this._promptResolve({ confirmed: true, value: this._inputValue });
this._promptResolve = null;
}
this._resolve?.(true);
this._resolve = null;
}
render() {
if (!this._open) return nothing;
const lang = this.hass?.language || "en";
return html`
<ha-dialog open @closed=${this._cancel}>
<div class="dialog-title">${this._title}</div>
<div class="content">
${this._message}
${this._inputLabel ? html`
<!-- Native <input> rather than <ha-textfield>: HA loads
ha-textfield lazily for its own panels, so inside this custom
panel it can be unregistered and render with zero height —
the prompt then shows no field at all (caught live testing
the pause/replace prompts; same fix as complete-dialog). -->
<label class="field">
<span class="field-label">${this._inputLabel}</span>
<input class="field-input"
type="${this._inputType || "text"}"
.value=${this._inputValue}
@input=${(e: Event) => (this._inputValue = (e.target as HTMLInputElement).value)} />
</label>
` : nothing}
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._cancel}>
${t("cancel", lang)}
</ha-button>
<ha-button
class="${this._danger ? "danger" : ""}"
@click=${this._confirmAction}
>
${this._confirmText}
</ha-button>
</div>
</ha-dialog>
`;
}
static styles = css`
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.field { display: flex; flex-direction: column; gap: 4px; margin-top: 12px; }
.field-label { font-size: 12px; color: var(--secondary-text-color); }
.field-input {
padding: 8px 10px; font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
font-family: inherit; width: 100%; box-sizing: border-box;
}
.field-input:focus { outline: none; border-color: var(--primary-color); }
.content {
padding: 8px 0;
min-width: 280px;
line-height: 1.5;
display: flex;
flex-direction: column;
gap: 12px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
ha-textfield {
display: block;
}
ha-button.danger {
--mdc-theme-primary: var(--error-color, #f44336);
}
`;
}
if (!customElements.get("maintenance-confirm-dialog")) {
customElements.define("maintenance-confirm-dialog", MaintenanceConfirmDialog);
}