diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index f23733bb79..b64cd092dd 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -1,19 +1,12 @@ +/* eslint-disable lit/prefer-static-styles */ import "@material/mwc-button"; import { genClientId } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; +import { html, LitElement, nothing, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { LocalizeFunc } from "../common/translations/localize"; import "../components/ha-alert"; import "../components/ha-checkbox"; import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; -import "../components/ha-form/ha-form"; import "../components/ha-formfield"; import "../components/ha-markdown"; import { AuthProvider, autocompleteLoginFields } from "../data/auth"; @@ -21,7 +14,7 @@ import { DataEntryFlowStep, DataEntryFlowStepForm, } from "../data/data_entry_flow"; -import "./ha-password-manager-polyfill"; +import "./ha-auth-form"; type State = "loading" | "error" | "step"; @@ -49,6 +42,10 @@ export class HaAuthFlow extends LitElement { @state() private _storeToken = false; + createRenderRoot() { + return this; + } + willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); @@ -79,13 +76,17 @@ export class HaAuthFlow extends LitElement { protected render() { return html` +
${this._renderForm()}
- `; } @@ -128,12 +129,6 @@ export class HaAuthFlow extends LitElement { (form as any).focus(); } }, 100); - - setTimeout(() => { - this.renderRoot.querySelector( - "ha-password-manager-polyfill" - )!.boundingRect = this.getBoundingClientRect(); - }, 500); } private _renderForm() { @@ -205,7 +200,7 @@ export class HaAuthFlow extends LitElement { > ` : nothing} - + > ${this.clientId === genClientId() && !["select_mfa_module", "mfa"].includes(step.step_id) ? html` @@ -395,20 +390,6 @@ export class HaAuthFlow extends LitElement { this._submitting = false; } } - - static get styles(): CSSResultGroup { - return css` - .action { - margin: 24px 0 8px; - text-align: center; - } - /* Align with the rest of the form. */ - .store-token { - margin-top: 10px; - margin-left: -16px; - } - `; - } } declare global { diff --git a/src/auth/ha-auth-form-string.ts b/src/auth/ha-auth-form-string.ts new file mode 100644 index 0000000000..89fa713e42 --- /dev/null +++ b/src/auth/ha-auth-form-string.ts @@ -0,0 +1,69 @@ +/* eslint-disable lit/prefer-static-styles */ +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators"; +import { HaFormString } from "../components/ha-form/ha-form-string"; +import "../components/ha-icon-button"; +import "./ha-auth-textfield"; + +@customElement("ha-auth-form-string") +export class HaAuthFormString extends HaFormString { + protected createRenderRoot() { + // add parent style to light dom + const style = document.createElement("style"); + style.innerHTML = HaFormString.elementStyles as unknown as string; + this.append(style); + return this; + } + + protected render(): TemplateResult { + return html` + + ` + : this.schema.description?.suffix + } + .validationMessage=${this.schema.required ? "Required" : undefined} + @input=${this._valueChanged} + @change=${this._valueChanged} + > + ${this.renderIcon()} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-auth-form-string": HaAuthFormString; + } +} diff --git a/src/auth/ha-auth-form.ts b/src/auth/ha-auth-form.ts new file mode 100644 index 0000000000..1a098caf0d --- /dev/null +++ b/src/auth/ha-auth-form.ts @@ -0,0 +1,30 @@ +/* eslint-disable lit/prefer-static-styles */ +import { customElement } from "lit/decorators"; +import { HaForm } from "../components/ha-form/ha-form"; +import "./ha-auth-form-string"; + +@customElement("ha-auth-form") +export class HaAuthForm extends HaForm { + protected fieldElementName(type: string): string { + if (type === "string") { + return `ha-auth-form-${type}`; + } + return super.fieldElementName(type); + } + + protected createRenderRoot() { + // add parent style to light dom + const style = document.createElement("style"); + style.innerHTML = HaForm.elementStyles as unknown as string; + this.append(style); + // attach it as soon as possible to make sure we fetch all events. + this.addValueChangedListener(this); + return this; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-auth-form": HaAuthForm; + } +} diff --git a/src/auth/ha-auth-textfield.ts b/src/auth/ha-auth-textfield.ts new file mode 100644 index 0000000000..7c4bb2c6cc --- /dev/null +++ b/src/auth/ha-auth-textfield.ts @@ -0,0 +1,186 @@ +/* eslint-disable lit/prefer-static-styles */ +import { html } from "lit"; +import { customElement } from "lit/decorators"; +import { HaTextField } from "../components/ha-textfield"; +import "@material/mwc-textfield/mwc-textfield.css"; + +@customElement("ha-auth-textfield") +export class HaAuthTextField extends HaTextField { + public render() { + return html` + + ${super.render()} + `; + } + + protected createRenderRoot() { + // add parent style to light dom + const style = document.createElement("style"); + style.innerHTML = HaTextField.elementStyles as unknown as string; + this.append(style); + return this; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-auth-textfield": HaAuthTextField; + } +} diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 9778edfc0a..54d59bd8e9 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -1,13 +1,7 @@ -import punycode from "punycode"; -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; +/* eslint-disable lit/prefer-static-styles */ +import { html, LitElement, nothing, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import punycode from "punycode"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { extractSearchParamsObject } from "../common/url/search-params"; import "../components/ha-alert"; @@ -61,13 +55,27 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { protected render() { if (this._error) { - return html`${this._error} ${this.redirectUri}`; + return html` + + ${this._error} ${this.redirectUri} + `; } if (!this._authProviders) { return html` +

${this.localize("ui.panel.page-authorize.initializing")}

`; } @@ -79,6 +87,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { const app = this.clientId && this.clientId in appNames; return html` + + ${!this._ownInstance ? html` ${app @@ -123,6 +150,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { `; } + createRenderRoot() { + return this; + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); @@ -217,25 +248,4 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { private async _handleAuthProviderPick(ev) { this._authProvider = ev.detail; } - - static get styles(): CSSResultGroup { - return css` - ha-pick-auth-provider { - display: block; - margin-top: 48px; - } - ha-auth-flow { - display: block; - margin-top: 24px; - } - ha-alert { - display: block; - margin: 16px 0; - } - p { - font-size: 14px; - line-height: 20px; - } - `; - } } diff --git a/src/auth/ha-password-manager-polyfill.ts b/src/auth/ha-password-manager-polyfill.ts deleted file mode 100644 index 7c66d11fe3..0000000000 --- a/src/auth/ha-password-manager-polyfill.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import { fireEvent } from "../common/dom/fire_event"; -import type { HaFormSchema } from "../components/ha-form/types"; -import { autocompleteLoginFields } from "../data/auth"; -import type { DataEntryFlowStep } from "../data/data_entry_flow"; - -declare global { - interface HTMLElementTagNameMap { - "ha-password-manager-polyfill": HaPasswordManagerPolyfill; - } - interface HASSDomEvents { - "form-submitted": undefined; - } -} - -const ENABLED_HANDLERS = [ - "homeassistant", - "legacy_api_password", - "command_line", -]; - -@customElement("ha-password-manager-polyfill") -export class HaPasswordManagerPolyfill extends LitElement { - @property({ attribute: false }) public step?: DataEntryFlowStep; - - @property({ attribute: false }) public stepData: any; - - @property({ attribute: false }) public boundingRect?: DOMRect; - - private _styleElement?: HTMLStyleElement; - - public connectedCallback() { - super.connectedCallback(); - this._styleElement = document.createElement("style"); - this._styleElement.textContent = css` - /* Polyfill form is sized and vertically aligned with true form, then positioned offscreen - rather than hiding so it does not create a new stacking context */ - .password-manager-polyfill { - position: absolute; - box-sizing: border-box; - } - /* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */ - .password-manager-polyfill > *:not(.wrapper), - .password-manager-polyfill > .wrapper > * { - position: relative; - left: 10000px; - } - /* Size and hide our polyfill fields */ - .password-manager-polyfill .underneath { - display: block; - box-sizing: border-box; - width: 100%; - padding: 0 16px; - border: 0; - z-index: -1; - height: 21px; - /* Transparency is only needed to hide during paint or in case of misalignment, - but LastPass will fail if it's 0, so we use 1% */ - opacity: 0.01; - } - .password-manager-polyfill input.underneath { - height: 28px; - margin-bottom: 30.5px; - } - /* Button position is not important, but size should not be zero */ - .password-manager-polyfill > input.underneath[type="submit"] { - width: 1px; - height: 1px; - margin: 0 auto; - overflow: hidden; - } - /* Ensure injected elements will be on top */ - .password-manager-polyfill > *:not(.underneath, .wrapper), - .password-manager-polyfill > .wrapper > *:not(.underneath) { - isolation: isolate; - z-index: auto; - } - `.toString(); - document.head.append(this._styleElement); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - this._styleElement?.remove(); - delete this._styleElement; - } - - protected createRenderRoot() { - // Add under document body so the element isn't placed inside any shadow roots - return document.body; - } - - protected render() { - if ( - this.step && - this.step.type === "form" && - this.step.step_id === "init" && - ENABLED_HANDLERS.includes(this.step.handler[0]) - ) { - return html` -
- ${autocompleteLoginFields(this.step.data_schema).map((input) => - this.render_input(input) - )} - -
- `; - } - return nothing; - } - - private render_input(schema: HaFormSchema) { - const inputType = schema.name.includes("password") ? "password" : "text"; - if (schema.type !== "string") { - return ""; - } - return html` - - - - - `; - } - - private _handleSubmit(ev: SubmitEvent) { - ev.preventDefault(); - fireEvent(this, "form-submitted"); - } - - private _valueChanged(ev: Event) { - const target = ev.target as HTMLInputElement; - this.stepData = { ...this.stepData, [target.id]: target.value }; - fireEvent(this, "value-changed", { - value: this.stepData, - }); - } -} diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts index 5cc63ce1d4..3118fcf948 100644 --- a/src/components/ha-form/ha-form-string.ts +++ b/src/components/ha-form/ha-form-string.ts @@ -1,11 +1,13 @@ +/* eslint-disable lit/prefer-static-styles */ import { mdiEye, mdiEyeOff } from "@mdi/js"; import { - css, CSSResultGroup, - html, LitElement, PropertyValues, TemplateResult, + css, + html, + nothing, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; @@ -32,7 +34,7 @@ export class HaFormString extends LitElement implements HaFormElement { @property({ type: Boolean }) public disabled = false; - @state() private _unmaskedPassword = false; + @state() protected unmaskedPassword = false; @query("ha-textfield") private _input?: HaTextField; @@ -43,14 +45,11 @@ export class HaFormString extends LitElement implements HaFormElement { } protected render(): TemplateResult { - const isPassword = MASKED_FIELDS.some((field) => - this.schema.name.includes(field) - ); return html` ` : this.schema.description?.suffix} @@ -70,14 +69,19 @@ export class HaFormString extends LitElement implements HaFormElement { @input=${this._valueChanged} @change=${this._valueChanged} > - ${isPassword - ? html`` - : ""} + ${this.renderIcon()} + `; + } + + protected renderIcon() { + if (!this.isPassword) return nothing; + return html` + `; } @@ -87,11 +91,11 @@ export class HaFormString extends LitElement implements HaFormElement { } } - private _toggleUnmaskedPassword(): void { - this._unmaskedPassword = !this._unmaskedPassword; + protected toggleUnmaskedPassword(): void { + this.unmaskedPassword = !this.unmaskedPassword; } - private _valueChanged(ev: Event): void { + protected _valueChanged(ev: Event): void { let value: string | undefined = (ev.target as HaTextField).value; if (this.data === value) { return; @@ -104,7 +108,7 @@ export class HaFormString extends LitElement implements HaFormElement { }); } - private get _stringType(): string { + protected get stringType(): string { if (this.schema.format) { if (["email", "url"].includes(this.schema.format)) { return this.schema.format; @@ -116,6 +120,10 @@ export class HaFormString extends LitElement implements HaFormElement { return "text"; } + protected get isPassword(): boolean { + return MASKED_FIELDS.some((field) => this.schema.name.includes(field)); + } + static get styles(): CSSResultGroup { return css` :host { diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 98c177f482..b02f7f5b0d 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -1,3 +1,4 @@ +/* eslint-disable lit/prefer-static-styles */ import { css, CSSResultGroup, @@ -135,7 +136,7 @@ export class HaForm extends LitElement implements HaFormElement { .required=${item.required || false} .context=${this._generateContext(item)} >` - : dynamicElement(`ha-form-${item.type}`, { + : dynamicElement(this.fieldElementName(item.type), { schema: item, data: getValue(this.data, item), label: this._computeLabel(item, this.data), @@ -152,6 +153,10 @@ export class HaForm extends LitElement implements HaFormElement { `; } + protected fieldElementName(type: string): string { + return `ha-form-${type}`; + } + private _generateContext( schema: HaFormSchema ): Record | undefined { @@ -169,10 +174,17 @@ export class HaForm extends LitElement implements HaFormElement { protected createRenderRoot() { const root = super.createRenderRoot(); // attach it as soon as possible to make sure we fetch all events. - root.addEventListener("value-changed", (ev) => { + this.addValueChangedListener(root); + return root; + } + + protected addValueChangedListener(element: Element | ShadowRoot) { + element.addEventListener("value-changed", (ev) => { ev.stopPropagation(); const schema = (ev.target as HaFormElement).schema as HaFormSchema; + if (ev.target === this) return; + const newValue = !schema.name ? ev.detail.value : { [schema.name]: ev.detail.value }; @@ -181,7 +193,6 @@ export class HaForm extends LitElement implements HaFormElement { value: { ...this.data, ...newValue }, }); }); - return root; } private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { diff --git a/src/html/authorize.html.template b/src/html/authorize.html.template index 740d2b2c79..b5fd0259f4 100644 --- a/src/html/authorize.html.template +++ b/src/html/authorize.html.template @@ -53,7 +53,7 @@ -

Initializing

+ <%= renderTemplate("_js_base.html.template") %> <%= renderTemplate("_preload_roboto.html.template") %>