Compare commits

..

3 Commits

Author SHA1 Message Date
Zack
728ea265e2 Colors 2022-01-24 09:44:30 -06:00
Zack Barett
d859b61365 Update src/translations/en.json
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-21 17:07:03 -06:00
Zack Barett
50bf69860f Move Developer Tools to Settings 2022-01-21 21:45:40 +00:00
345 changed files with 11235 additions and 23268 deletions

View File

@@ -41,7 +41,7 @@ jobs:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine build python3 -m pip install twine
export TWINE_USERNAME="__token__" export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,11 @@
diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644 index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644
--- a/polyfillLoaders/EventTarget.js --- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
+++ b/polyfillLoaders/EventTarget.js +++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
@@ -6,16 +6,15 @@ @@ -1,14 +1,15 @@
let _ET; -let _ET, ET;
let ET; +let _ET;
+let ET;
export default async function EventTarget() { export default async function EventTarget() {
- return ET || init(); - return ET || init();
+ return ET || init(); + return ET || init();
@@ -26,4 +27,3 @@ index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c960
+ } + }
+ return (ET = _ET); + return (ET = _ET);
} }
//# sourceMappingURL=EventTarget.js.map

View File

@@ -1,4 +1,5 @@
include README.md include README.md
include LICENSE.md
graft hass_frontend graft hass_frontend
graft hass_frontend_es5 graft hass_frontend_es5
recursive-exclude * *.py[co] recursive-exclude * *.py[co]

View File

@@ -10,7 +10,7 @@ module.exports.ignorePackages = ({ latestBuild }) => [
]; ];
// Files from NPM packages that we should replace with empty file // Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => module.exports.emptyPackages = ({ latestBuild }) =>
[ [
// Contains all color definitions for all material color sets. // Contains all color definitions for all material color sets.
// We don't use it // We don't use it
@@ -28,11 +28,6 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
), ),
// This polyfill is loaded in workers to support ES5, filter it out. // This polyfill is loaded in workers to support ES5, filter it out.
latestBuild && require.resolve("proxy-polyfill/src/index.js"), latestBuild && require.resolve("proxy-polyfill/src/index.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
),
].filter(Boolean); ].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
@@ -201,7 +196,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath), publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isHassioBuild: true,
defineOverlay: { defineOverlay: {
__SUPERVISOR__: true, __SUPERVISOR__: true,
}, },

View File

@@ -26,11 +26,11 @@ module.exports = {
}, },
version() { version() {
const version = fs const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8") .readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
.match(/version\W+=\W(\d{8}\.\d)/); .match(/\d{8}\.\d+/);
if (!version) { if (!version) {
throw Error("Version not found"); throw Error("Version not found");
} }
return version[1]; return version[0];
}, },
}; };

View File

@@ -30,7 +30,6 @@ const createWebpackConfig = ({
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
isHassioBuild,
dontHash, dontHash,
}) => { }) => {
if (!dontHash) { if (!dontHash) {
@@ -118,9 +117,7 @@ const createWebpackConfig = ({
}, },
}), }),
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
new RegExp( new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|")
),
path.resolve(paths.polymer_dir, "src/util/empty.js") path.resolve(paths.polymer_dir, "src/util/empty.js")
), ),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),

View File

@@ -7,9 +7,6 @@ import "../../../../src/panels/lovelace/views/hui-view";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen"; import "./hc-launch-screen";
(window as any).loadCardHelpers = () =>
import("../../../../src/panels/lovelace/custom-card-helpers");
@customElement("hc-lovelace") @customElement("hc-lovelace")
class HcLovelace extends LitElement { class HcLovelace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
TARGET_LABEL="needs design preview" TARGET_LABEL="Needs design preview"
if [[ "$NETLIFY" != "true" ]]; then if [[ "$NETLIFY" != "true" ]]; then
echo "This script can only be run on Netlify" echo "This script can only be run on Netlify"

View File

@@ -20,7 +20,6 @@ module.exports = [
"editor-trigger", "editor-trigger",
"editor-condition", "editor-condition",
"editor-action", "editor-action",
"selectors",
"trace", "trace",
"trace-timeline", "trace-timeline",
], ],

View File

