Compare commits

...

107 Commits

Author SHA1 Message Date
Bram Kragten
21e441b682 Save height and width when you start filtering to make timing less fragile 2022-02-11 17:10:56 +01:00
Bram Kragten
737f7ba6b9 Migrate to mwc-item 2022-02-11 14:40:31 +01:00
Bram Kragten
a5862b86ca Fix search in add integration dialog 2022-02-11 14:12:22 +01:00
Zack Barett
099fa706a0 Make HA Form set required to false for selectors (#11649) 2022-02-11 14:05:07 +01:00
Zack Barett
ed84ce9692 HA Trigger to HA Form (#11645)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:12:12 -08:00
Zack Barett
9912d427f2 Geo Location Trigger to HA - Form (#11644)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:12:01 -08:00
Zack Barett
76f574f875 Convert Sun to Ha Form (#11647)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:11:57 -08:00
Zack Barett
ac90bb7088 MQTT Trigger to Ha-Form (#11643)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:11:45 -08:00
Zack Barett
ce9f83e9a2 Time Pattern to HA Form (#11648) 2022-02-10 14:11:29 -08:00
Zack Barett
fca7d2c5b0 Add Attribute Picker as a selector - add to state trigger (#11641)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 11:56:25 -08:00
Paulus Schoutsen
d7a5921e7b Allow uploading media (#11615)
* Allow uploading media

* Update path

* Use current item we already have

* Update src/panels/media-browser/ha-panel-media-browser.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use alert dialog and use button for add media

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-10 09:54:01 -08:00
Zack Barett
cefa2ee183 State Trigger -> HA Form (#11631)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-10 09:26:28 -06:00
Bram Kragten
0eeed85193 Bunch of fixes and cleanup (#11636) 2022-02-10 08:24:47 -06:00
Bram Kragten
fd80408de2 fix data-table row handlers (#11638) 2022-02-10 08:24:00 -06:00
Bram Kragten
467a5169c0 Migrate search bar to mwc (#11637) 2022-02-10 08:23:21 -06:00
Eric Severance
b0b3222b33 Generate random webhook_id and add copy button (#11568)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-10 11:43:45 +01:00
Joakim Sørensen
b053881cef Add missing type to create device automation/script heading (#11635)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-10 09:40:34 +00:00
Joakim Sørensen
92a9ed7080 Create error when trying to backup wile system in freeze (#11634)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-10 10:29:42 +01:00
Paulus Schoutsen
830b449006 Add support for opening camera media source (#11633)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-09 19:28:12 -06:00
Zack Barett
d38a8a317e Merged too fast for Bram :) Code improv (#11632) 2022-02-09 15:45:31 -08:00
Bram Kragten
a0aed9112c Migrate a bunch of paper-dropdowns (#11626) 2022-02-10 00:18:44 +01:00
Bram Kragten
ce3b8544b9 Fix service control for older browsers (#11629) 2022-02-09 17:15:00 -06:00
Bram Kragten
134ed7d303 Only load ha-selector when needed (#11630) 2022-02-09 17:14:25 -06:00
Bram Kragten
dc27871189 Set button role on button card and handle enter and space (#11627) 2022-02-09 16:36:16 -06:00
Bram Kragten
9c9bfa2b77 Update code editor to material 3 look (#11628) 2022-02-09 16:35:49 -06:00
Thomas Lovén
f02dd39619 Add loadCardHelpers to cast scope (#11616) 2022-02-09 23:01:05 +01:00
Patrick ZAJDA
d37d99223d Add aria-label to table headers with no title (#11503) 2022-02-09 18:10:41 +00:00
Steve Repsher
4db943c5ff Set initial focus for device, area, and entity dialogs (#11622) 2022-02-09 19:02:03 +01:00
Bram Kragten
ed001fb10b Convert time inputs to Lit + mwc (#11609) 2022-02-09 18:20:56 +01:00
Bram Kragten
5f43715dd8 Update lit-virtualizer (#11623) 2022-02-09 18:20:25 +01:00
Bram Kragten
5435218187 Make textarea grow on input (#11618) 2022-02-09 16:58:37 +00:00
Bram Kragten
4ef5f3af89 Replace checkboxes in list items with check-list-item (#11610)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-09 07:50:48 -08:00
Zack Barett
9eea17b793 Convert Automation Action Choose to HA Form (#11597)
* Convert Auatomation Action Choose to HA Form

* remove log

* Remove Import
2022-02-07 17:45:16 -06:00
Joakim Sørensen
6a51e2aaad Only show stable add-ons in the store if not advanced mode (#11596) 2022-02-07 23:18:52 +01:00
Bram Kragten
2d33327d88 dark mode fixes (#11595) 2022-02-07 14:33:09 -06:00
Bram Kragten
e9ec2da917 Fix clearing device in device action (#11594) 2022-02-07 11:58:36 -06:00
Bram Kragten
09d46dac61 Convert device automation picker to mwc (#11592)
Co-authored-by: Zack <zackbarett@hey.com>
2022-02-07 10:53:23 -06:00
Bram Kragten
236fa14ec3 Convert area-devices picker (#11588) 2022-02-07 10:52:44 -06:00
Bram Kragten
2cb37820df Convert icon picker to ha-combobox (#11586)
Co-authored-by: Zack <zackbarett@hey.com>
2022-02-07 16:43:49 +00:00
Bram Kragten
869fa91ae5 Convert entity-attribute picker to ha-combo-box (#11587) 2022-02-07 10:22:08 -06:00
Bram Kragten
22df03427f Fix number selector (#11585) 2022-02-07 10:17:32 -06:00
Bram Kragten
e72a4e4a20 Convert HaFormSchemas to use selectors (#11589) 2022-02-07 10:06:04 -06:00
Bram Kragten
ca8d31c6bb Migrate (input) select entities to mwc (#11591) 2022-02-07 10:04:37 -06:00
Bram Kragten
354ea88984 Update links on info page (#11590) 2022-02-07 15:08:54 +01:00
Bram Kragten
76af6e48cd Convert entity picker to ha-combo (#11560)
* Convert entity picker to ha-combo

* Update ha-entity-picker.ts

* Handle empty better

* Clear value when no device/area/entity
2022-02-07 10:59:11 +01:00
Bram Kragten
d05f807b9d Covert area picker to combo-box (#11562) 2022-02-06 14:31:46 -08:00
Bram Kragten
4092f7f75d Convert selectors to MWC (#11543) 2022-02-06 14:29:28 -08:00
Bram Kragten
04668ad809 Remember filter between navigation (#11565) 2022-02-06 14:26:42 -08:00
J. Nick Koston
9be5a15c77 Add integration_discovery to discovery sources (#11564) 2022-02-05 17:44:48 +01:00
Franck Nijhof
21d86f4797 Link via device on device page (#11554)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-02-04 21:16:01 -06:00
Bram Kragten
45e6ec1ee2 New date picker (#11555) 2022-02-04 16:19:36 -08:00
Bram Kragten
9b97faa5e3 Migrate combobox to mwc (#11546) 2022-02-04 15:46:09 -08:00
Bram Kragten
8730c122fd Allow translate gas total (#11547) 2022-02-04 16:22:37 +00:00
Paulus Schoutsen
0046252e32 Add selectors to ha-form (#11534) 2022-02-04 12:47:21 +01:00
Kuba Wolanin
f47440083e Add entity id autocompletion to YAML code editors (#11099) 2022-02-04 11:02:09 +01:00
Yosi Levy
bfaf44f9d1 Merge pull request #11475 from yosilevy/config-info-rtl-fix
Fix config card rtl issues
2022-02-04 05:22:29 +02:00
Paulus Schoutsen
deba6a0db4 Remove optional field from ha-form schema type (#11538) 2022-02-03 16:30:37 -08:00
Bram Kragten
890ad9a1c8 Bumped version to 20220203.0 2022-02-03 20:27:33 +01:00
Bram Kragten
8466ef371a Add name of integration to diagnostics when more than 1 (#11523) 2022-02-03 13:18:48 -06:00
Bram Kragten
4e55460799 Clear old src when disconnected so we can't fetch it with the wrong t… (#11528) 2022-02-03 07:54:26 -08:00
Bram Kragten
5fde6e659d Revert "Mobile click accessibility" (#11526) 2022-02-03 16:33:24 +01:00
Bram Kragten
148bb99d89 Use css to hide hint in quickbar (#11527) 2022-02-03 09:28:59 -06:00
Bram Kragten
0540bae707 Fix dialog heading aria label (#11524)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-02-03 14:54:37 +00:00
Bram Kragten
0c6f647f53 Handle unknown toggle state (#11522) 2022-02-03 15:43:41 +01:00
Kuba Wolanin
3aca67d511 Add filtering to system log card and error log card (#11166)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-03 11:15:27 +01:00
fpro1212
0e41a408e7 Design home - Fix GitHub Links (#11519) 2022-02-03 10:03:26 +01:00
Paulus Schoutsen
19e1eaf2d7 Guard load diagnostics (#11518) 2022-02-03 09:59:04 +01:00
Joakim Sørensen
5e80a2b465 Make sure we load data in update card (#11516) 2022-02-03 09:56:38 +01:00
Marc Mueller
866a57cde4 Only upload wheels to PyPI (#11514) 2022-02-02 09:57:25 -08:00
Bram Kragten
a88da0e39a Merge branch 'master' into dev 2022-02-02 14:30:14 +01:00
Bram Kragten
21a8fac477 Bumped version to 20220202.0 2022-02-02 14:28:17 +01:00
Zack Barett
ca5ce04a38 Scene to have history (#11510) 2022-02-01 16:42:21 -06:00
Bram Kragten
7c4b9a0410 20220201.0 (#11508)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Thomas Lovén <thomasloven@gmail.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2022-02-01 11:18:14 -06:00
Bram Kragten
de6f06ea6d Bumped version to 20220201.0 2022-02-01 18:06:05 +01:00
Bram Kragten
bbc8e323e8 Add start point to device energy graph (#11507) 2022-02-01 18:02:43 +01:00
Bram Kragten
89b6863ae3 Add alert to OZW and legacy Z-Wave panels (#11506) 2022-02-01 15:21:51 +00:00
Bram Kragten
3f1850e9eb unset error when navigating away in media browser (#11505) 2022-02-01 09:16:24 -06:00
Bram Kragten
54d6b5b6f3 Handle config flow errors better (#11499)
* Handle config flow errors better

* Use body for error message

* Update dialog-data-entry-flow.ts
2022-02-01 11:22:14 +01:00
Steve Repsher
fb55ab197f Mobile click accessibility (#11447) 2022-02-01 00:07:49 +01:00
Zack Barett
cc2db9a761 Place System Dashboards at the top with a colored icon in the Dashboard Configuration (#11500) 2022-02-01 00:06:29 +01:00
Paulus Schoutsen
58ba3e5c22 Some fixes for media panel (#11485) 2022-01-31 12:17:06 -06:00
Bram Kragten
182ffccd0c Remove interpolation from history graph (#11498) 2022-01-31 18:07:00 +01:00
Patrick ZAJDA
ce99d14ee0 Add missing em dash for non-disabled entities and devices (#11493) 2022-01-31 17:01:24 +01:00
Yosi Levy
8ce160b9ce Energy setup wizard missing localization entries (#11469) 2022-01-31 10:32:31 +01:00
Paulus Schoutsen
fe33714c8b Bump Lit (#11481) 2022-01-31 10:29:13 +01:00
J. Nick Koston
afbe85625c Add gate to the list of device classes to pick for overrides (#11487) 2022-01-30 21:37:13 -06:00
Yosi Levy
cb47ee7721 Energy setup wizard missing localization entries (#11469) 2022-01-29 11:57:04 -06:00
Zack Barett
5caa256f1b Fix Safari Battery Percent on device page (#11480) 2022-01-29 09:47:22 +01:00
Zack Barett
c66dfb84f9 Fix Quick bar having false text (#11474) 2022-01-29 09:47:06 +01:00
Philip Allgaier
df1d703e4e Adjust device translations to handle device vs service consistently (#11472) 2022-01-29 09:46:32 +01:00
Marc Mueller
ce0ced0b6a Move to setup.cfg and config for build-system (#11484) 2022-01-28 21:18:17 -08:00
Zack Barett
730e9b144d When refreshing updates, notify user when finished (#11464)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-28 13:03:45 -06:00
Thomas Lovén
69ff8dd0c4 Use ha-slider in ha-form-integer (#11471) 2022-01-28 10:17:29 -06:00
Yosi Levy
8d2c716fbe Fix config card rtl issues 2022-01-28 17:45:12 +02:00
Paulus Schoutsen
389a100b46 Fix storing Google pin (#11470) 2022-01-28 09:55:34 +01:00
Paulus Schoutsen
9fee7a2829 Merge pull request #11462 from home-assistant/dev 2022-01-27 10:42:12 -08:00
Paulus Schoutsen
a91897821a Bumped version to 20220127.0 2022-01-27 10:22:19 -08:00
Bram Kragten
815a2a07ff Fix mobile styling quickbar (#11459) 2022-01-27 09:58:41 -08:00
Bram Kragten
b8d3eb76ac Set frontendVersion sooner (#11460) 2022-01-27 09:57:15 -08:00
Bram Kragten
ba75c2e7af Little cleanup (#11461) 2022-01-27 09:56:50 -08:00
Bram Kragten
f04b844223 Check if energy integration is enabled (#11458) 2022-01-27 15:27:40 +00:00
Joakim Sørensen
242bad0a29 Use documentationUrl instead of manifest for core integrations (#11450) 2022-01-27 15:49:55 +01:00
Franck Nijhof
8b20b2b63c Adjust values in Energy Dashboard row (#11452) 2022-01-27 15:39:20 +01:00
Philip Allgaier
e0c8efc5e6 Fix Lovelace view edit mode "Done" button styling (#11449) 2022-01-27 09:44:05 +01:00
Paulus Schoutsen
f59c30ac04 Fix discovery name (#11445) 2022-01-26 21:14:28 +01:00
Bram Kragten
e4b9c08b45 Add padding between control buttons and progress bar 2022-01-26 20:32:50 +01:00
Bram Kragten
04e63eefe2 media_class of integrations should be app (#11444) 2022-01-26 20:02:56 +01:00
234 changed files with 4857 additions and 5631 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,5 +1,4 @@
include README.md
include LICENSE.md
graft hass_frontend
graft hass_frontend_es5
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
module.exports.emptyPackages = ({ latestBuild }) =>
module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
[
// Contains all color definitions for all material color sets.
// We don't use it
@@ -28,6 +28,11 @@ module.exports.emptyPackages = ({ latestBuild }) =>
),
// This polyfill is loaded in workers to support ES5, filter it out.
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);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
@@ -196,6 +201,7 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
isHassioBuild: true,
defineOverlay: {
__SUPERVISOR__: true,
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,17 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form";
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: {
title: string;
@@ -14,6 +20,49 @@ const SCHEMAS: {
schema: HaFormSchema[];
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",
translations: {
@@ -50,13 +99,11 @@ const SCHEMAS: {
{
type: "boolean",
name: "bool",
optional: true,
default: false,
},
{
type: "integer",
name: "int",
optional: true,
default: 10,
},
{
@@ -67,7 +114,6 @@ const SCHEMAS: {
{
type: "string",
name: "string",
optional: true,
default: "Default",
},
{
@@ -77,7 +123,6 @@ const SCHEMAS: {
["other", "other"],
],
name: "select",
optional: true,
default: "default",
},
{
@@ -87,7 +132,6 @@ const SCHEMAS: {
other: "Other",
},
name: "multi",
optional: true,
default: ["default"],
},
{
@@ -108,7 +152,6 @@ const SCHEMAS: {
{
type: "integer",
name: "int with default",
optional: true,
default: 10,
},
{
@@ -122,7 +165,6 @@ const SCHEMAS: {
{
type: "integer",
name: "int range optional",
optional: true,
valueMin: 0,
valueMax: 10,
},
@@ -148,7 +190,6 @@ const SCHEMAS: {
["other", "Other"],
],
name: "select optional",
optional: true,
},
{
type: "select",
@@ -161,7 +202,6 @@ const SCHEMAS: {
["option", "1000"],
],
name: "select many otions",
optional: true,
default: "default",
},
],
@@ -190,7 +230,6 @@ const SCHEMAS: {
option: "1000",
},
name: "multi many otions",
optional: true,
default: ["default"],
},
],
@@ -239,23 +278,35 @@ const SCHEMAS: {
valueMin: 1,
valueMax: 65535,
name: "port",
optional: true,
default: 80,
},
{ type: "string", name: "path", optional: true, default: "/" },
{ type: "boolean", name: "ssl", optional: true, default: false },
{ type: "string", name: "path", default: "/" },
{ type: "boolean", name: "ssl", default: false },
],
},
];
@customElement("demo-components-ha-form")
class DemoHaForm extends LitElement {
@state() private hass!: HomeAssistant;
private data = SCHEMAS.map(
({ schema, data }) => data || computeInitialHaFormData(schema)
);
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 {
return html`
${SCHEMAS.map((info, idx) => {
@@ -278,6 +329,7 @@ class DemoHaForm extends LitElement {
(slot) => html`
<ha-form
slot=${slot}
.hass=${this.hass}
.data=${this.data[idx]}
.schema=${info.schema}
.error=${info.error}

View File

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

View File

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

View File

@@ -47,7 +47,9 @@ export class DialogHassioBackupUpload
scrimClickAction
escapeKeyAction
hideActions
.heading=${true}
.heading=${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
@closed=${this.closeDialog}
>
<div slot="heading">

View File

@@ -71,7 +71,7 @@ class HassioBackupDialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${true}
.heading=${this._backup.name}
>
<div slot="heading">
<ha-header-bar>

View File

@@ -65,7 +65,9 @@ class HassioHardwareDialog extends LitElement {
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
.heading=${this._dialogParams.supervisor.localize(
"dialog.hardware.title"
)}
>
<div class="header" slot="heading">
<h2>

View File

@@ -94,7 +94,7 @@ export class DialogHassioNetwork
open
scrimClickAction
escapeKeyAction
.heading=${true}
.heading=${this.supervisor.localize("dialog.network.title")}
hideActions
@closed=${this.closeDialog}
>

View File

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

View File

@@ -205,16 +205,6 @@ class HassioCoreInfo extends LitElement {
color: var(--secondary-text-color);
--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 {
color: var(--secondary-text-color);
}

View File

@@ -440,16 +440,6 @@ class HassioHostInfo extends LitElement {
color: var(--secondary-text-color);
--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 {
color: var(--secondary-text-color);
}

View File

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

View File

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

3
pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[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
rm -rf dist
python3 setup.py -q sdist
python3 -m twine upload dist/* --skip-existing
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing

View File

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

21
setup.cfg Normal file
View File

@@ -0,0 +1,21 @@
[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,14 +1,7 @@
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(
name="home-assistant-frontend",
version="20220126.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,
)
setup()

View File

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

View File

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

View File

@@ -120,6 +120,7 @@ export const computeStateDisplay = (
if (
domain === "button" ||
domain === "input_button" ||
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,7 @@
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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -50,36 +37,12 @@ interface AreaDevices {
devices: string[];
}
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (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>
<paper-item-body two-line="">
<div class="name">${item.name}</div>
<div secondary>${item.devices.length} devices</div>
</paper-item-body>
</paper-item>`;
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (
item
) => html`<mwc-list-item twoline>
<span>${item.name}</span>
<span slot="secondary">${item.devices.length} devices</span>
</mwc-list-item>`;
@customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@@ -117,9 +80,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
@state() private _areaPicker = true;
@state() private _devices?: DeviceRegistryEntry[];
@@ -302,71 +262,30 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
`;
}
return html`
<vaadin-combo-box-light
<ha-combo-box
.hass=${this.hass}
item-value-path="id"
item-id-path="id"
item-label-path="name"
.items=${areas}
.value=${this._value}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
.renderer=${rowRenderer}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`}
@value-changed=${this._areaPicked}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`}
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() {
return this.value || [];
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private async _switchPicker() {
this._areaPicker = !this._areaPicker;
}
@@ -398,22 +317,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
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;
}
`;
}
}
declare global {

View File

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

View File

@@ -1,18 +1,9 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@material/mwc-list/mwc-list-item";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { mdiCheck } from "@mdi/js";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
@@ -46,36 +37,12 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<Device> = (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>
<paper-item-body two-line>
${item.name}
<span secondary>${item.area}</span>
</paper-item-body>
</paper-item>`;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
.twoline=${!!item.area}
>
<span>${item.name}</span>
<span slot="secondary">${item.area}</span>
</mwc-list-item>`;
@customElement("ha-device-picker")
export class HaDevicePicker extends SubscribeMixin(LitElement) {
@@ -138,7 +105,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
if (!devices.length) {
return [
{
id: "",
id: "no_devices",
area: "",
name: this.hass.localize("ui.components.device-picker.no_devices"),
},
@@ -234,7 +201,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
if (!outputDevices.length) {
return [
{
id: "",
id: "no_devices",
area: "",
name: this.hass.localize("ui.components.device-picker.no_match"),
},
@@ -303,7 +270,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id"
item-id-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
@@ -317,7 +283,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
private _deviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
let newValue = ev.detail.value;
if (newValue === "no_devices") {
newValue = "";
}
if (newValue !== this._value) {
this._setValue(newValue);
@@ -335,19 +305,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change");
}, 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 {

View File

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

View File

@@ -1,54 +1,14 @@
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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatAttributeName } from "../../data/entity_attributes";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
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")
class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -68,7 +28,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
@@ -78,7 +38,10 @@ class HaEntityAttributePicker extends LitElement {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
? Object.keys(state.attributes).map((key) => ({
value: key,
label: formatAttributeName(key),
}))
: [];
}
}
@@ -89,100 +52,31 @@ class HaEntityAttributePicker extends LitElement {
}
return html`
<vaadin-combo-box-light
.value=${this._value}
<ha-combo-box
.hass=${this.hass}
.value=${this.value || ""}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.disabled=${this.disabled || !this.entityId}
.allowCustomValue=${this.allowCustomValue}
attr-for-value="bind-value"
${comboBoxRenderer(rowRenderer)}
item-value-path="value"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value ? formatAttributeName(this._value) : ""}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<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>
</ha-combo-box>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
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;
}
`;
this.value = ev.detail.value;
}
}

View File

@@ -1,25 +1,16 @@
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 "@material/mwc-list/mwc-list-item";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
@@ -27,35 +18,15 @@ import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (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;
}
</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>`;
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
(item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -107,19 +78,19 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public hideClearIcon = false;
@property({ type: Boolean }) private _opened = false;
@state() private _opened = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
this.comboBox?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
this.comboBox?.focus();
});
}
@@ -144,6 +115,27 @@ export class HaEntityPicker extends LitElement {
}
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) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
@@ -156,7 +148,10 @@ export class HaEntityPicker extends LitElement {
);
}
states = entityIds.sort().map((key) => hass!.states[key]);
states = entityIds.sort().map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}));
if (includeDeviceClasses) {
states = states.filter(
@@ -196,6 +191,9 @@ export class HaEntityPicker extends LitElement {
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
@@ -241,64 +239,25 @@ export class HaEntityPicker extends LitElement {
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
<ha-combo-box
item-value-path="entity_id"
item-label-path="entity_id"
item-label-path="friendly_name"
.hass=${this.hass}
.value=${this._value}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
${comboBoxRenderer(rowRenderer)}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
<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>
</ha-combo-box>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
@@ -308,6 +267,7 @@ export class HaEntityPicker extends LitElement {
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
@@ -317,9 +277,9 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)
(entityState) =>
entityState.entity_id.toLowerCase().includes(filterString) ||
computeStateName(entityState).toLowerCase().includes(filterString)
);
}
@@ -330,22 +290,6 @@ export class HaEntityPicker extends LitElement {
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;
}
`;
}
}
declare global {

View File

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

View File

@@ -1,17 +1,8 @@
import { mdiCheck } 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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -76,54 +67,24 @@ export class HaStatisticPicker extends LitElement {
id: string;
name: string;
state?: HassEntity;
// 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
? html`<state-badge
slot="item-icon"
.stateObj=${item.state}
></state-badge>`
: ""}
<paper-item-body two-line="">
${item.name}
<span secondary
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/more-info/statistics/")}
>${this.hass.localize(
"ui.components.statistic-picker.learn_more"
)}</a
>`
: item.id}</span
>
</paper-item-body>
</paper-icon-item>`;
}> = (item) => html`<mwc-list-item graphic="avatar" twoline>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item.state}></state-badge>`
: ""}
<span>${item.name}</span>
<span slot="secondary"
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/more-info/statistics/")}
>${this.hass.localize(
"ui.components.statistic-picker.learn_more"
)}</a
>`
: item.id}</span
>
</mwc-list-item>`;
private _getStatistics = memoizeOne(
(
@@ -293,19 +254,6 @@ export class HaStatisticPicker extends LitElement {
fireEvent(this, "change");
}, 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 {

View File

@@ -1,4 +1,4 @@
import { html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types";
@@ -103,6 +103,20 @@ class HaStatisticsPicker extends LitElement {
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 {

View File

@@ -1,4 +1,3 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
@@ -12,39 +11,12 @@ import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
paper-item {
padding: 0;
margin: -10px;
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>`;
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
item
) => html`<mwc-list-item twoline>
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
</mwc-list-item>`;
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {

View File

@@ -1,19 +1,6 @@
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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
@@ -41,38 +28,18 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
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-svg-icon";
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item
// eslint-disable-next-line lit/prefer-static-styles
) => 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 class=${classMap({ "add-new": item.area_id === "add_new" })}>
<paper-item-body two-line>${item.name}</paper-item-body>
</paper-item>`;
) => html`<mwc-list-item
class=${classMap({ "add-new": item.area_id === "add_new" })}
>
${item.name}
</mwc-list-item>`;
@customElement("ha-area-picker")
export class HaAreaPicker extends SubscribeMixin(LitElement) {
@@ -125,7 +92,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@state() private _opened?: boolean;
@query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _filter?: string;
private _init = false;
@@ -145,13 +114,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
this.comboBox?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
this.comboBox?.focus();
});
}
@@ -170,7 +139,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
if (!areas.length) {
return [
{
area_id: "",
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null,
},
@@ -294,7 +263,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
if (!outputAreas.length) {
outputAreas = [
{
area_id: "",
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null,
},
@@ -339,52 +308,25 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
return html``;
}
return html`
<vaadin-combo-box-light
<ha-combo-box
.hass=${this.hass}
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"
.value=${this.value}
.disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${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>
</ha-combo-box>
`;
}
@@ -392,9 +334,29 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._areas?.find((area) => area.area_id === areaId)
);
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
private _filterChanged(ev: CustomEvent): void {
this._filter = ev.detail.value;
if (!this._filter) {
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() {
@@ -406,9 +368,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
}
private _areaChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue !== "add_new") {
if (newValue === "no_areas") {
newValue = "";
}
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
if (newValue !== this._value) {
this._setValue(newValue);
}
@@ -425,6 +392,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
inputLabel: this.hass.localize(
"ui.components.area-picker.add_dialog.name"
),
defaultValue:
newValue === "add_new_suggestion" ? this._filter : undefined,
confirm: async (name) => {
if (!name) {
return;
@@ -445,6 +414,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this.entityFilter,
this.noAdd
);
await this.updateComplete;
await this.comboBox.updateComplete;
this._setValue(area.area_id);
} catch (err: any) {
showAlertDialog(this, {
@@ -465,19 +436,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change");
}, 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 {

View File

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

View File

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

View File

@@ -1,8 +1,16 @@
import type {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";
declare global {
interface HASSDomEvents {
@@ -24,10 +32,15 @@ export class HaCodeEditor extends ReactiveElement {
@property() public mode = "yaml";
public hass?: HomeAssistant;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false;
@property() public error = false;
@state() private _value = "";
@@ -110,43 +123,92 @@ export class HaCodeEditor extends ReactiveElement {
private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
const extensions = [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((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: [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
extensions,
}),
root: 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() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
@@ -163,10 +225,9 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value });
}
// Only Lit 2.0 will use this
static get styles(): CSSResultGroup {
return css`
:host(.error-state) div.cm-wrap .cm-gutters {
:host(.error-state) .cm-gutters {
border-color: var(--error-state-color, red);
}
`;

View File

@@ -1,37 +1,78 @@
import "@material/mwc-list/mwc-list-item";
import { 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 "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
// eslint-disable-next-line lit/prefer-static-styles
const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-item {
margin: -5px -10px;
registerStyles(
"vaadin-combo-box-item",
css`
:host {
padding: 0;
}
</style>
<paper-item>${item}</paper-item>`;
:host([focused]:not([disabled])) {
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
}
: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")
export class HaComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public items?: [];
@property() public placeholder?: string;
@property() public filteredItems?: [];
@property() public validationMessage?: string;
@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 })
public allowCustomValue?: boolean;
@@ -46,24 +87,25 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@state() private _opened?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" })
private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
public open() {
this.updateComplete.then(() => {
(this._comboBox as any)?.open();
this._comboBox?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
this._comboBox?.inputElement?.focus();
});
}
public get selectedItem() {
return (this._comboBox as any).selectedItem;
return this._comboBox.selectedItem;
}
protected render(): TemplateResult {
@@ -72,55 +114,78 @@ export class HaComboBox extends LitElement {
.itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath}
.value=${this.value}
.value=${this.value || ""}
.items=${this.items}
.filteredItems=${this.filteredItems}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
${comboBoxRenderer(this.renderer || defaultRowRenderer)}
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
attr-for-value="value"
>
<paper-input
<ha-textfield
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.validationMessage=${this.validationMessage}
.errorMessage=${this.errorMessage}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
>
${this.value
? html`
<ha-icon-button
.label=${this.hass.localize("ui.components.combo-box.clear")}
.path=${mdiClose}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize("ui.components.combo-box.show")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
${this.value
? html`<ha-svg-icon
aria-label=${this.hass?.localize("ui.components.combo-box.clear")}
class="clear-button"
.path=${mdiClose}
@click=${this._clearValue}
></ha-svg-icon>`
: ""}
<ha-svg-icon
aria-label=${this.hass?.localize("ui.components.combo-box.show")}
class="toggle-button"
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
@click=${this._toggleOpen}
></ha-svg-icon>
</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) {
ev.stopPropagation();
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>) {
this._opened = ev.detail.value;
// delay this so we can handle click event before setting _opened
setTimeout(() => {
this._opened = ev.detail.value;
}, 0);
// @ts-ignore
fireEvent(this, ev.type, ev.detail);
}
@@ -141,11 +206,38 @@ export class HaComboBox extends LitElement {
static get styles(): CSSResultGroup {
return css`
paper-input > ha-icon-button {
:host {
display: block;
width: 100%;
}
vaadin-combo-box-light {
position: relative;
}
ha-textfield {
width: 100%;
}
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
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,135 +1,76 @@
import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
const i18n = {
monthNames: [
"January",
"February",
"March",
"April",
"May",
"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 loadDatePickerDialog = () => import("./ha-dialog-date-picker");
if (date !== undefined) {
return { day: date, month, year };
}
return undefined;
},
export interface datePickerDialogParams {
value?: string;
min?: string;
max?: string;
locale?: string;
onChange: (value: string) => void;
}
const showDatePickerDialog = (
element: HTMLElement,
dialogParams: datePickerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-date-picker",
dialogImport: loadDatePickerDialog,
dialogParams,
});
};
@customElement("ha-date-input")
export class HaDateInput extends LitElement {
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property() public value?: string;
@property({ type: Boolean }) public disabled = false;
@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() {
return html`<vaadin-date-picker-light
return html`<paper-input
.label=${this.label}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
attr-for-value="value"
.i18n=${i18n}
no-label-float
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
no-label-float
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>
</vaadin-date-picker-light>`;
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>`;
}
private _valueChanged(ev: CustomEvent) {
if (
!this.value ||
(this._inited && !this._compareStringDates(ev.detail.value, this.value))
) {
this.value = ev.detail.value;
private _openDialog() {
if (this.disabled) {
return;
}
showDatePickerDialog(this, {
min: "1970-01-01",
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, "value-changed", { value: ev.detail.value });
fireEvent(this, "value-changed", { 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 {
return css`
paper-input {

View File

@@ -0,0 +1,106 @@
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,6 +1,7 @@
import { Dialog } from "@material/mwc-dialog";
import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types";
@@ -21,8 +22,7 @@ export const createCloseHeading = (
`;
@customElement("ha-dialog")
// @ts-expect-error
export class HaDialog extends Dialog {
export class HaDialog extends DialogBase {
public scrollToPos(x: number, y: number) {
this.contentElement?.scrollTo(x, y);
}
@@ -31,77 +31,75 @@ export class HaDialog extends Dialog {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
}
protected static get styles(): CSSResultGroup {
return [
Dialog.styles,
css`
.mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
}
.mdc-dialog__actions span:nth-child(2) {
flex: var(--primary-action-button-flex, unset);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
.mdc-dialog__title::before {
display: block;
height: 20px;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(
--ha-dialog-border-radius,
var(--ha-card-border-radius, 4px)
);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: inherit;
}
.header_title {
margin-right: 40px;
}
[dir="rtl"].header_button {
right: auto;
left: 16px;
}
[dir="rtl"].header_title {
margin-left: 40px;
margin-right: 0px;
}
`,
];
}
static override styles = [
styles,
css`
.mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
}
.mdc-dialog__actions span:nth-child(2) {
flex: var(--primary-action-button-flex, unset);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
.mdc-dialog__title::before {
display: block;
height: 20px;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(
--ha-dialog-border-radius,
var(--ha-card-border-radius, 4px)
);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: inherit;
}
.header_title {
margin-right: 40px;
}
[dir="rtl"].header_button {
right: auto;
left: 16px;
}
[dir="rtl"].header_title {
margin-left: 40px;
margin-right: 0px;
}
`,
];
}
declare global {

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import type { LitElement } from "lit";
import { Selector } from "../../data/selector";
import type { HaDurationData } from "../ha-duration-input";
export type HaFormSchema =
@@ -9,14 +10,24 @@ export type HaFormSchema =
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema;
| HaFormTimeSchema
| HaFormSelector;
export interface HaFormBaseSchema {
name: string;
// This value is applied if no data is submitted for this field
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string; suggested_value?: HaFormData };
description?: {
suffix?: string;
// 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
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

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

View File

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

View File

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

View File

@@ -1,8 +1,3 @@
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 {
HassEntity,
HassServiceTarget,

View File

@@ -1,10 +1,12 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { StringSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-textarea";
import "../ha-textfield";
@customElement("ha-selector-text")
export class HaTextSelector extends LitElement {
@@ -20,27 +22,50 @@ export class HaTextSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private _unmaskedPassword = false;
protected render() {
if (this.selector.text?.multiline) {
return html`<paper-textarea
return html`<ha-textarea
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value}
.value=${this.value || ""}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
@input=${this._handleChange}
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>`;
.required=${this.required}
autogrow
></ha-textarea>`;
}
return html`<paper-input
required
.value=${this.value}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
.label=${this.label}
></paper-input>`;
return html`<ha-textfield
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange}
.label=${this.label || ""}
.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) {
@@ -50,6 +75,27 @@ export class HaTextSelector extends LitElement {
}
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 {

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { property, state } from "lit/decorators";
@@ -11,39 +10,12 @@ import "./ha-combo-box";
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
item
// eslint-disable-next-line lit/prefer-static-styles
) => html`<style>
paper-item {
padding: 0;
margin: -10px;
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>`;
) => html`<mwc-list-item twoline>
<span>${item.name}</span>
<span slot="secondary"
>${item.name === item.service ? "" : item.service}</span
>
</mwc-list-item>`;
class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;

View File

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

View File

@@ -1,11 +1,11 @@
import { Switch } from "@material/mwc-switch/deprecated";
import { css, CSSResultGroup } from "lit";
import { SwitchBase } from "@material/mwc-switch/deprecated/mwc-switch-base";
import { styles } from "@material/mwc-switch/deprecated/mwc-switch.css";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { forwardHaptic } from "../data/haptics";
@customElement("ha-switch")
// @ts-expect-error
export class HaSwitch extends Switch {
export class HaSwitch extends SwitchBase {
// Generate a haptic vibration.
// 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.
@@ -13,10 +13,6 @@ export class HaSwitch extends Switch {
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.addEventListener("change", () => {
if (this.haptic) {
forwardHaptic("light");
@@ -24,29 +20,30 @@ export class HaSwitch extends Switch {
});
}
static get styles(): CSSResultGroup {
return [
Switch.styles,
css`
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__track {
background-color: var(--switch-checked-track-color);
border-color: var(--switch-checked-track-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--switch-unchecked-button-color);
border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
`,
];
}
static override styles = [
styles,
css`
:host {
--mdc-theme-secondary: var(--switch-checked-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__track {
background-color: var(--switch-checked-track-color);
border-color: var(--switch-checked-track-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--switch-unchecked-button-color);
border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
`,
];
}
declare global {

View File

@@ -0,0 +1,60 @@
import { TextAreaBase } from "@material/mwc-textarea/mwc-textarea-base";
import { styles as textfieldStyles } from "@material/mwc-textfield/mwc-textfield.css";
import { styles as textareaStyles } from "@material/mwc-textarea/mwc-textarea.css";
import { css, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-textarea")
export class HaTextArea extends TextAreaBase {
@property({ type: Boolean, reflect: true }) autogrow = false;
updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (this.autogrow && changedProperties.has("value")) {
this.mdcRoot.dataset.value = this.value + '=\u200B"'; // add a zero-width space to correctly wrap
}
}
static override styles = [
textfieldStyles,
textareaStyles,
css`
:host([autogrow]) {
max-height: 200px;
}
:host([autogrow]) .mdc-text-field {
position: relative;
min-height: 74px;
min-width: 178px;
}
:host([autogrow]) .mdc-text-field:after {
content: attr(data-value);
margin-top: 23px;
margin-bottom: 9px;
line-height: 1.5rem;
min-height: 42px;
padding: 0px 32px 0 16px;
letter-spacing: var(
--mdc-typography-subtitle1-letter-spacing,
0.009375em
);
visibility: hidden;
white-space: pre-wrap;
}
:host([autogrow]) .mdc-text-field__input {
position: absolute;
height: calc(100% - 32px);
}
:host([autogrow]) .mdc-text-field.mdc-text-field--no-label:after {
margin-top: 16px;
margin-bottom: 16px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-textarea": HaTextArea;
}
}

View File

@@ -1,10 +1,32 @@
import { TextField } from "@material/mwc-textfield";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import { TemplateResult, html, PropertyValues, css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-textfield")
export class HaTextField extends TextField {
override renderIcon(_icon: string, isTrailingIcon = false): TemplateResult {
export class HaTextField extends TextFieldBase {
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
(changedProperties.has("invalid") &&
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
this.invalid ? this.errorMessage || "Invalid" : ""
);
this.reportValidity();
}
}
protected override renderIcon(
_icon: string,
isTrailingIcon = false
): TemplateResult {
const type = isTrailingIcon ? "trailing" : "leading";
return html`
@@ -16,6 +38,38 @@ export class HaTextField extends TextField {
</span>
`;
}
static override styles = [
styles,
css`
.mdc-text-field__input {
width: var(--ha-textfield-input-width, 100%);
}
.mdc-text-field:not(.mdc-text-field--with-leading-icon) {
padding: var(--text-field-padding, 0px 16px);
}
.mdc-text-field__affix--suffix {
padding-left: var(--text-field-suffix-padding-left, 12px);
padding-right: var(--text-field-suffix-padding-right, 0px);
}
input {
text-align: var(--text-field-text-align);
}
/* Chrome, Safari, Edge, Opera */
:host([no-spinner]) input::-webkit-outer-spin-button,
:host([no-spinner]) input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
:host([no-spinner]) input[type="number"] {
-moz-appearance: textfield;
}
`,
];
}
declare global {

View File

@@ -2,12 +2,13 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import "./paper-time-input";
import "./ha-base-time-input";
import { FrontendLocaleData } from "../data/translation";
import type { TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
export class HaTimeInput extends LitElement {
@property() public locale!: FrontendLocaleData;
@property({ attribute: false }) public locale!: FrontendLocaleData;
@property() public value?: string;
@@ -15,9 +16,6 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "hide-label" }) public hideLabel =
false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@@ -35,40 +33,44 @@ export class HaTimeInput extends LitElement {
}
return html`
<paper-time-input
<ha-base-time-input
.label=${this.label}
.hour=${hours}
.min=${parts[1]}
.sec=${parts[2]}
.hours=${Number(hours)}
.minutes=${Number(parts[1])}
.seconds=${Number(parts[2])}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (numberHours >= 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
.hideLabel=${this.hideLabel}
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
></paper-time-input>
></ha-base-time-input>
`;
}
private _timeChanged(ev) {
let value = ev.target.value;
private _timeChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) {
ev.stopPropagation();
const eventValue = ev.detail.value;
const useAMPM = useAmPm(this.locale);
let hours = Number(ev.target.hour || 0);
if (value && useAMPM) {
if (ev.target.amPm === "PM" && hours < 12) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
if (ev.target.amPm === "AM" && hours === 12) {
if (eventValue.amPm === "AM" && hours === 12) {
hours = 0;
}
value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${
ev.target.sec || "00"
}`;
}
const value = `${hours.toString().padStart(2, "0")}:${
eventValue.minutes ? eventValue.minutes.toString().padStart(2, "0") : "00"
}:${
eventValue.seconds ? eventValue.seconds.toString().padStart(2, "0") : "00"
}`;
if (value === this.value) {
return;
}
this.value = value;
fireEvent(this, "change");
fireEvent(this, "value-changed", {

View File

@@ -2,6 +2,7 @@ import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-code-editor";
const isEmpty = (obj: Record<string, unknown>): boolean => {
@@ -18,6 +19,8 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@@ -56,8 +59,10 @@ export class HaYamlEditor extends LitElement {
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}
@value-changed=${this._onChange}
dir="ltr"

View File

@@ -55,7 +55,11 @@ class DialogMediaPlayerBrowse extends LitElement {
escapeKeyAction
hideActions
flexContent
.heading=${true}
.heading=${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
@closed=${this.closeDialog}
>
<ha-header-bar slot="heading">

View File

@@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -30,7 +29,6 @@ import { debounce } from "../../common/util/debounce";
import { getSignedPath } from "../../data/auth";
import type { MediaPlayerItem } from "../../data/media-player";
import {
browseLocalMediaPlayer,
browseMediaPlayer,
BROWSER_PLAYER,
MediaClassBrowserSettings,
@@ -50,6 +48,7 @@ import "../ha-circular-progress";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source";
declare global {
interface HASSDomEvents {
@@ -115,6 +114,19 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
public async refresh() {
const currentId = this.navigateIds[this.navigateIds.length - 1];
try {
this._currentItem = await this._fetchData(
this.entityId,
currentId.media_content_id,
currentId.media_content_type
);
} catch (err) {
this._setError(err);
}
}
public play(): void {
if (this._currentItem?.can_play) {
this._runAction(this._currentItem);
@@ -255,8 +267,10 @@ export class HaMediaPlayerBrowse extends LitElement {
${child.thumbnail
? html`
<div
class="${child.media_class ===
"directory"
class="${[
"app",
"directory",
].includes(child.media_class)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
@@ -415,6 +429,9 @@ export class HaMediaPlayerBrowse extends LitElement {
if (!changedProps.has("navigateIds")) {
return;
}
this._setError(undefined);
const oldNavigateIds = changedProps.get("navigateIds") as
| this["navigateIds"]
| undefined;

View File

@@ -1,497 +0,0 @@
/**
Adapted from paper-time-input from
https://github.com/ryanburns23/paper-time-input
MIT Licensed. Copyright (c) 2017 Ryan Burns
`<paper-time-input>` Polymer element to accept a time with paper-input & paper-dropdown-menu
Inspired by the time input in google forms
### Styling
`<paper-time-input>` provides the following custom properties and mixins for styling:
Custom property | Description | Default
----------------|-------------|----------
`--paper-time-input-dropdown-ripple-color` | dropdown ripple color | `--primary-color`
`--paper-time-input-cotnainer` | Mixin applied to the inputs | `{}`
`--paper-time-dropdown-input-cotnainer` | Mixin applied to the dropdown input | `{}`
*/
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
export class PaperTimeInput extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
@apply --paper-font-common-base;
}
paper-input {
width: 30px;
text-align: center;
--paper-input-container-input: {
/* Damn you firefox
* Needed to hide spin num in firefox
* http://stackoverflow.com/questions/3790935/can-i-hide-the-html5-number-input-s-spin-box
*/
-moz-appearance: textfield;
@apply --paper-time-input-cotnainer;
}
--paper-input-container-input-webkit-spinner: {
-webkit-appearance: none;
margin: 0;
display: none;
}
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
}
paper-dropdown-menu {
width: 55px;
padding: 0;
/* Force ripple to use the whole container */
--paper-dropdown-menu-ripple: {
color: var(
--paper-time-input-dropdown-ripple-color,
var(--primary-color)
);
}
--paper-input-container-input: {
@apply --paper-font-button;
text-align: center;
padding-left: 5px;
@apply --paper-time-dropdown-input-cotnainer;
}
--paper-input-container-underline: {
border-color: transparent;
}
--paper-input-container-underline-focus: {
border-color: transparent;
}
}
paper-item {
cursor: pointer;
text-align: center;
font-size: 14px;
}
paper-listbox {
padding: 0;
}
label {
@apply --paper-font-caption;
color: var(
--paper-input-container-color,
var(--secondary-text-color)
);
}
.time-input-wrap {
@apply --layout-horizontal;
@apply --layout-no-wrap;
justify-content: var(--paper-time-input-justify-content, normal);
}
[hidden] {
display: none !important;
}
#millisec {
width: 38px;
}
.no-suffix {
margin-left: -2px;
}
</style>
<label hidden$="[[hideLabel]]">[[label]]</label>
<div class="time-input-wrap">
<!-- Hour Input -->
<paper-input
id="hour"
type="number"
inputmode="numeric"
value="{{hour}}"
label="[[hourLabel]]"
on-change="_shouldFormatHour"
on-focus="_onFocus"
required
prevent-invalid-input
auto-validate="[[autoValidate]]"
maxlength="2"
max="[[_computeHourMax(format)]]"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span suffix slot="suffix">:</span>
</paper-input>
<!-- Min Input -->
<paper-input
class$="[[_computeClassNames(enableSecond)]]"
id="min"
type="number"
inputmode="numeric"
value="{{min}}"
label="[[minLabel]]"
on-change="_formatMin"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span hidden$="[[!enableSecond]]" suffix slot="suffix">:</span>
</paper-input>
<!-- Sec Input -->
<paper-input
class$="[[_computeClassNames(enableMillisecond)]]"
id="sec"
type="number"
inputmode="numeric"
value="{{sec}}"
label="[[secLabel]]"
on-change="_formatSec"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
hidden$="[[!enableSecond]]"
>
<span hidden$="[[!enableMillisecond]]" suffix slot="suffix">:</span>
</paper-input>
<!-- Millisec Input -->
<paper-input
id="millisec"
type="number"
value="{{millisec}}"
label="[[millisecLabel]]"
on-change="_formatMillisec"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="3"
max="999"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
hidden$="[[!enableMillisecond]]"
>
</paper-input>
<!-- Dropdown Menu -->
<paper-dropdown-menu
id="dropdown"
required=""
hidden$="[[_equal(format, 24)]]"
no-label-float=""
disabled="[[disabled]]"
>
<paper-listbox
attr-for-selected="name"
selected="{{amPm}}"
slot="dropdown-content"
>
<paper-item name="AM">AM</paper-item>
<paper-item name="PM">PM</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
`;
}
static get properties() {
return {
/**
* Label for the input
*/
label: {
type: String,
value: "Time",
},
/**
* auto validate time inputs
*/
autoValidate: {
type: Boolean,
value: true,
},
/**
* hides the label
*/
hideLabel: {
type: Boolean,
value: false,
},
/**
* float the input labels
*/
floatInputLabels: {
type: Boolean,
value: false,
},
/**
* always float the input labels
*/
alwaysFloatInputLabels: {
type: Boolean,
value: false,
},
/**
* 12 or 24 hr format
*/
format: {
type: Number,
value: 12,
},
/**
* disables the inputs
*/
disabled: {
type: Boolean,
value: false,
},
/**
* hour
*/
hour: {
type: String,
notify: true,
},
/**
* minute
*/
min: {
type: String,
notify: true,
},
/**
* second
*/
sec: {
type: String,
notify: true,
},
/**
* milli second
*/
millisec: {
type: String,
notify: true,
},
/**
* Label for the hour input
*/
hourLabel: {
type: String,
value: "",
},
/**
* Label for the min input
*/
minLabel: {
type: String,
value: "",
},
/**
* Label for the sec input
*/
secLabel: {
type: String,
value: "",
},
/**
* Label for the milli sec input
*/
millisecLabel: {
type: String,
value: "",
},
/**
* show the sec field
*/
enableSecond: {
type: Boolean,
value: false,
},
/**
* show the milli sec field
*/
enableMillisecond: {
type: Boolean,
value: false,
},
/**
* limit hours input
*/
noHoursLimit: {
type: Boolean,
value: false,
},
/**
* AM or PM
*/
amPm: {
type: String,
notify: true,
value: "AM",
},
/**
* Formatted time string
*/
value: {
type: String,
notify: true,
readOnly: true,
computed: "_computeTime(min, hour, sec, millisec, amPm)",
},
};
}
/**
* Validate the inputs
* @return {boolean}
*/
validate() {
let valid = true;
// Validate hour & min fields
if (!this.$.hour.validate() || !this.$.min.validate()) {
valid = false;
}
// Validate second field
if (this.enableSecond && !this.$.sec.validate()) {
valid = false;
}
// Validate milli second field
if (this.enableMillisecond && !this.$.millisec.validate()) {
valid = false;
}
// Validate AM PM if 12 hour time
if (this.format === 12 && !this.$.dropdown.validate()) {
valid = false;
}
return valid;
}
/**
* Create time string
*/
_computeTime(min, hour, sec, millisec, amPm) {
let str;
if (
hour ||
min ||
(sec && this.enableSecond) ||
(millisec && this.enableMillisecond)
) {
hour = hour || "00";
min = min || "00";
sec = sec || "00";
millisec = millisec || "000";
str = hour + ":" + min;
// add sec field
if (this.enableSecond && sec) {
str = str + ":" + sec;
}
// add milli sec field
if (this.enableMillisecond && millisec) {
str = str + ":" + millisec;
}
// No ampm on 24 hr time
if (this.format === 12) {
str = str + " " + amPm;
}
}
return str;
}
_onFocus(ev) {
ev.target.inputElement.inputElement.select();
}
/**
* Format milli sec
*/
_formatMillisec() {
if (this.millisec.toString().length === 1) {
this.millisec = this.millisec.toString().padStart(3, "0");
}
}
/**
* Format sec
*/
_formatSec() {
if (this.sec.toString().length === 1) {
this.sec = this.sec.toString().padStart(2, "0");
}
}
/**
* Format min
*/
_formatMin() {
if (this.min.toString().length === 1) {
this.min = this.min.toString().padStart(2, "0");
}
}
/**
* Format hour
*/
_shouldFormatHour() {
if (this.format === 24 && this.hour.toString().length === 1) {
this.hour = this.hour.toString().padStart(2, "0");
}
}
/**
* 24 hour format has a max hr of 23
*/
_computeHourMax(format) {
if (this.noHoursLimit) {
return null;
}
if (format === 12) {
return format;
}
return 23;
}
_equal(n1, n2) {
return n1 === n2;
}
_computeClassNames(hasSuffix) {
return hasSuffix ? " " : "no-suffix";
}
}
customElements.define("paper-time-input", PaperTimeInput);

View File

@@ -1,8 +1,3 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -11,6 +6,8 @@ import { stringCompare } from "../../common/string/compare";
import { fetchUsers, User } from "../../data/user";
import { HomeAssistant } from "../../types";
import "./ha-user-badge";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
class HaUserPicker extends LitElement {
public hass?: HomeAssistant;
@@ -37,34 +34,31 @@ class HaUserPicker extends LitElement {
protected render(): TemplateResult {
return html`
<paper-dropdown-menu-light
<mwc-select
.label=${this.label}
.disabled=${this.disabled}
.value=${this.value}
@selected=${this._userChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${this.value}
attr-for-selected="data-user-id"
@iron-select=${this._userChanged}
>
<paper-icon-item data-user-id="">
${this.noUserLabel ||
this.hass?.localize("ui.components.user-picker.no_user")}
</paper-icon-item>
${this._sortedUsers(this.users).map(
(user) => html`
<paper-icon-item data-user-id=${user.id}>
<ha-user-badge
.hass=${this.hass}
.user=${user}
slot="item-icon"
></ha-user-badge>
${user.name}
</paper-icon-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this.users?.length === 0
? html`<mwc-list-item value="">
${this.noUserLabel ||
this.hass?.localize("ui.components.user-picker.no_user")}
</mwc-list-item>`
: ""}
${this._sortedUsers(this.users).map(
(user) => html`
<mwc-list-item graphic="avatar" .value=${user.id}>
<ha-user-badge
.hass=${this.hass}
.user=${user}
slot="graphic"
></ha-user-badge>
${user.name}
</mwc-list-item>
`
)}
</mwc-select>
`;
}
@@ -78,10 +72,10 @@ class HaUserPicker extends LitElement {
}
private _userChanged(ev) {
const newValue = ev.detail.item.dataset.userId;
const newValue = ev.target.value;
if (newValue !== this.value) {
this.value = ev.detail.value;
this.value = newValue;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
@@ -94,15 +88,9 @@ class HaUserPicker extends LitElement {
:host {
display: inline-block;
}
paper-dropdown-menu-light {
mwc-list {
display: block;
}
paper-listbox {
min-width: 200px;
}
paper-icon-item {
cursor: pointer;
}
`;
}
}

View File

@@ -75,7 +75,7 @@ class HaUsersPickerLight extends LitElement {
)
)}
<ha-user-picker
.noUserLabel=${this.pickUserLabel ||
.label=${this.pickUserLabel ||
this.hass!.localize("ui.components.user-picker.add_user")}
.hass=${this.hass}
.users=${notSelectedUsers}

View File

@@ -115,3 +115,11 @@ export const updateCameraPrefs = (
entity_id: entityId,
...prefs,
});
const CAMERA_MEDIA_SOURCE_PREFIX = "media-source://camera/";
export const isCameraMediaSource = (mediaContentId: string) =>
mediaContentId.startsWith(CAMERA_MEDIA_SOURCE_PREFIX);
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);

View File

@@ -13,6 +13,7 @@ export const DISCOVERY_SOURCES = [
"ssdp",
"zeroconf",
"discovery",
"integration_discovery",
"mqtt",
"hassio",
];
@@ -104,18 +105,19 @@ export const localizeConfigFlowTitle = (
localize: LocalizeFunc,
flow: DataEntryFlowProgress
) => {
const placeholders = flow.context.title_placeholders || {};
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
if (
!flow.context.title_placeholders ||
Object.keys(flow.context.title_placeholders).length === 0
) {
return domainToName(localize, flow.handler);
}
const args: string[] = [];
placeholderKeys.forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(`component.${flow.handler}.config.flow_title`, ...args) ||
"name" in placeholders
? placeholders.name
: domainToName(localize, flow.handler);
return (
localize(
`component.${flow.handler}.config.flow_title`,
flow.context.title_placeholders
) ||
("name" in flow.context.title_placeholders
? flow.context.title_placeholders.name
: domainToName(localize, flow.handler))
);
};

View File

@@ -55,7 +55,13 @@ export const computeDeviceName = (
device.name_by_user ||
device.name ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device");
hass.localize(
"ui.panel.config.devices.unnamed_device",
"type",
hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
)
);
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);

View File

@@ -185,15 +185,6 @@ export const browseMediaPlayer = (
media_content_type: mediaContentType,
});
export const browseLocalMediaPlayer = (
hass: HomeAssistant,
mediaContentId?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_source/browse_media",
media_content_id: mediaContentId,
});
export const getCurrentProgress = (stateObj: MediaPlayerEntity): number => {
let progress = stateObj.attributes.media_position!;
@@ -321,8 +312,8 @@ export const computeMediaControls = (
return buttons.length > 0 ? buttons : undefined;
};
export const formatMediaTime = (seconds: number): string => {
if (!seconds) {
export const formatMediaTime = (seconds: number | undefined): string => {
if (seconds === undefined) {
return "";
}

View File

@@ -1,4 +1,5 @@
import { HomeAssistant } from "../types";
import { MediaPlayerItem } from "./media-player";
export interface ResolvedMediaSource {
url: string;
@@ -13,3 +14,38 @@ export const resolveMediaSource = (
type: "media_source/resolve_media",
media_content_id,
});
export const browseLocalMediaPlayer = (
hass: HomeAssistant,
mediaContentId?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_source/browse_media",
media_content_id: mediaContentId,
});
export const isLocalMediaSourceContentId = (mediaId: string) =>
mediaId.startsWith("media-source://media_source");
export const uploadLocalMedia = async (
hass: HomeAssistant,
media_content_id: string,
file: File
) => {
const fd = new FormData();
fd.append("media_content_id", media_content_id);
fd.append("file", file);
const resp = await hass.fetchWithAuth(
"/api/media_source/local_source/upload",
{
method: "POST",
body: fd,
}
);
if (resp.status === 413) {
throw new Error("Uploaded image is too large");
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}
return resp.json();
};

View File

@@ -1,7 +1,9 @@
export type Selector =
| AddonSelector
| AttributeSelector
| EntitySelector
| DeviceSelector
| DurationSelector
| AreaSelector
| TargetSelector
| NumberSelector
@@ -11,6 +13,7 @@ export type Selector =
| StringSelector
| ObjectSelector
| SelectSelector;
export interface EntitySelector {
entity: {
integration?: string;
@@ -19,6 +22,12 @@ export interface EntitySelector {
};
}
export interface AttributeSelector {
attribute: {
entity_id: string;
};
}
export interface DeviceSelector {
device: {
integration?: string;
@@ -31,6 +40,11 @@ export interface DeviceSelector {
};
}
export interface DurationSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
duration: {};
}
export interface AddonSelector {
addon: {
name?: string;
@@ -95,7 +109,22 @@ export interface ActionSelector {
export interface StringSelector {
text: {
multiline: boolean;
multiline?: boolean;
type?:
| "number"
| "text"
| "search"
| "tel"
| "url"
| "email"
| "password"
| "date"
| "month"
| "week"
| "time"
| "datetime-local"
| "color";
suffix?: string;
};
}

View File

@@ -82,7 +82,7 @@ async function processEvent(
event: SupervisorEvent,
key: string
) {
if (event.event !== "supervisor-update" || event.update_key !== key) {
if (event.event !== "supervisor_update" || event.update_key !== key) {
return;
}

View File

@@ -116,15 +116,14 @@ class DataEntryFlowDialog extends LitElement {
params.continueFlowId
);
} catch (err: any) {
this._step = undefined;
this._params = undefined;
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
)}: ${err.message || err.body}`,
});
return;
}
@@ -177,6 +176,7 @@ class DataEntryFlowDialog extends LitElement {
});
}
this._loading = undefined;
this._step = undefined;
this._params = undefined;
this._devices = undefined;
@@ -372,15 +372,14 @@ class DataEntryFlowDialog extends LitElement {
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err: any) {
this._step = undefined;
this._params = undefined;
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
)}: ${err.message || err.body}`,
});
return;
} finally {
@@ -405,6 +404,15 @@ class DataEntryFlowDialog extends LitElement {
this._loading = "loading_step";
try {
this._step = await step;
} catch (err: any) {
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err.message || err.body,
});
return;
} finally {
this._loading = undefined;
}

View File

@@ -1,7 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
@@ -122,13 +119,6 @@ class StepFlowCreateEntry extends LitElement {
.buttons > *:last-child {
margin-left: auto;
}
paper-dropdown-menu-light {
cursor: pointer;
}
paper-item {
cursor: pointer;
white-space: nowrap;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.device {
width: 100%;

View File

@@ -103,12 +103,13 @@ class StepFlowForm extends LitElement {
const allRequiredInfoFilledIn =
stepData === undefined
? // If no data filled in, just check that any field is required
this.step.data_schema.find((field) => !field.optional) === undefined
this.step.data_schema.find((field) => field.required) === undefined
: // If data is filled in, make sure all required fields are
stepData &&
this.step.data_schema.every(
(field) =>
field.optional || !["", undefined].includes(stepData![field.name])
!field.required ||
!["", undefined].includes(stepData![field.name])
);
if (!allRequiredInfoFilledIn) {

View File

@@ -1,5 +1,3 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import Fuse from "fuse.js";
import {
css,
@@ -22,6 +20,7 @@ import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import { configFlowContentStyles } from "./styles";
import "@material/mwc-list/mwc-list-item";
interface HandlerObj {
name: string;
@@ -97,12 +96,14 @@ class StepFlowPickHandler extends LitElement {
? handlers.map(
(handler: HandlerObj) =>
html`
<paper-icon-item
<mwc-list-item
graphic="medium"
hasMeta
@click=${this._handlerPicked}
.handler=${handler}
>
<img
slot="item-icon"
slot="graphic"
loading="lazy"
src=${brandsUrl({
domain: handler.slug,
@@ -113,9 +114,9 @@ class StepFlowPickHandler extends LitElement {
referrerpolicy="no-referrer"
/>
<paper-item-body> ${handler.name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
${handler.name}
<ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>
`
)
: html`
@@ -145,10 +146,26 @@ class StepFlowPickHandler extends LitElement {
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (this._filter === undefined && this.initialFilter !== undefined) {
this._filter = this.initialFilter;
}
super.willUpdate(changedProps);
if (this.initialFilter !== undefined && this._filter === "") {
this.initialFilter = undefined;
this._filter = "";
this._width = undefined;
this._height = undefined;
} else if (
this.hasUpdated &&
changedProps.has("_filter") &&
(!this._width || !this._height)
) {
// Store the width and height so that when we search, box doesn't jump
const boundingRect =
this.shadowRoot!.querySelector("div")!.getBoundingClientRect();
this._width = boundingRect.width;
this._height = boundingRect.height;
}
}
protected firstUpdated(changedProps) {
@@ -159,24 +176,6 @@ class StepFlowPickHandler extends LitElement {
);
}
protected updated(changedProps) {
super.updated(changedProps);
// Store the width and height so that when we search, box doesn't jump
const div = this.shadowRoot!.querySelector("div")!;
if (!this._width) {
const width = div.clientWidth;
if (width) {
this._width = width;
}
}
if (!this._height) {
const height = div.clientHeight;
if (height) {
this._height = height;
}
}
}
private _getHandlers() {
return this._filterHandlers(
this.handlers,
@@ -219,7 +218,7 @@ class StepFlowPickHandler extends LitElement {
}
search-input {
display: block;
margin: -12px 16px 0;
margin: 8px 16px 0;
}
ha-icon-next {
margin-right: 8px;
@@ -236,10 +235,6 @@ class StepFlowPickHandler extends LitElement {
max-height: calc(100vh - 134px);
}
}
paper-icon-item {
cursor: pointer;
margin-bottom: 4px;
}
p {
text-align: center;
padding: 16px;

View File

@@ -1,6 +1,3 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
@@ -15,7 +12,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-climate-control";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-slider";
import "../../../components/ha-switch";
import {
@@ -30,6 +26,9 @@ import {
compareClimateHvacModes,
} from "../../../data/climate";
import { HomeAssistant } from "../../../types";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { stopPropagation } from "../../../common/dom/stop_propagation";
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -169,109 +168,93 @@ class MoreInfoClimate extends LitElement {
<div class="container-hvac_modes">
<div class="controls">
<ha-paper-dropdown-menu
label-float
dynamic-align
<mwc-select
.label=${hass.localize("ui.card.climate.operation")}
.value=${stateObj.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationmodeChanged}
@closed=${stopPropagation}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.state}
@selected-changed=${this._handleOperationmodeChanged}
>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(`component.climate.state._.${mode}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${hass.localize(`component.climate.state._.${mode}`)}
</mwc-list-item>
`
)}
</mwc-select>
</div>
</div>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<div class="container-preset_modes">
<ha-paper-dropdown-menu
label-float
dynamic-align
<mwc-select
.label=${hass.localize("ui.card.climate.preset_mode")}
.value=${stateObj.attributes.preset_mode}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.preset_mode}
@selected-changed=${this._handlePresetmodeChanged}
>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.climate.preset_mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${hass.localize(
`state_attributes.climate.preset_mode.${mode}`
) || mode}
</mwc-list-item>
`
)}
</mwc-select>
</div>
`
: ""}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<div class="container-fan_list">
<ha-paper-dropdown-menu
label-float
dynamic-align
<mwc-select
.label=${hass.localize("ui.card.climate.fan_mode")}
.value=${stateObj.attributes.fan_mode}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanmodeChanged}
@closed=${stopPropagation}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.fan_mode}
@selected-changed=${this._handleFanmodeChanged}
>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.climate.fan_mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${hass.localize(
`state_attributes.climate.fan_mode.${mode}`
) || mode}
</mwc-list-item>
`
)}
</mwc-select>
</div>
`
: ""}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<div class="container-swing_list">
<ha-paper-dropdown-menu
label-float
dynamic-align
<mwc-select
.label=${hass.localize("ui.card.climate.swing_mode")}
.value=${stateObj.attributes.swing_mode}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.swing_mode}
@selected-changed=${this._handleSwingmodeChanged}
>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>${mode}</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<mwc-list-item .value=${mode}>${mode}</mwc-list-item>
`
)}
</mwc-select>
</div>
`
: ""}
@@ -366,7 +349,7 @@ class MoreInfoClimate extends LitElement {
}
private _handleFanmodeChanged(ev) {
const newVal = ev.detail.value;
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.fan_mode,
newVal,
@@ -376,14 +359,14 @@ class MoreInfoClimate extends LitElement {
}
private _handleOperationmodeChanged(ev) {
const newVal = ev.detail.value;
const newVal = ev.target.value;
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
hvac_mode: newVal,
});
}
private _handleSwingmodeChanged(ev) {
const newVal = ev.detail.value;
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_mode,
newVal,
@@ -393,7 +376,7 @@ class MoreInfoClimate extends LitElement {
}
private _handlePresetmodeChanged(ev) {
const newVal = ev.detail.value || null;
const newVal = ev.target.value || null;
this._callServiceHelper(
this.stateObj!.attributes.preset_mode,
newVal,
@@ -444,12 +427,9 @@ class MoreInfoClimate extends LitElement {
color: var(--primary-text-color);
}
ha-paper-dropdown-menu {
mwc-select {
width: 100%;
}
paper-item {
cursor: pointer;
margin-top: 8px;
}
ha-slider {
@@ -488,3 +468,9 @@ class MoreInfoClimate extends LitElement {
}
customElements.define("more-info-climate", MoreInfoClimate);
declare global {
interface HTMLElementTagNameMap {
"more-info-climate": MoreInfoClimate;
}
}

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