Compare commits

...

2 Commits

Author SHA1 Message Date
Aidan Timson
cc4a9d2d35 Test with more info dialog (not intended final) 2026-03-18 13:45:52 +00:00
Aidan Timson
7b49d9d861 Create experimental popover support for adaptive dialog 2026-03-18 13:45:38 +00:00
8 changed files with 416 additions and 80 deletions

View File

@@ -22,6 +22,7 @@ type DialogType =
| "basic-subtitle-below"
| "basic-subtitle-above"
| "allow-mode-change"
| "popover"
| "form"
| "actions"
| "large"
@@ -33,6 +34,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
@state() private _hass?: HomeAssistant;
@state() private _dialogAnchor?: HTMLElement;
protected firstUpdated() {
const hass = provideHass(this);
this._hass = hass;
@@ -69,6 +72,9 @@ export class DemoHaAdaptiveDialog extends LitElement {
<ha-button @click=${this._handleOpenDialog("form")}
>Adaptive dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("popover")}
>Desktop popover adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("allow-mode-change")}
>Adaptive dialog with allow mode change</ha-button
>
@@ -164,6 +170,22 @@ export class DemoHaAdaptiveDialog extends LitElement {
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "popover"}
desktop-mode="popover"
.dialogAnchor=${this._dialogAnchor}
header-title="Desktop popover adaptive dialog"
header-subtitle="Uses the opener as the popover anchor"
@closed=${this._handleClosed}
>
<div>
On desktop, this opens as an anchored popover. On narrow screens, it
still falls back to a bottom sheet.
</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "allow-mode-change"}
.allowModeChange=${this._openDialog === "allow-mode-change"}
header-title="Adaptive dialog with allow mode change"
header-subtitle="Resize the window while this dialog is open"
@@ -196,7 +218,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
<p>
The <code>ha-adaptive-dialog</code> component automatically switches
between two modes based on screen size:
between modes based on screen size:
</p>
<ul>
@@ -205,6 +227,11 @@ export class DemoHaAdaptiveDialog extends LitElement {
870px and height &gt; 500px). Renders as a centered dialog using
<code>ha-dialog</code>.
</li>
<li>
<strong>Desktop popover mode:</strong> Set
<code>desktop-mode="popover"</code> and pass a
<code>dialogAnchor</code> to render an anchored desktop popover.
</li>
<li>
<strong>Bottom sheet mode:</strong> Used on mobile devices and
smaller screens (width ≤ 870px or height ≤ 500px). Renders as a
@@ -213,7 +240,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ul>
<p>
By default, the mode is determined at mount time and then stays fixed
By default, the mode is determined when opened and then stays fixed
while the dialog is open. To allow switching modes while the viewport
changes, use the <code>allow-mode-change</code> attribute.
</p>
@@ -442,8 +469,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
<tr>
<td><code>width</code></td>
<td>
Preferred dialog width preset (dialog mode only, ignored in
bottom sheet mode).
Preferred dialog width preset (dialog and popover mode only,
ignored in bottom sheet mode).
</td>
<td><code>medium</code></td>
<td>
@@ -451,6 +478,12 @@ export class DemoHaAdaptiveDialog extends LitElement {
<code>full</code>
</td>
</tr>
<tr>
<td><code>desktop-mode</code></td>
<td>Desktop presentation mode.</td>
<td><code>dialog</code></td>
<td><code>dialog</code>, <code>popover</code></td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
@@ -578,19 +611,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
<tbody>
<tr>
<td><code>opened</code></td>
<td>
Fired when the adaptive dialog is shown (dialog mode only).
</td>
<td>Fired when the adaptive dialog is shown.</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>
Fired after the adaptive dialog is hidden (dialog mode only).
</td>
<td>Fired after the adaptive dialog is hidden.</td>
</tr>
<tr>
<td><code>after-show</code></td>
<td>Fired after show animation completes (dialog mode only).</td>
<td>Fired after show animation completes.</td>
</tr>
</tbody>
</table>
@@ -614,11 +643,13 @@ export class DemoHaAdaptiveDialog extends LitElement {
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
private _handleOpenDialog = (dialog: DialogType) => (ev?: Event) => {
this._dialogAnchor = ev?.currentTarget as HTMLElement | undefined;
this._openDialog = dialog;
};
private _handleClosed = () => {
this._dialogAnchor = undefined;
this._openDialog = false;
};

View File

@@ -1,15 +1,19 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query";
import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet";
import "./ha-dialog";
import "./ha-dialog-header";
import "./ha-icon-button";
import "./ha-dialog";
import type { DialogWidth } from "./ha-dialog";
type DialogSheetMode = "dialog" | "bottom-sheet";
type DesktopDialogMode = "dialog" | "popover";
type DialogSheetMode = DesktopDialogMode | "bottom-sheet";
/**
* Home Assistant adaptive dialog component
@@ -18,24 +22,23 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
* @extends {LitElement}
*
* @summary
* A responsive dialog component that automatically switches between a full dialog (ha-dialog)
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
* A responsive dialog component that automatically switches between a full dialog (ha-dialog),
* an anchored popover on desktop, and a bottom sheet (ha-bottom-sheet) based on screen size.
*
* @slot header - Replace the entire header area.
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
* @slot headerNavigationIcon - Leading header action (for example close/back button).
* @slot headerTitle - Custom title content (used when header-title is not set).
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
* @slot headerActionItems - Trailing header actions (for example buttons, menus).
* @slot - Dialog/sheet content body.
* @slot footer - Dialog/sheet footer content.
*
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog/sheet backdrop filter.
* @cssprop --dialog-box-shadow - Dialog box shadow (dialog mode only).
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
* @cssprop --dialog-box-shadow - Dialog box shadow (dialog and popover mode only).
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog and popover mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog and popover mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog and popover mode only).
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog/sheet scrim backdrop filter.
* @cssprop --dialog-backdrop-filter - Dialog/sheet scrim backdrop filter (legacy).
* @cssprop --mdc-dialog-scrim-color - Dialog/sheet scrim color (legacy).
@@ -46,30 +49,33 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
*
* @attr {boolean} open - Controls the dialog/sheet open state.
* @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard".
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog and popover mode only). Defaults to "medium".
* @attr {("dialog"|"popover")} desktop-mode - Desktop presentation. Defaults to "dialog".
* @attr {boolean} prevent-scrim-close - Prevents closing by clicking the scrim/overlay.
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
* @attr {boolean} flexcontent - Makes the content body a flex container.
* @attr {boolean} without-header - Hides the default header.
* @attr {boolean} allow-mode-change - When set, the component can switch between dialog and bottom-sheet modes as the viewport changes.
* @attr {boolean} allow-mode-change - When set, the component can switch between modes as the viewport changes.
* @prop {Element | null | undefined} dialogAnchor - Anchor element used when desktop-mode is set to "popover".
*
* @event opened - Fired when the dialog/sheet is shown.
* @event closed - Fired after the dialog/sheet is hidden.
* @event after-show - Fired after show animation completes.
*
* @remarks
* **Responsive Behavior:**
* **Responsive behavior:**
* The component automatically switches between dialog and bottom sheet modes based on viewport size.
* Dialog mode is used for screens wider than 870px and taller than 500px.
* Set `desktop-mode="popover"` together with `dialogAnchor` to show an anchored desktop popover instead.
* Bottom sheet mode is used for mobile devices and smaller screens.
*
* By default, the mode is determined once at mount time and is then kept stable to avoid state
* By default, the mode is determined when opened and is then kept stable to avoid state
* loss (like form resets) during viewport changes. Set `allow-mode-change` to opt into live
* mode switching while the dialog is open.
*
* **Focus Management:**
* **Focus management:**
* To automatically focus an element when opened, add the `autofocus` attribute to it.
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
@@ -93,9 +99,15 @@ export class HaAdaptiveDialog extends LitElement {
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@property({ type: String, reflect: true, attribute: "desktop-mode" })
public desktopMode: DesktopDialogMode = "dialog";
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
public preventScrimClose = false;
@property({ attribute: false })
public dialogAnchor?: Element | null;
@property({ attribute: "header-title" })
public headerTitle?: string;
@@ -116,28 +128,67 @@ export class HaAdaptiveDialog extends LitElement {
@state() private _mode: DialogSheetMode = "dialog";
@state() private _narrow = false;
@state() private _popoverOpen = false;
private _unsubMediaQuery?: () => void;
private _modeSet = false;
private _allowPopoverHide = false;
private _openPopoverRaf?: number;
connectedCallback() {
super.connectedCallback();
this._unsubMediaQuery = listenMediaQuery(
"(max-width: 870px), (max-height: 500px)",
(matches) => {
if (!this._modeSet || this.allowModeChange) {
this._mode = matches ? "bottom-sheet" : "dialog";
this._modeSet = true;
this._narrow = matches;
if (!this.open || this.allowModeChange) {
this._updateMode();
}
}
);
}
protected willUpdate(changedProperties: PropertyValues<this>) {
if (
changedProperties.has("open") ||
((!this.open || this.allowModeChange) &&
(changedProperties.has("desktopMode") ||
changedProperties.has("dialogAnchor") ||
changedProperties.has("allowModeChange")))
) {
this._updateMode();
}
if (
changedProperties.has("open") &&
!this.open &&
this._mode === "popover"
) {
this._allowPopoverHide = true;
}
if (!this.open || this._mode !== "popover") {
this._cancelPopoverOpen();
this._popoverOpen = false;
}
}
protected updated() {
if (this.open && this._mode === "popover" && !this._popoverOpen) {
this._schedulePopoverOpen();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this._cancelPopoverOpen();
this._unsubMediaQuery?.();
this._unsubMediaQuery = undefined;
this._modeSet = false;
this._allowPopoverHide = false;
}
render() {
@@ -152,48 +203,41 @@ export class HaAdaptiveDialog extends LitElement {
.open=${this.open}
.preventScrimClose=${this.preventScrimClose}
>
${!this.withoutHeader
? html`
<slot name="header" slot="header">
<ha-dialog-header
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ??
"Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span
slot="title"
class="title"
id="ha-dialog-title"
>
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle"
>${this.headerSubtitle}</span
>`
: html`<slot
name="headerSubtitle"
slot="subtitle"
></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>
`
: nothing}
${this._renderHeader(true)}
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-bottom-sheet>
`;
}
if (this._mode === "popover") {
return html`
<wa-popover
.open=${this._popoverOpen}
.anchor=${this.dialogAnchor ?? null}
placement="bottom-start"
auto-size="vertical"
auto-size-padding="16"
trap-focus
.ariaLabelledby=${this.ariaLabelledBy ||
(this.headerTitle !== undefined ? "ha-dialog-title" : undefined)}
.ariaDescribedby=${this.ariaDescribedBy}
@wa-show=${this._handlePopoverShow}
@wa-hide=${this._handlePopoverHide}
@wa-after-show=${this._handlePopoverAfterShow}
@wa-after-hide=${this._handlePopoverAfterHide}
>
<div class="popover-surface" @click=${this._handlePopoverClick}>
${this._renderHeader()}
<div class="content-wrapper">
<div class="body"><slot></slot></div>
</div>
<slot name="footer"></slot>
</div>
</wa-popover>
`;
}
return html`
<ha-dialog
.hass=${this.hass}
@@ -212,7 +256,7 @@ export class HaAdaptiveDialog extends LitElement {
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
@@ -225,9 +269,203 @@ export class HaAdaptiveDialog extends LitElement {
`;
}
private _renderHeader(slotHeader = false) {
if (this.withoutHeader) {
return nothing;
}
const content = html`
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
`;
return html`
${slotHeader
? html`<slot name="header" slot="header">${content}</slot>`
: html`<slot name="header">${content}</slot>`}
`;
}
private _updateMode() {
this._mode = this._computeMode();
}
private _schedulePopoverOpen() {
if (this._openPopoverRaf !== undefined) {
return;
}
this._openPopoverRaf = requestAnimationFrame(() => {
this._openPopoverRaf = undefined;
if (this.open && this._mode === "popover") {
this._popoverOpen = true;
}
});
}
private _cancelPopoverOpen() {
if (this._openPopoverRaf === undefined) {
return;
}
cancelAnimationFrame(this._openPopoverRaf);
this._openPopoverRaf = undefined;
}
private _computeMode(): DialogSheetMode {
if (this._narrow) {
return "bottom-sheet";
}
if (this.desktopMode === "popover" && this.dialogAnchor) {
return "popover";
}
return "dialog";
}
private _handlePopoverClick(ev: Event) {
const path = ev.composedPath();
const shouldClose = path.some((node) => {
if (!(node instanceof HTMLElement)) {
return false;
}
return (
node.getAttribute("data-dialog") === "close" ||
node.getAttribute("data-popover") === "close" ||
node.closest('[data-dialog="close"], [data-popover="close"]') !== null
);
});
const isHeaderNavigationClick = path.some(
(node) =>
node instanceof HTMLSlotElement && node.name === "headerNavigationIcon"
);
if (!shouldClose && !isHeaderNavigationClick) {
return;
}
this._allowPopoverHide = true;
this.open = false;
}
private _handlePopoverShow(ev: Event) {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
fireEvent(this, "opened");
}
private _handlePopoverHide(ev: Event) {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
if (this.preventScrimClose && !this._allowPopoverHide) {
ev.preventDefault();
}
}
private _handlePopoverAfterShow(ev: Event) {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
fireEvent(this, "after-show");
}
private _handlePopoverAfterHide(ev: Event) {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
this._allowPopoverHide = false;
fireEvent(this, "closed");
}
static get styles() {
return [
css`
wa-popover {
--full-width: var(
--ha-dialog-width-full,
min(95vw, var(--safe-width))
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
--wa-panel-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
--wa-color-surface-border: transparent;
--max-width: var(--width);
}
:host([width="small"]) wa-popover {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
}
:host([width="large"]) wa-popover {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-popover {
--width: var(--full-width);
}
wa-popover::part(body) {
padding: 0;
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
min-width: var(--width, var(--full-width));
max-width: var(--width);
max-height: var(
--ha-dialog-max-height,
calc(var(--safe-height) - var(--ha-space-20))
);
overflow: hidden;
color: var(--primary-text-color);
-webkit-backdrop-filter: var(
--ha-dialog-surface-backdrop-filter,
none
);
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
-webkit-user-select: text;
user-select: text;
}
.popover-surface {
display: flex;
flex-direction: column;
min-height: 0;
max-height: inherit;
}
ha-bottom-sheet {
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
--ha-bottom-sheet-surface-background: var(
@@ -241,6 +479,47 @@ export class HaAdaptiveDialog extends LitElement {
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
);
}
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.body {
position: relative;
padding: var(
--dialog-content-padding,
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
);
overflow: auto;
flex-grow: 1;
}
:host([flexcontent]) .body {
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
slot[name="footer"] {
display: block;
padding: 0;
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
box-sizing: border-box;
}
`,
];
}

View File

@@ -1,6 +1,5 @@
import type { LitElement } from "lit";
import { fireEvent } from "../common/dom/fire_event";
import type { HaDialog } from "../components/ha-dialog";
import type { Constructor } from "../types";
import type { HassDialogNext } from "./make-dialog-manager";
@@ -13,6 +12,8 @@ export const DialogMixin = <
class extends superClass implements HassDialogNext<P> {
declare public params?: P;
public dialogAnchor?: Element;
private _closePromise?: Promise<boolean>;
private _closeResolve?: (value: boolean) => void;
@@ -23,8 +24,9 @@ export const DialogMixin = <
}
const dialogElement = this.shadowRoot?.querySelector(
"ha-dialog"
) as HaDialog | null;
"ha-adaptive-dialog, ha-dialog, ha-bottom-sheet"
) as { open: boolean } | null;
if (dialogElement) {
this._closePromise = new Promise<boolean>((resolve) => {
this._closeResolve = resolve;

View File

@@ -21,11 +21,13 @@ declare global {
}
export interface HassDialog<T = unknown> extends HTMLElement {
dialogAnchor?: Element;
showDialog(params: T);
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
}
export interface HassDialogNext<T = unknown> extends HTMLElement {
dialogAnchor?: Element;
params?: T;
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
}
@@ -34,6 +36,7 @@ export interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
dialogParams?: T;
dialogAnchor?: Element;
addHistory?: boolean;
parentElement?: LitElement;
}
@@ -71,6 +74,7 @@ export const FOCUS_TARGET = Symbol.for("HA focus target");
* @param dialogImport Optional lazy import used when the dialog has not been loaded yet.
* @param parentElement Optional parent to append the dialog to instead of root element.
* @param addHistory Whether to add/update browser history so back navigation closes dialogs.
* @param dialogAnchor Optional anchor element used by anchored dialog variants.
* @returns `true` if the dialog was shown (or could be shown), `false` if it could not be loaded.
*/
export const showDialog = async (
@@ -79,7 +83,8 @@ export const showDialog = async (
dialogParams: unknown,
dialogImport?: () => Promise<unknown>,
parentElement?: LitElement,
addHistory = true
addHistory = true,
dialogAnchor?: Element
): Promise<boolean> => {
if (!(dialogTag in LOADED)) {
if (!dialogImport) {
@@ -124,7 +129,8 @@ export const showDialog = async (
dialogParams,
dialogImport,
parentElement,
addHistory
addHistory,
dialogAnchor
);
}
const dialogIndex = OPEN_DIALOG_STACK.findIndex(
@@ -169,8 +175,10 @@ export const showDialog = async (
}
if ("showDialog" in dialogElement!) {
dialogElement.dialogAnchor = dialogAnchor;
dialogElement.showDialog(dialogParams);
} else {
dialogElement!.dialogAnchor = dialogAnchor;
dialogElement!.params = dialogParams;
}
@@ -267,6 +275,7 @@ export const makeDialogManager = (element: LitElement & ProvideHassElement) => {
dialogTag,
dialogImport,
dialogParams,
dialogAnchor,
addHistory,
parentElement,
} = e.detail;
@@ -277,7 +286,8 @@ export const makeDialogManager = (element: LitElement & ProvideHassElement) => {
dialogParams,
dialogImport,
parentElement,
addHistory
addHistory,
dialogAnchor
);
}
);

View File

@@ -92,6 +92,7 @@ export interface MoreInfoDialogParams {
large?: boolean;
data?: Record<string, any>;
parentElement?: LitElement;
anchor?: Element;
}
type View = "info" | "history" | "settings" | "related" | "add_to";
@@ -118,6 +119,8 @@ const DEFAULT_VIEW: View = "info";
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public dialogAnchor?: Element;
@property({ type: Boolean, reflect: true }) public large = false;
@state() private _fill = false;
@@ -169,6 +172,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._childView = undefined;
this.large = params.large ?? false;
this._fill = false;
if (params.anchor) {
this.dialogAnchor = params.anchor;
}
this._open = true;
this._loadEntityRegistryEntry();
}
@@ -205,6 +211,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._currView = DEFAULT_VIEW;
this._childView = undefined;
this._isEscapeEnabled = true;
this.dialogAnchor = undefined;
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
window.removeEventListener("show-dialog", this._disableEscapeKeyClose);
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -581,6 +588,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
.hass=${this.hass}
.open=${this._open}
.width=${this._fill ? "full" : this.large ? "large" : "medium"}
desktop-mode=${this.dialogAnchor && !this.large && !this._fill
? "popover"
: "dialog"}
.dialogAnchor=${this.dialogAnchor ?? null}
@closed=${this._dialogClosed}
@opened=${this._handleOpened}
.preventScrimClose=${this._currView === "settings" ||

View File

@@ -106,7 +106,7 @@ export const handleAction = async (
config.camera_image ||
config.image_entity;
if (entityId) {
fireEvent(node, "hass-more-info", { entityId });
fireEvent(node, "hass-more-info", { entityId, anchor: node });
} else {
showToast(node, {
message: hass.localize(

View File

@@ -48,7 +48,8 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
(showEv as HASSDomEvent<unknown>).detail,
dialogImport,
undefined,
addHistory
addHistory,
undefined
);
});
}

View File

@@ -42,7 +42,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
data: ev.detail.data,
},
() => import("../dialogs/more-info/ha-more-info-dialog"),
ev.detail.parentElement
ev.detail.parentElement,
true,
ev.detail.anchor
);
}
};