mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Move auth components from shadow DOM to light DOM (#18015)
This commit is contained in:
parent
acb32ae5c8
commit
d8c98d8f96
@ -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`
|
||||
<style>
|
||||
.action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.store-token {
|
||||
margin-top: 10px;
|
||||
margin-left: -16px;
|
||||
}
|
||||
</style>
|
||||
<form>${this._renderForm()}</form>
|
||||
<ha-password-manager-polyfill
|
||||
.step=${this._step}
|
||||
.stepData=${this._stepData}
|
||||
@form-submitted=${this._handleSubmit}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-password-manager-polyfill>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
></ha-markdown>
|
||||
`
|
||||
: nothing}
|
||||
<ha-form
|
||||
<ha-auth-form
|
||||
.data=${this._stepData}
|
||||
.schema=${autocompleteLoginFields(step.data_schema)}
|
||||
.error=${step.errors}
|
||||
@ -213,7 +208,7 @@ export class HaAuthFlow extends LitElement {
|
||||
.computeLabel=${this._computeLabelCallback(step)}
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-form>
|
||||
></ha-auth-form>
|
||||
${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 {
|
||||
|
69
src/auth/ha-auth-form-string.ts
Normal file
69
src/auth/ha-auth-form-string.ts
Normal file
@ -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`
|
||||
<style>
|
||||
ha-auth-form-string {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-auth-form-string[own-margin] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
ha-auth-textfield {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
<ha-auth-textfield
|
||||
.type=${
|
||||
!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
.suffix=${
|
||||
this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix
|
||||
}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-auth-form-string": HaAuthFormString;
|
||||
}
|
||||
}
|
30
src/auth/ha-auth-form.ts
Normal file
30
src/auth/ha-auth-form.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
186
src/auth/ha-auth-textfield.ts
Normal file
186
src/auth/ha-auth-textfield.ts
Normal file
@ -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`
|
||||
<style>
|
||||
ha-auth-textfield {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
ha-auth-textfield:not([disabled]):hover
|
||||
:not(.mdc-text-field--invalid):not(.mdc-text-field--focused)
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-outlined-hover-border-color,
|
||||
rgba(0, 0, 0, 0.87)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--outlined) {
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-error-color,
|
||||
var(--mdc-theme-error, #b00020)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
+ .mdc-text-field-helper-line
|
||||
.mdc-text-field-character-counter,
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
.mdc-text-field__icon {
|
||||
color: var(
|
||||
--mdc-text-field-error-color,
|
||||
var(--mdc-theme-error, #b00020)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label,
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label::after {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-stroke-width: 2px;
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-focused-label-color,
|
||||
var(--mdc-theme-primary, rgba(98, 0, 238, 0.87))
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
.mdc-floating-label {
|
||||
color: #6200ee;
|
||||
color: var(--mdc-theme-primary, #6200ee);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input {
|
||||
color: var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input::placeholder {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-helper-text:not(
|
||||
.mdc-text-field-helper-text--validation-msg
|
||||
),
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field-helper-line:not(.mdc-text-field--invalid)
|
||||
.mdc-text-field-character-counter {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--outlined) {
|
||||
background-color: var(--mdc-text-field-disabled-fill-color, #fafafa);
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field.mdc-text-field--outlined
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-outlined-disabled-border-color,
|
||||
rgba(0, 0, 0, 0.06)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label::after {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled] .mdc-text-field .mdc-text-field__input,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input::placeholder {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-helper-text,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-character-counter {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
.mdc-floating-label {
|
||||
color: var(--mdc-theme-primary, #6200ee);
|
||||
}
|
||||
ha-auth-textfield[no-spinner] input::-webkit-outer-spin-button,
|
||||
ha-auth-textfield[no-spinner] input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
ha-auth-textfield[no-spinner] input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
${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;
|
||||
}
|
||||
}
|
@ -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`<ha-alert alert-type="error"
|
||||
>${this._error} ${this.redirectUri}</ha-alert
|
||||
>`;
|
||||
return html`
|
||||
<style>
|
||||
ha-authorize ha-alert {
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
<ha-alert alert-type="error"
|
||||
>${this._error} ${this.redirectUri}</ha-alert
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._authProviders) {
|
||||
return html`
|
||||
<style>
|
||||
ha-authorize p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
|
||||
`;
|
||||
}
|
||||
@ -79,6 +87,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
const app = this.clientId && this.clientId in appNames;
|
||||
|
||||
return html`
|
||||
<style>
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
${!this._ownInstance
|
||||
? html`<ha-alert .alertType=${app ? "info" : "warning"}>
|
||||
${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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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`
|
||||
<form
|
||||
class="password-manager-polyfill"
|
||||
style=${styleMap({
|
||||
top: `${this.boundingRect?.y || 148}px`,
|
||||
left: `calc(50% - ${
|
||||
(this.boundingRect?.width || 360) / 2
|
||||
}px - 10000px)`,
|
||||
width: `${this.boundingRect?.width || 360}px`,
|
||||
})}
|
||||
action="/auth"
|
||||
method="post"
|
||||
@submit=${this._handleSubmit}
|
||||
>
|
||||
${autocompleteLoginFields(this.step.data_schema).map((input) =>
|
||||
this.render_input(input)
|
||||
)}
|
||||
<input
|
||||
type="submit"
|
||||
value="Login"
|
||||
class="underneath"
|
||||
tabindex="-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private render_input(schema: HaFormSchema) {
|
||||
const inputType = schema.name.includes("password") ? "password" : "text";
|
||||
if (schema.type !== "string") {
|
||||
return "";
|
||||
}
|
||||
return html`
|
||||
<!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) -->
|
||||
<label for=${schema.name} class="underneath" aria-hidden="true">
|
||||
${schema.name}
|
||||
</label>
|
||||
<!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead -->
|
||||
<div class="wrapper" aria-hidden="true">
|
||||
<!-- LastPass fails with tabindex of -1, so we trick with -2 -->
|
||||
<input
|
||||
class="underneath"
|
||||
tabindex="-2"
|
||||
.id=${schema.name}
|
||||
.name=${schema.name}
|
||||
.type=${inputType}
|
||||
.value=${this.stepData[schema.name] || ""}
|
||||
.autocomplete=${schema.autocomplete}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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`
|
||||
<ha-textfield
|
||||
.type=${!isPassword
|
||||
? this._stringType
|
||||
: this._unmaskedPassword
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
@ -62,7 +61,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
.autoValidate=${this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
.suffix=${isPassword
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
@ -70,14 +69,19 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
${isPassword
|
||||
? html`<ha-icon-button
|
||||
toggles
|
||||
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderIcon() {
|
||||
if (!this.isPassword) return nothing;
|
||||
return html`
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${`${this.unmaskedPassword ? "Hide" : "Show"} password`}
|
||||
@click=${this.toggleUnmaskedPassword}
|
||||
.path=${this.unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
@ -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)}
|
||||
></ha-selector>`
|
||||
: 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<string, any> | 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) {
|
||||
|
@ -53,7 +53,7 @@
|
||||
<path d="M1824.98 272.606C1819.35 273.952 1813.59 274.696 1807.8 274.826C1797.84 274.826 1790.08 272.09 1784.51 266.616C1778.94 261.143 1776.16 253.006 1776.16 242.206V193.996H1761.02V173.996H1776.16V143.216H1799.59V173.976H1822.35V193.976H1799.59V237.976C1799.59 248.643 1803.73 253.976 1812 253.976C1815.42 253.833 1818.8 253.14 1822 251.926L1824.98 272.606Z" class="wordmark"/>
|
||||
</svg>
|
||||
</div>
|
||||
<ha-authorize><p>Initializing</p></ha-authorize>
|
||||
<ha-authorize></ha-authorize>
|
||||
</div>
|
||||
<%= renderTemplate("_js_base.html.template") %>
|
||||
<%= renderTemplate("_preload_roboto.html.template") %>
|
||||
|
Loading…
x
Reference in New Issue
Block a user