mirror of
https://github.com/home-assistant/frontend.git
synced 2026-03-29 06:13:55 +00:00
Compare commits
2 Commits
dev
...
adaptive-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4a9d2d35 | ||
|
|
7b49d9d861 |
@@ -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 > 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -48,7 +48,8 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
|
||||
(showEv as HASSDomEvent<unknown>).detail,
|
||||
dialogImport,
|
||||
undefined,
|
||||
addHistory
|
||||
addHistory,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user