@@ -3,7 +3,6 @@ import { html, LitElement, css, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
@customElement("demo-black-white-row") @customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement { class DemoBlackWhiteRow extends LitElement {

View File

@@ -188,7 +188,6 @@ class HaGallery extends LitElement {
.sidebar details { .sidebar details {
margin-top: 1em; margin-top: 1em;
margin-left: 1em;
} }
.sidebar summary { .sidebar summary {

View File

@@ -1,3 +0,0 @@
---
title: Selectors
---

View File

@@ -1,102 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { Selector } from "../../../../src/data/selector";
import "../../../../src/components/ha-selector/ha-selector";
const SCHEMAS: { name: string; selector: Selector }[] = [
{ name: "Addon", selector: { addon: {} } },
{ name: "Entity", selector: { entity: {} } },
{ name: "Device", selector: { device: {} } },
{ name: "Area", selector: { area: {} } },
{ name: "Target", selector: { target: {} } },
{
name: "Number",
selector: {
number: {
min: 0,
max: 10,
},
},
},
{ name: "Boolean", selector: { boolean: {} } },
{ name: "Time", selector: { time: {} } },
{ name: "Action", selector: { action: {} } },
{ name: "Text", selector: { text: { multiline: false } } },
{ name: "Text Multiline", selector: { text: { multiline: true } } },
{ name: "Object", selector: { object: {} } },
{
name: "Select",
selector: {
select: {
options: ["Everyone Home", "Some Home", "All gone"],
},
},
},
];
@customElement("demo-automation-selectors")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map(() => undefined);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${{ selector: info.selector, data: this.data[sampleIdx] }}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-selector
slot=${slot}
.hass=${this.hass}
.selector=${info.selector}
.label=${info.name}
.value=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-selector>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-selectors": DemoHaSelector;
}
}

View File

@@ -1,17 +1,11 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import "@material/mwc-button"; import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit"; import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types"; import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import "../../components/demo-black-white-row"; import "../../components/demo-black-white-row";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const SCHEMAS: { const SCHEMAS: {
title: string; title: string;
@@ -20,49 +14,6 @@ const SCHEMAS: {
schema: HaFormSchema[]; schema: HaFormSchema[];
data?: Record<string, any>; data?: Record<string, any>;
}[] = [ }[] = [
{
title: "Selectors",
translations: {
addon: "Addon",
entity: "Entity",
device: "Device",
area: "Area",
target: "Target",
number: "Number",
boolean: "Boolean",
time: "Time",
action: "Action",
text: "Text",
text_multiline: "Text Multiline",
object: "Object",
select: "Select",
},
schema: [
{ name: "addon", selector: { addon: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "Attribute",
selector: { attribute: { entity_id: "" } },
},
{ name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } },
{ name: "area", selector: { area: {} } },
{ name: "target", selector: { target: {} } },
{ name: "number", selector: { number: { min: 0, max: 10 } } },
{ name: "boolean", selector: { boolean: {} } },
{ name: "time", selector: { time: {} } },
{ name: "action", selector: { action: {} } },
{ name: "text", selector: { text: { multiline: false } } },
{ name: "text_multiline", selector: { text: { multiline: true } } },
{ name: "object", selector: { object: {} } },
{
name: "select",
selector: {
select: { options: ["Everyone Home", "Some Home", "All gone"] },
},
},
],
},
{ {
title: "Authentication", title: "Authentication",
translations: { translations: {
@@ -99,11 +50,13 @@ const SCHEMAS: {
{ {
type: "boolean", type: "boolean",
name: "bool", name: "bool",
optional: true,
default: false, default: false,
}, },
{ {
type: "integer", type: "integer",
name: "int", name: "int",
optional: true,
default: 10, default: 10,
}, },
{ {
@@ -114,6 +67,7 @@ const SCHEMAS: {
{ {
type: "string", type: "string",
name: "string", name: "string",
optional: true,
default: "Default", default: "Default",
}, },
{ {
@@ -123,6 +77,7 @@ const SCHEMAS: {
["other", "other"], ["other", "other"],
], ],
name: "select", name: "select",
optional: true,
default: "default", default: "default",
}, },
{ {
@@ -132,6 +87,7 @@ const SCHEMAS: {
other: "Other", other: "Other",
}, },
name: "multi", name: "multi",
optional: true,
default: ["default"], default: ["default"],
}, },
{ {
@@ -152,6 +108,7 @@ const SCHEMAS: {
{ {
type: "integer", type: "integer",
name: "int with default", name: "int with default",
optional: true,
default: 10, default: 10,
}, },
{ {
@@ -165,6 +122,7 @@ const SCHEMAS: {
{ {
type: "integer", type: "integer",
name: "int range optional", name: "int range optional",
optional: true,
valueMin: 0, valueMin: 0,
valueMax: 10, valueMax: 10,
}, },
@@ -190,6 +148,7 @@ const SCHEMAS: {
["other", "Other"], ["other", "Other"],
], ],
name: "select optional", name: "select optional",
optional: true,
}, },
{ {
type: "select", type: "select",
@@ -202,6 +161,7 @@ const SCHEMAS: {
["option", "1000"], ["option", "1000"],
], ],
name: "select many otions", name: "select many otions",
optional: true,
default: "default", default: "default",
}, },
], ],
@@ -230,6 +190,7 @@ const SCHEMAS: {
option: "1000", option: "1000",
}, },
name: "multi many otions", name: "multi many otions",
optional: true,
default: ["default"], default: ["default"],
}, },
], ],
@@ -278,35 +239,23 @@ const SCHEMAS: {
valueMin: 1, valueMin: 1,
valueMax: 65535, valueMax: 65535,
name: "port", name: "port",
optional: true,
default: 80, default: 80,
}, },
{ type: "string", name: "path", default: "/" }, { type: "string", name: "path", optional: true, default: "/" },
{ type: "boolean", name: "ssl", default: false }, { type: "boolean", name: "ssl", optional: true, default: false },
], ],
}, },
]; ];
@customElement("demo-components-ha-form") @customElement("demo-components-ha-form")
class DemoHaForm extends LitElement { class DemoHaForm extends LitElement {
@state() private hass!: HomeAssistant;
private data = SCHEMAS.map( private data = SCHEMAS.map(
({ schema, data }) => data || computeInitialHaFormData(schema) ({ schema, data }) => data || computeInitialHaFormData(schema)
); );
private disabled = SCHEMAS.map(() => false); private disabled = SCHEMAS.map(() => false);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${SCHEMAS.map((info, idx) => { ${SCHEMAS.map((info, idx) => {
@@ -329,7 +278,6 @@ class DemoHaForm extends LitElement {
(slot) => html` (slot) => html`
<ha-form <ha-form
slot=${slot} slot=${slot}
.hass=${this.hass}
.data=${this.data[idx]} .data=${this.data[idx]}
.schema=${info.schema} .schema=${info.schema}
.error=${info.error} .error=${info.error}

View File

@@ -21,12 +21,7 @@ const SCHEMAS: {
name: "One of each", name: "One of each",
input: { input: {
entity: { name: "Entity", selector: { entity: {} } }, entity: { name: "Entity", selector: { entity: {} } },
attribute: {
name: "Attribute",
selector: { attribute: { entity_id: "" } },
},
device: { name: "Device", selector: { device: {} } }, device: { name: "Device", selector: { device: {} } },
duration: { name: "Duration", selector: { duration: {} } },
addon: { name: "Addon", selector: { addon: {} } }, addon: { name: "Addon", selector: { addon: {} } },
area: { name: "Area", selector: { area: {} } }, area: { name: "Area", selector: { area: {} } },
target: { name: "Target", selector: { target: {} } }, target: { name: "Target", selector: { target: {} } },
@@ -53,19 +48,10 @@ const SCHEMAS: {
boolean: { name: "Boolean", selector: { boolean: {} } }, boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } }, time: { name: "Time", selector: { time: {} } },
action: { name: "Action", selector: { action: {} } }, action: { name: "Action", selector: { action: {} } },
text: { text: { name: "Text", selector: { text: { multiline: false } } },
name: "Text",
selector: { text: {} },
},
password: {
name: "Password",
selector: { text: { type: "password" } },
},
text_multiline: { text_multiline: {
name: "Text multiline", name: "Text multiline",
selector: { selector: { text: { multiline: true } },
text: { multiline: true },
},
}, },
object: { name: "Object", selector: { object: {} } }, object: { name: "Object", selector: { object: {} } },
select: { select: {

View File

@@ -17,7 +17,7 @@ We want to make it as easy for designers to contribute as it is for developers.
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas. - Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>. - Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion! - Find the lates UX <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
## Developers ## Developers

View File

@@ -42,9 +42,7 @@ class HassioAddonRepositoryEl extends LitElement {
const repo = this.repo; const repo = this.repo;
let _addons = this.addons; let _addons = this.addons;
if (!this.hass.userData?.showAdvanced) { if (!this.hass.userData?.showAdvanced) {
_addons = _addons.filter( _addons = _addons.filter((addon) => !addon.advanced);
(addon) => !addon.advanced && addon.stage === "stable"
);
} }
const addons = this._getAddons(_addons, this.filter); const addons = this._getAddons(_addons, this.filter);

View File

@@ -114,7 +114,7 @@ class HassioAddonConfig extends LitElement {
<div class="card-menu"> <div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button <ha-icon-button
.label=${this.supervisor.localize("common.menu")} .label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> ></ha-icon-button>

View File

@@ -191,7 +191,7 @@ export class HassioBackups extends LitElement {
@action=${this._handleAction} @action=${this._handleAction}
> >
<ha-icon-button <ha-icon-button
.label=${this.supervisor?.localize("common.menu")} .label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> ></ha-icon-button>

View File

@@ -17,27 +17,27 @@ export class DialogHassioBackupUpload
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _dialogParams?: HassioBackupUploadDialogParams; @state() private _params?: HassioBackupUploadDialogParams;
public async showDialog( public async showDialog(
dialogParams: HassioBackupUploadDialogParams params: HassioBackupUploadDialogParams
): Promise<void> { ): Promise<void> {
this._dialogParams = dialogParams; this._params = params;
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
if (this._dialogParams && !this._dialogParams.onboarding) { if (this._params && !this._params.onboarding) {
if (this._dialogParams.reloadBackup) { if (this._params.reloadBackup) {
this._dialogParams.reloadBackup(); this._params.reloadBackup();
} }
} }
this._dialogParams = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._dialogParams) { if (!this._params) {
return html``; return html``;
} }
@@ -47,20 +47,14 @@ export class DialogHassioBackupUpload
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions hideActions
.heading=${this.hass?.localize( .heading=${true}
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
@closed=${this.closeDialog} @closed=${this.closeDialog}
> >
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title" <span slot="title"> Upload backup </span>
>${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}</span
>
<ha-icon-button <ha-icon-button
.label=${this.hass?.localize("ui.common.close") || "Close"} .label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"
@@ -77,7 +71,7 @@ export class DialogHassioBackupUpload
private _backupUploaded(ev) { private _backupUploaded(ev) {
const backup = ev.detail.backup; const backup = ev.detail.backup;
this._dialogParams?.showBackup(backup.slug); this._params?.showBackup(backup.slug);
this.closeDialog(); this.closeDialog();
} }

View File

@@ -48,9 +48,9 @@ class HassioBackupDialog
@query("supervisor-backup-content") @query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent; private _backupContent!: SupervisorBackupContent;
public async showDialog(dialogParams: HassioBackupDialogParams) { public async showDialog(params: HassioBackupDialogParams) {
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug); this._backup = await fetchHassioBackupInfo(this.hass, params.slug);
this._dialogParams = dialogParams; this._dialogParams = params;
this._restoringBackup = false; this._restoringBackup = false;
} }
@@ -71,13 +71,13 @@ class HassioBackupDialog
open open
scrimClickAction scrimClickAction
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${this._backup.name} .heading=${true}
> >
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title">${this._backup.name}</span> <span slot="title">${this._backup.name}</span>
<ha-icon-button <ha-icon-button
.label=${this.hass?.localize("ui.common.close") || "Close"} .label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"
@@ -114,20 +114,12 @@ class HassioBackupDialog
@closed=${stopPropagation} @closed=${stopPropagation}
> >
<ha-icon-button <ha-icon-button
.label=${this.hass!.localize("ui.common.menu") || "Menu"} .label=${this.hass!.localize("common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> ></ha-icon-button>
<mwc-list-item <mwc-list-item>Download Backup</mwc-list-item>
>${this._dialogParams.supervisor?.localize( <mwc-list-item class="error">Delete Backup</mwc-list-item>
"backup.download_backup"
)}</mwc-list-item
>
<mwc-list-item class="error"
>${this._dialogParams.supervisor?.localize(
"backup.delete_backup_title"
)}</mwc-list-item
>
</ha-button-menu>` </ha-button-menu>`
: ""} : ""}
</ha-dialog> </ha-dialog>

View File

@@ -30,8 +30,8 @@ class HassioCreateBackupDialog extends LitElement {
@query("supervisor-backup-content") @query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent; private _backupContent!: SupervisorBackupContent;
public showDialog(dialogParams: HassioCreateBackupDialogParams) { public showDialog(params: HassioCreateBackupDialogParams) {
this._dialogParams = dialogParams; this._dialogParams = params;
this._creatingBackup = false; this._creatingBackup = false;
} }

View File

@@ -39,8 +39,8 @@ class HassioHardwareDialog extends LitElement {
@state() private _filter?: string; @state() private _filter?: string;
public showDialog(dialogParams: HassioHardwareDialogParams) { public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = dialogParams; this._dialogParams = params;
} }
public closeDialog() { public closeDialog() {
@@ -65,16 +65,14 @@ class HassioHardwareDialog extends LitElement {
scrimClickAction scrimClickAction
hideActions hideActions
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${this._dialogParams.supervisor.localize( .heading=${true}
"dialog.hardware.title"
)}
> >
<div class="header" slot="heading"> <div class="header" slot="heading">
<h2> <h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")} ${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2> </h2>
<ha-icon-button <ha-icon-button
.label=${this._dialogParams.supervisor.localize("common.close")} .label=${this.hass.localize("common.close")}
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
></ha-icon-button> ></ha-icon-button>

View File

@@ -94,7 +94,7 @@ export class DialogHassioNetwork
open open
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${this.supervisor.localize("dialog.network.title")} .heading=${true}
hideActions hideActions
@closed=${this.closeDialog} @closed=${this.closeDialog}
> >
@@ -104,7 +104,7 @@ export class DialogHassioNetwork
${this.supervisor.localize("dialog.network.title")} ${this.supervisor.localize("dialog.network.title")}
</span> </span>
<ha-icon-button <ha-icon-button
.label=${this.supervisor.localize("common.close")} .label=${this.hass.localize("common.close")}
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"

View File

@@ -19,21 +19,22 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries"; import { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA: HaFormSchema[] = [ const SCHEMA = [
{ {
type: "string",
name: "registry", name: "registry",
required: true, required: true,
selector: { text: {} },
}, },
{ {
type: "string",
name: "username", name: "username",
required: true, required: true,
selector: { text: {} },
}, },
{ {
type: "string",
name: "password", name: "password",
required: true, required: true,
selector: { text: { type: "password" } }, format: "password",
}, },
]; ];

View File

@@ -205,6 +205,16 @@ class HassioCoreInfo extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;
} }
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
overflow: auto;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@@ -186,7 +186,7 @@ class HassioHostInfo extends LitElement {
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
<ha-icon-button <ha-icon-button
.label=${this.supervisor.localize("common.menu")} .label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> ></ha-icon-button>
@@ -440,6 +440,16 @@ class HassioHostInfo extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;
} }
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
overflow: auto;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@@ -33,12 +33,8 @@ import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host"; import { updateOS } from "../../../src/data/hassio/host";
import { import { updateSupervisor } from "../../../src/data/hassio/supervisor";
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core"; import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -192,7 +188,13 @@ class UpdateAvailableCard extends LitElement {
</a>` </a>`
: ""} : ""}
<span></span> <span></span>
<ha-progress-button @click=${this._update} raised> <ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info?.state !== "running")}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")} ${this.supervisor.localize("common.update")}
</ha-progress-button> </ha-progress-button>
</div> </div>
@@ -210,22 +212,11 @@ class UpdateAvailableCard extends LitElement {
: "addon"; : "addon";
this._updateType = updateType as updateType; this._updateType = updateType as updateType;
switch (updateType) { if (updateType === "addon") {
case "addon":
if (!this.addonSlug) { if (!this.addonSlug) {
this.addonSlug = pathPart; this.addonSlug = pathPart;
} }
this._loadAddonData(); this._loadAddonData();
break;
case "core":
this._loadCoreData();
break;
case "supervisor":
this._loadSupervisorData();
break;
case "os":
this._loadOsData();
break;
} }
} }
@@ -317,51 +308,9 @@ class UpdateAvailableCard extends LitElement {
} }
} }
private async _loadSupervisorData() {
try {
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadCoreData() {
try {
const core = await fetchHassioHomeAssistantInfo(this.hass);
fireEvent(this, "supervisor-update", { core });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadOsData() {
try {
const os = await fetchHassioHassOsInfo(this.hass);
fireEvent(this, "supervisor-update", { os });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _update() { private async _update() {
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._error = undefined; this._error = undefined;
this._updating = true; this._updating = true;
try { try {
if (this._updateType === "addon") { if (this._updateType === "addon") {
await updateHassioAddon( await updateHassioAddon(

View File

@@ -22,18 +22,17 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.2", "@braintree/sanitize-url": "^5.0.2",
"@codemirror/autocomplete": "^0.19.12", "@codemirror/commands": "^0.19.5",
"@codemirror/commands": "^0.19.8", "@codemirror/gutter": "^0.19.4",
"@codemirror/gutter": "^0.19.9", "@codemirror/highlight": "^0.19.6",
"@codemirror/highlight": "^0.19.7", "@codemirror/history": "^0.19.0",
"@codemirror/history": "^0.19.2",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1", "@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.6", "@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.6", "@codemirror/state": "^0.19.4",
"@codemirror/stream-parser": "^0.19.5", "@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.6", "@codemirror/text": "^0.19.5",
"@codemirror/view": "^0.19.40", "@codemirror/view": "^0.19.15",
"@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40", "@formatjs/intl-locale": "^2.4.40",
@@ -46,7 +45,7 @@
"@fullcalendar/daygrid": "5.9.0", "@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "14.0.0-canary.261f2db59.0", "@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0", "@material/data-table": "14.0.0-canary.261f2db59.0",
"@material/mwc-button": "0.25.3", "@material/mwc-button": "0.25.3",
@@ -58,7 +57,7 @@
"@material/mwc-formfield": "0.25.3", "@material/mwc-formfield": "0.25.3",
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
"@material/mwc-linear-progress": "0.25.3", "@material/mwc-linear-progress": "0.25.3",
"@material/mwc-list": "^0.25.3", "@material/mwc-list": "0.25.3",
"@material/mwc-menu": "0.25.3", "@material/mwc-menu": "0.25.3",
"@material/mwc-radio": "0.25.3", "@material/mwc-radio": "0.25.3",
"@material/mwc-ripple": "0.25.3", "@material/mwc-ripple": "0.25.3",
@@ -67,7 +66,6 @@
"@material/mwc-switch": "0.25.3", "@material/mwc-switch": "0.25.3",
"@material/mwc-tab": "0.25.3", "@material/mwc-tab": "0.25.3",
"@material/mwc-tab-bar": "0.25.3", "@material/mwc-tab-bar": "0.25.3",
"@material/mwc-textarea": "^0.25.3",
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
@@ -89,14 +87,13 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4", "@thomasloven/round-slider": "0.5.4",
"@vaadin/combo-box": "^22.0.4", "@vaadin/vaadin-combo-box": "^21.0.2",
"@vaadin/vaadin-themable-mixin": "^22.0.4", "@vaadin/vaadin-date-picker": "^21.0.2",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.0.1",
"chart.js": "^3.3.2", "chart.js": "^3.3.2",
"comlink": "^4.3.1", "comlink": "^4.3.1",
"core-js": "^3.15.2", "core-js": "^3.15.2",
@@ -113,8 +110,8 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.1.2", "lit": "^2.0.2",
"lit-vaadin-helpers": "^0.3.0", "lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2", "marked": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
@@ -171,7 +168,6 @@
"@types/leaflet-draw": "^1", "@types/leaflet-draw": "^1",
"@types/marked": "^2", "@types/marked": "^2",
"@types/mocha": "^8", "@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1", "@types/sortablejs": "^1",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.32.0", "@typescript-eslint/eslint-plugin": "^4.32.0",
@@ -239,10 +235,10 @@
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.1.2", "lit": "^2.0.2",
"lit-html": "2.1.2", "lit-html": "2.0.1",
"lit-element": "3.1.2", "lit-element": "3.0.1",
"@lit/reactive-element": "1.2.1" "@lit/reactive-element": "1.0.1"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@@ -1,3 +0,0 @@
[build-system]
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta"

View File

@@ -11,6 +11,6 @@ yarn install
script/build_frontend script/build_frontend
rm -rf dist home_assistant_frontend.egg-info rm -rf dist
python3 -m build python3 setup.py -q sdist
python3 -m twine upload dist/*.whl --skip-existing python3 -m twine upload dist/* --skip-existing

View File

@@ -50,14 +50,14 @@ async function main(args) {
return; return;
} }
const setup = fs.readFileSync("setup.cfg", "utf8"); const setup = fs.readFileSync("setup.py", "utf8");
const version = setup.match(/\d{8}\.\d+/)[0]; const version = setup.match(/\d{8}\.\d+/)[0];
const newVersion = method(version); const newVersion = method(version);
console.log("Current version:", version); console.log("Current version:", version);
console.log("New version:", newVersion); console.log("New version:", newVersion);
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8"); fs.writeFileSync("setup.py", setup.replace(version, newVersion), "utf-8");
if (!commit) { if (!commit) {
return; return;

View File

@@ -1,21 +0,0 @@
[metadata]
name = home-assistant-frontend
version = 20220203.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*

View File

@@ -1,7 +1,14 @@
""" from setuptools import setup, find_packages
Entry point for setuptools. Required for editable installs.
TODO: Remove file after updating to pip 21.3
"""
from setuptools import setup
setup() setup(
name="home-assistant-frontend",
version="20220118.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,
)

View File

@@ -184,7 +184,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"person", "person",
"remote", "remote",
"script", "script",
"scene",
"sun", "sun",
"timer", "timer",
"vacuum", "vacuum",
@@ -235,7 +234,7 @@ export const DOMAINS_INPUT_ROW = [
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator"]; export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
/** States that we consider "off". */ /** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "off"]; export const STATES_OFF = ["closed", "locked", "off"];

View File

@@ -1,5 +1,5 @@
import type { HaDurationData } from "../../components/ha-duration-input"; import { HaDurationData } from "../../components/ha-duration-input";
import type { ForDict } from "../../data/automation"; import { ForDict } from "../../data/automation";
export const createDurationData = ( export const createDurationData = (
duration: string | number | ForDict | undefined duration: string | number | ForDict | undefined
@@ -19,9 +19,6 @@ export const createDurationData = (
} }
return { seconds: duration }; return { seconds: duration };
} }
if (!("days" in duration)) {
return duration;
}
const { days, minutes, seconds, milliseconds } = duration; const { days, minutes, seconds, milliseconds } = duration;
let hours = duration.hours || 0; let hours = duration.hours || 0;
hours = (hours || 0) + (days || 0) * 24; hours = (hours || 0) + (days || 0) * 24;

View File

@@ -13,19 +13,14 @@ export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
const formatDateTimeMem = memoizeOne( const formatDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );
// August 9, 2021, 8:23:15 AM // August 9, 2021, 8:23:15 AM
@@ -36,11 +31,7 @@ export const formatDateTimeWithSeconds = (
const formatDateTimeWithSecondsMem = memoizeOne( const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@@ -48,8 +39,7 @@ const formatDateTimeWithSecondsMem = memoizeOne(
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );
// 9/8/2021, 8:23 AM // 9/8/2021, 8:23 AM
@@ -60,17 +50,12 @@ export const formatDateTimeNumeric = (
const formatDateTimeNumericMem = memoizeOne( const formatDateTimeNumericMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );

View File

@@ -13,16 +13,11 @@ export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
const formatTimeMem = memoizeOne( const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );
// 9:15:24 PM || 21:15:24 // 9:15:24 PM || 21:15:24
@@ -33,17 +28,12 @@ export const formatTimeWithSeconds = (
const formatTimeWithSecondsMem = memoizeOne( const formatTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );
// Tuesday 7:00 PM || Tuesday 19:00 // Tuesday 7:00 PM || Tuesday 19:00
@@ -52,15 +42,10 @@ export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
const formatTimeWeekdayMem = memoizeOne( const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale.language, {
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
weekday: "long",
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale), hour12: useAmPm(locale),
} })
)
); );

View File

@@ -5,10 +5,7 @@ import type { ClassElement } from "../../types";
type Callback = (oldValue: any, newValue: any) => void; type Callback = (oldValue: any, newValue: any) => void;
class Storage { class Storage {
constructor(subscribe = true) { constructor() {
if (!subscribe) {
return;
}
window.addEventListener("storage", (ev: StorageEvent) => { window.addEventListener("storage", (ev: StorageEvent) => {
if (ev.key && this.hasKey(ev.key)) { if (ev.key && this.hasKey(ev.key)) {
this._storage[ev.key] = ev.newValue this._storage[ev.key] = ev.newValue
@@ -83,18 +80,15 @@ class Storage {
} }
} }
const subscribeStorage = new Storage(); const storage = new Storage();
export const LocalStorage = export const LocalStorage =
( (
storageKey?: string, storageKey?: string,
property?: boolean, property?: boolean,
subscribe = true,
propertyOptions?: PropertyDeclaration propertyOptions?: PropertyDeclaration
): any => ): any =>
(clsElement: ClassElement) => { (clsElement: ClassElement) => {
const storage = subscribe ? subscribeStorage : new Storage(false);
const key = String(clsElement.key); const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key); storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer const initVal = clsElement.initializer
@@ -103,7 +97,7 @@ export const LocalStorage =
storage.addFromStorage(storageKey); storage.addFromStorage(storageKey);
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc => const subscribe = (el: ReactiveElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => { storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue); el.requestUpdate(clsElement.key, oldValue);
}); });
@@ -137,19 +131,17 @@ export const LocalStorage =
configurable: true, configurable: true,
}, },
finisher(cls: typeof ReactiveElement) { finisher(cls: typeof ReactiveElement) {
if (property && subscribe) { if (property) {
const connectedCallback = cls.prototype.connectedCallback; const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback; const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () { cls.prototype.connectedCallback = function () {
connectedCallback.call(this); connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this); this[`__unbsubLocalStorage${key}`] = subscribe(this);
}; };
cls.prototype.disconnectedCallback = function () { cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this); disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`](); this[`__unbsubLocalStorage${key}`]();
}; };
}
if (property) {
cls.createProperty(clsElement.key, { cls.createProperty(clsElement.key, {
noAccessor: true, noAccessor: true,
...propertyOptions, ...propertyOptions,

View File

@@ -43,7 +43,7 @@ export const computeStateDisplay = (
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state !== undefined) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format. // If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`. // Attributes aren't available, we have to use `state`.
try { try {
const components = state.split(" "); const components = state.split(" ");
@@ -120,7 +120,6 @@ export const computeStateDisplay = (
if ( if (
domain === "button" || domain === "button" ||
domain === "input_button" || domain === "input_button" ||
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
return formatDateTime(new Date(compareState), locale); return formatDateTime(new Date(compareState), locale);

View File

@@ -1,10 +1,2 @@
export const clamp = (value: number, min: number, max: number) => export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max); Math.min(Math.max(value, min), max);
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(value, max) : value;
return result;
};

View File

@@ -1,10 +1,17 @@
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@@ -14,6 +21,12 @@ class SearchInput extends LitElement {
@property() public filter?: string; @property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: Boolean, attribute: "no-underline" })
public noUnderline = false;
@property({ type: Boolean }) @property({ type: Boolean })
public autofocus = false; public autofocus = false;
@@ -21,42 +34,49 @@ class SearchInput extends LitElement {
public label?: string; public label?: string;
public focus() { public focus() {
this._input?.focus(); this.shadowRoot!.querySelector("paper-input")!.focus();
} }
@query("ha-textfield", true) private _input!: HaTextField; @query("paper-input", true) private _input!: PaperInputElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-textfield <paper-input
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter || ""} .value=${this.filter}
.icon=${true} @value-changed=${this._filterInputChanged}
.iconTrailing=${this.filter} .noLabelFloat=${this.noLabelFloat}
@input=${this._filterInputChanged}
> >
<slot name="prefix" slot="leadingIcon"> <slot name="prefix" slot="prefix">
<ha-svg-icon <ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
tabindex="-1"
class="prefix"
.path=${mdiMagnify}
></ha-svg-icon>
</slot> </slot>
${this.filter && ${this.filter &&
html` html`
<ha-icon-button <ha-icon-button
slot="trailingIcon" slot="suffix"
@click=${this._clearSearch} @click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")} .label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose} .path=${mdiClose}
class="clear-button"
></ha-icon-button> ></ha-icon-button>
`} `}
</ha-textfield> </paper-input>
`; `;
} }
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(
this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line"
) as HTMLElement
).style.display = this.noUnderline ? "none" : "block";
}
}
private async _filterChanged(value: string) { private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) }); fireEvent(this, "value-changed", { value: String(value) });
} }
@@ -71,24 +91,15 @@ class SearchInput extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
display: inline-flex;
}
ha-svg-icon, ha-svg-icon,
ha-icon-button { ha-icon-button {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
ha-svg-icon {
outline: none;
}
ha-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
} }
.clear-button { ha-svg-icon.prefix {
--mdc-icon-size: 20px; margin: 8px;
}
ha-textfield {
display: inherit;
} }
`; `;
} }

View File

@@ -68,7 +68,6 @@ export class HaProgressButton extends LitElement {
--mdc-theme-primary: white; --mdc-theme-primary: white;
background-color: var(--success-color); background-color: var(--success-color);
transition: none; transition: none;
border-radius: 4px;
} }
mwc-button[raised].success { mwc-button[raised].success {
@@ -80,7 +79,6 @@ export class HaProgressButton extends LitElement {
--mdc-theme-primary: white; --mdc-theme-primary: white;
background-color: var(--error-color); background-color: var(--error-color);
transition: none; transition: none;
border-radius: 4px;
} }
mwc-button[raised].error { mwc-button[raised].error {

View File

@@ -183,7 +183,12 @@ class StateHistoryChartLine extends LitElement {
prevValues = datavalues; prevValues = datavalues;
}; };
const addDataSet = (nameY: string, fill = false, color?: string) => { const addDataSet = (
nameY: string,
step = false,
fill = false,
color?: string
) => {
if (!color) { if (!color) {
color = getGraphColorByIndex(colorIndex, computedStyles); color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++; colorIndex++;
@@ -193,7 +198,7 @@ class StateHistoryChartLine extends LitElement {
fill: fill ? "origin" : false, fill: fill ? "origin" : false,
borderColor: color, borderColor: color,
backgroundColor: color + "7F", backgroundColor: color + "7F",
stepped: "before", stepped: step ? "before" : false,
pointRadius: 0, pointRadius: 0,
data: [], data: [],
}); });
@@ -234,12 +239,14 @@ class StateHistoryChartLine extends LitElement {
addDataSet( addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", { `${this.hass.localize("ui.card.climate.current_temperature", {
name: name, name: name,
})}` })}`,
true
); );
if (hasHeat) { if (hasHeat) {
addDataSet( addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`, `${this.hass.localize("ui.card.climate.heating", { name: name })}`,
true, true,
true,
computedStyles.getPropertyValue("--state-climate-heat-color") computedStyles.getPropertyValue("--state-climate-heat-color")
); );
// The "heating" series uses steppedArea to shade the area below the current // The "heating" series uses steppedArea to shade the area below the current
@@ -249,6 +256,7 @@ class StateHistoryChartLine extends LitElement {
addDataSet( addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`, `${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
true, true,
true,
computedStyles.getPropertyValue("--state-climate-cool-color") computedStyles.getPropertyValue("--state-climate-cool-color")
); );
// The "cooling" series uses steppedArea to shade the area below the current // The "cooling" series uses steppedArea to shade the area below the current
@@ -260,19 +268,22 @@ class StateHistoryChartLine extends LitElement {
`${this.hass.localize("ui.card.climate.target_temperature_mode", { `${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name, name: name,
mode: this.hass.localize("ui.card.climate.high"), mode: this.hass.localize("ui.card.climate.high"),
})}` })}`,
true
); );
addDataSet( addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", { `${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name, name: name,
mode: this.hass.localize("ui.card.climate.low"), mode: this.hass.localize("ui.card.climate.low"),
})}` })}`,
true
); );
} else { } else {
addDataSet( addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", { `${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name, name: name,
})}` })}`,
true
); );
} }
@@ -307,12 +318,14 @@ class StateHistoryChartLine extends LitElement {
addDataSet( addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", { `${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name, name: name,
})}` })}`,
true
); );
addDataSet( addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", { `${this.hass.localize("ui.card.humidifier.on_entity", {
name: name, name: name,
})}`, })}`,
true,
true true
); );
@@ -324,7 +337,9 @@ class StateHistoryChartLine extends LitElement {
pushData(new Date(entityState.last_changed), series); pushData(new Date(entityState.last_changed), series);
}); });
} else { } else {
addDataSet(name); // Only interpolate for sensors
const isStep = domain !== "sensor";
addDataSet(name, isStep);
let lastValue: number; let lastValue: number;
let lastDate: Date; let lastDate: Date;

View File

@@ -1,3 +1,4 @@
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
@@ -30,7 +31,6 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon"; import "../ha-svg-icon";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "@lit-labs/virtualizer";
declare global { declare global {
// for fire event // for fire event
@@ -70,7 +70,6 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData<T = any> extends DataTableSortColumnData { export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: (data: any, row: T) => TemplateResult | string; template?: (data: any, row: T) => TemplateResult | string;
width?: string; width?: string;
@@ -295,7 +294,6 @@ export class HaDataTable extends LitElement {
}; };
return html` return html`
<div <div
aria-label=${column.label}
class="mdc-data-table__header-cell ${classMap(classes)}" class="mdc-data-table__header-cell ${classMap(classes)}"
style=${column.width style=${column.width
? styleMap({ ? styleMap({
@@ -339,47 +337,41 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: html` : html`
<lit-virtualizer <div
scroller
class="mdc-data-table__content scroller ha-scrollbar" class="mdc-data-table__content scroller ha-scrollbar"
@scroll=${this._saveScrollPos} @scroll=${this._saveScrollPos}
.items=${this._items} >
.renderItem=${this._renderRow} ${scroll({
></lit-virtualizer> items: this._items,
`} layout: Layout1d,
</div> renderItem: (row: DataTableRowData, index) => {
</div>
`;
}
private _renderRow = (
row: DataTableRowData,
index: number
): TemplateResult => {
// not sure how this happens... // not sure how this happens...
if (!row) { if (!row) {
return html``; return html``;
} }
if (row.append) { if (row.append) {
return html` <div class="mdc-data-table__row">${row.content}</div> `; return html`
<div class="mdc-data-table__row">${row.content}</div>
`;
} }
if (row.empty) { if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `; return html` <div class="mdc-data-table__row"></div> `;
} }
return html` return html`
<div <div
aria-rowindex=${index + 2} aria-rowindex=${index! + 2}
role="row" role="row"
.rowId=${row[this.id]} .rowId=${row[this.id]}
@click=${this._handleRowClick} @click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({ class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes( "mdc-data-table__row--selected":
String(row[this.id]) this._checkedRows.includes(String(row[this.id])),
),
clickable: this.clickable, clickable: this.clickable,
})}" })}"
aria-selected=${ifDefined( aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id])) ? true : undefined this._checkedRows.includes(String(row[this.id]))
? true
: undefined
)} )}
.selectable=${row.selectable !== false} .selectable=${row.selectable !== false}
> >
@@ -394,13 +386,16 @@ export class HaDataTable extends LitElement {
@change=${this._handleRowCheckboxClick} @change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]} .rowId=${row[this.id]}
.disabled=${row.selectable === false} .disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(String(row[this.id]))} .checked=${this._checkedRows.includes(
String(row[this.id])
)}
> >
</ha-checkbox> </ha-checkbox>
</div> </div>
` `
: ""} : ""}
${Object.entries(this.columns).map(([key, column]) => { ${Object.entries(this.columns).map(
([key, column]) => {
if (column.hidden) { if (column.hidden) {
return ""; return "";
} }
@@ -408,8 +403,10 @@ export class HaDataTable extends LitElement {
<div <div
role="cell" role="cell"
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": column.type === "numeric", "mdc-data-table__cell--numeric":
"mdc-data-table__cell--icon": column.type === "icon", column.type === "numeric",
"mdc-data-table__cell--icon":
column.type === "icon",
"mdc-data-table__cell--icon-button": "mdc-data-table__cell--icon-button":
column.type === "icon-button", column.type === "icon-button",
"mdc-data-table__cell--overflow-menu": "mdc-data-table__cell--overflow-menu":
@@ -419,18 +416,31 @@ export class HaDataTable extends LitElement {
})}" })}"
style=${column.width style=${column.width
? styleMap({ ? styleMap({
[column.grows ? "minWidth" : "width"]: column.width, [column.grows ? "minWidth" : "width"]:
maxWidth: column.maxWidth ? column.maxWidth : "", column.width,
maxWidth: column.maxWidth
? column.maxWidth
: "",
}) })
: ""} : ""}
> >
${column.template ? column.template(row[key], row) : row[key]} ${column.template
? column.template(row[key], row)
: row[key]}
</div> </div>
`; `;
}
)}
</div>
`;
},
})} })}
</div> </div>
`}
</div>
</div>
`; `;
}; }
private async _sortFilterData() { private async _sortFilterData() {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
@@ -526,7 +536,7 @@ export class HaDataTable extends LitElement {
} }
} }
private _handleRowCheckboxClick = (ev: Event) => { private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox; const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId; const rowId = (checkbox as any).rowId;
@@ -539,16 +549,16 @@ export class HaDataTable extends LitElement {
this._checkedRows = this._checkedRows.filter((row) => row !== rowId); this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
} }
this._checkedRowsChanged(); this._checkedRowsChanged();
}; }
private _handleRowClick = (ev: Event) => { private _handleRowClick(ev: Event) {
const target = ev.target as HTMLElement; const target = ev.target as HTMLElement;
if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) { if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) {
return; return;
} }
const rowId = (ev.currentTarget as any).rowId; const rowId = (ev.currentTarget as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
}; }
private _checkedRowsChanged() { private _checkedRowsChanged() {
// force scroller to update, change it's items // force scroller to update, change it's items
@@ -561,9 +571,6 @@ export class HaDataTable extends LitElement {
} }
private _handleSearchChange(ev: CustomEvent): void { private _handleSearchChange(ev: CustomEvent): void {
if (this.filter) {
return;
}
this._debounceSearch(ev.detail.value); this._debounceSearch(ev.detail.value);
} }
@@ -928,10 +935,11 @@ export class HaDataTable extends LitElement {
} }
.table-header { .table-header {
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
padding: 0 16px;
} }
search-input { search-input {
display: block; position: relative;
flex: 1; top: 2px;
} }
slot[name="header"] { slot[name="header"] {
display: block; display: block;
@@ -944,7 +952,6 @@ export class HaDataTable extends LitElement {
} }
.scroller { .scroller {
height: calc(100% - 57px); height: calc(100% - 57px);
overflow: overlay !important;
} }
.mdc-data-table__table.auto-height .scroller { .mdc-data-table__table.auto-height .scroller {
@@ -960,9 +967,6 @@ export class HaDataTable extends LitElement {
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }
lit-virtualizer {
contain: size layout !important;
}
`, `,
]; ];
} }

View File

@@ -1,7 +1,20 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -37,12 +50,36 @@ interface AreaDevices {
devices: string[]; devices: string[];
} }
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = ( // eslint-disable-next-line lit/prefer-static-styles
item const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
) => html`<mwc-list-item twoline> paper-item {
<span>${item.name}</span> padding: 0;
<span slot="secondary">${item.devices.length} devices</span> margin: -10px;
</mwc-list-item>`; margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line="">
<div class="name">${item.name}</div>
<div secondary>${item.devices.length} devices</div>
</paper-item-body>
</paper-item>`;
@customElement("ha-area-devices-picker") @customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@@ -80,6 +117,9 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" }) @property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[]; public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
@state() private _areaPicker = true; @state() private _areaPicker = true;
@state() private _devices?: DeviceRegistryEntry[]; @state() private _devices?: DeviceRegistryEntry[];
@@ -262,30 +302,71 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
`; `;
} }
return html` return html`
<ha-combo-box <vaadin-combo-box-light
.hass=${this.hass}
item-value-path="id" item-value-path="id"
item-id-path="id" item-id-path="id"
item-label-path="name" item-label-path="name"
.items=${areas} .items=${areas}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaPicked}
>
<paper-input
.label=${this.label === undefined && this.hass .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device") ? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`} : `${this.label} in area`}
@value-changed=${this._areaPicked} class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value
? html`<ha-icon-button
class="clear-button"
.label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
.path=${mdiClose}
@click=${this._clearValue}
no-ripple
></ha-icon-button> `
: ""}
${areas.length > 0
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button"
></ha-icon-button>
`
: ""}
</div>
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
>Choose individual devices</mwc-button
> >
</ha-combo-box>
<mwc-button @click=${this._switchPicker}>
Choose individual devices
</mwc-button>
`; `;
} }
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue([]);
}
private get _value() { private get _value() {
return this.value || []; return this.value || [];
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private async _switchPicker() { private async _switchPicker() {
this._areaPicker = !this._areaPicker; this._areaPicker = !this._areaPicker;
} }
@@ -317,6 +398,22 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return css`
.suffix {
display: flex;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,5 +1,7 @@
import "@material/mwc-list/mwc-list-item"; import "@polymer/paper-input/paper-input";
import "@material/mwc-select/mwc-select"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -8,6 +10,7 @@ import {
deviceAutomationsEqual, deviceAutomationsEqual,
} from "../../data/device_automation"; } from "../../data/device_automation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-paper-dropdown-menu";
const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
@@ -64,12 +67,14 @@ export abstract class HaDeviceAutomationPicker<
this._createNoAutomation = createNoAutomation; this._createNoAutomation = createNoAutomation;
} }
private get _value() { private get _key() {
if (!this.value) { if (
return ""; !this.value ||
} deviceAutomationsEqual(
this._createNoAutomation(this.deviceId),
if (!this._automations.length) { this.value
)
) {
return NO_AUTOMATION_KEY; return NO_AUTOMATION_KEY;
} }
@@ -88,32 +93,42 @@ export abstract class HaDeviceAutomationPicker<
if (this._renderEmpty) { if (this._renderEmpty) {
return html``; return html``;
} }
const value = this._value;
return html` return html`
<mwc-select <ha-paper-dropdown-menu
.label=${this.label} .label=${this.label}
.value=${value} .value=${this.value
@selected=${this._automationChanged} ? this._localizeDeviceAutomation(this.hass, this.value)
.disabled=${this._automations.length === 0} : ""}
?disabled=${this._automations.length === 0}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._key}
attr-for-selected="key"
@iron-select=${this._automationChanged}
>
<paper-item
key=${NO_AUTOMATION_KEY}
.automation=${this._createNoAutomation(this.deviceId)}
hidden
> >
${value === NO_AUTOMATION_KEY
? html`<mwc-list-item .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT} ${this.NO_AUTOMATION_TEXT}
</mwc-list-item>` </paper-item>
: ""} <paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden>
${value === UNKNOWN_AUTOMATION_KEY
? html`<mwc-list-item .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT} ${this.UNKNOWN_AUTOMATION_TEXT}
</mwc-list-item>` </paper-item>
: ""}
${this._automations.map( ${this._automations.map(
(automation, idx) => html` (automation, idx) => html`
<mwc-list-item .value=${`${automation.device_id}_${idx}`}> <paper-item
key=${`${this.deviceId}_${idx}`}
.automation=${automation}
>
${this._localizeDeviceAutomation(this.hass, automation)} ${this._localizeDeviceAutomation(this.hass, automation)}
</mwc-list-item> </paper-item>
` `
)} )}
</mwc-select> </paper-listbox>
</ha-paper-dropdown-menu>
`; `;
} }
@@ -123,6 +138,14 @@ export abstract class HaDeviceAutomationPicker<
if (changedProps.has("deviceId")) { if (changedProps.has("deviceId")) {
this._updateDeviceInfo(); this._updateDeviceInfo();
} }
// The value has changed, force the listbox to update
if (changedProps.has("value") || changedProps.has("_renderEmpty")) {
const listbox = this.shadowRoot!.querySelector("paper-listbox")!;
if (listbox) {
listbox._selectSelected(this._key);
}
}
} }
private async _updateDeviceInfo() { private async _updateDeviceInfo() {
@@ -145,16 +168,9 @@ export abstract class HaDeviceAutomationPicker<
} }
private _automationChanged(ev) { private _automationChanged(ev) {
const value = ev.target.value; if (ev.detail.item.automation) {
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) { this._setValue(ev.detail.item.automation);
return;
} }
const [deviceId, idx] = value.split("_");
const automation = this._automations[idx];
if (automation.device_id !== deviceId) {
return;
}
this._setValue(automation);
} }
private _setValue(automation: T) { private _setValue(automation: T) {
@@ -167,9 +183,14 @@ export abstract class HaDeviceAutomationPicker<
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
mwc-select { ha-paper-dropdown-menu {
width: 100%; width: 100%;
margin-top: 4px; }
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
} }
`; `;
} }

View File

@@ -1,9 +1,18 @@
import "@material/mwc-list/mwc-list-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; css,
import { customElement, property, query, state } from "lit/decorators"; CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { mdiCheck } from "@mdi/js";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare"; import { stringCompare } from "../../common/string/compare";
@@ -37,12 +46,36 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
) => boolean; ) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item // eslint-disable-next-line lit/prefer-static-styles
.twoline=${!!item.area} const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
> paper-item {
<span>${item.name}</span> padding: 0;
<span slot="secondary">${item.area}</span> margin: -10px;
</mwc-list-item>`; margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}
<span secondary>${item.area}</span>
</paper-item-body>
</paper-item>`;
@customElement("ha-device-picker") @customElement("ha-device-picker")
export class HaDevicePicker extends SubscribeMixin(LitElement) { export class HaDevicePicker extends SubscribeMixin(LitElement) {
@@ -105,7 +138,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
if (!devices.length) { if (!devices.length) {
return [ return [
{ {
id: "no_devices", id: "",
area: "", area: "",
name: this.hass.localize("ui.components.device-picker.no_devices"), name: this.hass.localize("ui.components.device-picker.no_devices"),
}, },
@@ -201,7 +234,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
if (!outputDevices.length) { if (!outputDevices.length) {
return [ return [
{ {
id: "no_devices", id: "",
area: "", area: "",
name: this.hass.localize("ui.components.device-picker.no_match"), name: this.hass.localize("ui.components.device-picker.no_match"),
}, },
@@ -270,6 +303,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.renderer=${rowRenderer} .renderer=${rowRenderer}
.disabled=${this.disabled} .disabled=${this.disabled}
item-value-path="id" item-value-path="id"
item-id-path="id"
item-label-path="name" item-label-path="name"
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged} @value-changed=${this._deviceChanged}
@@ -283,11 +317,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
private _deviceChanged(ev: PolymerChangedEvent<string>) { private _deviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
let newValue = ev.detail.value; const newValue = ev.detail.value;
if (newValue === "no_devices") {
newValue = "";
}
if (newValue !== this._value) { if (newValue !== this._value) {
this._setValue(newValue); this._setValue(newValue);
@@ -305,6 +335,19 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
@@ -145,12 +145,6 @@ class HaEntitiesPickerLight extends LitElement {
this._updateEntities([...currentEntities, toAdd]); this._updateEntities([...currentEntities, toAdd]);
} }
static override styles = css`
ha-entity-picker {
margin-top: 8px;
}
`;
} }
declare global { declare global {

View File

@@ -1,14 +1,54 @@
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { formatAttributeName } from "../../data/entity_attributes"; import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box"; import { formatAttributeName } from "../../util/hass-attributes-util";
import type { HaComboBox } from "../ha-combo-box"; import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-item {
padding: 0;
margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>${formatAttributeName(item)}</paper-item>`;
@customElement("ha-entity-attribute-picker") @customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement { class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -28,7 +68,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false; @property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) { protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
@@ -38,10 +78,7 @@ class HaEntityAttributePicker extends LitElement {
if (changedProps.has("_opened") && this._opened) { if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined; const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state (this._comboBox as any).items = state
? Object.keys(state.attributes).map((key) => ({ ? Object.keys(state.attributes)
value: key,
label: formatAttributeName(key),
}))
: []; : [];
} }
} }
@@ -52,31 +89,100 @@ class HaEntityAttributePicker extends LitElement {
} }
return html` return html`
<ha-combo-box <vaadin-combo-box-light
.hass=${this.hass} .value=${this._value}
.value=${this.value || ""} .allowCustomValue=${this.allowCustomValue}
attr-for-value="bind-value"
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label ?? .label=${this.label ??
this.hass.localize( this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute" "ui.components.entity.entity-attribute-picker.attribute"
)} )}
.value=${this._value ? formatAttributeName(this._value) : ""}
.disabled=${this.disabled || !this.entityId} .disabled=${this.disabled || !this.entityId}
.allowCustomValue=${this.allowCustomValue} class="input"
item-value-path="value" autocapitalize="none"
item-label-path="label" autocomplete="off"
@opened-changed=${this._openedChanged} autocorrect="off"
@value-changed=${this._valueChanged} spellcheck="false"
> >
</ha-combo-box> <div class="suffix" slot="suffix">
${this.value
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
.path=${mdiClose}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button"
tabindex="-1"
></ha-icon-button>
</div>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value; this._opened = ev.detail.value;
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev: PolymerChangedEvent<string>) {
this.value = ev.detail.value; const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResultGroup {
return css`
.suffix {
display: flex;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
} }
} }

View File

@@ -1,16 +1,25 @@
import "@material/mwc-list/mwc-list-item"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; css,
import { customElement, property, query, state } from "lit/decorators"; CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
@@ -18,15 +27,35 @@ import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> = const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
(item) => paper-icon-item {
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}> padding: 0;
${item.state margin: -8px;
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>` }
: ""} #content {
<span>${item.friendly_name}</span> display: flex;
<span slot="secondary">${item.entity_id}</span> align-items: center;
</mwc-list-item>`; }
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item}></state-badge>
<paper-item-body two-line="">
${computeStateName(item)}
<span secondary>${item.entity_id}</span>
</paper-item-body>
</paper-icon-item>`;
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -78,19 +107,19 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public hideClearIcon = false; @property({ type: Boolean }) public hideClearIcon = false;
@state() private _opened = false; @property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
public open() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.comboBox?.open(); (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
}); });
} }
public focus() { public focus() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.comboBox?.focus(); this.shadowRoot?.querySelector("paper-input")?.focus();
}); });
} }
@@ -115,27 +144,6 @@ export class HaEntityPicker extends LitElement {
} }
let entityIds = Object.keys(hass.states); let entityIds = Object.keys(hass.states);
if (!entityIds.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon: "mdi:magnify",
},
},
];
}
if (includeDomains) { if (includeDomains) {
entityIds = entityIds.filter((eid) => entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid)) includeDomains.includes(computeDomain(eid))
@@ -148,10 +156,7 @@ export class HaEntityPicker extends LitElement {
); );
} }
states = entityIds.sort().map((key) => ({ states = entityIds.sort().map((key) => hass!.states[key]);
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}));
if (includeDeviceClasses) { if (includeDeviceClasses) {
states = states.filter( states = states.filter(
@@ -191,9 +196,6 @@ export class HaEntityPicker extends LitElement {
last_changed: "", last_changed: "",
last_updated: "", last_updated: "",
context: { id: "", user_id: null }, context: { id: "", user_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
attributes: { attributes: {
friendly_name: this.hass!.localize( friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match" "ui.components.entity.entity-picker.no_match"
@@ -239,25 +241,64 @@ export class HaEntityPicker extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-combo-box <vaadin-combo-box-light
item-value-path="entity_id" item-value-path="entity_id"
item-label-path="friendly_name" item-label-path="entity_id"
.hass=${this.hass}
.value=${this._value} .value=${this._value}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states} .filteredItems=${this._states}
.renderer=${rowRenderer} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
> >
</ha-combo-box> <paper-input
.autofocus=${this.autofocus}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value && !this.hideClearIcon
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
.path=${mdiClose}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button"
tabindex="-1"
></ha-icon-button>
</div>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() { private get _value() {
return this.value || ""; return this.value || "";
} }
@@ -267,7 +308,6 @@ export class HaEntityPicker extends LitElement {
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;
if (newValue !== this._value) { if (newValue !== this._value) {
this._setValue(newValue); this._setValue(newValue);
@@ -277,9 +317,9 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter( (this.comboBox as any).filteredItems = this._states.filter(
(entityState) => (state) =>
entityState.entity_id.toLowerCase().includes(filterString) || state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(entityState).toLowerCase().includes(filterString) computeStateName(state).toLowerCase().includes(filterString)
); );
} }
@@ -290,6 +330,22 @@ export class HaEntityPicker extends LitElement {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return css`
.suffix {
display: flex;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
} }
declare global { declare global {

View File

@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const"; import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics"; import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-formfield"; import "../ha-formfield";
@@ -39,26 +39,21 @@ export class HaEntityToggle extends LitElement {
return html` <ha-switch disabled></ha-switch> `; return html` <ha-switch disabled></ha-switch> `;
} }
if ( if (this.stateObj.attributes.assumed_state) {
this.stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN
) {
return html` return html`
<ha-icon-button <ha-icon-button
.label=${`Turn ${computeStateName(this.stateObj)} off`} .label=${`Turn ${computeStateName(this.stateObj)} off`}
.path=${mdiFlashOff} .path=${mdiFlashOff}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOff} @click=${this._turnOff}
class=${!this._isOn && this.stateObj.state !== UNKNOWN ?state-active=${!this._isOn}
? "state-active"
: ""}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.label=${`Turn ${computeStateName(this.stateObj)} on`} .label=${`Turn ${computeStateName(this.stateObj)} on`}
.path=${mdiFlash} .path=${mdiFlash}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOn} @click=${this._turnOn}
class=${this._isOn ? "state-active" : ""} ?state-active=${this._isOn}
></ha-icon-button> ></ha-icon-button>
`; `;
} }
@@ -68,7 +63,7 @@ export class HaEntityToggle extends LitElement {
this._isOn ? "off" : "on" this._isOn ? "off" : "on"
}`} }`}
.checked=${this._isOn} .checked=${this._isOn}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged} @change=${this._toggleChanged}
></ha-switch>`; ></ha-switch>`;
@@ -161,11 +156,10 @@ export class HaEntityToggle extends LitElement {
min-width: 38px; min-width: 38px;
} }
ha-icon-button { ha-icon-button {
--mdc-icon-button-size: 40px;
color: var(--ha-icon-button-inactive-color, var(--primary-text-color)); color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
transition: color 0.5s; transition: color 0.5s;
} }
ha-icon-button.state-active { ha-icon-button[state-active] {
color: var(--ha-icon-button-active-color, var(--primary-color)); color: var(--ha-icon-button-active-color, var(--primary-color));
} }
ha-switch { ha-switch {

View File

@@ -147,7 +147,7 @@ export class HaStateLabelBadge extends LitElement {
default: default:
return entityState.state === UNKNOWN || return entityState.state === UNKNOWN ||
entityState.state === UNAVAILABLE entityState.state === UNAVAILABLE
? "" ? "-"
: isNumericState(entityState) : isNumericState(entityState)
? formatNumber(entityState.state, this.hass!.locale) ? formatNumber(entityState.state, this.hass!.locale)
: computeStateDisplay( : computeStateDisplay(

View File

@@ -1,8 +1,17 @@
import { mdiCheck } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -67,12 +76,41 @@ export class HaStatisticPicker extends LitElement {
id: string; id: string;
name: string; name: string;
state?: HassEntity; state?: HassEntity;
}> = (item) => html`<mwc-list-item graphic="avatar" twoline> // eslint-disable-next-line lit/prefer-static-styles
}> = (item) => html`<style>
paper-icon-item {
padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
a {
color: var(--primary-color);
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
${item.state ${item.state
? html`<state-badge slot="graphic" .stateObj=${item.state}></state-badge>` ? html`<state-badge
slot="item-icon"
.stateObj=${item.state}
></state-badge>`
: ""} : ""}
<span>${item.name}</span> <paper-item-body two-line="">
<span slot="secondary" ${item.name}
<span secondary
>${item.id === "" || item.id === "__missing" >${item.id === "" || item.id === "__missing"
? html`<a ? html`<a
target="_blank" target="_blank"
@@ -84,7 +122,8 @@ export class HaStatisticPicker extends LitElement {
>` >`
: item.id}</span : item.id}</span
> >
</mwc-list-item>`; </paper-item-body>
</paper-icon-item>`;
private _getStatistics = memoizeOne( private _getStatistics = memoizeOne(
( (
@@ -254,6 +293,19 @@ export class HaStatisticPicker extends LitElement {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types"; import type { PolymerChangedEvent } from "../../polymer-types";
@@ -103,20 +103,6 @@ class HaStatisticsPicker extends LitElement {
this._updateStatistics([...currentEntities, toAdd]); this._updateStatistics([...currentEntities, toAdd]);
} }
static get styles(): CSSResultGroup {
return css`
:host {
width: 200px;
display: block;
}
ha-statistic-picker {
display: block;
width: 100%;
margin-top: 8px;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,3 +1,4 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -11,12 +12,39 @@ import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box"; import { HaComboBox } from "./ha-combo-box";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = ( // eslint-disable-next-line lit/prefer-static-styles
item const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
) => html`<mwc-list-item twoline> paper-item {
<span>${item.name}</span> padding: 0;
<span slot="secondary">${item.slug}</span> margin: -10px;
</mwc-list-item>`; margin-left: 0px;
}
#content {
display: flex;
align-items: center;
}
:host([selected]) paper-item {
margin-left: 0;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}
<span secondary>${item.slug}</span>
</paper-item-body>
</paper-item>`;
@customElement("ha-addon-picker") @customElement("ha-addon-picker")
class HaAddonPicker extends LitElement { class HaAddonPicker extends LitElement {

View File

@@ -1,6 +1,19 @@
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -28,18 +41,38 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item item
) => html`<mwc-list-item // eslint-disable-next-line lit/prefer-static-styles
class=${classMap({ "add-new": item.area_id === "add_new" })} ) => html`<style>
> paper-item {
${item.name} padding: 0;
</mwc-list-item>`; margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}>
<paper-item-body two-line>${item.name}</paper-item-body>
</paper-item>`;
@customElement("ha-area-picker") @customElement("ha-area-picker")
export class HaAreaPicker extends SubscribeMixin(LitElement) { export class HaAreaPicker extends SubscribeMixin(LitElement) {
@@ -92,9 +125,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@state() private _opened?: boolean; @state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
private _filter?: string;
private _init = false; private _init = false;
@@ -114,13 +145,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
public open() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.comboBox?.open(); (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
}); });
} }
public focus() { public focus() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.comboBox?.focus(); this.shadowRoot?.querySelector("paper-input")?.focus();
}); });
} }
@@ -139,7 +170,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
if (!areas.length) { if (!areas.length) {
return [ return [
{ {
area_id: "no_areas", area_id: "",
name: this.hass.localize("ui.components.area-picker.no_areas"), name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null, picture: null,
}, },
@@ -263,7 +294,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
if (!outputAreas.length) { if (!outputAreas.length) {
outputAreas = [ outputAreas = [
{ {
area_id: "no_areas", area_id: "",
name: this.hass.localize("ui.components.area-picker.no_match"), name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null, picture: null,
}, },
@@ -308,25 +339,52 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
return html``; return html``;
} }
return html` return html`
<ha-combo-box <vaadin-combo-box-light
.hass=${this.hass}
item-value-path="area_id" item-value-path="area_id"
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.value=${this.value} .value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
<paper-input
.label=${this.label === undefined && this.hass .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area") ? this.hass.localize("ui.components.area-picker.area")
: this.label} : this.label}
.placeholder=${this.placeholder .placeholder=${this.placeholder
? this._area(this.placeholder)?.name ? this._area(this.placeholder)?.name
: undefined} : undefined}
.renderer=${rowRenderer} .disabled=${this.disabled}
@filter-changed=${this._filterChanged} class="input"
@opened-changed=${this._openedChanged} autocapitalize="none"
@value-changed=${this._areaChanged} autocomplete="off"
autocorrect="off"
spellcheck="false"
> >
</ha-combo-box> ${this.value
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.area-picker.clear"
)}
.path=${mdiClose}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize("ui.components.area-picker.toggle")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
@@ -334,29 +392,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._areas?.find((area) => area.area_id === areaId) this._areas?.find((area) => area.area_id === areaId)
); );
private _filterChanged(ev: CustomEvent): void { private _clearValue(ev: Event) {
this._filter = ev.detail.value; ev.stopPropagation();
if (!this._filter) { this._setValue("");
this.comboBox.filteredItems = this.comboBox.items;
return;
}
// @ts-ignore
if (!this.noAdd && this.comboBox._comboBox.filteredItems?.length === 0) {
this.comboBox.filteredItems = [
{
area_id: "add_new_suggestion",
name: this.hass.localize(
"ui.components.area-picker.add_new_sugestion",
{ name: this._filter }
),
picture: null,
},
];
} else {
this.comboBox.filteredItems = this.comboBox.items?.filter((item) =>
item.name.toLowerCase().includes(this._filter!.toLowerCase())
);
}
} }
private get _value() { private get _value() {
@@ -368,14 +406,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
} }
private _areaChanged(ev: PolymerChangedEvent<string>) { private _areaChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); const newValue = ev.detail.value;
let newValue = ev.detail.value;
if (newValue === "no_areas") { if (newValue !== "add_new") {
newValue = "";
}
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
if (newValue !== this._value) { if (newValue !== this._value) {
this._setValue(newValue); this._setValue(newValue);
} }
@@ -392,8 +425,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
inputLabel: this.hass.localize( inputLabel: this.hass.localize(
"ui.components.area-picker.add_dialog.name" "ui.components.area-picker.add_dialog.name"
), ),
defaultValue:
newValue === "add_new_suggestion" ? this._filter : undefined,
confirm: async (name) => { confirm: async (name) => {
if (!name) { if (!name) {
return; return;
@@ -414,8 +445,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this.entityFilter, this.entityFilter,
this.noAdd this.noAdd
); );
await this.updateComplete;
await this.comboBox.updateComplete;
this._setValue(area.area_id); this._setValue(area.area_id);
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -436,6 +465,19 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,14 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import {
formatAttributeName,
formatAttributeValue,
STATE_ATTRIBUTES,
} from "../data/entity_attributes";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import hassAttributeUtil, {
formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
@customElement("ha-attributes") @customElement("ha-attributes")
@@ -27,7 +25,7 @@ class HaAttributes extends LitElement {
} }
const attributes = this.computeDisplayAttributes( const attributes = this.computeDisplayAttributes(
STATE_ATTRIBUTES.concat( Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
this.extraFilters ? this.extraFilters.split(",") : [] this.extraFilters ? this.extraFilters.split(",") : []
) )
); );
@@ -122,7 +120,7 @@ class HaAttributes extends LitElement {
private formatAttribute(attribute: string): string | TemplateResult { private formatAttribute(attribute: string): string | TemplateResult {
if (!this.stateObj) { if (!this.stateObj) {
return ""; return "-";
} }
const value = this.stateObj.attributes[attribute]; const value = this.stateObj.attributes[attribute];
return formatAttributeValue(this.hass, value); return formatAttributeValue(this.hass, value);

View File

@@ -1,313 +0,0 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
export interface TimeChangedEvent {
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
amPm?: "AM" | "PM";
}
@customElement("ha-base-time-input")
export class HaBaseTimeInput extends LitElement {
/**
* Label for the input
*/
@property() label?: string;
/**
* auto validate time inputs
*/
@property({ type: Boolean }) autoValidate = false;
/**
* determines if inputs are required
*/
@property({ type: Boolean }) public required?: boolean;
/**
* 12 or 24 hr format
*/
@property({ type: Number }) format: 12 | 24 = 12;
/**
* disables the inputs
*/
@property({ type: Boolean }) disabled = false;
/**
* hour
*/
@property({ type: Number }) hours = 0;
/**
* minute
*/
@property({ type: Number }) minutes = 0;
/**
* second
*/
@property({ type: Number }) seconds = 0;
/**
* milli second
*/
@property({ type: Number }) milliseconds = 0;
/**
* Label for the hour input
*/
@property() hourLabel = "";
/**
* Label for the min input
*/
@property() minLabel = "";
/**
* Label for the sec input
*/
@property() secLabel = "";
/**
* Label for the milli sec input
*/
@property() millisecLabel = "";
/**
* show the sec field
*/
@property({ type: Boolean }) enableSecond = false;
/**
* show the milli sec field
*/
@property({ type: Boolean }) enableMillisecond = false;
/**
* limit hours input
*/
@property({ type: Boolean }) noHoursLimit = false;
/**
* AM or PM
*/
@property() amPm: "AM" | "PM" = "AM";
/**
* Formatted time string
*/
@property() value?: string;
protected render(): TemplateResult {
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<div class="time-input-wrap">
<ha-textfield
id="hour"
type="number"
inputmode="numeric"
.value=${this.hours}
.label=${this.hourLabel}
name="hours"
@input=${this._valueChanged}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
.max=${this._hourMax}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
<ha-textfield
id="min"
type="number"
inputmode="numeric"
.value=${this._formatValue(this.minutes)}
.label=${this.minLabel}
@input=${this._valueChanged}
@focus=${this._onFocus}
name="minutes"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
.suffix=${this.enableSecond ? ":" : ""}
class=${this.enableSecond ? "has-suffix" : ""}
>
</ha-textfield>
${this.enableSecond
? html`<ha-textfield
id="sec"
type="number"
inputmode="numeric"
.value=${this._formatValue(this.seconds)}
.label=${this.secLabel}
@input=${this._valueChanged}
@focus=${this._onFocus}
name="seconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
.suffix=${this.enableMillisecond ? ":" : ""}
class=${this.enableMillisecond ? "has-suffix" : ""}
>
</ha-textfield>`
: ""}
${this.enableMillisecond
? html`<ha-textfield
id="millisec"
type="number"
.value=${this._formatValue(this.milliseconds, 3)}
.label=${this.millisecLabel}
@input=${this._valueChanged}
@focus=${this._onFocus}
name="milliseconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="3"
max="999"
min="0"
.disabled=${this.disabled}
>
</ha-textfield>`
: ""}
${this.format === 24
? ""
: html`<mwc-select
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
name="amPm"
naturalMenuWidth
fixedMenuPosition
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
<mwc-list-item value="AM">AM</mwc-list-item>
<mwc-list-item value="PM">PM</mwc-list-item>
</mwc-select>`}
</div>
`;
}
private _valueChanged(ev) {
this[ev.target.name] =
ev.target.name === "amPm" ? ev.target.value : Number(ev.target.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds,
milliseconds: this.milliseconds,
};
if (this.format === 12) {
value.amPm = this.amPm;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onFocus(ev) {
ev.target.select();
}
/**
* Format time fragments
*/
private _formatValue(value: number, padding = 2) {
return value.toString().padStart(padding, "0");
}
/**
* 24 hour format has a max hr of 23
*/
private get _hourMax() {
if (this.noHoursLimit) {
return null;
}
if (this.format === 12) {
return 12;
}
return 23;
}
static styles = css`
:host {
display: block;
}
.time-input-wrap {
display: flex;
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
}
ha-textfield {
width: 40px;
text-align: center;
--mdc-shape-small: 0;
--text-field-appearance: none;
--text-field-padding: 0 4px;
--text-field-suffix-padding-left: 2px;
--text-field-suffix-padding-right: 0;
--text-field-text-align: center;
}
ha-textfield.hasSuffix {
--text-field-padding: 0 0 0 4px;
}
ha-textfield:first-child {
--text-field-border-top-left-radius: var(--mdc-shape-medium);
}
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
mwc-select {
--mdc-shape-small: 0;
width: 85px;
}
label {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: var(
--mdc-typography-body2-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
line-height: var(--mdc-typography-body2-line-height, 1.25rem);
font-weight: var(--mdc-typography-body2-font-weight, 400);
letter-spacing: var(
--mdc-typography-body2-letter-spacing,
0.0178571429em
);
text-decoration: var(--mdc-typography-body2-text-decoration, inherit);
text-transform: var(--mdc-typography-body2-text-transform, inherit);
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-base-time-input": HaBaseTimeInput;
}
}

View File

@@ -1,10 +1,10 @@
import "@material/mwc-list/mwc-list-item"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@material/mwc-select/mwc-select"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint"; import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@@ -24,11 +24,7 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public open() { public open() {
const select = this.shadowRoot?.querySelector("mwc-select"); this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
if (select) {
// @ts-expect-error
select.menuOpen = true;
}
} }
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
@@ -49,29 +45,32 @@ class HaBluePrintPicker extends LitElement {
return html``; return html``;
} }
return html` return html`
<mwc-select <paper-dropdown-menu-light
.label=${this.label || .label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")} this.hass.localize("ui.components.blueprint-picker.label")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
@selected=${this._blueprintChanged} horizontal-align="left"
@closed=${stopPropagation}
> >
<mwc-list-item value=""> <paper-listbox
slot="dropdown-content"
.selected=${this.value}
attr-for-selected="data-blueprint-path"
@iron-select=${this._blueprintChanged}
>
<paper-item data-blueprint-path="">
${this.hass.localize( ${this.hass.localize(
"ui.components.blueprint-picker.select_blueprint" "ui.components.blueprint-picker.select_blueprint"
)} )}
</mwc-list-item> </paper-item>
${this._processedBlueprints(this.blueprints).map( ${this._processedBlueprints(this.blueprints).map(
(blueprint) => html` (blueprint) => html`
<mwc-list-item .value=${blueprint.path}> <paper-item data-blueprint-path=${blueprint.path}>
${blueprint.name} ${blueprint.name}
</mwc-list-item> </paper-item>
` `
)} )}
</mwc-select> </paper-listbox>
</paper-dropdown-menu-light>
`; `;
} }
@@ -85,10 +84,10 @@ class HaBluePrintPicker extends LitElement {
} }
private _blueprintChanged(ev) { private _blueprintChanged(ev) {
const newValue = ev.target.value; const newValue = ev.detail.item.dataset.blueprintPath;
if (newValue !== this.value) { if (newValue !== this.value) {
this.value = newValue; this.value = ev.detail.value;
setTimeout(() => { setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change"); fireEvent(this, "change");
@@ -101,11 +100,15 @@ class HaBluePrintPicker extends LitElement {
:host { :host {
display: inline-block; display: inline-block;
} }
mwc-select { paper-dropdown-menu-light {
width: 100%; width: 100%;
min-width: 200px; min-width: 200px;
display: block; display: block;
} }
paper-item {
cursor: pointer;
min-width: 200px;
}
`; `;
} }
} }

View File

@@ -1,24 +0,0 @@
import { css } from "lit";
import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base";
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { customElement } from "lit/decorators";
@customElement("ha-check-list-item")
export class HaCheckListItem extends CheckListItemBase {
static override styles = [
styles,
controlStyles,
css`
:host {
--mdc-theme-secondary: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-check-list-item": HaCheckListItem;
}
}

View File

@@ -1,18 +1,12 @@
import { CheckboxBase } from "@material/mwc-checkbox/mwc-checkbox-base"; import { Checkbox } from "@material/mwc-checkbox";
import { styles } from "@material/mwc-checkbox/mwc-checkbox.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-checkbox") @customElement("ha-checkbox")
export class HaCheckbox extends CheckboxBase { export class HaCheckbox extends Checkbox {
static override styles = [ public firstUpdated() {
styles, super.firstUpdated();
css` this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
:host {
--mdc-theme-secondary: var(--primary-color);
} }
`,
];
} }
declare global { declare global {

View File

@@ -0,0 +1,141 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin";
import "./ha-icon";
import "./ha-icon-button";
/*
* @appliesMixin EventsMixin
*/
class HaClimateControl extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<style>
/* local DOM styles go here */
:host {
@apply --layout-flex;
@apply --layout-horizontal;
@apply --layout-justified;
}
.in-flux#target_temperature {
color: var(--error-color);
}
#target_temperature {
@apply --layout-self-center;
font-size: 200%;
direction: ltr;
}
.control-buttons {
font-size: 200%;
text-align: right;
}
ha-icon-button {
--mdc-icon-size: 32px;
}
</style>
<!-- local DOM goes here -->
<div id="target_temperature">[[value]] [[units]]</div>
<div class="control-buttons">
<div>
<ha-icon-button on-click="incrementValue">
<ha-icon icon="hass:chevron-up"></ha-icon>
</ha-icon-button>
</div>
<div>
<ha-icon-button on-click="decrementValue">
<ha-icon icon="hass:chevron-down"></ha-icon>
</ha-icon-button>
</div>
</div>
`;
}
static get properties() {
return {
value: {
type: Number,
observer: "valueChanged",
},
units: {
type: String,
},
min: {
type: Number,
},
max: {
type: Number,
},
step: {
type: Number,
value: 1,
},
};
}
temperatureStateInFlux(inFlux) {
this.$.target_temperature.classList.toggle("in-flux", inFlux);
}
_round(val) {
// round value to precision derived from step
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
}
incrementValue() {
const newval = this._round(this.value + this.step);
if (this.value < this.max) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
}
if (newval <= this.max) {
// If no initial target_temp
// this forces control to start
// from the min configured instead of 0
if (newval <= this.min) {
this.value = this.min;
} else {
this.value = newval;
}
} else {
this.value = this.max;
}
}
decrementValue() {
const newval = this._round(this.value - this.step);
if (this.value > this.min) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
}
if (newval >= this.min) {
this.value = newval;
} else {
this.value = this.min;
}
}
valueChanged() {
// when the last_changed timestamp is changed,
// trigger a potential event fire in
// the future, as long as last changed is far enough in the
// past.
if (this.last_changed) {
window.setTimeout(() => {
const now = Date.now();
if (now - this.last_changed >= 2000) {
this.fire("change");
this.temperatureStateInFlux(false);
this.last_changed = null;
}
}, 2010);
}
}
}
customElements.define("ha-climate-control", HaClimateControl);

View File

@@ -1,138 +0,0 @@
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-icon-button";
@customElement("ha-climate-control")
class HaClimateControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value!: number;
@property() public unit = "";
@property() public min?: number;
@property() public max?: number;
@property() public step = 1;
private _lastChanged?: number;
@query("#target_temperature") private _targetTemperature!: HTMLElement;
protected render(): TemplateResult {
return html`
<div id="target_temperature">${this.value} ${this.unit}</div>
<div class="control-buttons">
<div>
<ha-icon-button
.path=${mdiChevronUp}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_up"
)}
@click=${this._incrementValue}
>
</ha-icon-button>
</div>
<div>
<ha-icon-button
.path=${mdiChevronDown}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_down"
)}
@click=${this._decrementValue}
>
</ha-icon-button>
</div>
</div>
`;
}
protected updated(changedProperties) {
if (changedProperties.has("value")) {
this._valueChanged();
}
}
private _temperatureStateInFlux(inFlux) {
this._targetTemperature.classList.toggle("in-flux", inFlux);
}
private _round(value) {
// Round value to precision derived from step.
// Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
}
private _incrementValue() {
const newValue = this._round(this.value + this.step);
this._processNewValue(newValue);
}
private _decrementValue() {
const newValue = this._round(this.value - this.step);
this._processNewValue(newValue);
}
private _processNewValue(value) {
const newValue = conditionalClamp(value, this.min, this.max);
if (this.value !== newValue) {
this.value = newValue;
this._lastChanged = Date.now();
this._temperatureStateInFlux(true);
}
}
private _valueChanged() {
// When the last_changed timestamp is changed,
// trigger a potential event fire in the future,
// as long as last_changed is far enough in the past.
if (this._lastChanged) {
window.setTimeout(() => {
const now = Date.now();
if (now - this._lastChanged! >= 2000) {
fireEvent(this, "change");
this._temperatureStateInFlux(false);
this._lastChanged = undefined;
}
}, 2010);
}
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
justify-content: space-between;
}
.in-flux {
color: var(--error-color);
}
#target_temperature {
align-self: center;
font-size: 28px;
direction: ltr;
}
.control-buttons {
font-size: 24px;
text-align: right;
}
ha-icon-button {
--mdc-icon-size: 32px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-climate-control": HaClimateControl;
}
}

View File

@@ -1,16 +1,8 @@
import type {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { loadCodeMirror } from "../resources/codemirror.ondemand"; import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -32,15 +24,10 @@ export class HaCodeEditor extends ReactiveElement {
@property() public mode = "yaml"; @property() public mode = "yaml";
public hass?: HomeAssistant;
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false; @property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false;
@property() public error = false; @property() public error = false;
@state() private _value = ""; @state() private _value = "";
@@ -123,7 +110,11 @@ export class HaCodeEditor extends ReactiveElement {
private async _load(): Promise<void> { private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror(); this._loadedCodeMirror = await loadCodeMirror();
const extensions = [
this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
extensions: [
this._loadedCodeMirror.lineNumbers(), this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(), this._loadedCodeMirror.history(),
@@ -149,66 +140,13 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.EditorView.updateListener.of((update) => this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update) this._onUpdate(update)
), ),
]; ],
if (!this.readOnly && this.autocompleteEntities && this.hass) {
extensions.push(
this._loadedCodeMirror.autocompletion({
override: [this._entityCompletions.bind(this)],
maxRenderedOptions: 10,
})
);
}
this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
extensions,
}), }),
root: this.shadowRoot!, root: this.shadowRoot!,
parent: this.shadowRoot!, parent: this.shadowRoot!,
}); });
} }
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
}
const options = Object.keys(states).map((key) => ({
type: "variable",
label: key,
detail: states[key].attributes.friendly_name,
info: `State: ${states[key].state}`,
}));
return options;
});
private _entityCompletions(
context: CompletionContext
): CompletionResult | null | Promise<CompletionResult | null> {
const entityWord = context.matchBefore(/[a-z_]{3,}\./);
if (
!entityWord ||
(entityWord.from === entityWord.to && !context.explicit)
) {
return null;
}
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
}
return {
from: Number(entityWord.from),
options: states,
span: /^\w*.\w*$/,
};
}
private _blockKeyboardShortcuts() { private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation()); this.addEventListener("keydown", (ev) => ev.stopPropagation());
} }
@@ -225,9 +163,10 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value }); fireEvent(this, "value-changed", { value: this._value });
} }
// Only Lit 2.0 will use this
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host(.error-state) .cm-gutters { :host(.error-state) div.cm-wrap .cm-gutters {
border-color: var(--error-state-color, red); border-color: var(--error-state-color, red);
} }
`; `;

View File

@@ -1,78 +1,37 @@
import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@polymer/paper-input/paper-input";
import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light"; import "@polymer/paper-item/paper-item";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield";
registerStyles( // eslint-disable-next-line lit/prefer-static-styles
"vaadin-combo-box-item", const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
css` paper-item {
:host { margin: -5px -10px;
padding: 0; padding: 0;
} }
:host([focused]:not([disabled])) { </style>
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12); <paper-item>${item}</paper-item>`;
}
:host([selected]:not([disabled])) {
background-color: transparent;
color: var(--mdc-theme-primary);
--mdc-ripple-color: var(--mdc-theme-primary);
--mdc-theme-text-primary-on-background: var(--mdc-theme-primary);
}
:host([selected]:not([disabled])):before {
background-color: var(--mdc-theme-primary);
opacity: 0.12;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
:host([selected][focused]:not([disabled])):before {
opacity: 0.24;
}
:host(:hover:not([disabled])) {
background-color: transparent;
}
[part="content"] {
width: 100%;
}
[part="checkmark"] {
display: none;
}
`
);
@customElement("ha-combo-box") @customElement("ha-combo-box")
export class HaComboBox extends LitElement { export class HaComboBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public value?: string;
@property() public placeholder?: string; @property() public items?: [];
@property() public validationMessage?: string; @property() public filteredItems?: [];
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid?: boolean;
@property({ type: Boolean }) public icon?: boolean;
@property() public items?: any[];
@property() public filteredItems?: any[];
@property({ attribute: "allow-custom-value", type: Boolean }) @property({ attribute: "allow-custom-value", type: Boolean })
public allowCustomValue?: boolean; public allowCustomValue?: boolean;
@@ -87,25 +46,24 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" }) @state() private _opened?: boolean;
private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
public open() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this._comboBox?.open(); (this._comboBox as any)?.open();
}); });
} }
public focus() { public focus() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this._comboBox?.inputElement?.focus(); this.shadowRoot?.querySelector("paper-input")?.focus();
}); });
} }
public get selectedItem() { public get selectedItem() {
return this._comboBox.selectedItem; return (this._comboBox as any).selectedItem;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -114,78 +72,55 @@ export class HaComboBox extends LitElement {
.itemValuePath=${this.itemValuePath} .itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath} .itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath} .itemLabelPath=${this.itemLabelPath}
.value=${this.value || ""} .value=${this.value}
.items=${this.items} .items=${this.items}
.filteredItems=${this.filteredItems} .filteredItems=${this.filteredItems}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled} .disabled=${this.disabled}
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)} ${comboBoxRenderer(this.renderer || defaultRowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
attr-for-value="value"
> >
<ha-textfield <paper-input
.label=${this.label} .label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled} .disabled=${this.disabled}
.validationMessage=${this.validationMessage}
.errorMessage=${this.errorMessage}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
> >
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
${this.value ${this.value
? html`<ha-svg-icon ? html`
aria-label=${this.hass?.localize("ui.components.combo-box.clear")} <ha-icon-button
class="clear-button" .label=${this.hass.localize("ui.components.combo-box.clear")}
.path=${mdiClose} .path=${mdiClose}
slot="suffix"
class="clear-button"
@click=${this._clearValue} @click=${this._clearValue}
></ha-svg-icon>` ></ha-icon-button>
`
: ""} : ""}
<ha-svg-icon
aria-label=${this.hass?.localize("ui.components.combo-box.show")} <ha-icon-button
class="toggle-button" .label=${this.hass.localize("ui.components.combo-box.show")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown} .path=${this._opened ? mdiMenuUp : mdiMenuDown}
@click=${this._toggleOpen} slot="suffix"
></ha-svg-icon> class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
} }
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) =>
html`<mwc-list-item>
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</mwc-list-item>`;
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { value: undefined }); fireEvent(this, "value-changed", { value: undefined });
} }
private _toggleOpen(ev: Event) {
if (this._opened) {
this._comboBox?.close();
ev.stopPropagation();
} else {
this._comboBox?.inputElement.focus();
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
// delay this so we can handle click event before setting _opened
setTimeout(() => {
this._opened = ev.detail.value; this._opened = ev.detail.value;
}, 0);
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail); fireEvent(this, ev.type, ev.detail);
} }
@@ -206,38 +141,11 @@ export class HaComboBox extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { paper-input > ha-icon-button {
display: block;
width: 100%;
}
vaadin-combo-box-light {
position: relative;
}
ha-textfield {
width: 100%;
}
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-svg-icon {
color: var(--input-dropdown-icon-color);
position: absolute;
cursor: pointer;
}
.toggle-button {
right: 12px;
top: -10px;
}
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.clear-button {
--mdc-icon-size: 20px;
top: -7px;
right: 36px;
}
`; `;
} }
} }

View File

@@ -1,76 +1,135 @@
import { mdiCalendar } from "@mdi/js"; import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light";
import { customElement, property } from "lit/decorators"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { formatDateNumeric } from "../common/datetime/format_date"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker"); const i18n = {
monthNames: [
export interface datePickerDialogParams { "January",
value?: string; "February",
min?: string; "March",
max?: string; "April",
locale?: string; "May",
onChange: (value: string) => void; "June",
"July",
"August",
"September",
"October",
"November",
"December",
],
weekdays: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
firstDayOfWeek: 0,
week: "Week",
calendar: "Calendar",
clear: "Clear",
today: "Today",
cancel: "Cancel",
formatTitle: (monthName, fullYear) => monthName + " " + fullYear,
formatDate: (d: { day: number; month: number; year: number }) =>
[
("0000" + String(d.year)).slice(-4),
("0" + String(d.month + 1)).slice(-2),
("0" + String(d.day)).slice(-2),
].join("-"),
parseDate: (text: string) => {
const parts = text.split("-");
const today = new Date();
let date;
let month = today.getMonth();
let year = today.getFullYear();
if (parts.length === 3) {
year = parseInt(parts[0]);
if (parts[0].length < 3 && year >= 0) {
year += year < 50 ? 2000 : 1900;
}
month = parseInt(parts[1]) - 1;
date = parseInt(parts[2]);
} else if (parts.length === 2) {
month = parseInt(parts[0]) - 1;
date = parseInt(parts[1]);
} else if (parts.length === 1) {
date = parseInt(parts[0]);
} }
const showDatePickerDialog = ( if (date !== undefined) {
element: HTMLElement, return { day: date, month, year };
dialogParams: datePickerDialogParams }
): void => { return undefined;
fireEvent(element, "show-dialog", { },
dialogTag: "ha-dialog-date-picker",
dialogImport: loadDatePickerDialog,
dialogParams,
});
}; };
@customElement("ha-date-input") @customElement("ha-date-input")
export class HaDateInput extends LitElement { export class HaDateInput extends LitElement {
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property() public value?: string; @property() public value?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public label?: string; @property() public label?: string;
@query("vaadin-date-picker-light", true) private _datePicker;
private _inited = false;
updated(changedProps: PropertyValues) {
if (changedProps.has("value")) {
this._datePicker.value = this.value;
this._inited = true;
}
}
render() { render() {
return html`<paper-input return html`<vaadin-date-picker-light
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
attr-for-value="value"
.i18n=${i18n}
>
<paper-input
.label=${this.label} .label=${this.label}
.disabled=${this.disabled} .disabled=${this.disabled}
no-label-float no-label-float
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
> >
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>`; </paper-input>
</vaadin-date-picker-light>`;
} }
private _openDialog() { private _valueChanged(ev: CustomEvent) {
if (this.disabled) { if (
return; !this.value ||
} (this._inited && !this._compareStringDates(ev.detail.value, this.value))
showDatePickerDialog(this, { ) {
min: "1970-01-01", this.value = ev.detail.value;
value: this.value,
onChange: (value) => this._valueChanged(value),
locale: this.locale.language,
});
}
private _valueChanged(value: string) {
if (this.value !== value) {
this.value = value;
fireEvent(this, "change"); fireEvent(this, "change");
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value: ev.detail.value });
} }
} }
private _compareStringDates(a: string, b: string): boolean {
const aParts = a.split("-");
const bParts = b.split("-");
let i = 0;
for (const aPart of aParts) {
if (Number(aPart) !== Number(bParts[i])) {
return false;
}
i++;
}
return true;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input { paper-input {

View File

@@ -1,106 +0,0 @@
import "@material/mwc-button/mwc-button";
import "app-datepicker";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleDialog } from "../resources/styles";
import { datePickerDialogParams } from "./ha-date-input";
import "./ha-dialog";
@customElement("ha-dialog-date-picker")
export class HaDialogDatePicker extends LitElement {
@property() public value?: string;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@state() private _params?: datePickerDialogParams;
@state() private _value?: string;
public showDialog(params: datePickerDialogParams): void {
this._params = params;
this._value = params.value;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
render() {
if (!this._params) {
return html``;
}
return html`<ha-dialog open @closed=${this.closeDialog}>
<app-datepicker
.value=${this._value}
.min=${this._params.min}
.max=${this._params.max}
.locale=${this._params.locale}
@datepicker-value-updated=${this._valueChanged}
></app-datepicker>
<mwc-button slot="secondaryAction" @click=${this._setToday}
>today</mwc-button
>
<mwc-button slot="primaryAction" dialogaction="cancel" class="cancel-btn">
cancel
</mwc-button>
<mwc-button slot="primaryAction" @click=${this._setValue}>ok</mwc-button>
</ha-dialog>`;
}
private _valueChanged(ev: CustomEvent) {
this._value = ev.detail.value;
}
private _setToday() {
this._value = new Date().toISOString().split("T")[0];
}
private _setValue() {
this._params?.onChange(this._value!);
this.closeDialog();
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--justify-action-buttons: space-between;
}
app-datepicker {
--app-datepicker-accent-color: var(--primary-color);
--app-datepicker-bg-color: transparent;
--app-datepicker-color: var(--primary-text-color);
--app-datepicker-disabled-day-color: var(--disabled-text-color);
--app-datepicker-focused-day-color: var(--text-primary-color);
--app-datepicker-focused-year-bg-color: var(--primary-color);
--app-datepicker-selector-color: var(--secondary-text-color);
--app-datepicker-separator-color: var(--divider-color);
--app-datepicker-weekday-color: var(--secondary-text-color);
}
app-datepicker::part(calendar-day):focus {
outline: none;
}
@media all and (min-width: 450px) {
ha-dialog {
--mdc-dialog-min-width: 300px;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
app-datepicker {
width: 100%;
}
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-date-picker": HaDialogDatePicker;
}
}

View File

@@ -1,7 +1,6 @@
import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base"; import { Dialog } from "@material/mwc-dialog";
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit"; import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -22,7 +21,8 @@ export const createCloseHeading = (
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends DialogBase { // @ts-expect-error
export class HaDialog extends Dialog {
public scrollToPos(x: number, y: number) { public scrollToPos(x: number, y: number) {
this.contentElement?.scrollTo(x, y); this.contentElement?.scrollTo(x, y);
} }
@@ -31,8 +31,9 @@ export class HaDialog extends DialogBase {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`; return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
} }
static override styles = [ protected static get styles(): CSSResultGroup {
styles, return [
Dialog.styles,
css` css`
.mdc-dialog { .mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color); --mdc-dialog-scroll-divider-color: var(--divider-color);
@@ -101,6 +102,7 @@ export class HaDialog extends DialogBase {
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,8 +1,7 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input"; import "./paper-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
export interface HaDurationData { export interface HaDurationData {
hours?: number; hours?: number;
@@ -33,69 +32,110 @@ class HaDurationInput extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-base-time-input <paper-time-input
.label=${this.label} .label=${this.label}
.required=${this.required} .required=${this.required}
.autoValidate=${this.required} .autoValidate=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
errorMessage="Required" error-message="Required"
enableSecond enable-second
.enableMillisecond=${this.enableMillisecond} .enableMillisecond=${this.enableMillisecond}
format="24" format="24"
.hours=${this._hours} .hour=${this._parseDuration(this._hours)}
.minutes=${this._minutes} .min=${this._parseDuration(this._minutes)}
.seconds=${this._seconds} .sec=${this._parseDuration(this._seconds)}
.milliseconds=${this._milliseconds} .millisec=${this._parseDurationMillisec(this._milliseconds)}
@value-changed=${this._durationChanged} @hour-changed=${this._hourChanged}
noHoursLimit @min-changed=${this._minChanged}
hourLabel="hh" @sec-changed=${this._secChanged}
minLabel="mm" @millisec-changed=${this._millisecChanged}
secLabel="ss" float-input-labels
millisecLabel="ms" no-hours-limit
></ha-base-time-input> always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
millisec-label="ms"
></paper-time-input>
`; `;
} }
private get _hours() { private get _hours() {
return this.data?.hours ? Number(this.data.hours) : 0; return this.data && this.data.hours ? Number(this.data.hours) : 0;
} }
private get _minutes() { private get _minutes() {
return this.data?.minutes ? Number(this.data.minutes) : 0; return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
} }
private get _seconds() { private get _seconds() {
return this.data?.seconds ? Number(this.data.seconds) : 0; return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
} }
private get _milliseconds() { private get _milliseconds() {
return this.data?.milliseconds ? Number(this.data.milliseconds) : 0; return this.data && this.data.milliseconds
? Number(this.data.milliseconds)
: 0;
} }
private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { private _parseDuration(value) {
ev.stopPropagation(); return value.toString().padStart(2, "0");
const value = { ...ev.detail.value };
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
} else if (value.milliseconds > 999) {
value.seconds += Math.floor(value.milliseconds / 1000);
value.milliseconds %= 1000;
} }
if (value.seconds > 59) { private _parseDurationMillisec(value) {
value.minutes += Math.floor(value.seconds / 60); return value.toString().padStart(3, "0");
value.seconds %= 60;
} }
if (value.minutes > 59) { private _hourChanged(ev) {
value.hours += Math.floor(value.minutes / 60); this._durationChanged(ev, "hours");
value.minutes %= 60;
} }
private _minChanged(ev) {
this._durationChanged(ev, "minutes");
}
private _secChanged(ev) {
this._durationChanged(ev, "seconds");
}
private _millisecChanged(ev) {
this._durationChanged(ev, "milliseconds");
}
private _durationChanged(ev, unit) {
let value = Number(ev.detail.value);
if (value === this[`_${unit}`]) {
return;
}
let hours = this._hours;
let minutes = this._minutes;
if (unit === "seconds" && value > 59) {
minutes += Math.floor(value / 60);
value %= 60;
}
if (unit === "minutes" && value > 59) {
hours += Math.floor(value / 60);
value %= 60;
}
const newValue: HaDurationData = {
hours,
minutes,
seconds: this._seconds,
};
if (this.enableMillisecond || this._milliseconds) {
newValue.milliseconds = this._milliseconds;
}
newValue[unit] = value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value: newValue,
}); });
} }
} }

