Compare commits

...

194 Commits

Author SHA1 Message Date
Zack
d4bdfeea81 Open and Close 2022-02-11 15:36:39 -06:00
Zack
d4ed377277 Stash 2022-02-11 12:18:03 -06:00
Zack Barett
db7cac5782 Fix Lovelace Empty Menu when not advanced or admin (#11660) 2022-02-11 10:31:45 -06: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
Bram Kragten
a064ca0856 Merge pull request #11443 from home-assistant/dev 2022-01-26 18:38:18 +01:00
Bram Kragten
6044ea92ad Merge branch 'master' into dev 2022-01-26 18:09:18 +01:00
Bram Kragten
17e8215420 Bumped version to 20220126.0 2022-01-26 18:07:08 +01:00
Philip Allgaier
a4ae1bee79 Sort all elements on the area page (#11338) 2022-01-26 17:06:12 +00:00
Philip Allgaier
7d335d7d85 Convert ha-climate-control ot Lit and add tooltips to buttons (#10921)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-01-26 16:50:50 +00:00
Zack Barett
7c194d8910 Update Media Browser to styling from Mockup (#11424)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 17:42:25 +01:00
Mattias Persson
a92100bb0a Add viewport initial-scale for iOS devices (#11330) 2022-01-26 16:57:23 +01:00
Erik Montnemery
303af611d1 Remove unused keys from hassAttributeUtil (#10944)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 15:56:10 +00:00
Robin Wittebol
559b6e9d5b Apply header bottom border and fix header height (#10986) 2022-01-26 16:26:44 +01:00
David F. Mulcahey
75a95ff675 Fix ZHA device reconfiguration dialog (#11016) 2022-01-26 16:19:40 +01:00
Philip Allgaier
3024ee43f9 Fix entity config page rendering for disabled entities (#11439) 2022-01-26 14:54:38 +00:00
Philip Allgaier
b34b92fa87 Give the design page menu some space (#11441) 2022-01-26 15:48:34 +01:00
Philip Allgaier
1832ed0a48 Ensure tag QR code gets shown for each dialog opening (#11438) 2022-01-26 10:13:20 +00:00
Paulus Schoutsen
f398692e75 Improve cloud dashboard (#11422)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 10:00:50 +00:00
Joakim Sørensen
68bee4dd58 Change more_updates base translation (#11437) 2022-01-26 10:31:14 +01:00
Joakim Sørensen
f1297e1f36 Update styling for show more updates (#11435) 2022-01-26 10:30:19 +01:00
Joakim Sørensen
953e3e060b Add version for service devices (#11436) 2022-01-26 10:29:34 +01:00
Bram Kragten
c37f660718 Update translations 2022-01-26 09:54:48 +01:00
Philip Allgaier
02754369a6 Update history and logbook panel path when making selections (#11428) 2022-01-26 09:39:29 +01:00
Philip Allgaier
0df9e9932f Fix 24:XX time issue in Chrome (#11426) 2022-01-26 09:37:09 +01:00
Paulus Schoutsen
eddb392ad0 Always show QR code, and with white bg (#11434) 2022-01-26 00:17:23 -08:00
Philip Allgaier
e8ba349447 Fix border-radius for progress button success and error (#11432) 2022-01-25 19:39:23 -06:00
Bram Kragten
5be22d46ab Don't add quickbar to history (#11429) 2022-01-25 19:46:17 +00:00
Philip Allgaier
ffaff30b46 Fix various supervisor tooltip and aria-label issues (#10878) 2022-01-25 16:36:35 +00:00
Yosi Levy
c4cad5bccd Missing translation, code editor make ltr, checkbox alignment in rtl (#11419) 2022-01-25 08:46:33 -06:00
Steve Repsher
e4085fe1f6 Allow tab to show/hide password button for keyboard accessibility (#11416) 2022-01-24 21:26:44 -06:00
Paulus Schoutsen
8bfef92c86 Bumped version to 20220124.0 2022-01-24 15:52:45 -08:00
Patrick ZAJDA
0c07178c0a Add em dash "—" instead of a blank value in devices and entities tables to improve accessibility (#11078) 2022-01-24 20:18:23 +01:00
Michael Gorven
1010777139 Add weekday to formatTimeWeekday() (#11020) 2022-01-24 19:25:45 +01:00
Philip Allgaier
e57477c16a Use consistent font size for quick bar "Nothing found" note (#11418) 2022-01-24 18:14:11 +00:00
Radu Cotescu
30fa92c120 fix #11041: The gauge card doesn't render correctly on iOS 15.2 / macOS 12.1 in the companion apps (#11363)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Radu Cotescu <radu-likes-to-code@cotescu.com>
2022-01-24 17:58:02 +00:00
Zack Barett
b32438dc18 Remove padding (#11417) 2022-01-24 17:54:00 +00:00
Zack Barett
614bd2f451 Fix Quickbar for Safari - Change to MWC Textfield (#11414) 2022-01-24 09:14:29 -08:00
Paulus Schoutsen
6c12a5a4b1 Allow an external sidebar (#11347) 2022-01-24 09:08:35 -08:00
Paulus Schoutsen
bbcec38450 Play audio in the bottom bar media player (#11413)
Co-authored-by: Zack <zackbarett@hey.com>
2022-01-24 17:07:47 +00:00
Paulus Schoutsen
416e2e26c0 Add check for updates in config menu (#11415)
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-01-24 16:48:07 +00:00
Zack Barett
1a7164b466 Fix Logbook Icons, Card Editor Close/Cancel buttons, View Editor Dirty (#11153) 2022-01-24 17:36:49 +01:00
Philip Allgaier
3ddcd2d0f6 Ensure forecast temperatures are properly positioned + show em-dash when n/a (#9066)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-01-24 10:10:44 -06:00
Bram Kragten
648c02e622 Don't sync dev tools service data between tabs (#10980) 2022-01-24 08:43:28 -06:00
Erik Montnemery
b0b953bfac Set mandatory bool service data without a default value to false (#11094) 2022-01-24 10:27:38 +01:00
Zack Barett
abeaa63005 Lovelace Menu Edits (#11323) 2022-01-24 10:14:32 +01:00
Paulus Schoutsen
9cd23374f4 Hide actions footer if no action links (#11388) 2022-01-24 10:06:29 +01:00
Philip Allgaier
72bd5f84d6 Use en "–" and em "—" dashes consistently (#11401) 2022-01-24 10:02:44 +01:00
Paulus Schoutsen
22b4550fdf Rename media browser to media (#11412) 2022-01-24 02:13:47 +01:00
Paulus Schoutsen
87c22229e0 Add demo for selectors (#11398)
* Add demo for selectors

* Update label name
2022-01-22 14:44:50 -08:00
Philip Allgaier
971fd8dc60 Adjust padding for "No items" in media browser (#11397) 2022-01-22 02:51:03 +01:00
Philip Allgaier
049c3caadd Remove "authSig" from media player source (#11394) 2022-01-22 02:27:40 +01:00
Philip Allgaier
fb2a24d11e Remove capitalization from media player state card media title (#11396) 2022-01-21 19:01:15 -06:00
Paulus Schoutsen
d4646bac01 Restore energy config in quickbar (#11391) 2022-01-21 17:06:42 -06:00
Paulus Schoutsen
14e5b2a7a5 Single device links to device page (#11387) 2022-01-21 14:10:01 -08:00
Zack Barett
734a733a4c Add Search Icon to Config Dashboard (#11375)
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-21 14:06:25 -08:00
Philip Allgaier
8f31c182f6 Remove SVG icons from accessibility tree (#11389) 2022-01-21 15:56:36 -06:00
Zack Barett
e51a819bfd Move energy to Dashboards (#11386) 2022-01-21 13:30:24 -08:00
Zack Barett
05d7e85aa3 Remove Show button on Update. Make row clickable (#11385) 2022-01-21 13:28:05 -08:00
Paulus Schoutsen
cf527e4bc2 Hide download diagnostics if config entry is not loaded (#11383) 2022-01-21 09:41:02 -08:00
Matthias de Baat
197b581e8e Adding results of the Configuration menu user test (#11381)
* Adding results of the Configuration menu user test

* Update title

* Add sidebar entry

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-21 08:39:17 -08:00
Paulus Schoutsen
f75bf1f676 Check for updates in add-on store (#11382) 2022-01-21 08:16:45 -08:00
Allen Porter
28df79cfda Enable/Disable LL-HLS support based on http/2 availability (#11372)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Philip Allgaier <philip.allgaier@gmx.de>
2022-01-21 08:06:00 -08:00
Zack Barett
3bf19883a8 Fix Date Time Helper (#11367) 2022-01-21 08:36:39 -06:00
Zack Barett
303e065433 Media Browser Bar (#11369)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-20 14:37:30 -08:00
Joakim Sørensen
7ad0b37a9e Use backend logic for partial backup while updating (#11364) 2022-01-20 10:12:52 -08:00
Joakim Sørensen
930c7e4afa Add backup size to backup table (#11365) 2022-01-20 10:08:53 -06:00
Paulus Schoutsen
81faae6f74 Allow downloading device diagnostics (#11370) 2022-01-19 20:48:24 -08:00
Philip Allgaier
f7fc83ac12 Add space between number input field and unit (#11366) 2022-01-19 12:46:38 -08:00
Paulus Schoutsen
21a099ee9f Clean up users table (#11333)
* Clean up users table

* Add decicated icon for data tables

* Change tooltip and icons

* Only use icons for narrow view

* Shorten headers

* Add chips to the user detail dialog

* Lint

* Hide system badge on mobile
2022-01-19 12:28:13 -08:00
Paulus Schoutsen
7d1ce1b240 Allow creating automation from TTS try dialog (#11161) 2022-01-19 11:32:24 -06:00
Matthias de Baat
d1f1309198 Added examples and corrected some text (#11156) 2022-01-19 09:02:22 -08:00
Philip Allgaier
68dd818f7a Translate "No integration" in device dashboard table (#11362) 2022-01-19 08:57:46 -08:00
Paulus Schoutsen
50bea33a19 Localize config flow title (#11358) 2022-01-18 14:18:22 -08:00
Paulus Schoutsen
069f08b55e Bumped version to 20211229.1 2022-01-10 15:30:53 -08:00
Bram Kragten
204ccf8b40 Wait with navigate until history.back is done (#11152)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-10 15:30:48 -08:00
Bram Kragten
0ab8f8fd7c Merge pull request #11043 from home-assistant/dev 2021-12-29 10:51:08 +01:00
Bram Kragten
9b0b2c5b71 Merge pull request #11033 from home-assistant/dev 2021-12-27 20:59:22 +01:00
Bram Kragten
0800c702fb Merge pull request #10981 from home-assistant/dev 2021-12-20 14:01:20 +01:00
Bram Kragten
b7bd7c1065 Merge pull request #10930 from home-assistant/dev 2021-12-15 13:48:42 +01:00
Bram Kragten
61bae5da64 Merge pull request #10880 from home-assistant/dev 2021-12-12 13:49:22 +01:00
Bram Kragten
bdd13db8cf Merge pull request #10869 from home-assistant/dev 2021-12-11 17:32:55 +01:00
Paulus Schoutsen
cdc3d11181 Merge pull request #10846 from home-assistant/dev 2021-12-09 14:05:30 -08:00
Paulus Schoutsen
8f729e2a95 Merge pull request #10818 from home-assistant/dev 2021-12-06 15:21:37 -08:00
Paulus Schoutsen
bc9195f7d5 20211203.0 (#10788)
* Fix thingktalk dialog (#10600)

* Add picture uploader to area (#10544)

* Update image-cropper-dialog.ts

* WebRTC fix for Safari (#10602)

* Update MDI to v6.5.95 (#10618)

* Remove deprecated icons (#10622)

* Improve startup experience by removing AppBar skeleton (#10569)

* Correct ZHA LQI sort in device children dialog (#10616)

* Remove add-on store tab (#10624)

* Add markers-updated to ha-locations-editor (#10601)

* Use ha-form for onboarding-create-user (#10604)

* Fix datatable checkbox width (#10631)

* Move updates (#10626)

* Add correct button label to "no_state" statistics fix dialog (#10628)

* Update Lovelace Cast app ID (#10592)

* Cast fixes (#10598)

* Remove customize UI (#10632)

* Show updates on dashboard for dev (#10637)

* Area Card (#10141)

Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Bumped version to 20211117.0

* Fix back button color (#10650)

* Fix active tab (#10654)

* Remove ha-alert actionText (#10646)

* Use ha-formfield around backup checkbox (#10653)

* Simplify launch screen svg (#10643)

* Always render groups/areas in a single column (#10655)

* Send error message to sender (#10660)

* Add frequency device class for sensor (#10621)

* Fix color over slotted image in ha-alert (#10652)

* Make ha-chip-set slot-able (#10647)

* Remove core note on update page (#10661)

* Add iconColor to ha-config-navigation entries (#10658)

* Use white for icons with backgound (#10672)

* Fix color overlay in ha-alert content (#10674)

* Add scenes and scripts as buttons in footer of area cards (#10673)

* Add scenes and scripts as chips in footer of area cards

* Remove unused chips config type

* Update src/panels/lovelace/common/generate-lovelace-config.ts

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Fix typing

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Fix dark main-content and split gallery demo (#10675)

* Make "Show more" show everything starting from yesterday (#10533)

* Use component to ensure relative-time in Glance card gets updated (#10666)

* Limit setting up supervisor subscriptions to the supervisor panel (#10680)

* Remove first part of the update description (#10669)

* Fixing typo in #10626 (#10686)

* Bumped version to 20211123.0

* Update background colors of navigation icons (#10691)

* Render update card on add-on page (#10681)

* Fix addon slug (#10693)

* Improve device information when via device is unknown (#10685)

* Don't make button disabled on error (#10699)

* Use app-header-text-color (#10711)

* Finish up config changes (#10710)

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

* Fix typo on config page + adjust icon color (#10713)

* Add ha-faded (#10651)

* Use `ha-icon-button` in `ha-icon-overflow-menu` (#10692)

* Prevent errors in `more-info-climate` if no modes are provided despite support flags (#10694)

* Make "Energy distribution today" translatable (#10696)

* Default to yaml editing when there are multiple states in condition (#10481)

* Filter out disabled entities in the statistics dev tools (#10677)

* Convert cover UI to Lit + ensure proper tilt rendering (#10671)

* Fixed ellipsis usage on graph legend entries. (#10707)

* Ensure required translations are loaded in safe-mode (#10709)

* Ensure markdown card input is a string (#10705)

* Fix chip text color variable overrides (#10722)

* Ensure `conditional` rows getting `state_color` value (#10708)

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

* Fixed invalid hour handling in AMPM mode (#10717)

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

* Installation type property during onboarding was misspelled (#10721)

* Dashboard tweaks (#10729)

* Tweak how scenes behave in generated lovelace (#10730)

* Bumped version to 20211130.0

* Improve hls stream view error handling (#10714)

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

* Move companion app config from sidebar to configuration dashboard (#10733)

* Move companion app config from sidebar to configuration dashboard

* Remove translation refrence

* Fix typo (#10734)

* Revert 10711 (#10736)

* Use backend for day month stats in energy dashboard (#10728)

* Handle 0 updates and show back on supervisor panels (#10744)

* Hide ha-icon-next if narrow (#10746)

* Change the area of scenes in editor (#10731)

* Fix faded element in change log (#10737)

* Updated text (#10747)

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

* Focus Add-ons & Backups in config panel when clicking Supervisor in sidebar (#10745)

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

* Add SmartStart/QR scan support for Z-Wave JS (#10726)

* Show disabled entity names on the device page (#10743)

* Show disabled entity names on the device page

* Update src/panels/config/devices/device-detail/ha-device-entities-card.ts

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

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

* Bumped version to 20211201.0

* Fix pointer/more-info inconsistencies for entity rows (#10025)

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

* Make graph colors themable (#10698)

* Use puzzle for addons and blur entries on click (#10755)

* Fix create backup checkbox (#10756)

* Use unit system definitions for weather units (#10657)

* handle ha-radio and ha-checkbox in ha-formfield (#10759)

* Fix SU sidebar issues (#10757)

* Use add-ons for mobile header (#10760)

* Hide updates for dev as well (#10761)

* Remove thingtalk cleanup create new automation dialog (#10748)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add missing translation (#10769)

* Update hui-graph-header-footer.ts (#10476)

* Group entities in area card by domain (#10767)

* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

* Update hui-area-card.ts

* Use chips for button rows (#10770)

* Bumped version to 20211202.0

* Show add devices fab on devices page for ZJS (#10771)

* Add default icons for button entities (#10774)

* Remove handling of the supervisor panel from the sidebar (#10773)

* Tweak ZJS dashboard (#10772)

* Guard for non numeric states (#10775)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Use correct styling for cloud certificate dialog (#10782)

* Allow overriding device class (#10777)

* Restore flex alignment for select and input-select rows (#10783)

* Add support for local only users (#10784)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Differentiate between assigned and targeting scene/automations/script (#10781)

* Add provisioned device overview to zwave js (#10785)

* Use groupBy (#10786)

* Ensure we always have an active theme name (fixes dark theme issues) (#10780)

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

* safari doesnt support overflow-wrap: anywhere

* Fix entity marker (#10787)

* Bumped version to 20211203.0

Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Lasse Rosenow <10547444+LasseRosenow@users.noreply.github.com>
Co-authored-by: David F. Mulcahey <david.mulcahey@me.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Laszlo Magyar <lmagyar1973@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Nathan Orick <cnathanorick@gmail.com>
Co-authored-by: Luca Cavalli <lcavalli@users.noreply.github.com>
Co-authored-by: amitfin <amittein@gmail.com>
Co-authored-by: Matthias de Baat <hello@matthiasdebaat.com>
Co-authored-by: rianadon <ryanadolf123@gmail.com>
Co-authored-by: Carlos Garcia Saura <CarlosGS@users.noreply.github.com>
2021-12-03 09:36:27 -08:00
Bram Kragten
7f1a321075 Merge pull request #10590 from home-assistant/dev 2021-11-09 22:06:29 +01:00
Bram Kragten
72b9f8636d Merge pull request #10578 from home-assistant/dev 2021-11-08 18:54:44 +01:00
Bram Kragten
c9cd316c0c Play dummy media to prevent app from closing (#10531) 2021-11-08 13:04:22 +01:00
Bram Kragten
6cf3580fb4 Merge pull request #10506 from home-assistant/dev 2021-11-03 11:02:34 +01:00
Bram Kragten
5d91aefb55 Merge pull request #10453 from home-assistant/dev 2021-10-28 20:24:05 +02:00
Bram Kragten
e3c0530941 Merge pull request #10426 from home-assistant/dev 2021-10-27 21:16:47 +02:00
Paulus Schoutsen
2c9223ed80 Merge pull request #10415 from home-assistant/dev (#10415)
* Use MWC components for ha-form (#10120)

* Dont create icon for supervisor (#10191)

* Fix import (#10206)

* Add "gas" device_class to customize (and sort existing ones) (#10196)

* Make zone names readable on map in dark mode (#10195)

* Tweak ha-form (#10194)

* Extract black/white row into component (#10212)

* Extract black/white row into component

* Remove unused import

* Fix dirty check/leaving automation editor (#10211)

* Add selector demo to gallery (#10213)

* Fix icon overlay for person badges (#10201)

* Convert iframe panel to Lit (#10216)

* Allow disabling an ha-form (#10218)

* Fix alarm panel badge (#10221)

* Add missing validation text (#10225)

* Apply flat polyfill globally (#10222)

* Add ha-bar to gallery (#10242)

* Handle text overflow for tabs (#10239)

* Remove "battery" device class from fixed icon list (#10246)

* Add ha-chip to gallery (#10252)

* Add netlify build script for gallery (#10253)

* Add ha-label-badge to gallery (#10248)

* Use correct build url (#10258)

* Remove "Hass.io" from translation (#10257)

* Update demo template (#10256)

* Add WebRTC stream player (#10193)

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

* Add tamper device class for binary sensor (#10268)

* Fix missing translatable energy texts (#10230)

* Consolidate all icon button logic into `<ha-icon-button>` + ensure tooltip (#9230)

* Fix sizing / positioning error for trace graph node with subsequent branches (#10049)

* Initial support for entity category (#10266)

* Add support for device configuration URL (#10251)

* Add support for device configuration URL

* Lint

* Tweak text

* Bump mdc/mwc to 0.25.2 (#10271)

* Bumped version to 20211014.0

* Warn if iframe won't be able to load the website (#10217)

* Disable ha-form while submitting entry flow (#10290)

* Convert all warning classes to ha-alert (#10289)

* ABC automation types + use MWC (#10287)

* Add "capitalize" option to `hui-timestamp-display` (#10280)

* Add additional binary device classes to inversion list (#10152)

* Fix energy onboarding `add_solar_production` button (#10275) (#10286)

* Unify default dashboard name (#10254)

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

* Fix icon buttons in Safari (#10293)

* Only render badge value if there is no icon and no image (#10310)

* Update MDI to v6.3.95 (#10313)

* Rename `stream_type` to `frontend_stream_type` (#10298)

* Fix translation key energy distribution solar (#10316)

* Prevent mwc-list-item from opening up quick-bar (#10317)

* Remove element resize hook (#10300)

* Improve WebRTC stream error handling and cleanup (#10302)

* Fix formatting of weather extrema temperatures (#10306)

* Ensure current active dark modes gets used for manually set themes (#10307)

* Add views dropdown and footer actions to the "move to view" dialog (#10172)

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

* Icon Picker (#10161)

* Use maxLiveSyncPlaybackRate in ha-hls-player (#10323)

* Revise grid neutrality energy dashboard card, modify energy dashboard presentation to match (#10054)

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

* Fix `ha-icon-button` in `ha-file-upload` (#10328)

* Use error for protection mode alert (#10315)

* Change unsupported reason container to software (#10325)

* Migrate all paper checkbox elements to mwc (#10329)

* Migrate all paper-radio elements to mwc-radio (#10327)

* Correct grid neutrality card tooltip, make consistent with new colors (#10326)

* Fix select options for add-on config (#10330)

* Migrate all paper dialogs to mwc (#10333)

* Stack gas and solar sources (#10244)

* Set default value when enabling optional value (#10247)

* Fix overflow icon color in backup dialog (#10331)

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

* Convert default state icons (#10223)

* Convert default state icons

* update

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

* Update ha-config-core.js

* Update

* Finish

* Add siren icon

* FIx

* Add curtain icons

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

* Use secondary-text-color for trailing icon (#10340)

* Use svg icons for default panels (#10342)

* Tweak icon picker a bit (#10319)

* Add support for `no-state` and `entity-no-longer-available` statistic… (#10345)

* Change dark mode input fill color (#10341)

* Replace paper progress with mwc-linear-progess (#10339)

* Bumped version to 20211020.0

* Add auto slider/box mode to number entity (#10272)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Correct automation editor event action translation (#10355)

* Convert cloud account config to Lit (#10350)

* Restore proper state badge image behavior (#10369)

* Add to do list support to markdown (#10129)

* Catch error if input_datetime state is incorrect (#10237)

* Update MDI to v6.4.95 (#10389)

* Remove deprecated icons that where replaced (#10371)

* Make all automation type pickers use natural width to be able to show… (#10391)

* Trim device name from entities on device page (#10285)

* Update markdown card to allow word to be broken (#10387)

* Fix Full Calendar Background color (#10373)

* Add additional properties to zwave_js device info panel (#10132)

* Fix various `slugify()` issues + add tests (#10383)

* Add stopPropagation to move click handlers (#10379)

* Use ha-chip for alarm control panel card (#10393)

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

* Fix timezone issues with date formatting for ES5 (#10370)

* Add automation editor to gallery (#10392)

* Use ha-chip instead of ha-label-badge for add-on capabilities (#10398)

* Do not close edit dialog when more info is escaped (#10249)

* Ensure Sortable is recreated when card editors are reopened (#10382)

* Ensure explicit `false` values from customize form get stored (#10381)

* Add running device class to binary sensor (#10400)

* Ensure consistent card look on device config page (#10386)

* Add "Keep me logged in" checkbox within login flow (#10226)

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

* Update delay label (#10284)

* Introduced ha-icon-overflow-menu component (#10352)

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

* Use ha-alert to warn about logs from custom integrations (#10396)

* Add support for hiding current weather in forecast card (#10267)

* Allow configuration_url to point to an internal panel (#10395)

* Bump Lit (#10409)

* Bump format js (#10405)

* Bump codemirror (#10404)

* Bump and patch material elements (#10406)

* Add blueprint scripts (#9504)

* Make device classes in logbook translatable (#10376)

* Improve device info add to Lovelace (#10413)

* Add navigation option from more-info to history (#9717)

* Move entities to center column on device page (#10412)

* Bumped version to 20211026.0

* Shrink new section titles in more-info dialog a bit (#10414)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Jack Wilsdon <jack.wilsdon@gmail.com>
Co-authored-by: Josh McCarty <josh@joshmccarty.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: chriss158 <edgi@arcor.de>
Co-authored-by: Kyle Niewiada <aav7fl@users.noreply.github.com>
Co-authored-by: MartinT <44962077+MartinTuroci@users.noreply.github.com>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
Co-authored-by: Will Adler <will@wtadler.com>
Co-authored-by: Rogério Ribeiro <zroger499@gmail.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Nathan Orick <cnathanorick@gmail.com>
Co-authored-by: Tobias Kündig <tobias@offline.ch>
Co-authored-by: Marc Hörsken <mback2k@users.noreply.github.com>
2021-10-26 13:35:46 -07:00
366 changed files with 24777 additions and 11880 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

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

View File

@@ -20,6 +20,7 @@ module.exports = [
"editor-trigger",
"editor-condition",
"editor-action",
"selectors",
"trace",
"trace-timeline",
],
@@ -36,6 +37,10 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "user-test",
header: "User Tests",
},
{
category: "design.home-assistant.io",
header: "Design Documentation",

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

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

View File

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

View File

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

View File

@@ -5,15 +5,23 @@ title: Alerts
# Alert `<ha-alert>`
The alert offers four severity levels that set a distinctive icon and color.
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
<ha-alert alert-type="error">
This is an error alert — check it out!
</ha-alert>
<ha-alert alert-type="warning">This is an warning alert — check it out!</ha-alert>
<ha-alert alert-type="warning">
This is an warning alert — check it out!
</ha-alert>
<ha-alert alert-type="info">This is an info alert — check it out!</ha-alert>
<ha-alert alert-type="info">
This is an info alert — check it out!
</ha-alert>
<ha-alert alert-type="success">This is an success alert — check it out!</ha-alert>
<ha-alert alert-type="success">
This is an success alert — check it out!
</ha-alert>
**Note:** This component is by [MUI](https://mui.com/components/alert/) and is not documented in the [Material Design guidelines](https://material.io).
**Note:** This component is by <a href="https://mui.com/components/alert/" rel="noopener noreferrer" target="_blank">MUI</a> and is not documented in the <a href="https://material.io" rel="noopener noreferrer" target="_blank">Material Design guidelines</a>.
1. [Guidelines](#guidelines)
2. [Implementation](#implementation)
@@ -21,8 +29,8 @@ The alert offers four severity levels that set a distinctive icon and color.
### Resources
| Type | Link | Status |
|----------------|----------------------------------|-----------|
| Design | [Home Assistant DesignKit](https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit) (Figma) | Available |
| Implementation | [Web Component](https://github.com/home-assistant/frontend/blob/dev/src/components/ha-alert.ts) (GitHub) | Available |
| Design | <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Home Assistant DesignKit</a> (Figma) | Available |
| Implementation | <a href="https://github.com/home-assistant/frontend/blob/dev/src/components/ha-alert.ts" rel="noopener noreferrer" target="_blank">Web Component</a> (GitHub) | Available |
## Guidelines
### Usage
@@ -64,78 +72,93 @@ Actions must have a tab index of 0 so that they can be reached by keyboard-only
### Example Usage
**Alert type**
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
<ha-alert alert-type="error">
This is an error alert — check it out!
</ha-alert>
<ha-alert alert-type="warning">This is an warning alert — check it out!</ha-alert>
<ha-alert alert-type="warning">
This is an warning alert — check it out!
</ha-alert>
<ha-alert alert-type="info">This is an info alert — check it out!</ha-alert>
<ha-alert alert-type="info">
This is an info alert — check it out!
</ha-alert>
<ha-alert alert-type="success">This is an success alert — check it out!</ha-alert>
<ha-alert alert-type="success">
This is an success alert — check it out!
</ha-alert>
```html
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
<ha-alert alert-type="warning">This is a warning alert — check it out!</ha-alert>
<ha-alert alert-type="info">This is an info alert — check it out!</ha-alert>
<ha-alert alert-type="success">This is a success alert — check it out!</ha-alert>
<ha-alert alert-type="error">
This is an error alert — check it out!
</ha-alert>
<ha-alert alert-type="warning">
This is a warning alert — check it out!
</ha-alert>
<ha-alert alert-type="info">
This is an info alert — check it out!
</ha-alert>
<ha-alert alert-type="success">
This is a success alert — check it out!
</ha-alert>
```
**Title**
The `title ` option should not be used without a description.
<ha-alert alert-type="error" title="Error">
This is an error alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="warning">
<title>Warning</title>
This is an warning alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="info">
<title>Info</title>
This is an info alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="success">
<title>Success </title>
This is an success alert — <strong>check it out!</strong>
<ha-alert alert-type="success" title="Success">
This is an success alert — check it out!
</ha-alert>
```html
<ha-alert alert-type="error" title="Error">
This is an error alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="warning">
<title>Warning</title>
This is an warning alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="info">
<title>Info</title>
This is an info alert — <strong>check it out!</strong>
</ha-alert>
<ha-alert alert-type="success">
<title>Success </title>
This is an success alert — <strong>check it out!</strong>
<ha-alert alert-type="success" title="Success">
This is an success alert — check it out!
</ha-alert>
```
**Dismissable**
*Documentation coming soon*
<ha-alert alert-type="success" dismissable>
This is an success alert — check it out!
</ha-alert>
```html
<ha-alert alert-type="success" dismissable>
This is an success alert — check it out!
</ha-alert>
```
**Slotted action**
<ha-alert alert-type="success">
This is an success alert — check it out!
<mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert>
```html
<ha-alert alert-type="success">
This is an success alert — check it out!
<mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert>
```
**Slotted icon**
*Documentation coming soon*
**Slotted action**
*Documentation coming soon*
**Right to left**
*Documentation coming soon*
<ha-alert alert-type="success" rtl>
This is an info alert — check it out!
</ha-alert>
```html
<ha-alert alert-type="success" rtl>
This is an info alert — check it out!
</ha-alert>
```
### API
**Properties/Attributes**

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

@@ -0,0 +1,202 @@
---
title: "User Test: Configuration menu"
---
# User Test: Configuration menu (10-17 January, 2022)
At the end of last year, we created one Configuration menu by merging Supervisor. In the next iteration, we want to organize our menu by creating logical grouping and combining duplicated features. We are conducting this test to see if we are on the right track.
* Anyone could join
* Respondents recruited on Twitter, Reddit and Home Assistant Forum
* This test is open for 10 days
* UsabilityHub for user test
* Figma for prototype
* 6 questions
* 3 tasks
* Due to some limitations by UsabilityHub, it only worked on desktop
# Results
915 respondents took part in this test and they gave 407 comments. In general there isnt a significant difference between:
* How long a respondent has been using Home Assistant
* Installation method
* How many visits to its Home Assistant in the past 3 months
* Home Assistant expertise
## Overall menu change
This prototype organized our menu by creating logical grouping and combining duplicated features. What do people think of this change?
### Stats
* 2% (21) Like extremely
* 30% (276) Like very much
* 53% (481) Neutral
* 12% (108) Dislike very much
* 3% (26) Dislike extremely
*3 respondents passed*
### Comments summary
**Like**
* Clean and decluttered
* Style looks better
* Faster to use
* Merging Supervisor into different pages
* Moving Developer tools to Settings
**Dislike**
* Moving Developer tools to Settings
* More clicks for scripts and helpers
* Too many changes at once causes a high learning curve
* Removing the word `Integrations` makes it harder to find them
* Difference between `Addons` and `Services` is a bit subtle
* No clear distinction between `Developer` and `System`
* Material Design got the Google image
**Suggestions**
* More top level menu items for example logs.
* What are settings and what not? Maybe better to name it `Configuration`
* Devices are a first-class citizen in the domain of Home Assistant, and so shouldn't be tucked away in "Settings"
* Rename Developer tools (or make it only for Home Assistant developers)
* Separate administration (for instance creating users / adding lights etc) from development activities (creating automations and scripts)
* Search Bar in Settings
* Feature to put menu items in sidebar
* Unification of add-ons and integrations
* Adding New hints to show what changed
* Give `About` a less prominent size
* Accordion view option which puts every tab below
* Dev mode and a Prod Mode
* Always show config menu (on bigger screens)
### Conclusion
We should keep our focus on organizing our menu by creating logical grouping and combining duplicated features. With these changes we make more people happy:
* Reconsider putting `Logs` as a top-level menu item
* Add a search bar
* Use the word `Integrations` with `Devices & Services`
* Moving `Developer tools` to `Settings` is a good idea
* Rename `Developer tools` to for example `Tools`
* Add `New` explanation popups to what has changed
* We could rename `Configuration` to `Settings`
* Give `About` a less prominent size
## Helpers
In Home Assistant you can create toggles, text fields, number sliders, timers and counters. Also known as `Helpers`. Where should they be placed?
### Stats
* 78% (709) respondents are using helpers. They use it for:
* 92% (645) automations and scenes
* 62% (422) dashboards
* 43% (296) virtual devices
### Comments summary
Some respondents commented that they think `Helpers` shouldnt be listed under `Automations & Services`. Although almost all respondents use it for that specific purpose.
### Conclusion
Helpers is, in addition to `Automations & Services`, also partly seen as virtual devices and dashboard entities.
* We might consider promoting them in their own top-level menu item
* Rename `Helpers` to something with `controls`
## Add person
The first task in this user test was to add a person. Since this has not changed in the current menu structure, this should be an easy assignment. How do people experience the navigation to this feature?
### Stats
95% reached the goal screen and 98% marked the task as completed. There were 18 common paths.
After the task we asked how easy it was to add a person.
* 41% (378) Extremely easy
* 48% (440) Fairly easy
* 7% (67) Neutral
* 2% (19) Somewhat difficult
* 1% (11) Very difficult
### Comments summary
*No mentionable comments *
### Conclusion
This test showed that the current navigation design works.
## YAML
In Home Assistant you can make configuration changes in YAML files. To make these changes take effect you have to reload your YAML in the UI or do a restart. How are people doing this and can they find it in this new design?
### Stats
83% reached the goal screen and 87% marked the task as completed. There were 59 common paths.
After the task we asked how easy it was to reload the YAML changes.
* 4% (40) Extremely easy
* 22% (204) Fairly easy
* 20% (179) Neutral
* 37% (336) Somewhat difficult
* 17% (156) Very difficult
And we asked if they have seen that we've moved some functionality from current `Server Controls` to `Developer Tools`.
* 57% (517) Yes
* 43% (398) No
### Comments summary
**Like**
* YAML in Developer tools
**Dislike**
* Hidden restart and reload
* YAML in Developer Tools
* Combining `Developer tools` with `Server management`
* Reload Home Assistant button isn't clear what it does
* Reload/restart Home Assistant in Developer Tools
**Suggestions**
* Reload all YAML button
* Dev mode and a Prod Mode
* Show restart/reload as buttons in System instead of overflow menu
* Explain that you can reload YAML when you want to restart your system
* YAML reloading under System
### Conclusion
This test showed two different kinds of user groups: UI and YAML users.
* Moving `Developer tools` to `Settings` is a good idea
* YAML users want reload YAML and Home Assistant restart in `System`
* Move the restart and reload button to the `System` page from the overflow menu
* Add suggestion to reload YAML when a user wants to restart
* Add reload all YAML button
## Logs
### Stats
70% reached the goal screen and 77% marked the task as completed. There were 48 common paths.
After the task we asked to find out why your Elgato light isn't working.
* 6% (57) Extremely easy
* 28% (254) Fairly easy
* 21% (188) Neutral
* 21% (196) Somewhat difficult
* 24% (220) Very difficult
### Comments summary
**Suggestions**
* Log errors on the integration page
* Problem solving center
### Conclusion
Although this test shows that a large number of respondents manage to complete the task, they find it difficult to find out the light isnt working.
* Add logs errors/warnings to the integration page
* Reconsider putting `Logs` as a top-level menu item
## Learnings for next user test
* Explain that topic is closed for comments so that you can do this test without any influence
* Mobile test should work on mobile
* Testing on an iPad got some bugs
* People like doing these kind of test and we should do them more often

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

@@ -95,7 +95,7 @@ class HassioAddonStore extends LitElement {
${this.supervisor.localize("store.repositories")}
</mwc-list-item>
<mwc-list-item>
${this.supervisor.localize("common.reload")}
${this.supervisor.localize("store.check_updates")}
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)

View File

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

View File

@@ -119,7 +119,7 @@ export class HassioBackups extends LitElement {
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({
name: {
title: this.supervisor?.localize("backup.name") || "",
title: this.supervisor.localize("backup.name"),
sortable: true,
filterable: true,
grows: true,
@@ -127,8 +127,16 @@ export class HassioBackups extends LitElement {
html`${entry || backup.slug}
<div class="secondary">${backup.secondary}</div>`,
},
size: {
title: this.supervisor.localize("backup.size"),
width: "15%",
hidden: narrow,
filterable: true,
sortable: true,
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
},
date: {
title: this.supervisor?.localize("backup.created") || "",
title: this.supervisor.localize("backup.created"),
width: "15%",
direction: "desc",
hidden: narrow,
@@ -183,16 +191,16 @@ export class HassioBackups extends LitElement {
@action=${this._handleAction}
>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.supervisor?.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item>
${this.supervisor?.localize("common.reload")}
${this.supervisor.localize("common.reload")}
</mwc-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<mwc-list-item>
${this.supervisor?.localize("backup.upload_backup")}
${this.supervisor.localize("backup.upload_backup")}
</mwc-list-item>`
: ""}
</ha-button-menu>

View File

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

View File

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

View File

@@ -30,8 +30,8 @@ class HassioCreateBackupDialog extends LitElement {
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
public showDialog(params: HassioCreateBackupDialogParams) {
this._dialogParams = params;
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
this._dialogParams = dialogParams;
this._creatingBackup = false;
}
@@ -57,7 +57,7 @@ class HassioCreateBackupDialog extends LitElement {
)}
>
${this._creatingBackup
? html` <ha-circular-progress active></ha-circular-progress>`
? html`<ha-circular-progress active></ha-circular-progress>`
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}

View File

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

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}
>
@@ -104,7 +104,7 @@ export class DialogHassioNetwork
${this.supervisor.localize("dialog.network.title")}
</span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this.supervisor.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"

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

@@ -186,7 +186,7 @@ class HassioHostInfo extends LitElement {
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
@@ -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

@@ -29,16 +29,16 @@ import {
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
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";
@@ -103,7 +103,7 @@ class UpdateAvailableCard extends LitElement {
@state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null;
@state() private _updating = false;
@state() private _error?: string;
@@ -138,7 +138,7 @@ class UpdateAvailableCard extends LitElement {
name: this._name,
})}
</p>`
: this._action === null
: !this._updating
? html`
${this._changelogContent
? html`
@@ -172,18 +172,13 @@ class UpdateAvailableCard extends LitElement {
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
${this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})}
</p>`}
</div>
${this._version !== this._version_latest && this._action === null
${this._version !== this._version_latest && !this._updating
? html`
<div class="card-actions">
${changelog
@@ -197,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>
@@ -221,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;
}
}
@@ -317,39 +317,60 @@ 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() {
this._error = undefined;
if (this._shouldCreateBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._action = "update";
this._error = undefined;
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass);
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
@@ -358,11 +379,12 @@ class UpdateAvailableCard extends LitElement {
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
this._updating = false;
return;
}
}
fireEvent(this, "update-complete");
this._updating = false;
}
static get styles(): CSSResultGroup {

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",
@@ -168,6 +171,7 @@
"@types/leaflet-draw": "^1",
"@types/marked": "^2",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.32.0",
@@ -235,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="20220118.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,
)
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

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ export const computeStateDisplay = (
if (domain === "input_datetime") {
if (state !== undefined) {
// If trying to display an explicit state, need to parse the explict state to `Date` then format.
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
try {
const components = state.split(" ");
@@ -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,2 +1,10 @@
export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(value, max) : value;
return result;
};

View File

@@ -1,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

@@ -77,7 +77,7 @@ export const computeLocalize = async (
await loadPolyfillLocales(language);
// Everytime any of the parameters change, invalidate the strings cache.
// Every time any of the parameters change, invalidate the strings cache.
cache._localizationCache = {};
return (key, ...args) => {

View File

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

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

@@ -0,0 +1,64 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-svg-icon";
@customElement("ha-data-table-icon")
class HaDataTableIcon extends LitElement {
@property() public tooltip!: string;
@property() public path!: string;
@state() private _hovered = false;
protected render(): TemplateResult {
return html`
${this._hovered ? html`<div>${this.tooltip}</div>` : ""}
<ha-svg-icon .path=${this.path}></ha-svg-icon>
`;
}
protected override firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const show = () => {
this._hovered = true;
};
const hide = () => {
this._hovered = false;
};
this.addEventListener("mouseenter", show);
this.addEventListener("focus", show);
this.addEventListener("mouseleave", hide);
this.addEventListener("blur", hide);
this.addEventListener("tap", hide);
}
static get styles() {
return css`
:host {
display: inline-block;
position: relative;
}
ha-svg-icon {
color: var(--secondary-text-color);
}
div {
position: absolute;
right: 28px;
z-index: 1002;
outline: none;
font-size: 10px;
line-height: 1;
background-color: var(--paper-tooltip-background, #616161);
color: var(--paper-tooltip-text-color, white);
padding: 8px;
border-radius: 2px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-data-table-icon": HaDataTableIcon;
}
}

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
@@ -56,8 +56,8 @@ export interface SortingChangedEvent {
export type SortingDirection = "desc" | "asc" | null;
export interface DataTableColumnContainer {
[key: string]: DataTableColumnData;
export interface DataTableColumnContainer<T = any> {
[key: string]: DataTableColumnData<T>;
}
export interface DataTableSortColumnData {
@@ -68,10 +68,11 @@ export interface DataTableSortColumnData {
direction?: SortingDirection;
}
export interface DataTableColumnData extends DataTableSortColumnData {
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
title: TemplateResult | string;
label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: <T>(data: any, row: T) => TemplateResult | string;
template?: (data: any, row: T) => TemplateResult | string;
width?: string;
maxWidth?: string;
grows?: boolean;
@@ -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 { formatAttributeName } from "../../util/hass-attributes-util";
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

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

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

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

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import {
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -48,8 +47,11 @@ class HaHLSPlayer extends LitElement {
private _exoPlayer = false;
private static streamCount = 0;
public connectedCallback() {
super.connectedCallback();
HaHLSPlayer.streamCount += 1;
if (this.hasUpdated) {
this._startHls();
}
@@ -57,6 +59,7 @@ class HaHLSPlayer extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
HaHLSPlayer.streamCount -= 1;
this._cleanUp();
}
@@ -87,18 +90,9 @@ class HaHLSPlayer extends LitElement {
this._startHls();
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external || !this.allowExoPlayer) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
this._error = undefined;
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
@@ -122,7 +116,8 @@ class HaHLSPlayer extends LitElement {
return;
}
const useExoPlayer = await useExoPlayerPromise;
const useExoPlayer =
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
const masterPlaylist = await (await masterPlaylistPromise).text();
if (!this.isConnected) {
@@ -186,6 +181,28 @@ class HaHLSPlayer extends LitElement {
});
};
private _isLLHLSSupported(): boolean {
// LL-HLS keeps multiple requests in flight, which can run into browser limitations without
// an http/2 proxy to pipeline requests. However, a small number of streams active at
// once should be OK.
// The stream count may be incremented multiple times before this function is called to check
// the count e.g. when loading a page with many streams on it. The race can work in our favor
// so we now have a better idea on if we'll use too many browser connections later.
if (HaHLSPlayer.streamCount <= 2) {
return true;
}
if (
!("performance" in window) ||
performance.getEntriesByType("resource").length === 0
) {
return false;
}
const perfEntry = performance.getEntriesByType(
"resource"
)[0] as PerformanceResourceTiming;
return "nextHopProtocol" in perfEntry && perfEntry.nextHopProtocol === "h2";
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
Hls: typeof HlsType,
@@ -197,6 +214,7 @@ class HaHLSPlayer extends LitElement {
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
maxLiveSyncPlaybackRate: 2,
lowLatencyMode: this._isLLHLSSupported(),
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);

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;
}
@@ -130,6 +131,35 @@ export class HaServiceControl extends LitElement {
this._value = this.value;
}
if (oldValue?.service !== this.value?.service) {
let updatedDefaultValue = false;
if (this._value && serviceData) {
// Set mandatory bools without a default value to false
if (!this._value.data) {
this._value.data = {};
}
serviceData.fields.forEach((field) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
this._value!.data![field.key] === undefined
) {
updatedDefaultValue = true;
this._value!.data![field.key] = false;
}
});
}
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this._value,
},
});
}
}
if (this._value?.data) {
const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this._value.data) {
@@ -203,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"
)}
@@ -251,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"
)}

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