Migrate add/edit resources dialog to @material/web (#21933)

* Remove dashboard resources options from advanced mode

* Add ha-dialog-new, use it for dashboard resources

* Add ha-dialog-new shake; Move resources delete to table

* Improve ha-dialog-new, resource-detail

* Rename ha-dialog-new to ha-md-dialog

* Update src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts

Fix dialogClosed method naming

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Add ha-md-dialog polyfill

* Fix ha-md-dialog for iOS 12

* Fix ha-md-dialog polyfill loading

* Fix ha-md-dialog open prop

* Fix ha-md-dialog legacy loading

* Improve ha-md-dialog legacy loading

* Fix multiple polyfill loads in ha-md-dialog

* Fix polyfill handleOpen in ha-md-dialog

* Improve polyfill handleOpen in ha-md-dialog

* Improve polyfill handleOpen ordering in ha-md-dialog

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Wendelin 2024-09-16 14:27:13 +02:00 committed by GitHub
parent ab91a4b814
commit 9e4dc0d39e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 287 additions and 115 deletions

View File

@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/") staticPath("polyfills/")
); );
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
} }
function copyLoaderJS(staticDir) { function copyLoaderJS(staticDir) {

View File

@ -106,6 +106,7 @@
"date-fns-tz": "3.1.3", "date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11", "element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",

View File

@ -0,0 +1,146 @@
import { MdDialog } from "@material/web/dialog/dialog";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends MdDialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
_handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
...super.styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: 400;
--md-dialog-headline-size: 1.574rem;
--md-dialog-supporting-text-size: 1rem;
--md-dialog-supporting-text-line-height: 1.5rem;
@media all and (max-width: 450px), all and (max-height: 500px) {
min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
min-height: 100%;
max-height: 100%;
border-radius: 0;
}
}
:host ::slotted(ha-dialog-header) {
display: contents;
}
.scrim {
z-index: 10; // overlay navigation
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}

View File

@ -322,22 +322,16 @@ export class HaConfigLovelaceDashboards extends LitElement {
hasFab hasFab
clickable clickable
> >
${this.hass.userData?.showAdvanced <ha-button-menu slot="toolbar-icon" activatable>
? html` <ha-icon-button
<ha-button-menu slot="toolbar-icon" activatable> slot="trigger"
<ha-icon-button .label=${this.hass.localize("ui.common.menu")}
slot="trigger" .path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.menu")} ></ha-icon-button>
.path=${mdiDotsVertical} <ha-clickable-list-item href="/config/lovelace/resources">
></ha-icon-button> ${this.hass.localize("ui.panel.config.lovelace.resources.caption")}
<ha-clickable-list-item href="/config/lovelace/resources"> </ha-clickable-list-item>
${this.hass.localize( </ha-button-menu>
"ui.panel.config.lovelace.resources.caption"
)}
</ha-clickable-list-item>
</ha-button-menu>
`
: ""}
<ha-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(

View File

@ -1,13 +1,16 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { mdiClose } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-button";
import { SchemaUnion } from "../../../../components/ha-form/types"; import { SchemaUnion } from "../../../../components/ha-form/types";
import { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource"; import { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail"; import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
@ -40,6 +43,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(params: LovelaceResourceDetailsDialogParams): void { public showDialog(params: LovelaceResourceDetailsDialogParams): void {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
@ -55,32 +60,52 @@ export class DialogLovelaceResourceDetail extends LitElement {
} }
} }
public closeDialog(): void { private _dialogClosed(): void {
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
public closeDialog(): void {
this._dialog?.close();
}
protected render() { protected render() {
if (!this._params) { if (!this._params) {
return nothing; return nothing;
} }
const urlInvalid = !this._data?.url || this._data.url.trim() === ""; const urlInvalid = !this._data?.url || this._data.url.trim() === "";
const dialogTitle =
this._params.resource?.url ||
this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
const ariaLabel = this._params.resource?.url
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.edit_resource"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
return html` return html`
<ha-dialog <ha-md-dialog
open open
@closed=${this.closeDialog} disable-cancel-action
scrimClickAction @closed=${this._dialogClosed}
escapeKeyAction .ariaLabel=${ariaLabel}
.heading=${createCloseHeading(
this.hass,
this._params.resource
? this._params.resource.url
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
)
)}
> >
<div> <ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
</ha-dialog-header>
<div slot="content">
<ha-alert <ha-alert
alert-type="warning" alert-type="warning"
.title=${this.hass!.localize( .title=${this.hass!.localize(
@ -101,34 +126,24 @@ export class DialogLovelaceResourceDetail extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
</div> </div>
${this._params.resource <div slot="actions">
? html` <mwc-button @click=${this.closeDialog}>
<mwc-button ${this.hass!.localize("ui.common.cancel")}
slot="secondaryAction" </mwc-button>
class="warning" <mwc-button
@click=${this._deleteResource} @click=${this._updateResource}
.disabled=${this._submitting} .disabled=${urlInvalid || !this._data?.res_type || this._submitting}
> >
${this.hass!.localize( ${this._params.resource
"ui.panel.config.lovelace.resources.detail.delete" ? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.update"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.create"
)} )}
</mwc-button> </mwc-button>
` </div>
: nothing} </ha-md-dialog>
<mwc-button
slot="primaryAction"
@click=${this._updateResource}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
>
${this._params.resource
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.update"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.create"
)}
</mwc-button>
</ha-dialog>
`; `;
} }
@ -231,21 +246,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
this._submitting = false; this._submitting = false;
} }
} }
private async _deleteResource() {
this._submitting = true;
try {
if (await this._params!.removeResource()) {
this.closeDialog();
}
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return haStyleDialog;
}
} }
declare global { declare global {

View File

@ -1,4 +1,4 @@
import { mdiPlus } from "@mdi/js"; import { mdiDelete, mdiPlus } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -109,6 +109,20 @@ export class HaConfigLovelaceRescources extends LitElement {
) || resource.type} ) || resource.type}
`, `,
}, },
delete: {
title: "",
type: "icon-button",
minWidth: "48px",
maxWidth: "48px",
showNarrow: true,
template: (resource) =>
html`<ha-icon-button
@click=${this._removeResource}
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDelete}
.resource=${resource}
></ha-icon-button>`,
},
}) })
); );
@ -235,46 +249,49 @@ export class HaConfigLovelaceRescources extends LitElement {
); );
loadLovelaceResources([updated], this.hass!); loadLovelaceResources([updated], this.hass!);
}, },
removeResource: async () => {
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_title"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_text",
{ url: resource!.url }
),
dismissText: this.hass!.localize("ui.common.cancel"),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
}))
) {
return false;
}
try {
await deleteResource(this.hass!, resource!.id);
this._resources = this._resources!.filter((res) => res !== resource);
showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_header"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_body"
),
confirmText: this.hass.localize("ui.common.refresh"),
dismissText: this.hass.localize("ui.common.not_now"),
confirm: () => location.reload(),
});
return true;
} catch (err: any) {
return false;
}
},
}); });
} }
private _removeResource = async (event: any) => {
const resource = event.currentTarget.resource as LovelaceResource;
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_title"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_text",
{ url: resource.url }
),
dismissText: this.hass!.localize("ui.common.cancel"),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
}))
) {
return false;
}
try {
await deleteResource(this.hass!, resource.id);
this._resources = this._resources!.filter(({ id }) => id !== resource.id);
showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_header"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_body"
),
confirmText: this.hass.localize("ui.common.refresh"),
dismissText: this.hass.localize("ui.common.not_now"),
confirm: () => location.reload(),
});
return true;
} catch (err: any) {
return false;
}
};
private _handleSortingChanged(ev: CustomEvent) { private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail; this._activeSorting = ev.detail;
} }

