From 264818bc70839742eb00699b571689cd359f07eb Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 27 Mar 2026 13:06:47 +0000 Subject: [PATCH] Fix floating ha-toast (#30344) --- src/common/feature-detect/support-popover.ts | 11 ++ src/components/ha-toast.ts | 130 +++++++++++-------- 2 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 src/common/feature-detect/support-popover.ts diff --git a/src/common/feature-detect/support-popover.ts b/src/common/feature-detect/support-popover.ts new file mode 100644 index 0000000000..414383fee8 --- /dev/null +++ b/src/common/feature-detect/support-popover.ts @@ -0,0 +1,11 @@ +/** + * Indicates whether the current browser supports the Popover API. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API + */ +export const popoverSupported = globalThis?.HTMLElement?.prototype + ? Object.prototype.hasOwnProperty.call( + globalThis.HTMLElement.prototype, + "popover" + ) + : false; diff --git a/src/components/ha-toast.ts b/src/components/ha-toast.ts index af57df5503..995af16929 100644 --- a/src/components/ha-toast.ts +++ b/src/components/ha-toast.ts @@ -1,9 +1,9 @@ -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, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../common/dom/fire_event"; +import { popoverSupported } from "../common/feature-detect/support-popover"; import { nextRender } from "../common/util/render-status"; export type ToastCloseReason = @@ -22,9 +22,6 @@ export class HaToast extends LitElement { @property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000; - @query("wa-popup") - private _popup?: WaPopup; - @query(".toast") private _toast?: HTMLDivElement; @@ -48,7 +45,6 @@ export class HaToast extends LitElement { clearTimeout(this._dismissTimer); if (this._active && this._visible) { - this._popup?.reposition(); this._setDismissTimer(); return; } @@ -62,7 +58,7 @@ export class HaToast extends LitElement { return; } - this._popup?.reposition(); + this._showToastPopover(); await nextRender(); if (transitionId !== this._transitionId) { @@ -102,6 +98,7 @@ export class HaToast extends LitElement { return; } + this._hideToastPopover(); this._active = false; await this.updateComplete; @@ -123,6 +120,34 @@ export class HaToast extends LitElement { } } + private _isPopoverOpen(): boolean { + if (!this._toast || !popoverSupported) { + return false; + } + + try { + return this._toast.matches(":popover-open"); + } catch { + return false; + } + } + + private _showToastPopover(): void { + if (!this._toast || !popoverSupported || this._isPopoverOpen()) { + return; + } + + this._toast.showPopover?.(); + } + + private _hideToastPopover(): void { + if (!this._toast || !popoverSupported || !this._isPopoverOpen()) { + return; + } + + this._toast.hidePopover?.(); + } + private async _waitForTransitionEnd(): Promise { const toastEl = this._toast; if (!toastEl) { @@ -139,59 +164,46 @@ export class HaToast extends LitElement { protected render() { return html` - - -
- ${this.labelText} -
- - -
+ ${this.labelText} +
+ +
- +
`; } static override styles = css` - #toast-anchor { - position: fixed; - bottom: calc(var(--ha-space-2) + var(--safe-area-inset-bottom)); - inset-inline-start: 50%; - transform: translateX(-50%); - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; - } - - wa-popup::part(popup) { - padding: 0; - border-radius: var(--ha-border-radius-sm); - box-shadow: var(--wa-shadow-l); - overflow: hidden; - } - .toast { + position: fixed; + inset-block-start: auto; + inset-inline-end: auto; + inset-block-end: calc( + var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) + ); + inset-inline-start: 50%; + margin: 0; + width: max-content; + height: auto; + border: none; + overflow: hidden; box-sizing: border-box; min-width: min( 350px, calc( - 100vw - var(--ha-space-4) - var(--safe-area-inset-left) - var( - --safe-area-inset-right + 100vw - var(--ha-space-4) - var(--safe-area-inset-left, 0px) - var( + --safe-area-inset-right, + 0px ) ) ); @@ -200,20 +212,25 @@ export class HaToast extends LitElement { display: flex; align-items: center; gap: var(--ha-space-2); - padding: var(--ha-space-2) var(--ha-space-3); + padding: var(--ha-space-3); color: var(--ha-color-on-neutral-loud); background-color: var(--ha-color-neutral-10); border-radius: var(--ha-border-radius-sm); + box-shadow: var(--wa-shadow-l); opacity: 0; - transform: translateY(var(--ha-space-2)); + transform: translate(-50%, var(--ha-space-2)); transition: opacity var(--ha-animation-duration-fast, 150ms) ease, transform var(--ha-animation-duration-fast, 150ms) ease; } + .toast:not(.active) { + display: none; + } + .toast.visible { opacity: 1; - transform: translateY(0); + transform: translate(-50%, 0); } .message { @@ -229,13 +246,12 @@ export class HaToast extends LitElement { } @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) + 100vw - var(--safe-area-inset-left, 0px) - var( + --safe-area-inset-right, + 0px + ) ); border-radius: var(--ha-border-radius-square); }