Compare commits

...

12 Commits

Author SHA1 Message Date
Aidan Timson
a7c9be595e Clear params
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 12:54:07 +01:00
Aidan Timson
77bbc0c286 Make const 2026-04-08 12:51:44 +01:00
Aidan Timson
ffdafa369b ally: reduce announcements to 20 second intervals, last at 5 seconds 2026-04-08 12:50:52 +01:00
Aidan Timson
5d07dbdcb2 Plural rules for second(s) 2026-04-08 12:44:00 +01:00
Aidan Timson
e48592f98b Default 2026-04-08 12:27:16 +01:00
Aidan Timson
f97eff7e0c Refactor, clear toast on disconnect 2026-04-08 12:26:40 +01:00
Aidan Timson
0eeabcb8e9 Always clear 2026-04-08 12:23:03 +01:00
Aidan Timson
1b61b1451e Keep edit mode behavior 2026-04-08 12:20:42 +01:00
Aidan Timson
b3fbf08285 Remove extras
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 12:19:30 +01:00
Aidan Timson
62ab786d2e Use fetch instead of window reload on timeout 2026-04-08 12:17:28 +01:00
Aidan Timson
b4cba50795 Cancel action on dismiss 2026-04-08 12:06:48 +01:00
Aidan Timson
7c097c3244 Auto reload dashboard 2026-04-08 12:00:04 +01:00
4 changed files with 148 additions and 15 deletions

View File