View File

@ -10,7 +10,6 @@ export interface LovelaceResourceDetailsDialogParams {
updateResource: ( updateResource: (
updates: Partial<LovelaceResourcesMutableParams> updates: Partial<LovelaceResourcesMutableParams>
) => Promise<unknown>; ) => Promise<unknown>;
removeResource: () => Promise<boolean>;
} }
export const loadResourceDetailDialog = () => export const loadResourceDetailDialog = () =>

View File

@ -2584,6 +2584,7 @@
"cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.",
"detail": { "detail": {
"new_resource": "Add new resource", "new_resource": "Add new resource",
"edit_resource": "Edit resource",
"dismiss": "Close", "dismiss": "Close",
"warning_header": "Be cautious!", "warning_header": "Be cautious!",
"warning_text": "Adding resources can be dangerous, make sure you know the source of the resource and trust them. Bad resources could seriously harm your system.", "warning_text": "Adding resources can be dangerous, make sure you know the source of the resource and trust them. Bad resources could seriously harm your system.",

View File

@ -7033,6 +7033,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dialog-polyfill@npm:0.5.6":
version: 0.5.6
resolution: "dialog-polyfill@npm:0.5.6"
checksum: 10/42428793b04fd2e0a67dfb75838703488d7d05f73663c3251441ad6ed154b8dc71d65ed03d5a0ba4a83c6167c2e6f791cbe1574d0dca37dac1405ce3816033ca
languageName: node
linkType: hard
"didyoumean2@npm:4.1.0": "didyoumean2@npm:4.1.0":
version: 4.1.0 version: 4.1.0
resolution: "didyoumean2@npm:4.1.0" resolution: "didyoumean2@npm:4.1.0"
@ -9013,6 +9020,7 @@ __metadata:
deep-clone-simple: "npm:1.1.1" deep-clone-simple: "npm:1.1.1"
deep-freeze: "npm:0.0.1" deep-freeze: "npm:0.0.1"
del: "npm:7.1.0" del: "npm:7.1.0"
dialog-polyfill: "npm:0.5.6"
element-internals-polyfill: "npm:1.3.11" element-internals-polyfill: "npm:1.3.11"
eslint: "npm:8.57.0" eslint: "npm:8.57.0"
eslint-config-airbnb-base: "npm:15.0.0" eslint-config-airbnb-base: "npm:15.0.0"