mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-14 14:44:32 +00:00
Compare commits
12 Commits
dev
...
dashboard-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c9be595e | ||
|
|
77bbc0c286 | ||
|
|
ffdafa369b | ||
|
|
5d07dbdcb2 | ||
|
|
e48592f98b | ||
|
|
f97eff7e0c | ||
|
|
0eeabcb8e9 | ||
|
|
1b61b1451e | ||
|
|
b3fbf08285 | ||
|
|
62ab786d2e | ||
|
|
b4cba50795 | ||
|
|
7c097c3244 |
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user