diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index 29d91d314b73..8902c33dc615 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -60,6 +60,12 @@ function copyPolyfills(staticDir) { npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), staticPath("polyfills/") ); + + // dialog-polyfill css + copyFileDir( + npmPath("dialog-polyfill/dialog-polyfill.css"), + staticPath("polyfills/") + ); } function copyLoaderJS(staticDir) { diff --git a/package.json b/package.json index 1b9533e22c5e..c667a3afe1ea 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "date-fns-tz": "3.1.3", "deep-clone-simple": "1.1.1", "deep-freeze": "0.0.1", + "dialog-polyfill": "0.5.6", "element-internals-polyfill": "1.3.11", "fuse.js": "7.0.0", "google-timezones-json": "1.2.0", diff --git a/src/components/ha-md-dialog.ts b/src/components/ha-md-dialog.ts new file mode 100644 index 000000000000..cdddc994541d --- /dev/null +++ b/src/components/ha-md-dialog.ts @@ -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; + +/** + * 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((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; + } +} diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index c582d09ec91c..cdcab2a89997 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -322,22 +322,16 @@ export class HaConfigLovelaceDashboards extends LitElement { hasFab clickable > - ${this.hass.userData?.showAdvanced - ? html` - - - - ${this.hass.localize( - "ui.panel.config.lovelace.resources.caption" - )} - - - ` - : ""} + + + + ${this.hass.localize("ui.panel.config.lovelace.resources.caption")} + + -
+ + + ${dialogTitle} + +
- ${this._params.resource - ? html` - - ${this.hass!.localize( - "ui.panel.config.lovelace.resources.detail.delete" +
+ + ${this.hass!.localize("ui.common.cancel")} + + + ${this._params.resource + ? this.hass!.localize( + "ui.panel.config.lovelace.resources.detail.update" + ) + : this.hass!.localize( + "ui.panel.config.lovelace.resources.detail.create" )} - - ` - : nothing} - - ${this._params.resource - ? this.hass!.localize( - "ui.panel.config.lovelace.resources.detail.update" - ) - : this.hass!.localize( - "ui.panel.config.lovelace.resources.detail.create" - )} - - + +
+ `; } @@ -231,21 +246,6 @@ export class DialogLovelaceResourceDetail extends LitElement { 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 { diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index d43d572cfd8c..7727f57c6652 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -1,4 +1,4 @@ -import { mdiPlus } from "@mdi/js"; +import { mdiDelete, mdiPlus } from "@mdi/js"; import { css, CSSResultGroup, @@ -109,6 +109,20 @@ export class HaConfigLovelaceRescources extends LitElement { ) || resource.type} `, }, + delete: { + title: "", + type: "icon-button", + minWidth: "48px", + maxWidth: "48px", + showNarrow: true, + template: (resource) => + html``, + }, }) ); @@ -235,46 +249,49 @@ export class HaConfigLovelaceRescources extends LitElement { ); 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) { this._activeSorting = ev.detail; } diff --git a/src/panels/config/lovelace/resources/show-dialog-lovelace-resource-detail.ts b/src/panels/config/lovelace/resources/show-dialog-lovelace-resource-detail.ts index 66651da99bd2..e1a5caa3affa 100644 --- a/src/panels/config/lovelace/resources/show-dialog-lovelace-resource-detail.ts +++ b/src/panels/config/lovelace/resources/show-dialog-lovelace-resource-detail.ts @@ -10,7 +10,6 @@ export interface LovelaceResourceDetailsDialogParams { updateResource: ( updates: Partial ) => Promise; - removeResource: () => Promise; } export const loadResourceDetailDialog = () => diff --git a/src/translations/en.json b/src/translations/en.json index 9c32977c61b3..ea2252f25f8a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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.", "detail": { "new_resource": "Add new resource", + "edit_resource": "Edit resource", "dismiss": "Close", "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.", diff --git a/yarn.lock b/yarn.lock index 320f63a6cc58..7b625abc3176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7033,6 +7033,13 @@ __metadata: languageName: node 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": version: 4.1.0 resolution: "didyoumean2@npm:4.1.0" @@ -9013,6 +9020,7 @@ __metadata: deep-clone-simple: "npm:1.1.1" deep-freeze: "npm:0.0.1" del: "npm:7.1.0" + dialog-polyfill: "npm:0.5.6" element-internals-polyfill: "npm:1.3.11" eslint: "npm:8.57.0" eslint-config-airbnb-base: "npm:15.0.0"