View File

@@ -1,21 +1,21 @@
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import { css, html, LitElement, TemplateResult, PropertyValues } from "lit"; import { css, html, LitElement, TemplateResult, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { HaTextField } from "../ha-textfield";
import "../ha-textfield";
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types"; import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types";
@customElement("ha-form-float") @customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement { export class HaFormFloat extends LitElement implements HaFormElement {
@property({ attribute: false }) public schema!: HaFormFloatSchema; @property() public schema!: HaFormFloatSchema;
@property({ attribute: false }) public data!: HaFormFloatData; @property() public data!: HaFormFloatData;
@property() public label!: string; @property() public label!: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@query("ha-textfield") private _input?: HaTextField; @query("mwc-textfield") private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@@ -25,7 +25,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-textfield <mwc-textfield
inputMode="decimal" inputMode="decimal"
.label=${this.label} .label=${this.label}
.value=${this.data !== undefined ? this.data : ""} .value=${this.data !== undefined ? this.data : ""}
@@ -35,7 +35,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
.suffix=${this.schema.description?.suffix} .suffix=${this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined} .validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged} @input=${this._valueChanged}
></ha-textfield> ></mwc-textfield>
`; `;
} }
@@ -46,7 +46,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const source = ev.target as HaTextField; const source = ev.target as TextField;
const rawValue = source.value.replace(",", "."); const rawValue = source.value.replace(",", ".");
let value: number | undefined; let value: number | undefined;
@@ -81,7 +81,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
:host([own-margin]) { :host([own-margin]) {
margin-bottom: 5px; margin-bottom: 5px;
} }
ha-textfield { mwc-textfield {
display: block; display: block;
} }
`; `;

