From 872d1c684f3626d24e955077c49e12392e32a806 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:27:04 +0100 Subject: [PATCH] Simplify dialogs (#29848) --- gallery/src/pages/components/ha-selector.ts | 13 ++- landing-page/src/ha-landing-page.ts | 2 +- src/components/ha-bottom-sheet.ts | 2 +- src/components/ha-color-picker.ts | 36 ++++--- src/components/ha-dialog.ts | 19 ++-- src/components/ha-generic-picker.ts | 26 ++--- src/components/ha-icon-picker.ts | 5 +- src/components/ha-picker-combo-box.ts | 29 +++-- src/data/context.ts | 2 + src/dialogs/dialog-mixin.ts | 55 ++++++++++ src/dialogs/make-dialog-manager.ts | 101 +++++++++++++----- src/dialogs/more-info/ha-more-info-dialog.ts | 1 + src/dialogs/quick-bar/ha-quick-bar.ts | 2 +- src/dialogs/shortcuts/dialog-shortcuts.ts | 52 ++++----- .../shortcuts/show-shortcuts-dialog.ts | 4 +- src/onboarding/ha-onboarding.ts | 2 +- .../dialog-category-registry-detail.ts | 83 ++++++-------- .../config/labels/dialog-label-detail.ts | 101 +++++++----------- src/state/context-mixin.ts | 5 + src/state/dialog-manager-mixin.ts | 4 +- src/state/more-info-mixin.ts | 4 +- src/util/is_ios.ts | 5 +- 22 files changed, 310 insertions(+), 243 deletions(-) create mode 100644 src/dialogs/dialog-mixin.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 506bb8ad27..8a212a00b5 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -8,6 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; +import type { HASSDomEvent } from "../../../../src/common/dom/fire_event"; import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-settings-row"; @@ -16,7 +17,10 @@ import type { BlueprintInput } from "../../../../src/data/blueprint"; import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import type { FloorRegistryEntry } from "../../../../src/data/floor_registry"; import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry"; -import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; +import { + showDialog, + type ShowDialogParams, +} from "../../../../src/dialogs/make-dialog-manager"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; import type { HomeAssistant } from "../../../../src/types"; @@ -634,14 +638,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { }; }; - private _dialogManager = (e) => { - const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; + private _dialogManager = (e: HASSDomEvent>) => { + const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } = + e.detail; showDialog( this, - this.shadowRoot!, dialogTag, dialogParams, dialogImport, + parentElement, addHistory ); }; diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts index ecd0eaee0c..dfe9af8d61 100644 --- a/landing-page/src/ha-landing-page.ts +++ b/landing-page/src/ha-landing-page.ts @@ -118,7 +118,7 @@ class HaLandingPage extends LandingPageBaseElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - makeDialogManager(this, this.shadowRoot!); + makeDialogManager(this); if (window.innerWidth > 450) { import("../../src/resources/particles"); diff --git a/src/components/ha-bottom-sheet.ts b/src/components/ha-bottom-sheet.ts index f08dc8f7be..3ea210a35c 100644 --- a/src/components/ha-bottom-sheet.ts +++ b/src/components/ha-bottom-sheet.ts @@ -90,7 +90,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { await this.updateComplete; requestAnimationFrame(() => { - if (this.hass && isIosApp(this.hass)) { + if (this.hass && isIosApp(this.hass.auth.external)) { const element = this.renderRoot.querySelector("[autofocus]"); if (element !== null) { if (!element.id) { diff --git a/src/components/ha-color-picker.ts b/src/components/ha-color-picker.ts index 1c8902c60c..73a0a3c505 100644 --- a/src/components/ha-color-picker.ts +++ b/src/components/ha-color-picker.ts @@ -1,20 +1,20 @@ +import { consume, type ContextType } from "@lit/context"; import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; import { html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeKeys } from "../common/translations/localize"; -import type { HomeAssistant, ValueChangedEvent } from "../types"; +import { localizeContext } from "../data/context"; +import type { ValueChangedEvent } from "../types"; import "./ha-generic-picker"; import type { PickerComboBoxItem } from "./ha-picker-combo-box"; import type { PickerValueRenderer } from "./ha-picker-field"; @customElement("ha-color-picker") export class HaColorPicker extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - @property() public label?: string; @property() public helper?: string; @@ -34,12 +34,15 @@ export class HaColorPicker extends LitElement { @property({ type: Boolean }) public required = false; + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; + render() { const effectiveValue = this.value ?? this.defaultColor ?? ""; return html` { const items: PickerComboBoxItem[] = []; - const defaultSuffix = this.hass.localize( - "ui.components.color-picker.default" - ); + const defaultSuffix = + this.localize?.("ui.components.color-picker.default") || "Default"; const addDefaultSuffix = (label: string, isDefault: boolean) => isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label; if (includeNone) { const noneLabel = - this.hass.localize("ui.components.color-picker.none") || "None"; + this.localize?.("ui.components.color-picker.none") || "None"; items.push({ id: "none", primary: addDefaultSuffix(noneLabel, defaultColor === "none"), @@ -120,7 +124,7 @@ export class HaColorPicker extends LitElement { if (includeState) { const stateLabel = - this.hass.localize("ui.components.color-picker.state") || "State"; + this.localize?.("ui.components.color-picker.state") || "State"; items.push({ id: "state", primary: addDefaultSuffix(stateLabel, defaultColor === "state"), @@ -130,7 +134,7 @@ export class HaColorPicker extends LitElement { Array.from(THEME_COLORS).forEach((color) => { const themeLabel = - this.hass.localize( + this.localize?.( `ui.components.color-picker.colors.${color}` as LocalizeKeys ) || color; items.push({ @@ -184,7 +188,7 @@ export class HaColorPicker extends LitElement { return html` - ${this.hass.localize("ui.components.color-picker.none")} + ${this.localize?.("ui.components.color-picker.none") || "None"} `; } @@ -192,7 +196,7 @@ export class HaColorPicker extends LitElement { return html` - ${this.hass.localize("ui.components.color-picker.state")} + ${this.localize?.("ui.components.color-picker.state") || "State"} `; } @@ -200,7 +204,7 @@ export class HaColorPicker extends LitElement { return html` ${this._renderColorCircle(value)} - ${this.hass.localize( + ${this.localize?.( `ui.components.color-picker.colors.${value}` as LocalizeKeys ) || value} diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index e384c9e3fb..5f77565d7c 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -1,5 +1,6 @@ import "@home-assistant/webawesome/dist/components/dialog/dialog"; import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog"; +import { consume, type ContextType } from "@lit/context"; import { mdiClose } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { @@ -13,9 +14,9 @@ import { ifDefined } from "lit/directives/if-defined"; import type { HASSDomEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event"; import { withViewTransition } from "../common/util/view-transition"; +import { authContext, localizeContext } from "../data/context"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { haStyleScrollbar } from "../resources/styles"; -import type { HomeAssistant } from "../types"; import { isIosApp } from "../util/is_ios"; import "./ha-dialog-header"; import "./ha-icon-button"; @@ -84,8 +85,6 @@ type DialogHideEvent = CustomEvent<{ source?: Element }>; */ @customElement("ha-dialog") export class HaDialog extends ScrollableFadeMixin(LitElement) { - @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: "aria-labelledby" }) public ariaLabelledBy?: string; @@ -124,6 +123,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { @query(".body") public bodyContainer!: HTMLDivElement; + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; + + @state() + @consume({ context: authContext, subscribe: true }) + private auth?: ContextType; + @state() private _bodyScrolled = false; @@ -177,7 +184,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { @@ -214,13 +221,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) { await this.updateComplete; requestAnimationFrame(() => { - if (this.hass && isIosApp(this.hass)) { + if (this.auth?.external && isIosApp(this.auth.external)) { const element = this.querySelector("[autofocus]"); if (element !== null) { if (!element.id) { element.id = "ha-dialog-autofocus"; } - this.hass?.auth.external?.fireMessage({ + this.auth.external.fireMessage({ type: "focus_element", payload: { element_id: element.id, diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 4b14aefa6a..fb650ed495 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -1,5 +1,6 @@ import "@home-assistant/webawesome/dist/components/popover/popover"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; +import { consume, type ContextType } from "@lit/context"; import { mdiPlaylistPlus } from "@mdi/js"; import { css, @@ -13,10 +14,9 @@ import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { tinykeys } from "tinykeys"; import { fireEvent } from "../common/dom/fire_event"; -import { throttle } from "../common/util/throttle"; +import { authContext } from "../data/context"; import { PickerMixin } from "../mixins/picker-mixin"; import type { FuseWeightedKey } from "../resources/fuseMultiTerm"; -import type { HomeAssistant } from "../types"; import { isIosApp } from "../util/is_ios"; import "./ha-bottom-sheet"; import "./ha-button"; @@ -33,8 +33,6 @@ import "./ha-svg-icon"; @customElement("ha-generic-picker") export class HaGenericPicker extends PickerMixin(LitElement) { - @property({ attribute: false }) public hass?: HomeAssistant; - @property({ type: Boolean, attribute: "allow-custom-value" }) public allowCustomValue; @@ -115,6 +113,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) { @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; + @state() + @consume({ context: authContext, subscribe: true }) + private auth?: ContextType; + @state() private _opened = false; @state() private _pickerWrapperOpen = false; @@ -146,10 +148,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) { protected willUpdate(changedProperties: PropertyValues) { if (changedProperties.has("value")) { this._setUnknownValue(); - return; - } - if (changedProperties.has("hass")) { - this._throttleUnknownValue(); } } @@ -257,7 +255,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) { return html` = (item) => html` @customElement("ha-icon-picker") export class HaIconPicker extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; - @property() public value?: string; @property() public label?: string; @@ -111,7 +109,6 @@ export class HaIconPicker extends LitElement { protected render(): TemplateResult { return html` = ( @customElement("ha-picker-combo-box") export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { - @property({ attribute: false }) public hass?: HomeAssistant; - // eslint-disable-next-line lit/no-native-attributes @property({ type: Boolean }) public autofocus = false; @@ -162,6 +161,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { @query("ha-textfield") private _searchFieldElement?: HaTextField; + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; + + @state() + @consume({ context: localeContext, subscribe: true }) + private locale!: ContextType; + @state() private _items: PickerComboBoxItem[] = []; @state() private _selectedSection?: string; @@ -215,9 +222,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { const searchLabel = this.label ?? (this.allowCustomValue - ? (this.hass?.localize("ui.components.combo-box.search_or_custom") ?? + ? (this.localize?.("ui.components.combo-box.search_or_custom") ?? "Search | Add custom value") - : (this.hass?.localize("ui.common.search") ?? "Search")); + : (this.localize?.("ui.common.search") ?? "Search")); return html` @@ -350,7 +357,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { return caseInsensitiveStringCompare( sortLabelA, sortLabelB, - this.hass?.locale.language ?? navigator.language + this.locale?.language ?? navigator.language ); }); } @@ -367,7 +374,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { id: this._search, primary: this.customValueLabel ?? - this.hass?.localize("ui.components.combo-box.add_custom_item") ?? + this.localize?.("ui.components.combo-box.add_custom_item") ?? "Add custom item", secondary: `"${this._search}"`, icon_path: mdiPlus, @@ -401,10 +408,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { ? typeof this.notFoundLabel === "function" ? this.notFoundLabel(this._search) : this.notFoundLabel || - this.hass?.localize("ui.components.combo-box.no_match") || + this.localize?.("ui.components.combo-box.no_match") || "No matching items found" : this.emptyLabel || - this.hass?.localize("ui.components.combo-box.no_items") || + this.localize?.("ui.components.combo-box.no_items") || "No items available"} @@ -503,7 +510,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { id: searchString, primary: this.customValueLabel ?? - this.hass?.localize("ui.components.combo-box.add_custom_item") ?? + this.localize?.("ui.components.combo-box.add_custom_item") ?? "Add custom item", secondary: `"${searchString}"`, icon_path: mdiPlus, diff --git a/src/data/context.ts b/src/data/context.ts index c082091049..a7e19022ca 100644 --- a/src/data/context.ts +++ b/src/data/context.ts @@ -34,3 +34,5 @@ export const labelsContext = createContext("labels"); export const configEntriesContext = createContext("configEntries"); + +export const authContext = createContext("auth"); diff --git a/src/dialogs/dialog-mixin.ts b/src/dialogs/dialog-mixin.ts new file mode 100644 index 0000000000..0f92d28e9c --- /dev/null +++ b/src/dialogs/dialog-mixin.ts @@ -0,0 +1,55 @@ +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"; + +export const DialogMixin = < + P = unknown, + T extends Constructor = Constructor, +>( + superClass: T +) => + class extends superClass implements HassDialogNext

{ + declare public params?: P; + + private _closePromise?: Promise; + + private _closeResolve?: (value: boolean) => void; + + public closeDialog(_historyState?: any): Promise | boolean { + if (this._closePromise) { + return this._closePromise; + } + + const dialogElement = this.shadowRoot?.querySelector( + "ha-dialog" + ) as HaDialog | null; + if (dialogElement) { + this._closePromise = new Promise((resolve) => { + this._closeResolve = resolve; + }); + dialogElement.open = false; + } + return this._closePromise || true; + } + + private _removeDialog = (ev) => { + ev.stopPropagation(); + this._closeResolve?.(true); + this._closePromise = undefined; + this._closeResolve = undefined; + this.remove(); + }; + + connectedCallback() { + super.connectedCallback(); + this.addEventListener("closed", this._removeDialog, { once: true }); + } + + disconnectedCallback() { + fireEvent(this, "dialog-closed", { dialog: this.localName }); + this.removeEventListener("closed", this._removeDialog); + super.disconnectedCallback(); + } + }; diff --git a/src/dialogs/make-dialog-manager.ts b/src/dialogs/make-dialog-manager.ts index dabbfe6122..a7be30d801 100644 --- a/src/dialogs/make-dialog-manager.ts +++ b/src/dialogs/make-dialog-manager.ts @@ -1,6 +1,7 @@ +import type { LitElement } from "lit"; import { ancestorsWithProperty } from "../common/dom/ancestors-with-property"; import { deepActiveElement } from "../common/dom/deep-active-element"; -import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event"; +import type { HASSDomEvent } from "../common/dom/fire_event"; import { mainWindow } from "../common/dom/get_main_window"; import { nextRender } from "../common/util/render-status"; import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin"; @@ -19,18 +20,22 @@ declare global { } } -export interface HassDialog< - T = HASSDomEvents[ValidHassDomEvent], -> extends HTMLElement { +export interface HassDialog extends HTMLElement { showDialog(params: T); - closeDialog?: (historyState?: any) => boolean; + closeDialog?: (historyState?: any) => Promise | boolean; } -interface ShowDialogParams { +export interface HassDialogNext extends HTMLElement { + params?: T; + closeDialog?: (historyState?: any) => Promise | boolean; +} + +export interface ShowDialogParams { dialogTag: keyof HTMLElementTagNameMap; dialogImport: () => Promise; - dialogParams: T; + dialogParams?: T; addHistory?: boolean; + parentElement?: LitElement; } export interface DialogClosedParams { @@ -39,7 +44,6 @@ export interface DialogClosedParams { export interface DialogState { element: HTMLElement & ProvideHassElement; - root: ShadowRoot | HTMLElement; dialogTag: string; dialogParams: unknown; dialogImport?: () => Promise; @@ -47,7 +51,7 @@ export interface DialogState { } interface LoadedDialogInfo { - element: Promise; + element: Promise | null; closedFocusTargets?: Set; } @@ -57,12 +61,24 @@ const LOADED: LoadedDialogsDict = {}; const OPEN_DIALOG_STACK: DialogState[] = []; export const FOCUS_TARGET = Symbol.for("HA focus target"); +/** + * Shows a dialog element, lazy-loading it if needed, and optionally integrates + * dialog open/close behavior with browser history. + * + * @param element The host element that can provide `hass` and receives the dialog by default. + * @param dialogTag The custom element tag name of the dialog. + * @param dialogParams The params passed to the dialog via `showDialog()` or `params`. + * @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. + * @returns `true` if the dialog was shown (or could be shown), `false` if it could not be loaded. + */ export const showDialog = async ( - element: HTMLElement & ProvideHassElement, - root: ShadowRoot | HTMLElement, + element: LitElement & ProvideHassElement, dialogTag: string, dialogParams: unknown, dialogImport?: () => Promise, + parentElement?: LitElement, addHistory = true ): Promise => { if (!(dialogTag in LOADED)) { @@ -77,10 +93,18 @@ export const showDialog = async ( } LOADED[dialogTag] = { element: dialogImport().then(() => { - const dialogEl = document.createElement(dialogTag) as HassDialog; - element.provideHass(dialogEl); + const dialogEl = document.createElement(dialogTag) as + | HassDialogNext + | HassDialog; + + if ("showDialog" in dialogEl) { + // provide hass for legacy persistent dialogs + element.provideHass(dialogEl); + } + dialogEl.addEventListener("dialog-closed", _handleClosed); dialogEl.addEventListener("dialog-closed", _handleClosedFocus); + return dialogEl; }), }; @@ -96,10 +120,10 @@ export const showDialog = async ( }); return showDialog( element, - root, dialogTag, dialogParams, dialogImport, + parentElement, addHistory ); } @@ -111,7 +135,6 @@ export const showDialog = async ( } OPEN_DIALOG_STACK.push({ element, - root, dialogTag, dialogParams, dialogImport, @@ -134,12 +157,24 @@ export const showDialog = async ( FOCUS_TARGET ); - const dialogElement = await LOADED[dialogTag].element; + let dialogElement: HassDialogNext | HassDialog | null; - // Append it again so it's the last element in the root, - // so it's guaranteed to be on top of the other elements - root.appendChild(dialogElement); - dialogElement.showDialog(dialogParams); + if (LOADED[dialogTag] && LOADED[dialogTag].element === null) { + dialogElement = document.createElement(dialogTag) as HassDialogNext; + dialogElement.addEventListener("dialog-closed", _handleClosed); + dialogElement.addEventListener("dialog-closed", _handleClosedFocus); + LOADED[dialogTag].element = Promise.resolve(dialogElement); + } else { + dialogElement = await LOADED[dialogTag].element; + } + + if ("showDialog" in dialogElement!) { + dialogElement.showDialog(dialogParams); + } else { + dialogElement!.params = dialogParams; + } + + (parentElement || element).shadowRoot!.appendChild(dialogElement!); return true; }; @@ -152,7 +187,7 @@ export const closeDialog = async ( return true; } const dialogElement = await LOADED[dialogTag].element; - if (dialogElement.closeDialog) { + if (dialogElement && dialogElement.closeDialog) { return dialogElement.closeDialog(historyState) !== false; } return true; @@ -214,22 +249,34 @@ const _handleClosed = (ev: HASSDomEvent) => { mainWindow.history.back(); } } + + // cleanup element + if (ev.currentTarget && "params" in ev.currentTarget) { + const dialogElement = ev.currentTarget as HassDialogNext; + dialogElement.removeEventListener("dialog-closed", _handleClosed); + dialogElement.removeEventListener("dialog-closed", _handleClosedFocus); + LOADED[ev.detail.dialog].element = null; + } }; -export const makeDialogManager = ( - element: HTMLElement & ProvideHassElement, - root: ShadowRoot | HTMLElement -) => { +export const makeDialogManager = (element: LitElement & ProvideHassElement) => { element.addEventListener( "show-dialog", (e: HASSDomEvent>) => { - const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; + const { + dialogTag, + dialogImport, + dialogParams, + addHistory, + parentElement, + } = e.detail; + showDialog( element, - root, dialogTag, dialogParams, dialogImport, + parentElement, addHistory ); } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 4b8dda929f..9de52bb063 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -98,6 +98,7 @@ export interface MoreInfoDialogParams { tab?: View; large?: boolean; data?: Record; + parentElement?: LitElement; } type View = "info" | "history" | "settings" | "related" | "add_to"; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index d450728733..d85576cd7c 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -146,7 +146,7 @@ export class QuickBar extends LitElement { private _dialogOpened = async () => { this._opened = true; requestAnimationFrame(() => { - if (this.hass && isIosApp(this.hass)) { + if (this.hass && isIosApp(this.hass.auth.external)) { this.hass.auth.external!.fireMessage({ type: "focus_element", payload: { diff --git a/src/dialogs/shortcuts/dialog-shortcuts.ts b/src/dialogs/shortcuts/dialog-shortcuts.ts index 6736016143..4f6d7d9563 100644 --- a/src/dialogs/shortcuts/dialog-shortcuts.ts +++ b/src/dialogs/shortcuts/dialog-shortcuts.ts @@ -1,12 +1,13 @@ +import { consume, type ContextType } from "@lit/context"; import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; +import { customElement, state } from "lit/decorators"; import type { LocalizeKeys } from "../../common/translations/localize"; import "../../components/ha-alert"; -import "../../components/ha-svg-icon"; import "../../components/ha-dialog"; -import type { HomeAssistant } from "../../types"; +import "../../components/ha-svg-icon"; +import { localizeContext } from "../../data/context"; import { isMac } from "../../util/is_mac"; +import { DialogMixin } from "../dialog-mixin"; interface Text { textTranslationKey: LocalizeKeys; @@ -165,24 +166,10 @@ const _SHORTCUTS: Section[] = [ ]; @customElement("dialog-shortcuts") -class DialogShortcuts extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _open = false; - - public async showDialog(): Promise { - this._open = true; - } - - private _dialogClosed() { - this._open = false; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - public async closeDialog() { - this._open = false; - return true; - } +class DialogShortcuts extends DialogMixin(LitElement) { + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; private _renderShortcut( shortcutKeys: ShortcutString[], @@ -196,15 +183,13 @@ class DialogShortcuts extends LitElement { >${shortcutKey === CTRL_CMD ? isMac ? "⌘" - : this.hass.localize("ui.dialogs.shortcuts.keys.ctrl") + : this.localize("ui.dialogs.shortcuts.keys.ctrl") : typeof shortcutKey === "string" ? shortcutKey - : this.hass.localize( - shortcutKey.shortcutTranslationKey - )}` )} - ${this.hass.localize(descriptionKey)} + ${this.localize(descriptionKey)} `; } @@ -212,14 +197,13 @@ class DialogShortcuts extends LitElement { protected render() { return html`

${_SHORTCUTS.map( (section) => html` -

${this.hass.localize(section.titleTranslationKey)}

+

${this.localize(section.titleTranslationKey)}

${section.items.map((item) => { if ("shortcut" in item) { @@ -229,7 +213,7 @@ class DialogShortcuts extends LitElement { ); } return html`

- ${this.hass.localize((item as Text).textTranslationKey)} + ${this.localize((item as Text).textTranslationKey)}

`; })}
@@ -238,9 +222,9 @@ class DialogShortcuts extends LitElement {
- ${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", { + ${this.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", { user_profile: html`${this.hass.localize( + >${this.localize( "ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile" )}`, diff --git a/src/dialogs/shortcuts/show-shortcuts-dialog.ts b/src/dialogs/shortcuts/show-shortcuts-dialog.ts index bf0912081d..fe64581b38 100644 --- a/src/dialogs/shortcuts/show-shortcuts-dialog.ts +++ b/src/dialogs/shortcuts/show-shortcuts-dialog.ts @@ -1,8 +1,8 @@ +import type { LitElement } from "lit"; import { fireEvent } from "../../common/dom/fire_event"; -export const showShortcutsDialog = (element: HTMLElement) => +export const showShortcutsDialog = (element: LitElement) => fireEvent(element, "show-dialog", { dialogTag: "dialog-shortcuts", dialogImport: () => import("./dialog-shortcuts"), - dialogParams: {}, }); diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 65f0167a6f..9de54a2159 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -226,7 +226,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { ) { import("../resources/particles"); } - makeDialogManager(this, this.shadowRoot!); + makeDialogManager(this); import("../components/ha-language-picker"); } diff --git a/src/panels/config/category/dialog-category-registry-detail.ts b/src/panels/config/category/dialog-category-registry-detail.ts index 3cc87ac5e9..886f00ad88 100644 --- a/src/panels/config/category/dialog-category-registry-detail.ts +++ b/src/panels/config/category/dialog-category-registry-detail.ts @@ -1,24 +1,29 @@ +import { consume, type ContextType } from "@lit/context"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { customElement, state } from "lit/decorators"; import "../../../components/ha-alert"; +import "../../../components/ha-button"; import "../../../components/ha-dialog"; import "../../../components/ha-dialog-footer"; import "../../../components/ha-icon-picker"; -import "../../../components/ha-button"; import "../../../components/ha-textfield"; import type { CategoryRegistryEntry, CategoryRegistryEntryMutableParams, } from "../../../data/category_registry"; +import { localizeContext } from "../../../data/context"; +import { DialogMixin } from "../../../dialogs/dialog-mixin"; import { haStyleDialog } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail"; @customElement("dialog-category-registry-detail") -class DialogCategoryDetail extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; +class DialogCategoryDetail extends DialogMixin( + LitElement +) { + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; @state() private _name!: string; @@ -26,53 +31,32 @@ class DialogCategoryDetail extends LitElement { @state() private _error?: string; - @state() private _params?: CategoryRegistryDetailDialogParams; - @state() private _submitting?: boolean; - @state() private _open = false; - - public async showDialog( - params: CategoryRegistryDetailDialogParams - ): Promise { - this._params = params; - this._error = undefined; - this._open = true; - if (this._params.entry) { - this._name = this._params.entry.name || ""; - this._icon = this._params.entry.icon || null; + public connectedCallback(): void { + super.connectedCallback(); + if (this.params?.entry) { + this._name = this.params.entry.name || ""; + this._icon = this.params.entry.icon || null; } else { - this._name = this._params.suggestedName || ""; + this._name = this.params?.suggestedName || ""; this._icon = null; } - await this.updateComplete; - } - - public closeDialog(): void { - this._open = false; - } - - private _dialogClosed(): void { - this._error = ""; - this._params = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { - if (!this._params) { + if (!this.params) { return nothing; } - const entry = this._params.entry; + const entry = this.params.entry; const nameInvalid = !this._isNameValid(); return html` ${this._error ? html`${this._error}` @@ -81,8 +65,8 @@ class DialogCategoryDetail extends LitElement { @@ -102,7 +85,7 @@ class DialogCategoryDetail extends LitElement { appearance="plain" @click=${this.closeDialog} > - ${this.hass.localize("ui.common.cancel")} + ${this.localize("ui.common.cancel")} ${entry - ? this.hass.localize("ui.common.save") - : this.hass.localize("ui.common.add")} + ? this.localize("ui.common.save") + : this.localize("ui.common.add")} @@ -133,7 +116,7 @@ class DialogCategoryDetail extends LitElement { } private async _updateEntry() { - const create = !this._params!.entry; + const create = !this.params!.entry; this._submitting = true; let newValue: CategoryRegistryEntry | undefined; try { @@ -142,15 +125,15 @@ class DialogCategoryDetail extends LitElement { icon: this._icon || (create ? undefined : null), }; if (create) { - newValue = await this._params!.createEntry!(values); + newValue = await this.params!.createEntry!(values); } else { - newValue = await this._params!.updateEntry!(values); + newValue = await this.params!.updateEntry!(values); } this.closeDialog(); } catch (err: any) { this._error = err.message || - this.hass.localize("ui.panel.config.category.editor.unknown_error"); + this.localize("ui.panel.config.category.editor.unknown_error"); } finally { this._submitting = false; } diff --git a/src/panels/config/labels/dialog-label-detail.ts b/src/panels/config/labels/dialog-label-detail.ts index abf1bd1c35..ab45509961 100644 --- a/src/panels/config/labels/dialog-label-detail.ts +++ b/src/panels/config/labels/dialog-label-detail.ts @@ -1,28 +1,29 @@ +import { consume, type ContextType } from "@lit/context"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { customElement, state } from "lit/decorators"; import "../../../components/ha-alert"; import "../../../components/ha-button"; import "../../../components/ha-color-picker"; +import "../../../components/ha-dialog"; import "../../../components/ha-dialog-footer"; import "../../../components/ha-icon-picker"; import "../../../components/ha-switch"; -import "../../../components/ha-dialog"; import "../../../components/ha-textarea"; import "../../../components/ha-textfield"; +import { localizeContext } from "../../../data/context"; import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry"; -import type { HassDialog } from "../../../dialogs/make-dialog-manager"; +import { DialogMixin } from "../../../dialogs/dialog-mixin"; import { haStyleDialog } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; import type { LabelDetailDialogParams } from "./show-dialog-label-detail"; @customElement("dialog-label-detail") -class DialogLabelDetail - extends LitElement - implements HassDialog -{ - @property({ attribute: false }) public hass!: HomeAssistant; +class DialogLabelDetail extends DialogMixin( + LitElement +) { + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; @state() private _name!: string; @@ -34,53 +35,35 @@ class DialogLabelDetail @state() private _error?: string; - @state() private _params?: LabelDetailDialogParams; - @state() private _submitting = false; - @state() private _open = false; - - public showDialog(params: LabelDetailDialogParams): void { - this._params = params; - this._error = undefined; - if (this._params.entry) { - this._name = this._params.entry.name || ""; - this._icon = this._params.entry.icon || ""; - this._color = this._params.entry.color || ""; - this._description = this._params.entry.description || ""; + public connectedCallback(): void { + super.connectedCallback(); + if (this.params?.entry) { + this._name = this.params.entry.name || ""; + this._icon = this.params.entry.icon || ""; + this._color = this.params.entry.color || ""; + this._description = this.params.entry.description || ""; } else { - this._name = this._params.suggestedName || ""; + this._name = this.params?.suggestedName || ""; this._icon = ""; this._color = ""; this._description = ""; } - this._open = true; - } - - public closeDialog() { - this._open = false; - return true; - } - - private _dialogClosed(): void { - this._params = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { - if (!this._params) { + if (!this.params) { return nothing; } return html`
${this._error @@ -92,39 +75,35 @@ class DialogLabelDetail .value=${this._name} .configValue=${"name"} @input=${this._input} - .label=${this.hass!.localize("ui.dialogs.label-detail.name")} - .validationMessage=${this.hass!.localize( + .label=${this.localize("ui.dialogs.label-detail.name")} + .validationMessage=${this.localize( "ui.dialogs.label-detail.required_error_msg" )} required >
- ${this._params.entry && this._params.removeEntry + ${this.params.entry && this.params.removeEntry ? html` - ${this.hass!.localize("ui.common.delete")} + ${this.localize("ui.common.delete")} ` : html` @@ -142,7 +121,7 @@ class DialogLabelDetail slot="secondaryAction" @click=${this.closeDialog} > - ${this.hass.localize("ui.common.cancel")} + ${this.localize("ui.common.cancel")} `} - ${this._params.entry - ? this.hass!.localize("ui.common.update") - : this.hass!.localize("ui.common.create")} + ${this.params.entry + ? this.localize("ui.common.update") + : this.localize("ui.common.create")}
@@ -184,10 +163,10 @@ class DialogLabelDetail color: this._color.trim() || null, description: this._description.trim() || null, }; - if (this._params!.entry) { - await this._params!.updateEntry!(values); + if (this.params!.entry) { + await this.params!.updateEntry!(values); } else { - await this._params!.createEntry!(values); + await this.params!.createEntry!(values); } this.closeDialog(); } catch (err: any) { @@ -200,8 +179,8 @@ class DialogLabelDetail private async _deleteEntry() { this._submitting = true; try { - if (await this._params!.removeEntry!()) { - this._params = undefined; + if (await this.params!.removeEntry!()) { + this.params = undefined; } } finally { this._submitting = false; diff --git a/src/state/context-mixin.ts b/src/state/context-mixin.ts index 7f90fb0930..f600416391 100644 --- a/src/state/context-mixin.ts +++ b/src/state/context-mixin.ts @@ -2,6 +2,7 @@ import { ContextProvider } from "@lit/context"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { areasContext, + authContext, configContext, connectionContext, devicesContext, @@ -101,6 +102,10 @@ export const contextMixin = >( context: labelsContext, initialValue: [], }), + auth: new ContextProvider(this, { + context: authContext, + initialValue: this.hass?.auth, + }), }; protected hassConnected() { diff --git a/src/state/dialog-manager-mixin.ts b/src/state/dialog-manager-mixin.ts index 26ac3e509b..adf3c4c7d3 100644 --- a/src/state/dialog-manager-mixin.ts +++ b/src/state/dialog-manager-mixin.ts @@ -32,7 +32,7 @@ export const dialogManagerMixin = >( this.addEventListener("register-dialog", (e) => this.registerDialog(e.detail) ); - makeDialogManager(this, this.shadowRoot!); + makeDialogManager(this); } protected registerDialog({ @@ -44,10 +44,10 @@ export const dialogManagerMixin = >( this.addEventListener(dialogShowEvent, (showEv) => { showDialog( this, - this.shadowRoot!, dialogTag, (showEv as HASSDomEvent).detail, dialogImport, + undefined, addHistory ); }); diff --git a/src/state/more-info-mixin.ts b/src/state/more-info-mixin.ts index 9a0b64a2af..2dda563a8b 100644 --- a/src/state/more-info-mixin.ts +++ b/src/state/more-info-mixin.ts @@ -28,7 +28,6 @@ export default >(superClass: T) => private async _handleMoreInfo(ev: HASSDomEvent) { showDialog( this, - this.shadowRoot!, "ha-more-info-dialog", { entityId: ev.detail.entityId, @@ -42,7 +41,8 @@ export default >(superClass: T) => : false), data: ev.detail.data, }, - () => import("../dialogs/more-info/ha-more-info-dialog") + () => import("../dialogs/more-info/ha-more-info-dialog"), + ev.detail.parentElement ); } }; diff --git a/src/util/is_ios.ts b/src/util/is_ios.ts index f685e4d3bd..4e5d88a0d7 100644 --- a/src/util/is_ios.ts +++ b/src/util/is_ios.ts @@ -1,5 +1,6 @@ import type { HomeAssistant } from "../types"; import { isSafari } from "./is_safari"; -export const isIosApp = (hass: HomeAssistant): boolean => - !!hass.auth.external && isSafari; +export const isIosApp = ( + authExternal: HomeAssistant["auth"]["external"] +): boolean => !!authExternal && isSafari;