1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Simplify dialogs (#29848)

This commit is contained in:
Wendelin
2026-03-08 16:27:04 +01:00
committed by GitHub
parent d9d2ebfb9d
commit 872d1c684f
22 changed files with 310 additions and 243 deletions

View File

@@ -8,6 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry"; import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; 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-formfield";
import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row"; 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 { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry"; import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_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 { provideHass } from "../../../../src/fake_data/provide_hass";
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -634,14 +638,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
}; };
}; };
private _dialogManager = (e) => { private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
e.detail;
showDialog( showDialog(
this, this,
this.shadowRoot!,
dialogTag, dialogTag,
dialogParams, dialogParams,
dialogImport, dialogImport,
parentElement,
addHistory addHistory
); );
}; };

View File

@@ -118,7 +118,7 @@ class HaLandingPage extends LandingPageBaseElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
makeDialogManager(this, this.shadowRoot!); makeDialogManager(this);
if (window.innerWidth > 450) { if (window.innerWidth > 450) {
import("../../src/resources/particles"); import("../../src/resources/particles");

View File

@@ -90,7 +90,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
await this.updateComplete; await this.updateComplete;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this.hass && isIosApp(this.hass)) { if (this.hass && isIosApp(this.hass.auth.external)) {
const element = this.renderRoot.querySelector("[autofocus]"); const element = this.renderRoot.querySelector("[autofocus]");
if (element !== null) { if (element !== null) {
if (!element.id) { if (!element.id) {

View File

@@ -1,20 +1,20 @@
import { consume, type ContextType } from "@lit/context";
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; 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 { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize"; 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 "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box"; import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field"; import type { PickerValueRenderer } from "./ha-picker-field";
@customElement("ha-color-picker") @customElement("ha-color-picker")
export class HaColorPicker extends LitElement { export class HaColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public helper?: string; @property() public helper?: string;
@@ -34,12 +34,15 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
render() { render() {
const effectiveValue = this.value ?? this.defaultColor ?? ""; const effectiveValue = this.value ?? this.defaultColor ?? "";
return html` return html`
<ha-generic-picker <ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
.hideClearIcon=${!this.value && !!this.defaultColor} .hideClearIcon=${!this.value && !!this.defaultColor}
@@ -50,7 +53,7 @@ export class HaColorPicker extends LitElement {
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this.localize?.(
"ui.components.color-picker.no_colors_found" "ui.components.color-picker.no_colors_found"
)} )}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
@@ -78,7 +81,9 @@ export class HaColorPicker extends LitElement {
return [ return [
{ {
id: searchString, id: searchString,
primary: this.hass.localize("ui.components.color-picker.custom_color"), primary:
this.localize?.("ui.components.color-picker.custom_color") ||
"Custom color",
secondary: searchString, secondary: searchString,
}, },
]; ];
@@ -101,16 +106,15 @@ export class HaColorPicker extends LitElement {
): PickerComboBoxItem[] => { ): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = []; const items: PickerComboBoxItem[] = [];
const defaultSuffix = this.hass.localize( const defaultSuffix =
"ui.components.color-picker.default" this.localize?.("ui.components.color-picker.default") || "Default";
);
const addDefaultSuffix = (label: string, isDefault: boolean) => const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label; isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) { if (includeNone) {
const noneLabel = const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None"; this.localize?.("ui.components.color-picker.none") || "None";
items.push({ items.push({
id: "none", id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"), primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
@@ -120,7 +124,7 @@ export class HaColorPicker extends LitElement {
if (includeState) { if (includeState) {
const stateLabel = const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State"; this.localize?.("ui.components.color-picker.state") || "State";
items.push({ items.push({
id: "state", id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"), primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
@@ -130,7 +134,7 @@ export class HaColorPicker extends LitElement {
Array.from(THEME_COLORS).forEach((color) => { Array.from(THEME_COLORS).forEach((color) => {
const themeLabel = const themeLabel =
this.hass.localize( this.localize?.(
`ui.components.color-picker.colors.${color}` as LocalizeKeys `ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color; ) || color;
items.push({ items.push({
@@ -184,7 +188,7 @@ export class HaColorPicker extends LitElement {
return html` return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline"> <span slot="headline">
${this.hass.localize("ui.components.color-picker.none")} ${this.localize?.("ui.components.color-picker.none") || "None"}
</span> </span>
`; `;
} }
@@ -192,7 +196,7 @@ export class HaColorPicker extends LitElement {
return html` return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline"> <span slot="headline">
${this.hass.localize("ui.components.color-picker.state")} ${this.localize?.("ui.components.color-picker.state") || "State"}
</span> </span>
`; `;
} }
@@ -200,7 +204,7 @@ export class HaColorPicker extends LitElement {
return html` return html`
<span slot="start">${this._renderColorCircle(value)}</span> <span slot="start">${this._renderColorCircle(value)}</span>
<span slot="headline"> <span slot="headline">
${this.hass.localize( ${this.localize?.(
`ui.components.color-picker.colors.${value}` as LocalizeKeys `ui.components.color-picker.colors.${value}` as LocalizeKeys
) || value} ) || value}
</span> </span>

View File

@@ -1,5 +1,6 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog"; import "@home-assistant/webawesome/dist/components/dialog/dialog";
import type WaDialog from "@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 { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { import {
@@ -13,9 +14,9 @@ import { ifDefined } from "lit/directives/if-defined";
import type { HASSDomEvent } from "../common/dom/fire_event"; import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { withViewTransition } from "../common/util/view-transition"; import { withViewTransition } from "../common/util/view-transition";
import { authContext, localizeContext } from "../data/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios"; import { isIosApp } from "../util/is_ios";
import "./ha-dialog-header"; import "./ha-dialog-header";
import "./ha-icon-button"; import "./ha-icon-button";
@@ -84,8 +85,6 @@ type DialogHideEvent = CustomEvent<{ source?: Element }>;
*/ */
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends ScrollableFadeMixin(LitElement) { export class HaDialog extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: "aria-labelledby" }) @property({ attribute: "aria-labelledby" })
public ariaLabelledBy?: string; public ariaLabelledBy?: string;
@@ -124,6 +123,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
@query(".body") public bodyContainer!: HTMLDivElement; @query(".body") public bodyContainer!: HTMLDivElement;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: authContext, subscribe: true })
private auth?: ContextType<typeof authContext>;
@state() @state()
private _bodyScrolled = false; private _bodyScrolled = false;
@@ -177,7 +184,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
<slot name="headerNavigationIcon" slot="navigationIcon"> <slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button <ha-icon-button
data-dialog="close" data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"} .label=${this.localize?.("ui.common.close") ?? "Close"}
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
</slot> </slot>
@@ -214,13 +221,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete; await this.updateComplete;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this.hass && isIosApp(this.hass)) { if (this.auth?.external && isIosApp(this.auth.external)) {
const element = this.querySelector("[autofocus]"); const element = this.querySelector("[autofocus]");
if (element !== null) { if (element !== null) {
if (!element.id) { if (!element.id) {
element.id = "ha-dialog-autofocus"; element.id = "ha-dialog-autofocus";
} }
this.hass?.auth.external?.fireMessage({ this.auth.external.fireMessage({
type: "focus_element", type: "focus_element",
payload: { payload: {
element_id: element.id, element_id: element.id,

View File

@@ -1,5 +1,6 @@
import "@home-assistant/webawesome/dist/components/popover/popover"; import "@home-assistant/webawesome/dist/components/popover/popover";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume, type ContextType } from "@lit/context";
import { mdiPlaylistPlus } from "@mdi/js"; import { mdiPlaylistPlus } from "@mdi/js";
import { import {
css, css,
@@ -13,10 +14,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; 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 { PickerMixin } from "../mixins/picker-mixin";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm"; import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios"; import { isIosApp } from "../util/is_ios";
import "./ha-bottom-sheet"; import "./ha-bottom-sheet";
import "./ha-button"; import "./ha-button";
@@ -33,8 +33,6 @@ import "./ha-svg-icon";
@customElement("ha-generic-picker") @customElement("ha-generic-picker")
export class HaGenericPicker extends PickerMixin(LitElement) { export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, attribute: "allow-custom-value" }) @property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue; public allowCustomValue;
@@ -115,6 +113,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@state()
@consume({ context: authContext, subscribe: true })
private auth?: ContextType<typeof authContext>;
@state() private _opened = false; @state() private _opened = false;
@state() private _pickerWrapperOpen = false; @state() private _pickerWrapperOpen = false;
@@ -146,10 +148,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
protected willUpdate(changedProperties: PropertyValues) { protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("value")) { if (changedProperties.has("value")) {
this._setUnknownValue(); this._setUnknownValue();
return;
}
if (changedProperties.has("hass")) {
this._throttleUnknownValue();
} }
} }
@@ -257,7 +255,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
return html` return html`
<ha-picker-combo-box <ha-picker-combo-box
id="combo-box" id="combo-box"
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel} .label=${this.searchLabel}
.value=${this._selectedValue ?? this.value} .value=${this._selectedValue ?? this.value}
@@ -296,13 +293,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
); );
}; };
private _throttleUnknownValue = throttle(
this._setUnknownValue,
1000,
true,
false
);
private _renderHelper() { private _renderHelper() {
const showError = this.invalid && this.errorMessage; const showError = this.invalid && this.errorMessage;
const showHelper = !showError && this.helper; const showHelper = !showError && this.helper;
@@ -326,8 +316,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
this._comboBox?.setFieldValue(this._initialFieldValue); this._comboBox?.setFieldValue(this._initialFieldValue);
this._initialFieldValue = undefined; this._initialFieldValue = undefined;
} }
if (this.hass && isIosApp(this.hass)) { if (this.auth?.external && isIosApp(this.auth.external)) {
this.hass.auth.external!.fireMessage({ this.auth.external.fireMessage({
type: "focus_element", type: "focus_element",
payload: { payload: {
element_id: "combo-box", element_id: "combo-box",

View File

@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons"; import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { ValueChangedEvent } from "../types";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-generic-picker"; import "./ha-generic-picker";
import "./ha-icon"; import "./ha-icon";
@@ -88,8 +88,6 @@ const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
@customElement("ha-icon-picker") @customElement("ha-icon-picker")
export class HaIconPicker extends LitElement { export class HaIconPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string; @property() public value?: string;
@property() public label?: string; @property() public label?: string;
@@ -111,7 +109,6 @@ export class HaIconPicker extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-generic-picker <ha-generic-picker
.hass=${this.hass}
allow-custom-value allow-custom-value
.getItems=${this._getIconPickerItems} .getItems=${this._getIconPickerItems}
.helper=${this.helper} .helper=${this.helper}

View File

@@ -1,5 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer"; import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume, type ContextType } from "@lit/context";
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js"; import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
@@ -14,6 +15,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { localeContext, localizeContext } from "../data/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { import {
multiTermSortedSearch, multiTermSortedSearch,
@@ -21,7 +23,6 @@ import {
} from "../resources/fuseMultiTerm"; } from "../resources/fuseMultiTerm";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer"; import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import { isTouch } from "../util/is_touch"; import { isTouch } from "../util/is_touch";
import "./chips/ha-chip-set"; import "./chips/ha-chip-set";
import "./chips/ha-filter-chip"; import "./chips/ha-filter-chip";
@@ -90,8 +91,6 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
@customElement("ha-picker-combo-box") @customElement("ha-picker-combo-box")
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes // eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@@ -162,6 +161,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@query("ha-textfield") private _searchFieldElement?: HaTextField; @query("ha-textfield") private _searchFieldElement?: HaTextField;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@state() private _items: PickerComboBoxItem[] = []; @state() private _items: PickerComboBoxItem[] = [];
@state() private _selectedSection?: string; @state() private _selectedSection?: string;
@@ -215,9 +222,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
const searchLabel = const searchLabel =
this.label ?? this.label ??
(this.allowCustomValue (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") "Search | Add custom value")
: (this.hass?.localize("ui.common.search") ?? "Search")); : (this.localize?.("ui.common.search") ?? "Search"));
return html`<ha-textfield return html`<ha-textfield
.label=${searchLabel} .label=${searchLabel}
@@ -228,7 +235,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
<ha-icon-button <ha-icon-button
@click=${this._clearSearch} @click=${this._clearSearch}
slot="trailingIcon" slot="trailingIcon"
.label=${this.hass?.localize("ui.common.clear") || "Clear"} .label=${this.localize?.("ui.common.clear") || "Clear"}
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
</ha-textfield> </ha-textfield>
@@ -350,7 +357,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return caseInsensitiveStringCompare( return caseInsensitiveStringCompare(
sortLabelA, sortLabelA,
sortLabelB, 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, id: this._search,
primary: primary:
this.customValueLabel ?? this.customValueLabel ??
this.hass?.localize("ui.components.combo-box.add_custom_item") ?? this.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item", "Add custom item",
secondary: `"${this._search}"`, secondary: `"${this._search}"`,
icon_path: mdiPlus, icon_path: mdiPlus,
@@ -401,10 +408,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
? typeof this.notFoundLabel === "function" ? typeof this.notFoundLabel === "function"
? this.notFoundLabel(this._search) ? this.notFoundLabel(this._search)
: this.notFoundLabel || : this.notFoundLabel ||
this.hass?.localize("ui.components.combo-box.no_match") || this.localize?.("ui.components.combo-box.no_match") ||
"No matching items found" "No matching items found"
: this.emptyLabel || : this.emptyLabel ||
this.hass?.localize("ui.components.combo-box.no_items") || this.localize?.("ui.components.combo-box.no_items") ||
"No items available"}</span "No items available"}</span
> >
</ha-combo-box-item> </ha-combo-box-item>
@@ -503,7 +510,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
id: searchString, id: searchString,
primary: primary:
this.customValueLabel ?? this.customValueLabel ??
this.hass?.localize("ui.components.combo-box.add_custom_item") ?? this.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item", "Add custom item",
secondary: `"${searchString}"`, secondary: `"${searchString}"`,
icon_path: mdiPlus, icon_path: mdiPlus,

View File

@@ -34,3 +34,5 @@ export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
export const configEntriesContext = export const configEntriesContext =
createContext<ConfigEntry[]>("configEntries"); createContext<ConfigEntry[]>("configEntries");
export const authContext = createContext<HomeAssistant["auth"]>("auth");

View File

@@ -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<LitElement> = Constructor<LitElement>,
>(
superClass: T
) =>
class extends superClass implements HassDialogNext<P> {
declare public params?: P;
private _closePromise?: Promise<boolean>;
private _closeResolve?: (value: boolean) => void;
public closeDialog(_historyState?: any): Promise<boolean> | boolean {
if (this._closePromise) {
return this._closePromise;
}
const dialogElement = this.shadowRoot?.querySelector(
"ha-dialog"
) as HaDialog | null;
if (dialogElement) {
this._closePromise = new Promise<boolean>((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();
}
};

View File

@@ -1,6 +1,7 @@
import type { LitElement } from "lit";
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property"; import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element"; 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 { mainWindow } from "../common/dom/get_main_window";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin"; import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
@@ -19,18 +20,22 @@ declare global {
} }
} }
export interface HassDialog< export interface HassDialog<T = unknown> extends HTMLElement {
T = HASSDomEvents[ValidHassDomEvent],
> extends HTMLElement {
showDialog(params: T); showDialog(params: T);
closeDialog?: (historyState?: any) => boolean; closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
} }
interface ShowDialogParams<T> { export interface HassDialogNext<T = unknown> extends HTMLElement {
params?: T;
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
}
export interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap; dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>; dialogImport: () => Promise<unknown>;
dialogParams: T; dialogParams?: T;
addHistory?: boolean; addHistory?: boolean;
parentElement?: LitElement;
} }
export interface DialogClosedParams { export interface DialogClosedParams {
@@ -39,7 +44,6 @@ export interface DialogClosedParams {
export interface DialogState { export interface DialogState {
element: HTMLElement & ProvideHassElement; element: HTMLElement & ProvideHassElement;
root: ShadowRoot | HTMLElement;
dialogTag: string; dialogTag: string;
dialogParams: unknown; dialogParams: unknown;
dialogImport?: () => Promise<unknown>; dialogImport?: () => Promise<unknown>;
@@ -47,7 +51,7 @@ export interface DialogState {
} }
interface LoadedDialogInfo { interface LoadedDialogInfo {
element: Promise<HassDialog>; element: Promise<HassDialogNext | HassDialog> | null;
closedFocusTargets?: Set<Element>; closedFocusTargets?: Set<Element>;
} }
@@ -57,12 +61,24 @@ const LOADED: LoadedDialogsDict = {};
const OPEN_DIALOG_STACK: DialogState[] = []; const OPEN_DIALOG_STACK: DialogState[] = [];
export const FOCUS_TARGET = Symbol.for("HA focus target"); 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 ( export const showDialog = async (
element: HTMLElement & ProvideHassElement, element: LitElement & ProvideHassElement,
root: ShadowRoot | HTMLElement,
dialogTag: string, dialogTag: string,
dialogParams: unknown, dialogParams: unknown,
dialogImport?: () => Promise<unknown>, dialogImport?: () => Promise<unknown>,
parentElement?: LitElement,
addHistory = true addHistory = true
): Promise<boolean> => { ): Promise<boolean> => {
if (!(dialogTag in LOADED)) { if (!(dialogTag in LOADED)) {
@@ -77,10 +93,18 @@ export const showDialog = async (
} }
LOADED[dialogTag] = { LOADED[dialogTag] = {
element: dialogImport().then(() => { element: dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog; const dialogEl = document.createElement(dialogTag) as
element.provideHass(dialogEl); | HassDialogNext
| HassDialog;
if ("showDialog" in dialogEl) {
// provide hass for legacy persistent dialogs
element.provideHass(dialogEl);
}
dialogEl.addEventListener("dialog-closed", _handleClosed); dialogEl.addEventListener("dialog-closed", _handleClosed);
dialogEl.addEventListener("dialog-closed", _handleClosedFocus); dialogEl.addEventListener("dialog-closed", _handleClosedFocus);
return dialogEl; return dialogEl;
}), }),
}; };
@@ -96,10 +120,10 @@ export const showDialog = async (
}); });
return showDialog( return showDialog(
element, element,
root,
dialogTag, dialogTag,
dialogParams, dialogParams,
dialogImport, dialogImport,
parentElement,
addHistory addHistory
); );
} }
@@ -111,7 +135,6 @@ export const showDialog = async (
} }
OPEN_DIALOG_STACK.push({ OPEN_DIALOG_STACK.push({
element, element,
root,
dialogTag, dialogTag,
dialogParams, dialogParams,
dialogImport, dialogImport,
@@ -134,12 +157,24 @@ export const showDialog = async (
FOCUS_TARGET 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, if (LOADED[dialogTag] && LOADED[dialogTag].element === null) {
// so it's guaranteed to be on top of the other elements dialogElement = document.createElement(dialogTag) as HassDialogNext;
root.appendChild(dialogElement); dialogElement.addEventListener("dialog-closed", _handleClosed);
dialogElement.showDialog(dialogParams); 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; return true;
}; };
@@ -152,7 +187,7 @@ export const closeDialog = async (
return true; return true;
} }
const dialogElement = await LOADED[dialogTag].element; const dialogElement = await LOADED[dialogTag].element;
if (dialogElement.closeDialog) { if (dialogElement && dialogElement.closeDialog) {
return dialogElement.closeDialog(historyState) !== false; return dialogElement.closeDialog(historyState) !== false;
} }
return true; return true;
@@ -214,22 +249,34 @@ const _handleClosed = (ev: HASSDomEvent<DialogClosedParams>) => {
mainWindow.history.back(); 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 = ( export const makeDialogManager = (element: LitElement & ProvideHassElement) => {
element: HTMLElement & ProvideHassElement,
root: ShadowRoot | HTMLElement
) => {
element.addEventListener( element.addEventListener(
"show-dialog", "show-dialog",
(e: HASSDomEvent<ShowDialogParams<unknown>>) => { (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; const {
dialogTag,
dialogImport,
dialogParams,
addHistory,
parentElement,
} = e.detail;
showDialog( showDialog(
element, element,
root,
dialogTag, dialogTag,
dialogParams, dialogParams,
dialogImport, dialogImport,
parentElement,
addHistory addHistory
); );
} }

View File

@@ -98,6 +98,7 @@ export interface MoreInfoDialogParams {
tab?: View; tab?: View;
large?: boolean; large?: boolean;
data?: Record<string, any>; data?: Record<string, any>;
parentElement?: LitElement;
} }
type View = "info" | "history" | "settings" | "related" | "add_to"; type View = "info" | "history" | "settings" | "related" | "add_to";

View File

@@ -146,7 +146,7 @@ export class QuickBar extends LitElement {
private _dialogOpened = async () => { private _dialogOpened = async () => {
this._opened = true; this._opened = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this.hass && isIosApp(this.hass)) { if (this.hass && isIosApp(this.hass.auth.external)) {
this.hass.auth.external!.fireMessage({ this.hass.auth.external!.fireMessage({
type: "focus_element", type: "focus_element",
payload: { payload: {

View File

@@ -1,12 +1,13 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeKeys } from "../../common/translations/localize"; import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert"; import "../../components/ha-alert";
import "../../components/ha-svg-icon";
import "../../components/ha-dialog"; 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 { isMac } from "../../util/is_mac";
import { DialogMixin } from "../dialog-mixin";
interface Text { interface Text {
textTranslationKey: LocalizeKeys; textTranslationKey: LocalizeKeys;
@@ -165,24 +166,10 @@ const _SHORTCUTS: Section[] = [
]; ];
@customElement("dialog-shortcuts") @customElement("dialog-shortcuts")
class DialogShortcuts extends LitElement { class DialogShortcuts extends DialogMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @state()
@consume({ context: localizeContext, subscribe: true })
@state() private _open = false; private localize!: ContextType<typeof localizeContext>;
public async showDialog(): Promise<void> {
this._open = true;
}
private _dialogClosed() {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public async closeDialog() {
this._open = false;
return true;
}
private _renderShortcut( private _renderShortcut(
shortcutKeys: ShortcutString[], shortcutKeys: ShortcutString[],
@@ -196,15 +183,13 @@ class DialogShortcuts extends LitElement {
>${shortcutKey === CTRL_CMD >${shortcutKey === CTRL_CMD
? isMac ? isMac
? "⌘" ? "⌘"
: this.hass.localize("ui.dialogs.shortcuts.keys.ctrl") : this.localize("ui.dialogs.shortcuts.keys.ctrl")
: typeof shortcutKey === "string" : typeof shortcutKey === "string"
? shortcutKey ? shortcutKey
: this.hass.localize( : this.localize(shortcutKey.shortcutTranslationKey)}</span
shortcutKey.shortcutTranslationKey
)}</span
>` >`
)} )}
${this.hass.localize(descriptionKey)} ${this.localize(descriptionKey)}
</div> </div>
`; `;
} }
@@ -212,14 +197,13 @@ class DialogShortcuts extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-dialog <ha-dialog
.open=${this._open} open
@closed=${this._dialogClosed} .headerTitle=${this.localize("ui.dialogs.shortcuts.title")}
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
> >
<div class="content"> <div class="content">
${_SHORTCUTS.map( ${_SHORTCUTS.map(
(section) => html` (section) => html`
<h3>${this.hass.localize(section.titleTranslationKey)}</h3> <h3>${this.localize(section.titleTranslationKey)}</h3>
<div class="items"> <div class="items">
${section.items.map((item) => { ${section.items.map((item) => {
if ("shortcut" in item) { if ("shortcut" in item) {
@@ -229,7 +213,7 @@ class DialogShortcuts extends LitElement {
); );
} }
return html`<p> return html`<p>
${this.hass.localize((item as Text).textTranslationKey)} ${this.localize((item as Text).textTranslationKey)}
</p>`; </p>`;
})} })}
</div> </div>
@@ -238,9 +222,9 @@ class DialogShortcuts extends LitElement {
</div> </div>
<ha-alert slot="footer"> <ha-alert slot="footer">
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", { ${this.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts" user_profile: html`<a href="/profile/general#shortcuts"
>${this.hass.localize( >${this.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile" "ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a )}</a
>`, >`,

View File

@@ -1,8 +1,8 @@
import type { LitElement } from "lit";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
export const showShortcutsDialog = (element: HTMLElement) => export const showShortcutsDialog = (element: LitElement) =>
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-shortcuts", dialogTag: "dialog-shortcuts",
dialogImport: () => import("./dialog-shortcuts"), dialogImport: () => import("./dialog-shortcuts"),
dialogParams: {},
}); });

View File

@@ -226,7 +226,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
) { ) {
import("../resources/particles"); import("../resources/particles");
} }
makeDialogManager(this, this.shadowRoot!); makeDialogManager(this);
import("../components/ha-language-picker"); import("../components/ha-language-picker");
} }

View File

@@ -1,24 +1,29 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog"; import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer"; import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-button";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import type { import type {
CategoryRegistryEntry, CategoryRegistryEntry,
CategoryRegistryEntryMutableParams, CategoryRegistryEntryMutableParams,
} from "../../../data/category_registry"; } from "../../../data/category_registry";
import { localizeContext } from "../../../data/context";
import { DialogMixin } from "../../../dialogs/dialog-mixin";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail"; import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
@customElement("dialog-category-registry-detail") @customElement("dialog-category-registry-detail")
class DialogCategoryDetail extends LitElement { class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
@property({ attribute: false }) public hass!: HomeAssistant; LitElement
) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state() private _name!: string; @state() private _name!: string;
@@ -26,53 +31,32 @@ class DialogCategoryDetail extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _params?: CategoryRegistryDetailDialogParams;
@state() private _submitting?: boolean; @state() private _submitting?: boolean;
@state() private _open = false; public connectedCallback(): void {
super.connectedCallback();
public async showDialog( if (this.params?.entry) {
params: CategoryRegistryDetailDialogParams this._name = this.params.entry.name || "";
): Promise<void> { this._icon = this.params.entry.icon || null;
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;
} else { } else {
this._name = this._params.suggestedName || ""; this._name = this.params?.suggestedName || "";
this._icon = null; 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() { protected render() {
if (!this._params) { if (!this.params) {
return nothing; return nothing;
} }
const entry = this._params.entry; const entry = this.params.entry;
const nameInvalid = !this._isNameValid(); const nameInvalid = !this._isNameValid();
return html` return html`
<ha-dialog <ha-dialog
.hass=${this.hass} open
.open=${this._open}
header-title=${entry header-title=${entry
? this.hass.localize("ui.panel.config.category.editor.edit") ? this.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")} : this.localize("ui.panel.config.category.editor.create")}
prevent-scrim-close prevent-scrim-close
@closed=${this._dialogClosed}
> >
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -81,8 +65,8 @@ class DialogCategoryDetail extends LitElement {
<ha-textfield <ha-textfield
.value=${this._name} .value=${this._name}
@input=${this._nameChanged} @input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.name")} .label=${this.localize("ui.panel.config.category.editor.name")}
.validationMessage=${this.hass.localize( .validationMessage=${this.localize(
"ui.panel.config.category.editor.required_error_msg" "ui.panel.config.category.editor.required_error_msg"
)} )}
required required
@@ -90,10 +74,9 @@ class DialogCategoryDetail extends LitElement {
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .value=${this._icon ?? undefined}
.value=${this._icon}
@value-changed=${this._iconChanged} @value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.icon")} .label=${this.localize("ui.panel.config.category.editor.icon")}
></ha-icon-picker> ></ha-icon-picker>
</div> </div>
<ha-dialog-footer slot="footer"> <ha-dialog-footer slot="footer">
@@ -102,7 +85,7 @@ class DialogCategoryDetail extends LitElement {
appearance="plain" appearance="plain"
@click=${this.closeDialog} @click=${this.closeDialog}
> >
${this.hass.localize("ui.common.cancel")} ${this.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button <ha-button
slot="primaryAction" slot="primaryAction"
@@ -110,8 +93,8 @@ class DialogCategoryDetail extends LitElement {
.disabled=${nameInvalid || !!this._submitting} .disabled=${nameInvalid || !!this._submitting}
> >
${entry ${entry
? this.hass.localize("ui.common.save") ? this.localize("ui.common.save")
: this.hass.localize("ui.common.add")} : this.localize("ui.common.add")}
</ha-button> </ha-button>
</ha-dialog-footer> </ha-dialog-footer>
</ha-dialog> </ha-dialog>
@@ -133,7 +116,7 @@ class DialogCategoryDetail extends LitElement {
} }
private async _updateEntry() { private async _updateEntry() {
const create = !this._params!.entry; const create = !this.params!.entry;
this._submitting = true; this._submitting = true;
let newValue: CategoryRegistryEntry | undefined; let newValue: CategoryRegistryEntry | undefined;
try { try {
@@ -142,15 +125,15 @@ class DialogCategoryDetail extends LitElement {
icon: this._icon || (create ? undefined : null), icon: this._icon || (create ? undefined : null),
}; };
if (create) { if (create) {
newValue = await this._params!.createEntry!(values); newValue = await this.params!.createEntry!(values);
} else { } else {
newValue = await this._params!.updateEntry!(values); newValue = await this.params!.updateEntry!(values);
} }
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
this._error = this._error =
err.message || err.message ||
this.hass.localize("ui.panel.config.category.editor.unknown_error"); this.localize("ui.panel.config.category.editor.unknown_error");
} finally { } finally {
this._submitting = false; this._submitting = false;
} }

View File

@@ -1,28 +1,29 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-color-picker"; import "../../../components/ha-color-picker";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer"; import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/ha-dialog";
import "../../../components/ha-textarea"; import "../../../components/ha-textarea";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { localizeContext } from "../../../data/context";
import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry"; 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 { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { LabelDetailDialogParams } from "./show-dialog-label-detail"; import type { LabelDetailDialogParams } from "./show-dialog-label-detail";
@customElement("dialog-label-detail") @customElement("dialog-label-detail")
class DialogLabelDetail class DialogLabelDetail extends DialogMixin<LabelDetailDialogParams>(
extends LitElement LitElement
implements HassDialog<LabelDetailDialogParams> ) {
{ @state()
@property({ attribute: false }) public hass!: HomeAssistant; @consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state() private _name!: string; @state() private _name!: string;
@@ -34,53 +35,35 @@ class DialogLabelDetail
@state() private _error?: string; @state() private _error?: string;
@state() private _params?: LabelDetailDialogParams;
@state() private _submitting = false; @state() private _submitting = false;
@state() private _open = false; public connectedCallback(): void {
super.connectedCallback();
public showDialog(params: LabelDetailDialogParams): void { if (this.params?.entry) {
this._params = params; this._name = this.params.entry.name || "";
this._error = undefined; this._icon = this.params.entry.icon || "";
if (this._params.entry) { this._color = this.params.entry.color || "";
this._name = this._params.entry.name || ""; this._description = this.params.entry.description || "";
this._icon = this._params.entry.icon || "";
this._color = this._params.entry.color || "";
this._description = this._params.entry.description || "";
} else { } else {
this._name = this._params.suggestedName || ""; this._name = this.params?.suggestedName || "";
this._icon = ""; this._icon = "";
this._color = ""; this._color = "";
this._description = ""; 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() { protected render() {
if (!this._params) { if (!this.params) {
return nothing; return nothing;
} }
return html` return html`
<ha-dialog <ha-dialog
.hass=${this.hass} open
.open=${this._open} header-title=${this.params.entry
header-title=${this._params.entry ? this.params.entry.name || this.params.entry.label_id
? this._params.entry.name || this._params.entry.label_id : this.localize("ui.dialogs.label-detail.new_label")}
: this.hass!.localize("ui.dialogs.label-detail.new_label")}
prevent-scrim-close prevent-scrim-close
@closed=${this._dialogClosed}
> >
<div> <div>
${this._error ${this._error
@@ -92,39 +75,35 @@ class DialogLabelDetail
.value=${this._name} .value=${this._name}
.configValue=${"name"} .configValue=${"name"}
@input=${this._input} @input=${this._input}
.label=${this.hass!.localize("ui.dialogs.label-detail.name")} .label=${this.localize("ui.dialogs.label-detail.name")}
.validationMessage=${this.hass!.localize( .validationMessage=${this.localize(
"ui.dialogs.label-detail.required_error_msg" "ui.dialogs.label-detail.required_error_msg"
)} )}
required required
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.value=${this._icon} .value=${this._icon}
.hass=${this.hass}
.configValue=${"icon"} .configValue=${"icon"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass!.localize("ui.dialogs.label-detail.icon")} .label=${this.localize("ui.dialogs.label-detail.icon")}
></ha-icon-picker> ></ha-icon-picker>
<ha-color-picker <ha-color-picker
.value=${this._color} .value=${this._color}
.configValue=${"color"} .configValue=${"color"}
.hass=${this.hass}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass!.localize("ui.dialogs.label-detail.color")} .label=${this.localize("ui.dialogs.label-detail.color")}
></ha-color-picker> ></ha-color-picker>
<ha-textarea <ha-textarea
.value=${this._description} .value=${this._description}
.configValue=${"description"} .configValue=${"description"}
@input=${this._input} @input=${this._input}
.label=${this.hass!.localize( .label=${this.localize("ui.dialogs.label-detail.description")}
"ui.dialogs.label-detail.description"
)}
></ha-textarea> ></ha-textarea>
</div> </div>
</div> </div>
<ha-dialog-footer slot="footer"> <ha-dialog-footer slot="footer">
${this._params.entry && this._params.removeEntry ${this.params.entry && this.params.removeEntry
? html` ? html`
<ha-button <ha-button
slot="secondaryAction" slot="secondaryAction"
@@ -133,7 +112,7 @@ class DialogLabelDetail
@click=${this._deleteEntry} @click=${this._deleteEntry}
.disabled=${this._submitting} .disabled=${this._submitting}
> >
${this.hass!.localize("ui.common.delete")} ${this.localize("ui.common.delete")}
</ha-button> </ha-button>
` `
: html` : html`
@@ -142,7 +121,7 @@ class DialogLabelDetail
slot="secondaryAction" slot="secondaryAction"
@click=${this.closeDialog} @click=${this.closeDialog}
> >
${this.hass.localize("ui.common.cancel")} ${this.localize("ui.common.cancel")}
</ha-button> </ha-button>
`} `}
<ha-button <ha-button
@@ -150,9 +129,9 @@ class DialogLabelDetail
@click=${this._updateEntry} @click=${this._updateEntry}
.disabled=${this._submitting || !this._name} .disabled=${this._submitting || !this._name}
> >
${this._params.entry ${this.params.entry
? this.hass!.localize("ui.common.update") ? this.localize("ui.common.update")
: this.hass!.localize("ui.common.create")} : this.localize("ui.common.create")}
</ha-button> </ha-button>
</ha-dialog-footer> </ha-dialog-footer>
</ha-dialog> </ha-dialog>
@@ -184,10 +163,10 @@ class DialogLabelDetail
color: this._color.trim() || null, color: this._color.trim() || null,
description: this._description.trim() || null, description: this._description.trim() || null,
}; };
if (this._params!.entry) { if (this.params!.entry) {
await this._params!.updateEntry!(values); await this.params!.updateEntry!(values);
} else { } else {
await this._params!.createEntry!(values); await this.params!.createEntry!(values);
} }
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
@@ -200,8 +179,8 @@ class DialogLabelDetail
private async _deleteEntry() { private async _deleteEntry() {
this._submitting = true; this._submitting = true;
try { try {
if (await this._params!.removeEntry!()) { if (await this.params!.removeEntry!()) {
this._params = undefined; this.params = undefined;
} }
} finally { } finally {
this._submitting = false; this._submitting = false;

View File

@@ -2,6 +2,7 @@ import { ContextProvider } from "@lit/context";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
areasContext, areasContext,
authContext,
configContext, configContext,
connectionContext, connectionContext,
devicesContext, devicesContext,
@@ -101,6 +102,10 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
context: labelsContext, context: labelsContext,
initialValue: [], initialValue: [],
}), }),
auth: new ContextProvider(this, {
context: authContext,
initialValue: this.hass?.auth,
}),
}; };
protected hassConnected() { protected hassConnected() {

View File

@@ -32,7 +32,7 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
this.addEventListener("register-dialog", (e) => this.addEventListener("register-dialog", (e) =>
this.registerDialog(e.detail) this.registerDialog(e.detail)
); );
makeDialogManager(this, this.shadowRoot!); makeDialogManager(this);
} }
protected registerDialog({ protected registerDialog({
@@ -44,10 +44,10 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
this.addEventListener(dialogShowEvent, (showEv) => { this.addEventListener(dialogShowEvent, (showEv) => {
showDialog( showDialog(
this, this,
this.shadowRoot!,
dialogTag, dialogTag,
(showEv as HASSDomEvent<unknown>).detail, (showEv as HASSDomEvent<unknown>).detail,
dialogImport, dialogImport,
undefined,
addHistory addHistory
); );
}); });

View File

@@ -28,7 +28,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
private async _handleMoreInfo(ev: HASSDomEvent<MoreInfoDialogParams>) { private async _handleMoreInfo(ev: HASSDomEvent<MoreInfoDialogParams>) {
showDialog( showDialog(
this, this,
this.shadowRoot!,
"ha-more-info-dialog", "ha-more-info-dialog",
{ {
entityId: ev.detail.entityId, entityId: ev.detail.entityId,
@@ -42,7 +41,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
: false), : false),
data: ev.detail.data, data: ev.detail.data,
}, },
() => import("../dialogs/more-info/ha-more-info-dialog") () => import("../dialogs/more-info/ha-more-info-dialog"),
ev.detail.parentElement
); );
} }
}; };

View File

@@ -1,5 +1,6 @@
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { isSafari } from "./is_safari"; import { isSafari } from "./is_safari";
export const isIosApp = (hass: HomeAssistant): boolean => export const isIosApp = (
!!hass.auth.external && isSafari; authExternal: HomeAssistant["auth"]["external"]
): boolean => !!authExternal && isSafari;