1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Migrate integrations dialogs to wa (#29567)

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
Aidan Timson
2026-02-11 12:48:41 +00:00
committed by GitHub
parent 03917a5e1c
commit 3b3f8f3343
4 changed files with 207 additions and 194 deletions

View File

@@ -6,8 +6,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
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 "../../components/ha-dialog"; import "../../components/ha-wa-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import type { DataEntryFlowStep } from "../../data/data_entry_flow"; import type { DataEntryFlowStep } from "../../data/data_entry_flow";
import { import {
@@ -64,6 +63,8 @@ class DataEntryFlowDialog extends LitElement {
private _instance = instance; private _instance = instance;
@state() private _open = false;
@state() private _step: @state() private _step:
| DataEntryFlowStep | DataEntryFlowStep
| undefined | undefined
@@ -77,6 +78,7 @@ class DataEntryFlowDialog extends LitElement {
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> { public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params; this._params = params;
this._instance = instance++; this._instance = instance++;
this._open = true;
const curInstance = this._instance; const curInstance = this._instance;
let step: DataEntryFlowStep; let step: DataEntryFlowStep;
@@ -146,6 +148,17 @@ class DataEntryFlowDialog extends LitElement {
} }
public closeDialog() { public closeDialog() {
if (!this._params) {
return;
}
if (!this._open) {
this._dialogClosed();
return;
}
this._open = false;
}
private _dialogClosed(): void {
if (!this._params) { if (!this._params) {
return; return;
} }
@@ -174,6 +187,7 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDataEntryFlowProgress(); this._unsubDataEntryFlowProgress();
this._unsubDataEntryFlowProgress = undefined; this._unsubDataEntryFlowProgress = undefined;
} }
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -289,56 +303,53 @@ class DataEntryFlowDialog extends LitElement {
const dialogSubtitle = this._getDialogSubtitle(); const dialogSubtitle = this._getDialogSubtitle();
return html` return html`
<ha-dialog <ha-wa-dialog
open .hass=${this.hass}
@closed=${this.closeDialog} .open=${this._open}
scrimClickAction prevent-scrim-close
escapeKeyAction @after-show=${this._focusFormStep}
hideActions @closed=${this._dialogClosed}
.heading=${dialogTitle || true}
> >
<ha-dialog-header slot="heading"> <ha-icon-button
<ha-icon-button slot="headerNavigationIcon"
.label=${this.hass.localize("ui.common.close")} .label=${this.hass.localize("ui.common.close")}
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" data-dialog="close"
slot="navigationIcon" ></ha-icon-button>
></ha-icon-button>
<div <div
slot="title" slot="headerTitle"
class="dialog-title${this._step?.type === "form" ? " form" : ""}" class="dialog-title${this._step?.type === "form" ? " form" : ""}"
title=${dialogTitle} title=${dialogTitle}
> >
${dialogTitle} ${dialogTitle}
</div> </div>
${dialogSubtitle ${dialogSubtitle
? html` <div slot="subtitle">${dialogSubtitle}</div>` ? html` <div slot="headerSubtitle">${dialogSubtitle}</div>`
: nothing} : nothing}
${showDocumentationLink && !this._loading && this._step ${showDocumentationLink && !this._loading && this._step
? html` ? html`
<a <a
slot="actionItems" slot="headerActionItems"
class="help" class="help"
href=${this._params.manifest!.is_built_in href=${this._params.manifest!.is_built_in
? documentationUrl( ? documentationUrl(
this.hass, this.hass,
`/integrations/${this._params.manifest!.domain}` `/integrations/${this._params.manifest!.domain}`
) )
: this._params.manifest!.documentation} : this._params.manifest!.documentation}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
>
<ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
> >
<ha-icon-button </ha-icon-button
.label=${this.hass.localize("ui.common.help")} ></a>
.path=${mdiHelpCircle} `
> : nothing}
</ha-icon-button
></a>
`
: nothing}
</ha-dialog-header>
<div> <div>
${this._loading || this._step === null ${this._loading || this._step === null
? html` ? html`
@@ -358,6 +369,7 @@ class DataEntryFlowDialog extends LitElement {
${this._step.type === "form" ${this._step.type === "form"
? html` ? html`
<step-flow-form <step-flow-form
autofocus
narrow narrow
.flowConfig=${this._params.flowConfig} .flowConfig=${this._params.flowConfig}
.step=${this._step} .step=${this._step}
@@ -417,7 +429,7 @@ class DataEntryFlowDialog extends LitElement {
`} `}
`} `}
</div> </div>
</ha-dialog> </ha-wa-dialog>
`; `;
} }
@@ -547,11 +559,24 @@ class DataEntryFlowDialog extends LitElement {
}; };
} }
private _focusFormStep = async (): Promise<void> => {
if (this._step?.type !== "form" || !this._open) {
return;
}
await this.updateComplete;
(
this.renderRoot.querySelector(
"step-flow-form[autofocus]"
) as HTMLElement | null
)?.focus();
};
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-dialog { ha-wa-dialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
.dialog-title { .dialog-title {

View File

@@ -29,6 +29,8 @@ class StepFlowForm extends LitElement {
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "autofocus" }) public autoFocus = false;
@property({ attribute: false }) public step!: DataEntryFlowStepForm; @property({ attribute: false }) public step!: DataEntryFlowStepForm;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -45,6 +47,11 @@ class StepFlowForm extends LitElement {
private _errors?: Record<string, string>; private _errors?: Record<string, string>;
static shadowRootOptions: ShadowRootInit = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this.removeEventListener("keydown", this._handleKeyDown); this.removeEventListener("keydown", this._handleKeyDown);
@@ -83,6 +90,7 @@ class StepFlowForm extends LitElement {
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>` ? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
: ""} : ""}
<ha-form <ha-form
?autofocus=${this.autoFocus}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.data=${stepData} .data=${stepData}
@@ -133,10 +141,13 @@ class StepFlowForm extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
this.addEventListener("keydown", this._handleKeyDown); this.addEventListener("keydown", this._handleKeyDown);
} }
public override focus(_options?: FocusOptions): void {
this.renderRoot.querySelector("ha-form")?.focus();
}
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (changedProps.has("step") && this.step?.preview) { if (changedProps.has("step") && this.step?.preview) {

View File

@@ -1,11 +1,9 @@
import { mdiClose } from "@mdi/js";
import type { IFuseOptions } from "fuse.js"; import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import type { HassConfig } from "home-assistant-js-websocket"; import type { HassConfig } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
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 { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -17,10 +15,10 @@ import {
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-button-prev"; import "../../../components/ha-icon-button-prev";
import "../../../components/ha-list"; import "../../../components/ha-list";
import "../../../components/ha-spinner"; import "../../../components/ha-spinner";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input"; import "../../../components/search-input";
import { getConfigEntries } from "../../../data/config_entries"; import { getConfigEntries } from "../../../data/config_entries";
import { import {
@@ -145,6 +143,10 @@ class AddIntegrationDialog extends LitElement {
public closeDialog() { public closeDialog() {
this._open = false; this._open = false;
}
private _dialogClosed() {
this._open = false;
this._integrations = undefined; this._integrations = undefined;
this._helpers = undefined; this._helpers = undefined;
this._pickedBrand = undefined; this._pickedBrand = undefined;
@@ -361,7 +363,7 @@ class AddIntegrationDialog extends LitElement {
} }
protected render() { protected render() {
if (!this._open) { if (!this._open && !this._integrations && !this._helpers) {
return nothing; return nothing;
} }
const integrations = this._integrations const integrations = this._integrations
@@ -373,20 +375,38 @@ class AddIntegrationDialog extends LitElement {
findIntegration(this._integrations, this._pickedBrand) findIntegration(this._integrations, this._pickedBrand)
: undefined; : undefined;
return html`<ha-dialog const showingBrandView =
open (this._pickedBrand && (!this._integrations || pickedIntegration)) ||
@closed=${this.closeDialog} this._showDiscovered;
hideActions
.heading=${createCloseHeading( const flowsInProgress = showingBrandView
this.hass, ? this._getFlowsForCurrentView(pickedIntegration)
this.hass.localize("ui.panel.config.integrations.new") : [];
)}
const headerTitle = showingBrandView
? this._getBrandHeading(pickedIntegration, flowsInProgress)
: this.hass.localize("ui.panel.config.integrations.new");
return html`<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${headerTitle}
@closed=${this._dialogClosed}
> >
${(this._pickedBrand && (!this._integrations || pickedIntegration)) || ${showingBrandView
this._showDiscovered ? html`
? this._renderBrandView(pickedIntegration) ${!this._openedDirectly
? html`
<ha-icon-button-prev
slot="headerNavigationIcon"
@click=${this._prevClicked}
></ha-icon-button-prev>
`
: nothing}
${this._renderBrandView(pickedIntegration, flowsInProgress)}
`
: this._renderAll(integrations)} : this._renderAll(integrations)}
</ha-dialog>`; </ha-wa-dialog>`;
} }
private _getFlowsForCurrentView( private _getFlowsForCurrentView(
@@ -413,61 +433,53 @@ class AddIntegrationDialog extends LitElement {
return this._getFlowsInProgressForDomains(domains); return this._getFlowsInProgressForDomains(domains);
} }
private _renderBrandView( private _getBrandHeading(
integration: Brand | Integration | undefined integration: Brand | Integration | undefined,
): TemplateResult { flowsInProgress: DataEntryFlowProgress[]
const flowsInProgress = this._getFlowsForCurrentView(integration); ): string {
let heading: string;
if ( if (
integration?.iot_standards && integration?.iot_standards &&
!("integrations" in integration) && !("integrations" in integration) &&
!flowsInProgress.length !flowsInProgress.length
) { ) {
heading = this.hass.localize( return this.hass.localize(
"ui.panel.config.integrations.what_device_type" "ui.panel.config.integrations.what_device_type"
); );
} else if ( }
if (
integration && integration &&
!integration?.iot_standards && !integration?.iot_standards &&
!("integrations" in integration) && !("integrations" in integration) &&
flowsInProgress.length flowsInProgress.length
) { ) {
heading = this.hass.localize( return this.hass.localize(
"ui.panel.config.integrations.confirm_add_discovered" "ui.panel.config.integrations.confirm_add_discovered"
); );
} else {
heading = this.hass.localize("ui.panel.config.integrations.what_to_add");
} }
return html`<div slot="heading"> return this.hass.localize("ui.panel.config.integrations.what_to_add");
${this._openedDirectly }
? html`<ha-icon-button
class="header-close-button" private _renderBrandView(
.label=${this.hass.localize("ui.common.close")} integration: Brand | Integration | undefined,
.path=${mdiClose} flowsInProgress: DataEntryFlowProgress[]
dialogAction="close" ): TemplateResult {
></ha-icon-button>` return html`<ha-domain-integrations
: html`<ha-icon-button-prev .hass=${this.hass}
@click=${this._prevClicked} .domain=${this._pickedBrand}
></ha-icon-button-prev>`} .integration=${integration}
<h2 class="mdc-dialog__title">${heading}</h2> .flowsInProgress=${flowsInProgress}
</div> .navigateToResult=${this._navigateToResult}
<ha-domain-integrations .showManageLink=${this._showDiscovered}
.hass=${this.hass} style=${styleMap({
.domain=${this._pickedBrand} minWidth: `${this._width}px`,
.integration=${integration} minHeight: `581px`,
.flowsInProgress=${flowsInProgress} })}
.navigateToResult=${this._navigateToResult} @close-dialog=${this.closeDialog}
.showManageLink=${this._showDiscovered} @supported-by=${this._handleSupportedByEvent}
style=${styleMap({ @select-brand=${this._handleSelectBrandEvent}
minWidth: `${this._width}px`, ></ha-domain-integrations>`;
minHeight: `581px`,
})}
@close-dialog=${this.closeDialog}
@supported-by=${this._handleSupportedByEvent}
@select-brand=${this._handleSelectBrandEvent}
></ha-domain-integrations>`;
} }
private _handleSelectBrandEvent(ev: CustomEvent) { private _handleSelectBrandEvent(ev: CustomEvent) {
@@ -524,7 +536,7 @@ class AddIntegrationDialog extends LitElement {
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
return html`<search-input return html`<search-input
.hass=${this.hass} .hass=${this.hass}
dialogInitialFocus=${ifDefined(this._narrow ? undefined : "")} ?autofocus=${!this._narrow}
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._filterChanged} @value-changed=${this._filterChanged}
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -533,9 +545,7 @@ class AddIntegrationDialog extends LitElement {
@keypress=${this._maybeSubmit} @keypress=${this._maybeSubmit}
></search-input> ></search-input>
${integrations ${integrations
? html`<ha-list ? html`<ha-list ?autofocus=${this._narrow}>
dialogInitialFocus=${ifDefined(this._narrow ? "" : undefined)}
>
<lit-virtualizer <lit-virtualizer
scroller scroller
tabindex="-1" tabindex="-1"
@@ -809,25 +819,16 @@ class AddIntegrationDialog extends LitElement {
haStyleScrollbar, haStyleScrollbar,
haStyleDialog, haStyleDialog,
css` css`
@media all and (min-width: 550px) { ha-wa-dialog {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-dialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
search-input { search-input {
display: block; display: block;
margin: 16px 16px 0; margin: 0 16px;
} }
.divider { .divider {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
h2 {
padding-inline-end: 66px;
direction: var(--direction);
}
p { p {
text-align: center; text-align: center;
padding: 16px; padding: 16px;
@@ -853,43 +854,6 @@ class AddIntegrationDialog extends LitElement {
ha-integration-list-item { ha-integration-list-item {
width: 100%; width: 100%;
} }
ha-icon-button-prev,
.header-close-button {
color: var(--secondary-text-color);
position: absolute;
left: 16px;
top: 14px;
inset-inline-end: initial;
inset-inline-start: 16px;
direction: var(--direction);
}
.mdc-dialog__title {
margin: 0;
margin-bottom: 8px;
margin-left: 48px;
margin-inline-start: 48px;
margin-inline-end: initial;
padding: 24px 24px 0 24px;
color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87));
font-size: var(
--mdc-typography-headline6-font-size,
var(--ha-font-size-l)
);
line-height: var(--mdc-typography-headline6-line-height, 2rem);
font-weight: var(
--mdc-typography-headline6-font-weight,
var(--ha-font-weight-medium)
);
letter-spacing: var(
--mdc-typography-headline6-letter-spacing,
0.0125em
);
text-decoration: var(
--mdc-typography-headline6-text-decoration,
inherit
);
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
}
`, `,
]; ];
} }

View File

@@ -1,11 +1,14 @@
import { mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import type { YamlIntegrationDialogParams } from "./show-add-integration-dialog"; import type { YamlIntegrationDialogParams } from "./show-add-integration-dialog";
import "../../../components/ha-dialog";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-svg-icon";
import "../../../components/ha-wa-dialog";
@customElement("dialog-yaml-integration") @customElement("dialog-yaml-integration")
export class DialogYamlIntegration extends LitElement { export class DialogYamlIntegration extends LitElement {
@@ -13,11 +16,18 @@ export class DialogYamlIntegration extends LitElement {
@state() private _params?: YamlIntegrationDialogParams; @state() private _params?: YamlIntegrationDialogParams;
@state() private _open = false;
public showDialog(params: YamlIntegrationDialogParams): void { public showDialog(params: YamlIntegrationDialogParams): void {
this._params = params; this._params = params;
this._open = true;
} }
public closeDialog() { public closeDialog() {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -31,43 +41,51 @@ export class DialogYamlIntegration extends LitElement {
? documentationUrl(this.hass, `/integrations/${manifest.domain}`) ? documentationUrl(this.hass, `/integrations/${manifest.domain}`)
: manifest.documentation; : manifest.documentation;
return html` return html`
<ha-dialog <ha-wa-dialog
open .hass=${this.hass}
@closed=${this.closeDialog} .open=${this._open}
.heading=${this.hass.localize( header-title=${this.hass.localize(
"ui.panel.config.integrations.config_flow.yaml_only_title" "ui.panel.config.integrations.config_flow.yaml_only_title"
)} )}
@closed=${this._dialogClosed}
> >
<p> <p>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_flow.yaml_only" "ui.panel.config.integrations.config_flow.yaml_only"
)} )}
</p> </p>
<ha-button <ha-dialog-footer slot="footer">
appearance="plain" <ha-button
@click=${this.closeDialog} slot="secondaryAction"
slot="primaryAction" appearance="plain"
> @click=${this.closeDialog}
${this.hass.localize("ui.common.cancel")} >
</ha-button> ${this.hass.localize("ui.common.cancel")}
${docLink </ha-button>
? html`<ha-button ${docLink
appearance="plain" ? html`<ha-button
href=${docLink} slot="primaryAction"
target="_blank" appearance="plain"
rel="noreferrer noopener" href=${docLink}
slot="primaryAction" target="_blank"
@click=${this.closeDialog} rel="noreferrer noopener"
dialogInitialFocus @click=${this.closeDialog}
> autofocus
${this.hass.localize( >
"ui.panel.config.integrations.config_flow.open_documentation" <ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
)} ${this.hass.localize(
</ha-button>` "ui.panel.config.integrations.config_flow.open_documentation"
: html`<ha-button @click=${this.closeDialog} dialogInitialFocus> )}
${this.hass.localize("ui.common.ok")} </ha-button>`
</ha-button>`} : html`<ha-button
</ha-dialog> slot="primaryAction"
@click=${this.closeDialog}
autofocus
>
${this.hass.localize("ui.common.ok")}
</ha-button>`}
</ha-dialog-footer>
</ha-wa-dialog>
`; `;
} }
@@ -79,15 +97,10 @@ export class DialogYamlIntegration extends LitElement {
a { a {
text-decoration: none; text-decoration: none;
} }
ha-dialog { ha-wa-dialog {
/* Place above other dialogs */ /* Place above other dialogs */
--dialog-z-index: 104; --dialog-z-index: 104;
} }
@media all and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 400px;
}
}
`; `;
} }