1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-18 07:56:44 +01:00

Migrate ha-toast to wa-popup instead of wa-popover (#30327)

This commit is contained in:
Aidan Timson
2026-03-25 15:51:09 +00:00
committed by GitHub
parent 84d234a330
commit 3c012c30ac
2 changed files with 146 additions and 59 deletions

View File

@@ -1,7 +1,10 @@
import "@home-assistant/webawesome/dist/components/popover/popover"; import "@home-assistant/webawesome/dist/components/popup/popup";
import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover"; import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup";
import { css, html, LitElement } from "lit"; 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 = export type ToastCloseReason =
| "dismiss" | "dismiss"
@@ -19,23 +22,100 @@ export class HaToast extends LitElement {
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000; @property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
@query("wa-popover") @query("wa-popup")
private _popover?: WaPopover; private _popup?: WaPopup;
@query(".toast")
private _toast?: HTMLDivElement;
@state() private _active = false;
@state() private _visible = false;
private _dismissTimer?: ReturnType<typeof setTimeout>; private _dismissTimer?: ReturnType<typeof setTimeout>;
private _closeReason: ToastCloseReason = "programmatic"; private _closeReason: ToastCloseReason = "programmatic";
private _transitionId = 0;
public disconnectedCallback(): void { public disconnectedCallback(): void {
clearTimeout(this._dismissTimer); clearTimeout(this._dismissTimer);
this._transitionId += 1;
super.disconnectedCallback(); super.disconnectedCallback();
} }
public async show(): Promise<void> { public async show(): Promise<void> {
await this.updateComplete;
await this._popover?.show();
clearTimeout(this._dismissTimer); 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<void> {
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) { if (this.timeoutMs > 0) {
this._dismissTimer = setTimeout(() => { this._dismissTimer = setTimeout(() => {
this.hide("timeout"); this.hide("timeout");
@@ -43,53 +123,53 @@ export class HaToast extends LitElement {
} }
} }
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> { private async _waitForTransitionEnd(): Promise<void> {
clearTimeout(this._dismissTimer); const toastEl = this._toast;
this._closeReason = reason; if (!toastEl) {
await this._popover?.hide(); return;
} }
public close(reason: ToastCloseReason = "programmatic"): void { const animations = toastEl.getAnimations({ subtree: true });
this.hide(reason); if (animations.length === 0) {
} return;
}
private _handleAfterHide() { await Promise.allSettled(animations.map((animation) => animation.finished));
this.dispatchEvent(
new CustomEvent<ToastClosedEventDetail>("toast-closed", {
detail: { reason: this._closeReason },
bubbles: true,
composed: true,
})
);
this._closeReason = "programmatic";
} }
protected render() { protected render() {
return html` return html`
<div id="toast-anchor" aria-hidden="true"></div> <wa-popup
<wa-popover
for="toast-anchor"
placement="top" placement="top"
distance="16" .active=${this._active}
.distance=${16}
skidding="0" skidding="0"
without-arrow flip
@wa-after-hide=${this._handleAfterHide} shift
> >
<div class="toast" role="status" aria-live="polite"> <div id="toast-anchor" slot="anchor" aria-hidden="true"></div>
<div
class=${classMap({
toast: true,
visible: this._visible,
})}
role="status"
aria-live="polite"
>
<span class="message">${this.labelText}</span> <span class="message">${this.labelText}</span>
<div class="actions"> <div class="actions">
<slot name="action"></slot> <slot name="action"></slot>
<slot name="dismiss"></slot> <slot name="dismiss"></slot>
</div> </div>
</div> </div>
</wa-popover> </wa-popup>
`; `;
} }
static override styles = css` static override styles = css`
#toast-anchor { #toast-anchor {
position: fixed; 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%; inset-inline-start: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 1px; width: 1px;
@@ -98,22 +178,11 @@ export class HaToast extends LitElement {
pointer-events: none; pointer-events: none;
} }
wa-popover { wa-popup::part(popup) {
--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) {
padding: 0; padding: 0;
border-radius: 4px; border-radius: var(--ha-border-radius-sm);
box-shadow: var(--wa-shadow-l);
overflow: hidden;
} }
.toast { .toast {
@@ -121,8 +190,9 @@ export class HaToast extends LitElement {
min-width: min( min-width: min(
350px, 350px,
calc( calc(
100vw - 100vw - var(--ha-space-4) - var(--safe-area-inset-left) - var(
16px - var(--safe-area-inset-left) - var(--safe-area-inset-right) --safe-area-inset-right
)
) )
); );
max-width: 650px; max-width: 650px;
@@ -131,8 +201,19 @@ export class HaToast extends LitElement {
align-items: center; align-items: center;
gap: var(--ha-space-2); gap: var(--ha-space-2);
padding: var(--ha-space-2) var(--ha-space-3); padding: var(--ha-space-2) var(--ha-space-3);
color: var(--inverse-primary-text-color); color: var(--ha-color-on-neutral-loud);
background-color: var(--inverse-surface-color); 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 { .message {
@@ -144,23 +225,27 @@ export class HaToast extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--ha-space-2); 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) { @media all and (max-width: 450px), all and (max-height: 500px) {
wa-popup::part(popup) {
border-radius: var(--ha-border-radius-square);
}
.toast { .toast {
min-width: calc( min-width: calc(
100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right) 100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right)
); );
border-radius: 0; border-radius: var(--ha-border-radius-square);
} }
} }
`; `;
} }
declare global { declare global {
interface HTMLElementEventMap { interface HASSDomEvents {
"toast-closed": CustomEvent<ToastClosedEventDetail>; "toast-closed": ToastClosedEventDetail;
} }
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,10 +1,12 @@
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize"; import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button"; import "../components/ha-button";
import "../components/ha-icon-button"; import "../components/ha-icon-button";
import "../components/ha-toast"; import "../components/ha-toast";
import type { ToastClosedEventDetail } from "../components/ha-toast";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
export interface ShowToastParams { export interface ShowToastParams {
@@ -39,7 +41,7 @@ class NotificationManager extends LitElement {
await this._toast?.hide(); await this._toast?.hide();
} }
if (!parameters || parameters.duration === 0) { if (parameters.duration === 0) {
this._parameters = undefined; this._parameters = undefined;
return; return;
} }
@@ -57,7 +59,7 @@ class NotificationManager extends LitElement {
this._toast?.show(); this._toast?.show();
} }
private _toastClosed(_ev: HTMLElementEventMap["toast-closed"]) { private _toastClosed(_ev: HASSDomEvent<ToastClosedEventDetail>) {
this._parameters = undefined; this._parameters = undefined;
} }