View File

@@ -1,3 +1,7 @@
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import "@material/mwc-slider";
import type { Slider } from "@material/mwc-slider";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -10,22 +14,18 @@ import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox"; import { HaCheckbox } from "../ha-checkbox";
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
import "../ha-slider";
import { HaTextField } from "../ha-textfield";
@customElement("ha-form-integer") @customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement { export class HaFormInteger extends LitElement implements HaFormElement {
@property({ attribute: false }) public schema!: HaFormIntegerSchema; @property() public schema!: HaFormIntegerSchema;
@property({ attribute: false }) public data?: HaFormIntegerData; @property() public data?: HaFormIntegerData;
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@query("ha-textfield ha-slider") private _input?: @query("paper-input ha-slider") private _input?: HTMLElement;
| HaTextField
| HTMLInputElement;
private _lastValue?: HaFormIntegerData; private _lastValue?: HaFormIntegerData;
@@ -45,7 +45,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
<div> <div>
${this.label} ${this.label}
<div class="flex"> <div class="flex">
${!this.schema.required ${this.schema.optional
? html` ? html`
<ha-checkbox <ha-checkbox
@change=${this._handleCheckboxChange} @change=${this._handleCheckboxChange}
@@ -54,23 +54,22 @@ export class HaFormInteger extends LitElement implements HaFormElement {
></ha-checkbox> ></ha-checkbox>
` `
: ""} : ""}
<ha-slider <mwc-slider
pin discrete
ignore-bar-touch
.value=${this._value} .value=${this._value}
.min=${this.schema.valueMin} .min=${this.schema.valueMin}
.max=${this.schema.valueMax} .max=${this.schema.valueMax}
.disabled=${this.disabled || .disabled=${this.disabled ||
(this.data === undefined && !this.schema.required)} (this.data === undefined && this.schema.optional)}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-slider> ></mwc-slider>
</div> </div>
</div> </div>
`; `;
} }
return html` return html`
<ha-textfield <mwc-textfield
type="number" type="number"
inputMode="numeric" inputMode="numeric"
.label=${this.label} .label=${this.label}
@@ -81,7 +80,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.suffix=${this.schema.description?.suffix} .suffix=${this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined} .validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged} @input=${this._valueChanged}
></ha-textfield> ></mwc-textfield>
`; `;
} }
@@ -100,7 +99,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
return this.data; return this.data;
} }
if (!this.schema.required) { if (this.schema.optional) {
return this.schema.valueMin || 0; return this.schema.valueMin || 0;
} }
@@ -138,7 +137,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const source = ev.target as HaTextField | HTMLInputElement; const source = ev.target as TextField | Slider;
const rawValue = source.value; const rawValue = source.value;
let value: number | undefined; let value: number | undefined;
@@ -169,10 +168,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.flex { .flex {
display: flex; display: flex;
} }
ha-slider { mwc-slider {
flex: 1; flex: 1;
} }
ha-textfield { mwc-textfield {
display: block; display: block;
} }
`; `;

