diff --git a/src/components/ha-toast.ts b/src/components/ha-toast.ts index 818a7f6b5a..af57df5503 100644 --- a/src/components/ha-toast.ts +++ b/src/components/ha-toast.ts @@ -1,7 +1,10 @@ -import "@home-assistant/webawesome/dist/components/popover/popover"; -import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover"; +import "@home-assistant/webawesome/dist/components/popup/popup"; +import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup"; import { css, html, LitElement } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../common/dom/fire_event"; +import { nextRender } from "../common/util/render-status"; export type ToastCloseReason = | "dismiss" @@ -19,23 +22,100 @@ export class HaToast extends LitElement { @property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000; - @query("wa-popover") - private _popover?: WaPopover; + @query("wa-popup") + private _popup?: WaPopup; + + @query(".toast") + private _toast?: HTMLDivElement; + + @state() private _active = false; + + @state() private _visible = false; private _dismissTimer?: ReturnType; private _closeReason: ToastCloseReason = "programmatic"; + private _transitionId = 0; + public disconnectedCallback(): void { clearTimeout(this._dismissTimer); + this._transitionId += 1; super.disconnectedCallback(); } public async show(): Promise { - await this.updateComplete; - await this._popover?.show(); - clearTimeout(this._dismissTimer); + + if (this._active && this._visible) { + this._popup?.reposition(); + this._setDismissTimer(); + return; + } + + const transitionId = ++this._transitionId; + + this._active = true; + await this.updateComplete; + + if (transitionId !== this._transitionId) { + return; + } + + this._popup?.reposition(); + await nextRender(); + + if (transitionId !== this._transitionId) { + return; + } + + this._visible = true; + await this.updateComplete; + await this._waitForTransitionEnd(); + + if (transitionId !== this._transitionId) { + return; + } + + this._setDismissTimer(); + } + + public async hide(reason: ToastCloseReason = "programmatic"): Promise { + clearTimeout(this._dismissTimer); + this._closeReason = reason; + + if (!this._active) { + return; + } + + const transitionId = ++this._transitionId; + const wasVisible = this._visible; + + this._visible = false; + await this.updateComplete; + + if (wasVisible) { + await this._waitForTransitionEnd(); + } + + if (transitionId !== this._transitionId) { + return; + } + + this._active = false; + await this.updateComplete; + + fireEvent(this, "toast-closed", { + reason: this._closeReason, + }); + this._closeReason = "programmatic"; + } + + public close(reason: ToastCloseReason = "programmatic"): void { + this.hide(reason); + } + + private _setDismissTimer() { if (this.timeoutMs > 0) { this._dismissTimer = setTimeout(() => { this.hide("timeout"); @@ -43,53 +123,53 @@ export class HaToast extends LitElement { } } - public async hide(reason: ToastCloseReason = "programmatic"): Promise { - clearTimeout(this._dismissTimer); - this._closeReason = reason; - await this._popover?.hide(); - } + private async _waitForTransitionEnd(): Promise { + const toastEl = this._toast; + if (!toastEl) { + return; + } - public close(reason: ToastCloseReason = "programmatic"): void { - this.hide(reason); - } + const animations = toastEl.getAnimations({ subtree: true }); + if (animations.length === 0) { + return; + } - private _handleAfterHide() { - this.dispatchEvent( - new CustomEvent("toast-closed", { - detail: { reason: this._closeReason }, - bubbles: true, - composed: true, - }) - ); - this._closeReason = "programmatic"; + await Promise.allSettled(animations.map((animation) => animation.finished)); } protected render() { return html` - - -
+ +
${this.labelText}
- + `; } static override styles = css` #toast-anchor { position: fixed; - bottom: calc(8px + var(--safe-area-inset-bottom)); + bottom: calc(var(--ha-space-2) + var(--safe-area-inset-bottom)); inset-inline-start: 50%; transform: translateX(-50%); width: 1px; @@ -98,22 +178,11 @@ export class HaToast extends LitElement { pointer-events: none; } - wa-popover { - --arrow-size: 0; - --max-width: min( - 650px, - calc( - 100vw - - 16px - var(--safe-area-inset-left) - var(--safe-area-inset-right) - ) - ); - --show-duration: var(--ha-animation-duration-fast, 150ms); - --hide-duration: var(--ha-animation-duration-fast, 150ms); - } - - wa-popover::part(body) { + wa-popup::part(popup) { padding: 0; - border-radius: 4px; + border-radius: var(--ha-border-radius-sm); + box-shadow: var(--wa-shadow-l); + overflow: hidden; } .toast { @@ -121,8 +190,9 @@ export class HaToast extends LitElement { min-width: min( 350px, calc( - 100vw - - 16px - var(--safe-area-inset-left) - var(--safe-area-inset-right) + 100vw - var(--ha-space-4) - var(--safe-area-inset-left) - var( + --safe-area-inset-right + ) ) ); max-width: 650px; @@ -131,8 +201,19 @@ export class HaToast extends LitElement { align-items: center; gap: var(--ha-space-2); padding: var(--ha-space-2) var(--ha-space-3); - color: var(--inverse-primary-text-color); - background-color: var(--inverse-surface-color); + color: var(--ha-color-on-neutral-loud); + background-color: var(--ha-color-neutral-10); + border-radius: var(--ha-border-radius-sm); + opacity: 0; + transform: translateY(var(--ha-space-2)); + transition: + opacity var(--ha-animation-duration-fast, 150ms) ease, + transform var(--ha-animation-duration-fast, 150ms) ease; + } + + .toast.visible { + opacity: 1; + transform: translateY(0); } .message { @@ -144,23 +225,27 @@ export class HaToast extends LitElement { display: flex; align-items: center; gap: var(--ha-space-2); - color: rgba(255, 255, 255, 0.87); + color: var(--ha-color-on-neutral-loud); } @media all and (max-width: 450px), all and (max-height: 500px) { + wa-popup::part(popup) { + border-radius: var(--ha-border-radius-square); + } + .toast { min-width: calc( 100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right) ); - border-radius: 0; + border-radius: var(--ha-border-radius-square); } } `; } declare global { - interface HTMLElementEventMap { - "toast-closed": CustomEvent; + interface HASSDomEvents { + "toast-closed": ToastClosedEventDetail; } interface HTMLElementTagNameMap { diff --git a/src/managers/notification-manager.ts b/src/managers/notification-manager.ts index 00d082050e..1e129c2a73 100644 --- a/src/managers/notification-manager.ts +++ b/src/managers/notification-manager.ts @@ -1,10 +1,12 @@ import { mdiClose } from "@mdi/js"; import { html, LitElement, nothing } from "lit"; 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 { HomeAssistant } from "../types"; export interface ShowToastParams { @@ -39,7 +41,7 @@ class NotificationManager extends LitElement { await this._toast?.hide(); } - if (!parameters || parameters.duration === 0) { + if (parameters.duration === 0) { this._parameters = undefined; return; } @@ -57,7 +59,7 @@ class NotificationManager extends LitElement { this._toast?.show(); } - private _toastClosed(_ev: HTMLElementEventMap["toast-closed"]) { + private _toastClosed(_ev: HASSDomEvent) { this._parameters = undefined; }