@@ -26,6 +26,8 @@ export interface ToastClosedEventDetail {
export class HaToast extends LitElement {
@property({ attribute: "label-text" }) public labelText = "";
@property({ attribute: "announce-text" }) public announceText?: string;
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
@query(".toast")
@@ -186,8 +188,6 @@ export class HaToast extends LitElement {
active: this._active,
visible: this._visible,
})}
role="status"
aria-live="polite"
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
<span class="message">${this.labelText}</span>
@@ -196,6 +196,14 @@ export class HaToast extends LitElement {
<slot name="dismiss"></slot>
</div>
</div>
<span
class="assistive-message"
role="status"
aria-live=${this._active ? "polite" : "off"}
aria-atomic="true"
>
${this.announceText ?? this.labelText}
</span>
`;
}
@@ -246,6 +254,18 @@ export class HaToast extends LitElement {
min-width: 0;
}
.assistive-message {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.actions {
display: flex;
align-items: center;

View File

@@ -1,12 +1,16 @@
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
import "../components/ha-toast";
import type { ToastClosedEventDetail } from "../components/ha-toast";
import type {
ToastCloseReason,
ToastClosedEventDetail,
} from "../components/ha-toast";
import type { HomeAssistant } from "../types";
export interface ShowToastParams {
@@ -15,13 +19,18 @@ export interface ShowToastParams {
message:
| string
| { translationKey: LocalizeKeys; args?: Record<string, string> };
announceMessage?:
| string
| { translationKey: LocalizeKeys; args?: Record<string, string> };
action?: ToastActionParams;
onClose?: (reason: ToastCloseReason) => void;
duration?: number;
dismissable?: boolean;
}
export interface ToastActionParams {
action: () => void;
primary?: boolean;
text:
| string
| { translationKey: LocalizeKeys; args?: Record<string, string> };
@@ -72,8 +81,10 @@ class NotificationManager extends LitElement {
this._toast?.show();
}
private _toastClosed(_ev: HASSDomEvent<ToastClosedEventDetail>) {
private _toastClosed(ev: HASSDomEvent<ToastClosedEventDetail>) {
const onClose = this._parameters?.onClose;
this._parameters = undefined;
onClose?.(ev.detail.reason);
}
protected render() {
@@ -88,13 +99,23 @@ class NotificationManager extends LitElement {
this._parameters.message.args
)
: this._parameters.message}
.announceText=${this._parameters.announceMessage
? typeof this._parameters.announceMessage !== "string"
? this.hass.localize(
this._parameters.announceMessage.translationKey,
this._parameters.announceMessage.args
)
: this._parameters.announceMessage
: undefined}
.timeoutMs=${this._parameters.duration!}
@toast-closed=${this._toastClosed}
>
${this._parameters?.action
? html`
<ha-button
appearance="plain"
appearance=${ifDefined(
this._parameters?.action.primary ? undefined : "plain"
)}
size="small"
slot="action"
@click=${this._buttonClicked}

View File

@@ -40,6 +40,7 @@ import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
import type { Lovelace } from "./types";
import { generateDefaultView } from "./views/default-view";
import { fetchDashboards } from "../../data/lovelace/dashboard";
import type { ToastCloseReason } from "../../components/ha-toast";
(window as any).loadCardHelpers = () => import("./custom-card-helpers");
@@ -49,6 +50,11 @@ interface LovelacePanelConfig {
let editorLoaded = false;
let resourcesLoaded = false;
const EXTERNAL_UPDATE_INTERVAL = 1000;
const EXTERNAL_UPDATE_RELOAD_DELAY = 60;
const EXTERNAL_UPDATE_TOAST_ID = "lovelace_external_update";
const EXTERNAL_UPDATE_A11Y_ANNOUNCE_INTERVAL = 20;
const EXTERNAL_UPDATE_A11Y_FINAL_ANNOUNCE_SECONDS = 5;
declare global {
interface HASSDomEvents {
@@ -83,6 +89,14 @@ export class LovelacePanel extends LitElement {
private _loading = false;
private _externalUpdateCountdownSeconds = EXTERNAL_UPDATE_RELOAD_DELAY;
private _externalUpdateCountdownTimer?: number;
private _externalUpdateLastAnnouncedSeconds?: number;
private _externalUpdateAnnouncementMessage?: string;
public connectedCallback(): void {
super.connectedCallback();
if (
@@ -98,12 +112,14 @@ export class LovelacePanel extends LitElement {
);
} else if (this._fetchConfigOnConnect) {
// Config was changed when we were not at the lovelace panel
this._fetchConfig(false);
this._fetchConfig();
}
window.addEventListener("connection-status", this._handleConnectionStatus);
}
public disconnectedCallback(): void {
this._clearExternalUpdateReloadCountdown();
this._hideExternalUpdateReloadToast();
super.disconnectedCallback();
// On the main dashboard we want to stay subscribed as that one is cached.
if (this.urlPath !== null && this._unsubUpdates) {
@@ -171,7 +187,7 @@ export class LovelacePanel extends LitElement {
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.lovelace && this._panelState !== "error" && !this._loading) {
this._fetchConfig(false);
this._fetchConfig();
}
}
@@ -273,7 +289,7 @@ export class LovelacePanel extends LitElement {
private _handleConnectionStatus = (ev) => {
// reload lovelace on reconnect so we are sure we have the latest config
if (ev.detail === "connected") {
this._fetchConfig(false);
this._fetchConfig();
}
};
@@ -301,22 +317,97 @@ export class LovelacePanel extends LitElement {
return;
}
if (!this.lovelace?.editMode && this._panelState !== "yaml-editor") {
this._fetchConfig(false);
this._fetchConfig();
return;
}
this._startExternalUpdateReloadCountdown();
}
private _startExternalUpdateReloadCountdown() {
this._clearExternalUpdateReloadCountdown();
this._externalUpdateCountdownSeconds = EXTERNAL_UPDATE_RELOAD_DELAY;
this._externalUpdateLastAnnouncedSeconds = undefined;
this._externalUpdateAnnouncementMessage = undefined;
this._showExternalUpdateReloadToast();
this._externalUpdateCountdownTimer = window.setInterval(() => {
this._externalUpdateCountdownSeconds -= 1;
if (this._externalUpdateCountdownSeconds <= 0) {
this._clearExternalUpdateReloadCountdown();
this._refreshNowExternalUpdateReload();
return;
}
this._showExternalUpdateReloadToast();
}, EXTERNAL_UPDATE_INTERVAL);
}
private _showExternalUpdateReloadToast() {
const shouldAnnounce =
this._externalUpdateLastAnnouncedSeconds === undefined ||
this._externalUpdateCountdownSeconds ===
EXTERNAL_UPDATE_A11Y_FINAL_ANNOUNCE_SECONDS ||
this._externalUpdateCountdownSeconds %
EXTERNAL_UPDATE_A11Y_ANNOUNCE_INTERVAL ===
0;
if (shouldAnnounce) {
this._externalUpdateLastAnnouncedSeconds =
this._externalUpdateCountdownSeconds;
this._externalUpdateAnnouncementMessage = this.hass!.localize(
"ui.panel.lovelace.externally_updated_toast.countdown_message",
{
seconds: this._externalUpdateCountdownSeconds,
}
);
}
showToast(this, {
id: EXTERNAL_UPDATE_TOAST_ID,
message: this.hass!.localize(
"ui.panel.lovelace.externally_updated_toast.message"
"ui.panel.lovelace.externally_updated_toast.countdown_message",
{
seconds: this._externalUpdateCountdownSeconds,
}
),
announceMessage: this._externalUpdateAnnouncementMessage,
action: {
action: () => this._fetchConfig(false),
text: this.hass!.localize("ui.common.refresh"),
action: this._refreshNowExternalUpdateReload,
primary: true,
text: this.hass!.localize(
"ui.panel.lovelace.externally_updated_toast.refresh_now"
),
},
onClose: (reason: ToastCloseReason) => {
if (reason === "dismiss") {
this._clearExternalUpdateReloadCountdown();
}
},
duration: -1,
dismissable: false,
dismissable: true,
});
}
private _clearExternalUpdateReloadCountdown() {
if (this._externalUpdateCountdownTimer) {
clearInterval(this._externalUpdateCountdownTimer);
this._externalUpdateCountdownTimer = undefined;
}
this._externalUpdateLastAnnouncedSeconds = undefined;
this._externalUpdateAnnouncementMessage = undefined;
}
private _hideExternalUpdateReloadToast() {
showToast(this, {
id: EXTERNAL_UPDATE_TOAST_ID,
message: "",
duration: 0,
});
}
private _refreshNowExternalUpdateReload = () => {
this._clearExternalUpdateReloadCountdown();
this._hideExternalUpdateReloadToast();
this._fetchConfig();
};
public get urlPath() {
return this.panel!.url_path;
}
@@ -325,7 +416,7 @@ export class LovelacePanel extends LitElement {
this._fetchConfig(true);
}
private async _fetchConfig(forceDiskRefresh: boolean) {
private async _fetchConfig(forceDiskRefresh = false) {
this._loading = true;
let conf: LovelaceConfig;

View File

@@ -10069,7 +10069,8 @@
"starting": "Home Assistant is starting. Not everything may be available yet."
},
"externally_updated_toast": {
"message": "Dashboard updated in another session. Refreshing will discard your unsaved changes."
"countdown_message": "Dashboard updated in another session. This page will refresh in {seconds, plural, one {# second} other {# seconds}}.",
"refresh_now": "Refresh now"
},
"components": {
"timestamp-display": {