View File

@@ -1,27 +1,25 @@
import "@material/mwc-select/mwc-select";
import { mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@material/mwc-textfield";
import "@material/mwc-formfield";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues,
TemplateResult, TemplateResult,
PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu"; import "../ha-button-menu";
import { HaCheckListItem } from "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-formfield";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "../ha-textfield";
import { import {
HaFormElement, HaFormElement,
HaFormMultiSelectData, HaFormMultiSelectData,
HaFormMultiSelectSchema, HaFormMultiSelectSchema,
} from "./types"; } from "./types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
function optionValue(item: string | string[]): string { function optionValue(item: string | string[]): string {
return Array.isArray(item) ? item[0] : item; return Array.isArray(item) ? item[0] : item;
@@ -59,23 +57,23 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
: Object.entries(this.schema.options); : Object.entries(this.schema.options);
const data = this.data || []; const data = this.data || [];
// We will just render all checkboxes. const renderedOptions = options.map((item: string | [string, string]) => {
if (options.length < SHOW_ALL_ENTRIES_LIMIT) {
return html`<div>
${this.label}${options.map((item: string | [string, string]) => {
const value = optionValue(item); const value = optionValue(item);
return html` return html`
<ha-formfield .label=${optionLabel(item)}> <mwc-formfield .label=${optionLabel(item)}>
<ha-checkbox <ha-checkbox
.checked=${data.includes(value)} .checked=${data.includes(value)}
.value=${value} .value=${value}
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-checkbox> ></ha-checkbox>
</ha-formfield> </mwc-formfield>
`; `;
})} });
</div> `;
// We will just render all checkboxes.
if (options.length < SHOW_ALL_ENTRIES_LIMIT) {
return html`<div>${this.label}${renderedOptions}</div> `;
} }
return html` return html`
@@ -85,10 +83,8 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
corner="BOTTOM_START" corner="BOTTOM_START"
@opened=${this._handleOpen} @opened=${this._handleOpen}
@closed=${this._handleClose} @closed=${this._handleClose}
multi
activatable
> >
<ha-textfield <mwc-textfield
slot="trigger" slot="trigger"
.label=${this.label} .label=${this.label}
.value=${data .value=${data
@@ -96,25 +92,12 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
.join(", ")} .join(", ")}
.disabled=${this.disabled} .disabled=${this.disabled}
tabindex="-1" tabindex="-1"
></ha-textfield> ></mwc-textfield>
<ha-svg-icon <ha-svg-icon
slot="trigger" slot="trigger"
.path=${this._opened ? mdiMenuUp : mdiMenuDown} .path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
${options.map((item: string | [string, string]) => { ${renderedOptions}
const value = optionValue(item);
const selected = data.includes(value);
return html`<ha-check-list-item
left
.selected=${selected}
.activated=${selected}
@request-selected=${this._selectedChanged}
.value=${value}
.disabled=${this.disabled}
>
${optionLabel(item)}
</ha-check-list-item>`;
})}
</ha-button-menu> </ha-button-menu>
`; `;
} }
@@ -122,7 +105,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
protected firstUpdated() { protected firstUpdated() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
const { formElement, mdcRoot } = const { formElement, mdcRoot } =
this.shadowRoot?.querySelector("ha-textfield") || ({} as any); this.shadowRoot?.querySelector("mwc-textfield") || ({} as any);
if (formElement) { if (formElement) {
formElement.style.textOverflow = "ellipsis"; formElement.style.textOverflow = "ellipsis";
} }
@@ -142,23 +125,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
} }
} }
private _selectedChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (ev.detail.source === "property") {
return;
}
this._handleValueChanged(
(ev.target as HaCheckListItem).value,
ev.detail.selected
);
}
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
const { value, checked } = ev.target as HaCheckbox; const { value, checked } = ev.target as HaCheckbox;
this._handleValueChanged(value, checked);
}
private _handleValueChanged(value, checked: boolean): void {
let newValue: string[]; let newValue: string[];
if (checked) { if (checked) {
@@ -202,11 +171,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
display: block; display: block;
cursor: pointer; cursor: pointer;
} }
ha-formfield { mwc-formfield {
display: block; display: block;
padding-right: 16px; padding-right: 16px;
} }
ha-textfield { mwc-textfield {
display: block; display: block;
pointer-events: none; pointer-events: none;
} }

View File

@@ -29,7 +29,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.schema.required && this.schema.options!.length < 6) { if (!this.schema.optional && this.schema.options!.length < 6) {
return html` return html`
<div> <div>
${this.label} ${this.label}
@@ -59,7 +59,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueChanged} @selected=${this._valueChanged}
> >
${!this.schema.required ${this.schema.optional
? html`<mwc-list-item value=""></mwc-list-item>` ? html`<mwc-list-item value=""></mwc-list-item>`
: ""} : ""}
${this.schema.options!.map( ${this.schema.options!.map(

View File

@@ -1,17 +1,17 @@
import { mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiEye, mdiEyeOff } from "@mdi/js";
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues,
TemplateResult, TemplateResult,
PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type { import type {
HaFormElement, HaFormElement,
HaFormStringData, HaFormStringData,
@@ -32,7 +32,7 @@ export class HaFormString extends LitElement implements HaFormElement {
@state() private _unmaskedPassword = false; @state() private _unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField; @query("mwc-textfield") private _input?: HTMLElement;
public focus(): void { public focus(): void {
if (this._input) { if (this._input) {
@@ -45,7 +45,7 @@ export class HaFormString extends LitElement implements HaFormElement {
this.schema.name.includes(field) this.schema.name.includes(field)
); );
return html` return html`
<ha-textfield <mwc-textfield
.type=${!isPassword .type=${!isPassword
? this._stringType ? this._stringType
: this._unmaskedPassword : this._unmaskedPassword
@@ -62,12 +62,13 @@ export class HaFormString extends LitElement implements HaFormElement {
: this.schema.description?.suffix} : this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined} .validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged} @input=${this._valueChanged}
></ha-textfield> ></mwc-textfield>
${isPassword ${isPassword
? html`<ha-icon-button ? html`<ha-icon-button
toggles toggles
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`} .label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword} @click=${this._toggleUnmaskedPassword}
tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>` ></ha-icon-button>`
: ""} : ""}
@@ -85,11 +86,11 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
private _valueChanged(ev: Event): void { private _valueChanged(ev: Event): void {
let value: string | undefined = (ev.target as HaTextField).value; let value: string | undefined = (ev.target as TextField).value;
if (this.data === value) { if (this.data === value) {
return; return;
} }
if (value === "" && !this.schema.required) { if (value === "" && this.schema.optional) {
value = undefined; value = undefined;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@@ -118,7 +119,7 @@ export class HaFormString extends LitElement implements HaFormElement {
:host([own-margin]) { :host([own-margin]) {
margin-bottom: 5px; margin-bottom: 5px;
} }
ha-textfield { mwc-textfield {
display: block; display: block;
} }
ha-icon-button { ha-icon-button {

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -12,16 +12,11 @@ import "./ha-form-positive_time_period_dict";
import "./ha-form-select"; import "./ha-form-select";
import "./ha-form-string"; import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
import { HomeAssistant } from "../../types";
const getValue = (obj, item) => (obj ? obj[item.name] : null); const getValue = (obj, item) => (obj ? obj[item.name] : null);
let selectorImported = false;
@customElement("ha-form") @customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property() public hass!: HomeAssistant;
@property() public data!: HaFormDataContainer; @property() public data!: HaFormDataContainer;
@property() public schema!: HaFormSchema[]; @property() public schema!: HaFormSchema[];
@@ -47,18 +42,6 @@ export class HaForm extends LitElement implements HaFormElement {
} }
} }
willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (
!selectorImported &&
changedProperties.has("schema") &&
this.schema?.some((item) => "selector" in item)
) {
selectorImported = true;
import("../ha-selector/ha-selector");
}
}
protected render() { protected render() {
return html` return html`
<div class="root"> <div class="root">
@@ -79,17 +62,7 @@ export class HaForm extends LitElement implements HaFormElement {
</ha-alert> </ha-alert>
` `
: ""} : ""}
${"selector" in item ${dynamicElement(`ha-form-${item.type}`, {
? html`<ha-selector
.schema=${item}
.hass=${this.hass}
.selector=${item.selector}
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item)}
.disabled=${this.disabled}
.required=${item.required}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item, schema: item,
data: getValue(this.data, item), data: getValue(this.data, item),
label: this._computeLabel(item), label: this._computeLabel(item),
@@ -131,7 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
return css` return css`
.root { .root {
margin-bottom: -24px; margin-bottom: -24px;
overflow: clip visible; overflow: auto;
} }
.root > * { .root > * {
display: block; display: block;

View File

@@ -1,5 +1,4 @@
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import { Selector } from "../../data/selector";
import type { HaDurationData } from "../ha-duration-input"; import type { HaDurationData } from "../ha-duration-input";
export type HaFormSchema = export type HaFormSchema =
@@ -10,24 +9,14 @@ export type HaFormSchema =
| HaFormBooleanSchema | HaFormBooleanSchema
| HaFormSelectSchema | HaFormSelectSchema
| HaFormMultiSelectSchema | HaFormMultiSelectSchema
| HaFormTimeSchema | HaFormTimeSchema;
| HaFormSelector;
export interface HaFormBaseSchema { export interface HaFormBaseSchema {
name: string; name: string;
// This value is applied if no data is submitted for this field
default?: HaFormData; default?: HaFormData;
required?: boolean; required?: boolean;
description?: { optional?: boolean;
suffix?: string; description?: { suffix?: string; suggested_value?: HaFormData };
// This value will be set initially when form is loaded
suggested_value?: HaFormData;
};
}
export interface HaFormSelector extends HaFormBaseSchema {
type?: never;
selector: Selector;
} }
export interface HaFormConstantSchema extends HaFormBaseSchema { export interface HaFormConstantSchema extends HaFormBaseSchema {

View File

@@ -1,11 +1,11 @@
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; import { Formfield } from "@material/mwc-formfield";
import { styles } from "@material/mwc-formfield/mwc-formfield.css"; import { css, CSSResultGroup } from "lit";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield") @customElement("ha-formfield")
export class HaFormfield extends FormfieldBase { // @ts-expect-error
export class HaFormfield extends Formfield {
protected _labelClick() { protected _labelClick() {
const input = this.input; const input = this.input;
if (input) { if (input) {
@@ -23,8 +23,9 @@ export class HaFormfield extends FormfieldBase {
} }
} }
static override styles = [ protected static get styles(): CSSResultGroup {
styles, return [
Formfield.styles,
css` css`
:host(:not([alignEnd])) ::slotted(ha-switch) { :host(:not([alignEnd])) ::slotted(ha-switch) {
margin-right: 10px; margin-right: 10px;
@@ -36,6 +37,7 @@ export class HaFormfield extends FormfieldBase {
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,10 +1,16 @@
import { css, LitElement, PropertyValues, svg, TemplateResult } from "lit"; import { css, LitElement, PropertyValues, svg, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, 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 { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { afterNextRender } from "../common/util/render-status"; import { afterNextRender } from "../common/util/render-status";
import { FrontendLocaleData } from "../data/translation"; import { FrontendLocaleData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
import { isSafari } from "../util/is_safari";
// Safari version 15.2 and up behaves differently than other Safari versions.
// https://github.com/home-assistant/frontend/issues/10766
const isSafari152 = isSafari && /Version\/15\.[^0-1]/.test(navigator.userAgent);
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); const percentage = getValueInPercentage(normalize(value, min, max), min, max);
@@ -59,12 +65,12 @@ export class Gauge extends LitElement {
protected render() { protected render() {
return svg` return svg`
<svg viewBox="-50 -50 100 50" class="gauge"> <svg viewBox="0 0 100 50" class="gauge">
${ ${
!this.needle || !this.levels !this.needle || !this.levels
? svg`<path ? svg`<path
class="dial" class="dial"
d="M -40 0 A 40 40 0 0 1 40 0" d="M 10 50 A 40 40 0 0 1 90 50"
></path>` ></path>`
: "" : ""
} }
@@ -81,9 +87,9 @@ export class Gauge extends LitElement {
stroke="var(--info-color)" stroke="var(--info-color)"
class="level" class="level"
d="M d="M
${0 - 40 * Math.cos((angle * Math.PI) / 180)} ${50 - 40 * Math.cos((angle * Math.PI) / 180)}
${0 - 40 * Math.sin((angle * Math.PI) / 180)} ${50 - 40 * Math.sin((angle * Math.PI) / 180)}
A 40 40 0 0 1 40 0 A 40 40 0 0 1 90 50
" "
></path>`; ></path>`;
} }
@@ -92,9 +98,9 @@ export class Gauge extends LitElement {
stroke="${level.stroke}" stroke="${level.stroke}"
class="level" class="level"
d="M d="M
${0 - 40 * Math.cos((angle * Math.PI) / 180)} ${50 - 40 * Math.cos((angle * Math.PI) / 180)}
${0 - 40 * Math.sin((angle * Math.PI) / 180)} ${50 - 40 * Math.sin((angle * Math.PI) / 180)}
A 40 40 0 0 1 40 0 A 40 40 0 0 1 90 50
" "
></path>`; ></path>`;
}) })
@@ -104,16 +110,46 @@ export class Gauge extends LitElement {
this.needle this.needle
? svg`<path ? svg`<path
class="needle" class="needle"
d="M -25 -2.5 L -47.5 0 L -25 2.5 z" d="M 25 47.5 L 2.5 50 L 25 52.5 z"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })} style=${ifDefined(
!isSafari
? styleMap({ transform: `rotate(${this._angle}deg)` })
: undefined
)}
transform=${ifDefined(
isSafari
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
: undefined
)}
> >
` `
: svg`<path : svg`<path
class="value" class="value"
d="M -40 0 A 40 40 0 1 0 40 0" d="M 90 50.001 A 40 40 0 0 1 10 50"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })} style=${ifDefined(
!isSafari
? styleMap({ transform: `rotate(${this._angle}deg)` })
: undefined
)}
transform=${ifDefined(
isSafari
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
: undefined
)}
>` >`
} }
${
// Workaround for https://github.com/home-assistant/frontend/issues/6467
isSafari
? svg`<animateTransform
attributeName="transform"
type="rotate"
from="0 50 50"
to="${this._angle} 50 50"
dur="1s"
/>`
: ""
}
</path> </path>
</svg> </svg>
<svg class="text"> <svg class="text">
@@ -151,10 +187,12 @@ export class Gauge extends LitElement {
fill: none; fill: none;
stroke-width: 15; stroke-width: 15;
stroke: var(--gauge-color); stroke: var(--gauge-color);
transform-origin: 50% 100%;
transition: all 1s ease 0s; transition: all 1s ease 0s;
} }
.needle { .needle {
fill: var(--primary-text-color); fill: var(--primary-text-color);
transform-origin: 50% 100%;
transition: all 1s ease 0s; transition: all 1s ease 0s;
} }
.level { .level {

View File

@@ -9,6 +9,7 @@ import {
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
@@ -90,9 +91,18 @@ class HaHLSPlayer extends LitElement {
this._startHls(); this._startHls();
} }
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external || !this.allowExoPlayer) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> { private async _startHls(): Promise<void> {
this._error = undefined; this._error = undefined;
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
@@ -116,8 +126,7 @@ class HaHLSPlayer extends LitElement {
return; return;
} }
const useExoPlayer = const useExoPlayer = await useExoPlayerPromise;
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
const masterPlaylist = await (await masterPlaylistPromise).text(); const masterPlaylist = await (await masterPlaylistPromise).text();
if (!this.isConnected) { if (!this.isConnected) {

View File

@@ -1,13 +1,16 @@
import { mdiCheck, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons"; import { customIcons } from "../data/custom_icons";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button";
type IconItem = { type IconItem = {
icon: string; icon: string;
@@ -16,17 +19,35 @@ type IconItem = {
let iconItems: IconItem[] = []; let iconItems: IconItem[] = [];
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<mwc-list-item const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<style>
graphic="avatar" paper-icon-item {
> padding: 0;
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon> margin: -8px;
${item.icon} }
</mwc-list-item>`; #content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
<ha-icon .icon=${item.icon} slot="item-icon"></ha-icon>
<paper-item-body>${item.icon}</paper-item-body>
</paper-icon-item>`;
@customElement("ha-icon-picker") @customElement("ha-icon-picker")
export class HaIconPicker extends LitElement { export class HaIconPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string; @property() public value?: string;
@property() public label?: string; @property() public label?: string;
@@ -43,40 +64,51 @@ export class HaIconPicker extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@query("ha-combo-box", true) private comboBox!: HaComboBox; @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-combo-box <vaadin-combo-box-light
.hass=${this.hass}
item-value-path="icon" item-value-path="icon"
item-label-path="icon" item-label-path="icon"
.value=${this._value} .value=${this._value}
allow-custom-value allow-custom-value
.filteredItems=${iconItems} .filteredItems=${iconItems}
.label=${this.label} ${comboBoxRenderer(rowRenderer)}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}
.renderer=${rowRenderer}
icon
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
>
<paper-input
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}
> >
${this._value || this.placeholder ${this._value || this.placeholder
? html` ? html`
<ha-icon .icon=${this._value || this.placeholder} slot="icon"> <ha-icon .icon=${this._value || this.placeholder} slot="prefix">
</ha-icon> </ha-icon>
` `
: this.fallbackPath : this.fallbackPath
? html`<ha-svg-icon ? html`<ha-svg-icon
.path=${this.fallbackPath} .path=${this.fallbackPath}
slot="icon" slot="prefix"
></ha-svg-icon>` ></ha-svg-icon>`
: ""} : ""}
</ha-combo-box> <ha-icon-button
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
@@ -118,7 +150,6 @@ export class HaIconPicker extends LitElement {
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
this._setValue(ev.detail.value); this._setValue(ev.detail.value);
} }
@@ -127,7 +158,7 @@ export class HaIconPicker extends LitElement {
fireEvent( fireEvent(
this, this,
"value-changed", "value-changed",
{ value: this._value }, { value },
{ {
bubbles: false, bubbles: false,
composed: false, composed: false,
@@ -174,13 +205,17 @@ export class HaIconPicker extends LitElement {
return css` return css`
ha-icon, ha-icon,
ha-svg-icon { ha-svg-icon {
color: var(--primary-text-color);
position: relative; position: relative;
bottom: 2px; bottom: 2px;
} }
*[slot="prefix"] { *[slot="prefix"] {
margin-right: 8px; margin-right: 8px;
} }
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
`; `;
} }
} }

View File

@@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select"; import "@material/mwc-select/mwc-select";
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { mdiCamera } from "@mdi/js"; import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -10,8 +11,7 @@ import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert"; import "./ha-alert";
import "./ha-button-menu"; import "./ha-button-menu";
import "./ha-textfield"; import "@material/mwc-button/mwc-button";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-qr-scanner") @customElement("ha-qr-scanner")
class HaQrScanner extends LitElement { class HaQrScanner extends LitElement {
@@ -29,7 +29,7 @@ class HaQrScanner extends LitElement {
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("ha-textfield") private _manualInput?: HaTextField; @query("mwc-textfield") private _manualInput?: TextField;
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
@@ -102,11 +102,11 @@ class HaQrScanner extends LitElement {
</ha-alert> </ha-alert>
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p> <p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row"> <div class="row">
<ha-textfield <mwc-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")} .label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@keyup=${this._manualKeyup} @keyup=${this._manualKeyup}
@paste=${this._manualPaste} @paste=${this._manualPaste}
></ha-textfield> ></mwc-textfield>
<mwc-button @click=${this._manualSubmit} <mwc-button @click=${this._manualSubmit}
>${this.localize("ui.common.submit")}</mwc-button >${this.localize("ui.common.submit")}</mwc-button
> >
@@ -161,7 +161,7 @@ class HaQrScanner extends LitElement {
private _manualKeyup(ev: KeyboardEvent) { private _manualKeyup(ev: KeyboardEvent) {
if (ev.key === "Enter") { if (ev.key === "Enter") {
this._qrCodeScanned((ev.target as HaTextField).value); this._qrCodeScanned((ev.target as TextField).value);
} }
} }
@@ -199,7 +199,7 @@ class HaQrScanner extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
ha-textfield { mwc-textfield {
flex: 1; flex: 1;
margin-right: 8px; margin-right: 8px;
} }

View File

@@ -1,18 +1,12 @@
import { RadioBase } from "@material/mwc-radio/mwc-radio-base"; import { Radio } from "@material/mwc-radio";
import { styles } from "@material/mwc-radio/mwc-radio.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-radio") @customElement("ha-radio")
export class HaRadio extends RadioBase { export class HaRadio extends Radio {
static override styles = [ public firstUpdated() {
styles, super.firstUpdated();
css` this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
:host {
--mdc-theme-secondary: var(--primary-color);
} }
`,
];
} }
declare global { declare global {

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { AddonSelector } from "../../data/selector"; import { AddonSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -22,12 +22,6 @@ export class HaAddonSelector extends LitElement {
allow-custom-entity allow-custom-entity
></ha-addon-picker>`; ></ha-addon-picker>`;
} }
static styles = css`
ha-addon-picker {
width: 100%;
}
`;
} }
declare global { declare global {

View File

@@ -1,38 +0,0 @@
import "../entity/ha-entity-attribute-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: AttributeSelector;
@property() public value?: any;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this.selector.attribute.entity_id}
.value=${this.value}
.label=${this.label}
.disabled=${this.disabled}
allow-custom-value
></ha-entity-attribute-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-attribute": HaSelectorAttribute;
}
}

View File

@@ -1,37 +0,0 @@
import "../ha-duration-input";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { DurationSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DurationSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-duration-input
.label=${this.label}
.data=${this.value}
.disabled=${this.disabled}
.required=${this.required}
></ha-duration-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-duration": HaTimeDuration;
}
}

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@@ -5,7 +6,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { NumberSelector } from "../../data/selector"; import { NumberSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-slider"; import "../ha-slider";
import "../ha-textfield";
@customElement("ha-selector-number") @customElement("ha-selector-number")
export class HaNumberSelector extends LitElement { export class HaNumberSelector extends LitElement {
@@ -36,36 +36,39 @@ export class HaNumberSelector extends LitElement {
> >
</ha-slider>` </ha-slider>`
: ""} : ""}
<ha-textfield <paper-input
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?" pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label} .label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
.noLabelFloat=${this.selector.number.mode !== "box"}
class=${classMap({ single: this.selector.number.mode === "box" })} class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min} .min=${this.selector.number.min}
.max=${this.selector.number.max} .max=${this.selector.number.max}
.value=${this.value} .value=${this.value}
.step=${this.selector.number.step ?? 1} .step=${this.selector.number.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.suffix=${this.selector.number.unit_of_measurement}
type="number" type="number"
autoValidate auto-validate
?no-spinner=${this.selector.number.mode !== "box"} @value-changed=${this._handleInputChange}
@input=${this._handleInputChange}
> >
</ha-textfield>`; ${this.selector.number.unit_of_measurement
? html`<div slot="suffix">
${this.selector.number.unit_of_measurement}
</div>`
: ""}
</paper-input>`;
} }
private get _value() { private get _value() {
return this.value ?? 0; return this.value || 0;
} }
private _handleInputChange(ev) { private _handleInputChange(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = const value =
ev.target.value === "" || isNaN(ev.target.value) ev.detail.value === "" || isNaN(ev.detail.value)
? undefined ? undefined
: Number(ev.target.value); : Number(ev.detail.value);
if (this.value === value) { if (this.value === value) {
return; return;
} }
@@ -91,11 +94,7 @@ export class HaNumberSelector extends LitElement {
ha-slider { ha-slider {
flex: 1; flex: 1;
} }
ha-textfield {
--ha-textfield-input-width: 40px;
}
.single { .single {
--ha-textfield-input-width: unset;
flex: 1; flex: 1;
} }
`; `;

View File

@@ -18,7 +18,6 @@ export class HaObjectSelector extends LitElement {
protected render() { protected render() {
return html`<ha-yaml-editor return html`<ha-yaml-editor
.hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
.defaultValue=${this.value} .defaultValue=${this.value}

View File

@@ -1,11 +1,9 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { SelectSelector } from "../../data/selector"; import { SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "@material/mwc-select/mwc-select"; import "../ha-paper-dropdown-menu";
import "@material/mwc-list/mwc-list-item";
@customElement("ha-selector-select") @customElement("ha-selector-select")
export class HaSelectSelector extends LitElement { export class HaSelectSelector extends LitElement {
@@ -20,37 +18,46 @@ export class HaSelectSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html`<mwc-select return html`<ha-paper-dropdown-menu
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
@closed=${stopPropagation} .label=${this.label}
@selected=${this._valueChanged} >
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.value}
@selected-item-changed=${this._valueChanged}
> >
${this.selector.select.options.map( ${this.selector.select.options.map(
(item: string) => html` (item: string) => html`
<mwc-list-item .value=${item}>${item}</mwc-list-item> <paper-item .itemValue=${item}> ${item} </paper-item>
` `
)} )}
</mwc-select>`; </paper-listbox>
</ha-paper-dropdown-menu>`;
} }
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); if (this.disabled || !ev.detail.value) {
if (this.disabled || !ev.target.value) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: ev.target.value, value: ev.detail.value.itemValue,
}); });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
mwc-select { ha-paper-dropdown-menu {
width: 100%; width: 100%;
min-width: 200px;
display: block;
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
} }
`; `;
} }

View File

@@ -1,3 +1,8 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import "@polymer/paper-input/paper-input";
import { import {
HassEntity, HassEntity,
HassServiceTarget, HassServiceTarget,

View File

@@ -1,12 +1,10 @@
import { mdiEye, mdiEyeOff } from "@mdi/js"; import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import "@polymer/paper-input/paper-textarea";
import { customElement, property, state } from "lit/decorators"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { StringSelector } from "../../data/selector"; import { StringSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-textarea";
import "../ha-textfield";
@customElement("ha-selector-text") @customElement("ha-selector-text")
export class HaTextSelector extends LitElement { export class HaTextSelector extends LitElement {
@@ -22,50 +20,27 @@ export class HaTextSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private _unmaskedPassword = false;
protected render() { protected render() {
if (this.selector.text?.multiline) { if (this.selector.text?.multiline) {
return html`<ha-textarea return html`<paper-textarea
.label=${this.label} .label=${this.label}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
.value=${this.value || ""} .value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
@input=${this._handleChange} @value-changed=${this._handleChange}
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
.required=${this.required} ></paper-textarea>`;
autogrow
></ha-textarea>`;
} }
return html`<ha-textfield return html`<paper-input
.value=${this.value || ""} required
.placeholder=${this.placeholder || ""} .value=${this.value}
.placeholder=${this.placeholder}
.disabled=${this.disabled} .disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type} @value-changed=${this._handleChange}
@input=${this._handleChange} .label=${this.label}
.label=${this.label || ""} ></paper-input>`;
.suffix=${this.selector.text?.type === "password"
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.selector.text?.suffix}
.required=${this.required}
></ha-textfield>
${this.selector.text?.type === "password"
? html`<ha-icon-button
toggles
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`
: ""}`;
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
} }
private _handleChange(ev) { private _handleChange(ev) {
@@ -75,27 +50,6 @@ export class HaTextSelector extends LitElement {
} }
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
position: relative;
}
ha-textarea,
ha-textfield {
width: 100%;
}
ha-icon-button {
position: absolute;
top: 16px;
right: 16px;
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
}
`;
}
} }
declare global { declare global {

View File

@@ -22,6 +22,7 @@ export class HaTimeSelector extends LitElement {
.value=${this.value} .value=${this.value}
.locale=${this.hass.locale} .locale=${this.hass.locale}
.disabled=${this.disabled} .disabled=${this.disabled}
hide-label
enable-second enable-second
></ha-time-input> ></ha-time-input>
`; `;

View File

@@ -6,10 +6,8 @@ import { HomeAssistant } from "../../types";
import "./ha-selector-action"; import "./ha-selector-action";
import "./ha-selector-addon"; import "./ha-selector-addon";
import "./ha-selector-area"; import "./ha-selector-area";
import "./ha-selector-attribute";
import "./ha-selector-boolean"; import "./ha-selector-boolean";
import "./ha-selector-device"; import "./ha-selector-device";
import "./ha-selector-duration";
import "./ha-selector-entity"; import "./ha-selector-entity";
import "./ha-selector-number"; import "./ha-selector-number";
import "./ha-selector-object"; import "./ha-selector-object";
@@ -32,10 +30,12 @@ export class HaSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
public focus() { public focus() {
this.shadowRoot?.getElementById("selector")?.focus(); const input = this.shadowRoot!.getElementById("selector");
if (!input) {
return;
}
(input as HTMLElement).focus();
} }
private get _type() { private get _type() {
@@ -51,7 +51,6 @@ export class HaSelector extends LitElement {
label: this.label, label: this.label,
placeholder: this.placeholder, placeholder: this.placeholder,
disabled: this.disabled, disabled: this.disabled,
required: this.required,
id: "selector", id: "selector",
})} })}
`; `;

View File

@@ -17,7 +17,6 @@ import {
import { Selector } from "../data/selector"; import { Selector } from "../data/selector";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox"; import "./ha-checkbox";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-selector/ha-selector"; import "./ha-selector/ha-selector";
@@ -67,7 +66,7 @@ export class HaServiceControl extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected willUpdate(changedProperties: PropertyValues<this>) { protected updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("value")) { if (!changedProperties.has("value")) {
return; return;
} }
@@ -131,35 +130,6 @@ export class HaServiceControl extends LitElement {
this._value = this.value; this._value = this.value;
} }
if (oldValue?.service !== this.value?.service) {
let updatedDefaultValue = false;
if (this._value && serviceData) {
// Set mandatory bools without a default value to false
if (!this._value.data) {
this._value.data = {};
}
serviceData.fields.forEach((field) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
this._value!.data![field.key] === undefined
) {
updatedDefaultValue = true;
this._value!.data![field.key] = false;
}
});
}
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this._value,
},
});
}
}
if (this._value?.data) { if (this._value?.data) {
const yamlEditor = this._yamlEditor; const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this._value.data) { if (yamlEditor && yamlEditor.value !== this._value.data) {
@@ -233,12 +203,7 @@ export class HaServiceControl extends LitElement {
<p>${serviceData?.description}</p> <p>${serviceData?.description}</p>
${this._manifest ${this._manifest
? html` <a ? html` <a
href=${this._manifest.is_built_in href=${this._manifest.documentation}
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize( title=${this.hass.localize(
"ui.components.service-control.integration_doc" "ui.components.service-control.integration_doc"
)} )}
@@ -286,7 +251,6 @@ export class HaServiceControl extends LitElement {
: ""} : ""}
${shouldRenderServiceDataYaml ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.service-control.service_data" "ui.components.service-control.service_data"
)} )}

View File

@@ -1,3 +1,4 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
@@ -10,12 +11,39 @@ import "./ha-combo-box";
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = ( const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
item item
) => html`<mwc-list-item twoline> // eslint-disable-next-line lit/prefer-static-styles
<span>${item.name}</span> ) => html`<style>
<span slot="secondary" paper-item {
>${item.name === item.service ? "" : item.service}</span padding: 0;
> margin: -10px;
</mwc-list-item>`; margin-left: 0px;
}
#content {
display: flex;
align-items: center;
}
:host([selected]) paper-item {
margin-left: 10px;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}
<span secondary>${item.name === item.service ? "" : item.service}</span>
</paper-item-body>
</paper-item>`;
class HaServicePicker extends LitElement { class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -8,7 +8,6 @@ import {
mdiClose, mdiClose,
mdiCog, mdiCog,
mdiFormatListBulletedType, mdiFormatListBulletedType,
mdiHammer,
mdiLightningBolt, mdiLightningBolt,
mdiMenu, mdiMenu,
mdiMenuOpen, mdiMenuOpen,
@@ -44,6 +43,10 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -53,7 +56,7 @@ import "./ha-menu-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools"]; const SHOW_AFTER_SPACER = ["config"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body; const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
@@ -62,14 +65,12 @@ const SORT_VALUE_URL_PATHS = {
map: 2, map: 2,
logbook: 3, logbook: 3,
history: 4, history: 4,
"developer-tools": 9,
config: 11, config: 11,
}; };
const PANEL_ICONS = { const PANEL_ICONS = {
calendar: mdiCalendar, calendar: mdiCalendar,
config: mdiCog, config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt, energy: mdiLightningBolt,
history: mdiChartBox, history: mdiChartBox,
logbook: mdiFormatListBulletedType, logbook: mdiFormatListBulletedType,
@@ -188,6 +189,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
@@ -264,6 +267,13 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
@@ -546,7 +556,8 @@ class HaSidebar extends LitElement {
private _renderExternalConfiguration() { private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin && return html`${!this.hass.user?.is_admin &&
this.hass.auth.external?.config.hasSettingsScreen this._externalConfig &&
this._externalConfig.hasSettingsScreen
? html` ? html`
<a <a
role="option" role="option"
@@ -1019,19 +1030,6 @@ class HaSidebar extends LitElement {
white-space: nowrap; white-space: nowrap;
} }
.dev-tools {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 8px;
width: 256px;
box-sizing: border-box;
}
.dev-tools a {
color: var(--sidebar-icon-color);
}
.tooltip { .tooltip {
display: none; display: none;
position: absolute; position: absolute;

View File

@@ -3,7 +3,7 @@ import "@polymer/paper-slider";
const PaperSliderClass = customElements.get("paper-slider"); const PaperSliderClass = customElements.get("paper-slider");
let subTemplate; let subTemplate;
export class HaSlider extends PaperSliderClass { class HaSlider extends PaperSliderClass {
static get template() { static get template() {
if (!subTemplate) { if (!subTemplate) {
subTemplate = PaperSliderClass.template.cloneNode(true); subTemplate = PaperSliderClass.template.cloneNode(true);

View File

@@ -12,10 +12,7 @@ export class HaSvgIcon extends LitElement {
<svg <svg
viewBox=${this.viewBox || "0 0 24 24"} viewBox=${this.viewBox || "0 0 24 24"}
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
focusable="false" focusable="false">
role="img"
aria-hidden="true"
>
<g> <g>
${this.path ? svg`<path d=${this.path}></path>` : ""} ${this.path ? svg`<path d=${this.path}></path>` : ""}
</g> </g>

View File

@@ -1,11 +1,11 @@
import { SwitchBase } from "@material/mwc-switch/deprecated/mwc-switch-base"; import { Switch } from "@material/mwc-switch/deprecated";
import { styles } from "@material/mwc-switch/deprecated/mwc-switch.css"; import { css, CSSResultGroup } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { forwardHaptic } from "../data/haptics"; import { forwardHaptic } from "../data/haptics";
@customElement("ha-switch") @customElement("ha-switch")
export class HaSwitch extends SwitchBase { // @ts-expect-error
export class HaSwitch extends Switch {
// Generate a haptic vibration. // Generate a haptic vibration.
// Only set to true if the new value of the switch is applied right away when toggling. // Only set to true if the new value of the switch is applied right away when toggling.
// Do not add haptic when a user is required to press save. // Do not add haptic when a user is required to press save.
@@ -13,6 +13,10 @@ export class HaSwitch extends SwitchBase {
protected firstUpdated() { protected firstUpdated() {
super.firstUpdated(); super.firstUpdated();
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.addEventListener("change", () => { this.addEventListener("change", () => {
if (this.haptic) { if (this.haptic) {
forwardHaptic("light"); forwardHaptic("light");
@@ -20,12 +24,10 @@ export class HaSwitch extends SwitchBase {
}); });
} }
static override styles = [ static get styles(): CSSResultGroup {
styles, return [
Switch.styles,
css` css`
:host {
--mdc-theme-secondary: var(--switch-checked-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb { .mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color); background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color); border-color: var(--switch-checked-button-color);
@@ -45,6 +47,7 @@ export class HaSwitch extends SwitchBase {
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

Some files were not shown because too many files have changed in this diff Show More