Compare commits

...

10 Commits

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 { 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 { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
@@ -611,14 +615,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
};
};
private _dialogManager = (e) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
e.detail;
showDialog(
this,
this.shadowRoot!,
dialogTag,
dialogParams,
dialogImport,
parentElement,
addHistory
);
};

View File

@@ -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");

View File

@@ -69,7 +69,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) {

View File

@@ -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<typeof localizeContext>;
render() {
const effectiveValue = this.value ?? this.defaultColor ?? "";
return html`
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required}
.hideClearIcon=${!this.value && !!this.defaultColor}
@@ -50,7 +53,7 @@ export class HaColorPicker extends LitElement {
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.notFoundLabel=${this.hass.localize(
.notFoundLabel=${this.localize?.(
"ui.components.color-picker.no_colors_found"
)}
.getAdditionalItems=${this._getAdditionalItems}
@@ -78,7 +81,9 @@ export class HaColorPicker extends LitElement {
return [
{
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,
},
];
@@ -101,16 +106,15 @@ export class HaColorPicker extends LitElement {
): PickerComboBoxItem[] => {
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`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.none")}
${this.localize?.("ui.components.color-picker.none") || "None"}
</span>
`;
}
@@ -192,7 +196,7 @@ export class HaColorPicker extends LitElement {
return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.state")}
${this.localize?.("ui.components.color-picker.state") || "State"}
</span>
`;
}
@@ -200,7 +204,7 @@ export class HaColorPicker extends LitElement {
return html`
<span slot="start">${this._renderColorCircle(value)}</span>
<span slot="headline">
${this.hass.localize(
${this.localize?.(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) || value}
</span>

View File

@@ -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 {
@@ -11,9 +12,9 @@ import {
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
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";
@@ -77,8 +78,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;
@@ -117,6 +116,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
@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()
private _bodyScrolled = false;
@@ -162,7 +169,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.label=${this.localize?.("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
@@ -196,13 +203,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,

View File

@@ -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;
@@ -113,6 +111,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@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 _pickerWrapperOpen = false;
@@ -142,10 +144,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("value")) {
this._setUnknownValue();
return;
}
if (changedProperties.has("hass")) {
this._throttleUnknownValue();
}
}
@@ -252,7 +250,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
return html`
<ha-picker-combo-box
id="combo-box"
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel}
.value=${this.value}
@@ -291,13 +288,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
);
};
private _throttleUnknownValue = throttle(
this._setUnknownValue,
1000,
true,
false
);
private _renderHelper() {
const showError = this.invalid && this.errorMessage;
const showHelper = !showError && this.helper;
@@ -321,8 +311,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
this._comboBox?.setFieldValue(this._initialFieldValue);
this._initialFieldValue = undefined;
}
if (this.hass && isIosApp(this.hass)) {
this.hass.auth.external!.fireMessage({
if (this.auth?.external && isIosApp(this.auth.external)) {
this.auth.external.fireMessage({
type: "focus_element",
payload: {
element_id: "combo-box",

View File

@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
import "./ha-generic-picker";
import "./ha-icon";
@@ -88,8 +88,6 @@ const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (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`
<ha-generic-picker
.hass=${this.hass}
allow-custom-value
.getItems=${this._getIconPickerItems}
.helper=${this.helper}

View File

@@ -1,5 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume, type ContextType } from "@lit/context";
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
@@ -14,6 +15,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { localeContext, localizeContext } from "../data/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import {
multiTermSortedSearch,
@@ -21,7 +23,6 @@ import {
} from "../resources/fuseMultiTerm";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import { isTouch } from "../util/is_touch";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
@@ -90,8 +91,6 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
@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<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@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`<ha-textfield
.label=${searchLabel}
@@ -228,7 +235,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
<ha-icon-button
@click=${this._clearSearch}
slot="trailingIcon"
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
.label=${this.localize?.("ui.common.clear") || "Clear"}
.path=${mdiClose}
></ha-icon-button>
</ha-textfield>
@@ -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"}</span
>
</ha-combo-box-item>
@@ -507,7 +514,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,

View File

@@ -34,3 +34,5 @@ export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
export const configEntriesContext =
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 { 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<T = unknown> extends HTMLElement {
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;
dialogImport: () => Promise<unknown>;
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<unknown>;
@@ -47,7 +51,7 @@ export interface DialogState {
}
interface LoadedDialogInfo {
element: Promise<HassDialog>;
element: Promise<HassDialogNext | HassDialog> | null;
closedFocusTargets?: Set<Element>;
}
@@ -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<unknown>,
parentElement?: LitElement,
addHistory = true
): Promise<boolean> => {
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<DialogClosedParams>) => {
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<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
const {
dialogTag,
dialogImport,
dialogParams,
addHistory,
parentElement,
} = e.detail;
showDialog(
element,
root,
dialogTag,
dialogParams,
dialogImport,
parentElement,
addHistory
);
}

View File

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

View File

@@ -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: {

View File

@@ -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<void> {
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<typeof localizeContext>;
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
)}</span
: this.localize(shortcutKey.shortcutTranslationKey)}</span
>`
)}
${this.hass.localize(descriptionKey)}
${this.localize(descriptionKey)}
</div>
`;
}
@@ -212,14 +197,13 @@ class DialogShortcuts extends LitElement {
protected render() {
return html`
<ha-dialog
.open=${this._open}
@closed=${this._dialogClosed}
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
open
.headerTitle=${this.localize("ui.dialogs.shortcuts.title")}
>
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.hass.localize(section.titleTranslationKey)}</h3>
<h3>${this.localize(section.titleTranslationKey)}</h3>
<div class="items">
${section.items.map((item) => {
if ("shortcut" in item) {
@@ -229,7 +213,7 @@ class DialogShortcuts extends LitElement {
);
}
return html`<p>
${this.hass.localize((item as Text).textTranslationKey)}
${this.localize((item as Text).textTranslationKey)}
</p>`;
})}
</div>
@@ -238,9 +222,9 @@ class DialogShortcuts extends LitElement {
</div>
<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"
>${this.hass.localize(
>${this.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a
>`,

View File

@@ -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: {},
});

View File

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

View File

@@ -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<CategoryRegistryDetailDialogParams>(
LitElement
) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@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<void> {
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`
<ha-dialog
.hass=${this.hass}
.open=${this._open}
open
header-title=${entry
? this.hass.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")}
? this.localize("ui.panel.config.category.editor.edit")
: this.localize("ui.panel.config.category.editor.create")}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -81,8 +65,8 @@ class DialogCategoryDetail extends LitElement {
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
.validationMessage=${this.hass.localize(
.label=${this.localize("ui.panel.config.category.editor.name")}
.validationMessage=${this.localize(
"ui.panel.config.category.editor.required_error_msg"
)}
required
@@ -90,10 +74,9 @@ class DialogCategoryDetail extends LitElement {
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
.value=${this._icon ?? undefined}
@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>
</div>
<ha-dialog-footer slot="footer">
@@ -102,7 +85,7 @@ class DialogCategoryDetail extends LitElement {
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
${this.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@@ -110,8 +93,8 @@ class DialogCategoryDetail extends LitElement {
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.add")}
? this.localize("ui.common.save")
: this.localize("ui.common.add")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
@@ -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;
}

View File

@@ -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<LabelDetailDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
class DialogLabelDetail extends DialogMixin<LabelDetailDialogParams>(
LitElement
) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@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`
<ha-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._params.entry
? this._params.entry.name || this._params.entry.label_id
: this.hass!.localize("ui.dialogs.label-detail.new_label")}
open
header-title=${this.params.entry
? this.params.entry.name || this.params.entry.label_id
: this.localize("ui.dialogs.label-detail.new_label")}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<div>
${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
></ha-textfield>
<ha-icon-picker
.value=${this._icon}
.hass=${this.hass}
.configValue=${"icon"}
@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-color-picker
.value=${this._color}
.configValue=${"color"}
.hass=${this.hass}
@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-textarea
.value=${this._description}
.configValue=${"description"}
@input=${this._input}
.label=${this.hass!.localize(
"ui.dialogs.label-detail.description"
)}
.label=${this.localize("ui.dialogs.label-detail.description")}
></ha-textarea>
</div>
</div>
<ha-dialog-footer slot="footer">
${this._params.entry && this._params.removeEntry
${this.params.entry && this.params.removeEntry
? html`
<ha-button
slot="secondaryAction"
@@ -133,7 +112,7 @@ class DialogLabelDetail
@click=${this._deleteEntry}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.delete")}
${this.localize("ui.common.delete")}
</ha-button>
`
: html`
@@ -142,7 +121,7 @@ class DialogLabelDetail
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
${this.localize("ui.common.cancel")}
</ha-button>
`}
<ha-button
@@ -150,9 +129,9 @@ class DialogLabelDetail
@click=${this._updateEntry}
.disabled=${this._submitting || !this._name}
>
${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")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
@@ -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;

View File

@@ -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 = <T extends Constructor<HassBaseEl>>(
context: labelsContext,
initialValue: [],
}),
auth: new ContextProvider(this, {
context: authContext,
initialValue: this.hass?.auth,
}),
};
protected hassConnected() {

View File

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

View File

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

View File

@@ -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;