1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +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 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<typeof setTimeout>;
private _closeReason: ToastCloseReason = "programmatic";
private _transitionId = 0;
public disconnectedCallback(): void {
clearTimeout(this._dismissTimer);
this._transitionId += 1;
super.disconnectedCallback();
}
public async show(): Promise<void> {
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<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) {
this._dismissTimer = setTimeout(() => {
this.hide("timeout");
@@ -43,53 +123,53 @@ export class HaToast extends LitElement {
}
}
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> {
clearTimeout(this._dismissTimer);
this._closeReason = reason;
await this._popover?.hide();
}
private async _waitForTransitionEnd(): Promise<void> {
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<ToastClosedEventDetail>("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`
<div id="toast-anchor" aria-hidden="true"></div>
<wa-popover
for="toast-anchor"
<wa-popup
placement="top"
distance="16"
.active=${this._active}
.distance=${16}
skidding="0"
without-arrow
@wa-after-hide=${this._handleAfterHide}
flip
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>
<div class="actions">
<slot name="action"></slot>
<slot name="dismiss"></slot>
</div>
</div>
</wa-popover>
</wa-popup>
`;
}
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<ToastClosedEventDetail>;
interface HASSDomEvents {
"toast-closed": ToastClosedEventDetail;
}
interface HTMLElementTagNameMap {

View File

@@ -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<ToastClosedEventDetail>) {
this._parameters = undefined;
}