mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 06:29:43 +00:00 
			
		
		
		
	Compare commits
	
		
			213 Commits
		
	
	
		
			20220203.1
			...
			fix-entity
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a769f84755 | ||
|   | 7abf9c2473 | ||
|   | 298296a81f | ||
|   | 6907fa5c8e | ||
|   | 546461b70f | ||
|   | 4031009c26 | ||
|   | 91e4557625 | ||
|   | f0c4b92dbb | ||
|   | 04ae8c9d14 | ||
|   | 0158610d42 | ||
|   | 5ab6121581 | ||
|   | 3d9c31aef9 | ||
|   | acfeea5c92 | ||
|   | 75e8e17073 | ||
|   | 976fd4b32d | ||
|   | 49beafbe5f | ||
|   | 151f8d5524 | ||
|   | 48355aa98e | ||
|   | fc31929f41 | ||
|   | b7c149fcc1 | ||
|   | 02d058561b | ||
|   | 4e57fb1ec1 | ||
|   | 30f79c5a46 | ||
|   | 30f7252d84 | ||
|   | 8af795a7ce | ||
|   | 8576eeae41 | ||
|   | cd740ed135 | ||
|   | 892f774792 | ||
|   | aa504fe1f8 | ||
|   | be491451d5 | ||
|   | bad184210d | ||
|   | a43b3b64b3 | ||
|   | aa831a9adf | ||
|   | 43d4f55392 | ||
|   | 130c66fb24 | ||
|   | 684c232c8c | ||
|   | 1719d062b3 | ||
|   | 87290c4330 | ||
|   | fec0dc0032 | ||
|   | 70ca27c8c9 | ||
|   | 9ae1f01ad6 | ||
|   | 0113cc3cf6 | ||
|   | 2a98ace0b3 | ||
|   | 5f69a4c165 | ||
|   | 8db22d4f88 | ||
|   | 3204dbfc4d | ||
|   | 430b47fc4a | ||
|   | 5d8b3227f3 | ||
|   | b341ee9d38 | ||
|   | e6dbbc31a8 | ||
|   | 0010bf5a8f | ||
|   | 6e2e80a297 | ||
|   | aa9ff01030 | ||
|   | 7f8ecf57d7 | ||
|   | 6be6755f6f | ||
|   | 64459a06c6 | ||
|   | df35496c6e | ||
|   | aa988c758d | ||
|   | 1dd1095d19 | ||
|   | 7e68393c84 | ||
|   | 540c06c9f7 | ||
|   | f633cc2b0d | ||
|   | 1baaf76471 | ||
|   | 8263e299a8 | ||
|   | ebd6a26554 | ||
|   | 5335772a7a | ||
|   | f5b5414461 | ||
|   | 1e6f402d0f | ||
|   | ed9d886009 | ||
|   | 940f5c0002 | ||
|   | 15d1b8b2ac | ||
|   | 73855e6f99 | ||
|   | d230541256 | ||
|   | b1f369a355 | ||
|   | e6d1e86c64 | ||
|   | eb1f94c370 | ||
|   | 27750b8b5d | ||
|   | 564a725284 | ||
|   | a5ee610af5 | ||
|   | eaf97ee7f5 | ||
|   | a14d75deec | ||
|   | 72b5721c88 | ||
|   | 94b4b818aa | ||
|   | 98699b640a | ||
|   | decc0d3e0d | ||
|   | 2281f5bafa | ||
|   | 6cac7eeff0 | ||
|   | 794bc161c8 | ||
|   | 28cd9b6408 | ||
|   | 9b4c6eea63 | ||
|   | afe044d152 | ||
|   | dc2038916b | ||
|   | cf8e2a6d02 | ||
|   | 3269b2878b | ||
|   | 29e1b7b452 | ||
|   | 3d6d07e5bd | ||
|   | 7bac41fe41 | ||
|   | 6e4b027575 | ||
|   | 728c391b5d | ||
|   | 8999ca2ea0 | ||
|   | 4fc0617289 | ||
|   | 494cc3a569 | ||
|   | cc177ef911 | ||
|   | bc6ef7780c | ||
|   | b29563a254 | ||
|   | fe8a1152c4 | ||
|   | 26689a0a85 | ||
|   | 4f6a241817 | ||
|   | eae7e82127 | ||
|   | 9500ac498c | ||
|   | 5c5459bcaf | ||
|   | 246724c59e | ||
|   | 8f5c9295d3 | ||
|   | 0abafff4c9 | ||
|   | f88ce269a7 | ||
|   | 0dc56d7983 | ||
|   | cbd0ef6b65 | ||
|   | f923228078 | ||
|   | b55c7edd70 | ||
|   | bfb90632ac | ||
|   | 3a664d45a9 | ||
|   | 53607fe8c6 | ||
|   | 9dec0f8ccd | ||
|   | 89f4fe9d20 | ||
|   | f43655eea5 | ||
|   | 6563984fdd | ||
|   | 16d8eb0be3 | ||
|   | 965fc9bc4e | ||
|   | 56cb958a47 | ||
|   | f5feb1d8aa | ||
|   | e95065ed08 | ||
|   | 68a411838d | ||
|   | ba63ab8b7a | ||
|   | 26d4599ef4 | ||
|   | d049990f04 | ||
|   | 9c8d683a19 | ||
|   | 901677bbdf | ||
|   | 8bb2374b1b | ||
|   | 520896a3c2 | ||
|   | 92db272759 | ||
|   | fc654d86c6 | ||
|   | 523afe2f6f | ||
|   | 460b9003fc | ||
|   | 2ac0ad1d98 | ||
|   | a321432175 | ||
|   | 63c9b3f830 | ||
|   | 806b1296b0 | ||
|   | 7f90ffa82f | ||
|   | db33c38e21 | ||
|   | a8c1fdd21e | ||
|   | d86a18b80b | ||
|   | bef6591548 | ||
|   | e1c07f109c | ||
|   | fb66d224ae | ||
|   | ee1fd3e865 | ||
|   | a9bfea233c | ||
|   | 35cc291118 | ||
|   | db7cac5782 | ||
|   | 099fa706a0 | ||
|   | ed84ce9692 | ||
|   | 9912d427f2 | ||
|   | 76f574f875 | ||
|   | ac90bb7088 | ||
|   | ce9f83e9a2 | ||
|   | fca7d2c5b0 | ||
|   | d7a5921e7b | ||
|   | cefa2ee183 | ||
|   | 0eeed85193 | ||
|   | fd80408de2 | ||
|   | 467a5169c0 | ||
|   | b0b3222b33 | ||
|   | b053881cef | ||
|   | 92a9ed7080 | ||
|   | 830b449006 | ||
|   | d38a8a317e | ||
|   | a0aed9112c | ||
|   | ce3b8544b9 | ||
|   | 134ed7d303 | ||
|   | dc27871189 | ||
|   | 9c9bfa2b77 | ||
|   | f02dd39619 | ||
|   | d37d99223d | ||
|   | 4db943c5ff | ||
|   | ed001fb10b | ||
|   | 5f43715dd8 | ||
|   | 5435218187 | ||
|   | 4ef5f3af89 | ||
|   | 9eea17b793 | ||
|   | 6a51e2aaad | ||
|   | 2d33327d88 | ||
|   | e9ec2da917 | ||
|   | 09d46dac61 | ||
|   | 236fa14ec3 | ||
|   | 2cb37820df | ||
|   | 869fa91ae5 | ||
|   | 22df03427f | ||
|   | e72a4e4a20 | ||
|   | ca8d31c6bb | ||
|   | 354ea88984 | ||
|   | 76af6e48cd | ||
|   | d05f807b9d | ||
|   | 4092f7f75d | ||
|   | 04668ad809 | ||
|   | 9be5a15c77 | ||
|   | 21d86f4797 | ||
|   | 45e6ec1ee2 | ||
|   | 9b97faa5e3 | ||
|   | 8730c122fd | ||
|   | 0046252e32 | ||
|   | f47440083e | ||
|   | bfaf44f9d1 | ||
|   | deba6a0db4 | ||
|   | 8d2c716fbe | 
| @@ -16,6 +16,9 @@ | |||||||
|     "runem.lit-plugin", |     "runem.lit-plugin", | ||||||
|     "ms-python.vscode-pylance" |     "ms-python.vscode-pylance" | ||||||
|   ], |   ], | ||||||
|  |   "containerEnv": { | ||||||
|  |     "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" | ||||||
|  |   }, | ||||||
|   "settings": { |   "settings": { | ||||||
|     "terminal.integrated.shell.linux": "/bin/bash", |     "terminal.integrated.shell.linux": "/bin/bash", | ||||||
|     "files.eol": "\n", |     "files.eol": "\n", | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,11 +1,10 @@ | |||||||
| diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js | diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js | ||||||
| index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644 | index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644 | ||||||
| --- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js | --- a/polyfillLoaders/EventTarget.js | ||||||
| +++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js | +++ b/polyfillLoaders/EventTarget.js | ||||||
| @@ -1,14 +1,15 @@ | @@ -6,16 +6,15 @@ | ||||||
| -let _ET, ET; |  let _ET; | ||||||
| +let _ET; |  let ET; | ||||||
| +let ET; |  | ||||||
|  export default async function EventTarget() { |  export default async function EventTarget() { | ||||||
| -    return ET || init(); | -    return ET || init(); | ||||||
| +  return ET || init(); | +  return ET || init(); | ||||||
| @@ -27,3 +26,4 @@ index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc | |||||||
| +  } | +  } | ||||||
| +  return (ET = _ET); | +  return (ET = _ET); | ||||||
|  } |  } | ||||||
|  |  //# sourceMappingURL=EventTarget.js.map | ||||||
							
								
								
									
										631
									
								
								.yarn/releases/yarn-3.0.2.cjs
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										631
									
								
								.yarn/releases/yarn-3.0.2.cjs
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										785
									
								
								.yarn/releases/yarn-3.2.0.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										785
									
								
								.yarn/releases/yarn-3.2.0.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ plugins: | |||||||
|   - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs |   - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs | ||||||
|     spec: "@yarnpkg/plugin-interactive-tools" |     spec: "@yarnpkg/plugin-interactive-tools" | ||||||
|  |  | ||||||
| yarnPath: .yarn/releases/yarn-3.0.2.cjs | yarnPath: .yarn/releases/yarn-3.2.0.cjs | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. | This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. | ||||||
|  |  | ||||||
| [](https://demo.home-assistant.io/) | [](https://demo.home-assistant.io/) | ||||||
|  |  | ||||||
| - [View demo of Home Assistant](https://demo.home-assistant.io/) | - [View demo of Home Assistant](https://demo.home-assistant.io/) | ||||||
| - [More information about Home Assistant](https://home-assistant.io) | - [More information about Home Assistant](https://home-assistant.io) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ module.exports.ignorePackages = ({ latestBuild }) => [ | |||||||
| ]; | ]; | ||||||
|  |  | ||||||
| // Files from NPM packages that we should replace with empty file | // Files from NPM packages that we should replace with empty file | ||||||
| module.exports.emptyPackages = ({ latestBuild }) => | module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => | ||||||
|   [ |   [ | ||||||
|     // Contains all color definitions for all material color sets. |     // Contains all color definitions for all material color sets. | ||||||
|     // We don't use it |     // We don't use it | ||||||
| @@ -28,6 +28,15 @@ module.exports.emptyPackages = ({ latestBuild }) => | |||||||
|       ), |       ), | ||||||
|     // This polyfill is loaded in workers to support ES5, filter it out. |     // This polyfill is loaded in workers to support ES5, filter it out. | ||||||
|     latestBuild && require.resolve("proxy-polyfill/src/index.js"), |     latestBuild && require.resolve("proxy-polyfill/src/index.js"), | ||||||
|  |     // Icons in supervisor conflict with icons in HA so we don't load. | ||||||
|  |     isHassioBuild && | ||||||
|  |       require.resolve( | ||||||
|  |         path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") | ||||||
|  |       ), | ||||||
|  |     isHassioBuild && | ||||||
|  |       require.resolve( | ||||||
|  |         path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts") | ||||||
|  |       ), | ||||||
|   ].filter(Boolean); |   ].filter(Boolean); | ||||||
|  |  | ||||||
| module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | ||||||
| @@ -196,6 +205,7 @@ module.exports.config = { | |||||||
|       publicPath: publicPath(latestBuild, paths.hassio_publicPath), |       publicPath: publicPath(latestBuild, paths.hassio_publicPath), | ||||||
|       isProdBuild, |       isProdBuild, | ||||||
|       latestBuild, |       latestBuild, | ||||||
|  |       isHassioBuild: true, | ||||||
|       defineOverlay: { |       defineOverlay: { | ||||||
|         __SUPERVISOR__: true, |         __SUPERVISOR__: true, | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| const gulp = require("gulp"); | const gulp = require("gulp"); | ||||||
| const fs = require("fs"); | const fs = require("fs"); | ||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const marked = require("marked"); | const { marked } = require("marked"); | ||||||
| const glob = require("glob"); | const glob = require("glob"); | ||||||
| const yaml = require("js-yaml"); | const yaml = require("js-yaml"); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ const source = require("vinyl-source-stream"); | |||||||
| const vinylBuffer = require("vinyl-buffer"); | const vinylBuffer = require("vinyl-buffer"); | ||||||
| const gulp = require("gulp"); | const gulp = require("gulp"); | ||||||
| const fs = require("fs"); | const fs = require("fs"); | ||||||
| const foreach = require("gulp-foreach"); | const flatmap = require("gulp-flatmap"); | ||||||
| const merge = require("gulp-merge-json"); | const merge = require("gulp-merge-json"); | ||||||
| const rename = require("gulp-rename"); | const rename = require("gulp-rename"); | ||||||
| const transform = require("gulp-json-transform"); | const transform = require("gulp-json-transform"); | ||||||
| @@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () => | |||||||
|     }) |     }) | ||||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) |     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||||
|     .pipe( |     .pipe( | ||||||
|       foreach((stream, file) => { |       flatmap((stream, file) => { | ||||||
|         // For each language generate a merged json file. It begins with the master |         // For each language generate a merged json file. It begins with the master | ||||||
|         // translation as a failsafe for untranslated strings, and merges all parent |         // translation as a failsafe for untranslated strings, and merges all parent | ||||||
|         // tags into one file for each specific subtag |         // tags into one file for each specific subtag | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ const createWebpackConfig = ({ | |||||||
|   isProdBuild, |   isProdBuild, | ||||||
|   latestBuild, |   latestBuild, | ||||||
|   isStatsBuild, |   isStatsBuild, | ||||||
|  |   isHassioBuild, | ||||||
|   dontHash, |   dontHash, | ||||||
| }) => { | }) => { | ||||||
|   if (!dontHash) { |   if (!dontHash) { | ||||||
| @@ -117,7 +118,9 @@ const createWebpackConfig = ({ | |||||||
|         }, |         }, | ||||||
|       }), |       }), | ||||||
|       new webpack.NormalModuleReplacementPlugin( |       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") |         path.resolve(paths.polymer_dir, "src/util/empty.js") | ||||||
|       ), |       ), | ||||||
|       !isProdBuild && new LogStartCompilePlugin(), |       !isProdBuild && new LogStartCompilePlugin(), | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import { LovelaceConfig } from "../../../../src/data/lovelace"; | import { LovelaceConfig } from "../../../../src/data/lovelace"; | ||||||
| import { Lovelace } from "../../../../src/panels/lovelace/types"; | import { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||||
| @@ -7,6 +7,9 @@ import "../../../../src/panels/lovelace/views/hui-view"; | |||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import "./hc-launch-screen"; | import "./hc-launch-screen"; | ||||||
|  |  | ||||||
|  | (window as any).loadCardHelpers = () => | ||||||
|  |   import("../../../../src/panels/lovelace/custom-card-helpers"); | ||||||
|  |  | ||||||
| @customElement("hc-lovelace") | @customElement("hc-lovelace") | ||||||
| class HcLovelace extends LitElement { | class HcLovelace extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -17,6 +20,8 @@ class HcLovelace extends LitElement { | |||||||
|  |  | ||||||
|   @property() public urlPath: string | null = null; |   @property() public urlPath: string | null = null; | ||||||
|  |  | ||||||
|  |   @query("hui-view") private _huiView?: HTMLElement; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     const index = this._viewIndex; |     const index = this._viewIndex; | ||||||
|     if (index === undefined) { |     if (index === undefined) { | ||||||
| @@ -75,12 +80,12 @@ class HcLovelace extends LitElement { | |||||||
|           this.lovelaceConfig.background; |           this.lovelaceConfig.background; | ||||||
|  |  | ||||||
|         if (configBackground) { |         if (configBackground) { | ||||||
|           (this.shadowRoot!.querySelector( |           this._huiView!.style.setProperty( | ||||||
|             "hui-view" |  | ||||||
|           ) as HTMLElement)!.style.setProperty( |  | ||||||
|             "--lovelace-background", |             "--lovelace-background", | ||||||
|             configBackground |             configBackground | ||||||
|           ); |           ); | ||||||
|  |         } else { | ||||||
|  |           this._huiView!.style.removeProperty("--lovelace-background"); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -113,6 +118,9 @@ class HcLovelace extends LitElement { | |||||||
|       :host > * { |       :host > * { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|       } |       } | ||||||
|  |       hui-view { | ||||||
|  |         background: var(--lovelace-background, var(--primary-background-color)); | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,8 +2,3 @@ import "../../src/resources/ha-style"; | |||||||
| import "../../src/resources/roboto"; | import "../../src/resources/roboto"; | ||||||
| import "../../src/resources/safari-14-attachshadow-patch"; | import "../../src/resources/safari-14-attachshadow-patch"; | ||||||
| import "./ha-demo"; | import "./ha-demo"; | ||||||
|  |  | ||||||
| /* polyfill for paper-dropdown */ |  | ||||||
| setTimeout(() => { |  | ||||||
|   import("web-animations-js/web-animations-next-lite.min"); |  | ||||||
| }, 1000); |  | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ module.exports = [ | |||||||
|       "editor-trigger", |       "editor-trigger", | ||||||
|       "editor-condition", |       "editor-condition", | ||||||
|       "editor-action", |       "editor-action", | ||||||
|       "selectors", |  | ||||||
|       "trace", |       "trace", | ||||||
|       "trace-timeline", |       "trace-timeline", | ||||||
|     ], |     ], | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { html, LitElement, css, TemplateResult } from "lit"; | |||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
|  | import "../../../src/components/ha-card"; | ||||||
|  |  | ||||||
| @customElement("demo-black-white-row") | @customElement("demo-black-white-row") | ||||||
| class DemoBlackWhiteRow extends LitElement { | class DemoBlackWhiteRow extends LitElement { | ||||||
|   | |||||||
| @@ -3,10 +3,20 @@ import { html, css, LitElement, TemplateResult } from "lit"; | |||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import { describeAction } from "../../../../src/data/script_i18n"; | import { describeAction } from "../../../../src/data/script_i18n"; | ||||||
|  | import { getEntity } from "../../../../src/fake_data/entity"; | ||||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; | import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
|  |  | ||||||
| const actions = [ | const ENTITIES = [ | ||||||
|  |   getEntity("scene", "kitchen_morning", "scening", { | ||||||
|  |     friendly_name: "Kitchen Morning", | ||||||
|  |   }), | ||||||
|  |   getEntity("media_player", "kitchen", "playing", { | ||||||
|  |     friendly_name: "Sonos Kitchen", | ||||||
|  |   }), | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const ACTIONS = [ | ||||||
|   { wait_template: "{{ true }}", alias: "Something with an alias" }, |   { wait_template: "{{ true }}", alias: "Something with an alias" }, | ||||||
|   { delay: "0:05" }, |   { delay: "0:05" }, | ||||||
|   { wait_template: "{{ true }}" }, |   { wait_template: "{{ true }}" }, | ||||||
| @@ -19,8 +29,20 @@ const actions = [ | |||||||
|     device_id: "abcdefgh", |     device_id: "abcdefgh", | ||||||
|     domain: "plex", |     domain: "plex", | ||||||
|     entity_id: "media_player.kitchen", |     entity_id: "media_player.kitchen", | ||||||
|  |     type: "turn_on", | ||||||
|   }, |   }, | ||||||
|   { scene: "scene.kitchen_morning" }, |   { scene: "scene.kitchen_morning" }, | ||||||
|  |   { | ||||||
|  |     service: "scene.turn_on", | ||||||
|  |     target: { entity_id: "scene.kitchen_morning" }, | ||||||
|  |     metadata: {}, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     service: "media_player.play_media", | ||||||
|  |     target: { entity_id: "media_player.kitchen" }, | ||||||
|  |     data: { media_content_id: "", media_content_type: "" }, | ||||||
|  |     metadata: { title: "Happy Song" }, | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     wait_for_trigger: [ |     wait_for_trigger: [ | ||||||
|       { |       { | ||||||
| @@ -52,7 +74,7 @@ export class DemoAutomationDescribeAction extends LitElement { | |||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Actions"> |       <ha-card header="Actions"> | ||||||
|         ${actions.map( |         ${ACTIONS.map( | ||||||
|           (conf) => html` |           (conf) => html` | ||||||
|             <div class="action"> |             <div class="action"> | ||||||
|               <span>${describeAction(this.hass, conf as any)}</span> |               <span>${describeAction(this.hass, conf as any)}</span> | ||||||
| @@ -68,6 +90,7 @@ export class DemoAutomationDescribeAction extends LitElement { | |||||||
|     super.firstUpdated(changedProps); |     super.firstUpdated(changedProps); | ||||||
|     const hass = provideHass(this); |     const hass = provideHass(this); | ||||||
|     hass.updateTranslations(null, "en"); |     hass.updateTranslations(null, "en"); | ||||||
|  |     hass.addEntities(ENTITIES); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles() { |   static get styles() { | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t | |||||||
| import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id"; | import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id"; | ||||||
| import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; | import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; | ||||||
| import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; | import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; | ||||||
| import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene"; | import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene"; | ||||||
| import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; | import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; | ||||||
| import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger"; | import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger"; | ||||||
| import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| --- |  | ||||||
| title: Selectors |  | ||||||
| --- |  | ||||||
| @@ -1,102 +0,0 @@ | |||||||
| /* eslint-disable lit/no-template-arrow */ |  | ||||||
| import { LitElement, TemplateResult, html } from "lit"; |  | ||||||
| import { customElement, state } from "lit/decorators"; |  | ||||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; |  | ||||||
| import type { HomeAssistant } from "../../../../src/types"; |  | ||||||
| import "../../components/demo-black-white-row"; |  | ||||||
| import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; |  | ||||||
| import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; |  | ||||||
| import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; |  | ||||||
| import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; |  | ||||||
| import "../../../../src/panels/config/automation/trigger/ha-automation-trigger"; |  | ||||||
| import { Selector } from "../../../../src/data/selector"; |  | ||||||
| import "../../../../src/components/ha-selector/ha-selector"; |  | ||||||
|  |  | ||||||
| const SCHEMAS: { name: string; selector: Selector }[] = [ |  | ||||||
|   { name: "Addon", selector: { addon: {} } }, |  | ||||||
|  |  | ||||||
|   { name: "Entity", selector: { entity: {} } }, |  | ||||||
|   { name: "Device", selector: { device: {} } }, |  | ||||||
|   { name: "Area", selector: { area: {} } }, |  | ||||||
|   { name: "Target", selector: { target: {} } }, |  | ||||||
|   { |  | ||||||
|     name: "Number", |  | ||||||
|     selector: { |  | ||||||
|       number: { |  | ||||||
|         min: 0, |  | ||||||
|         max: 10, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { name: "Boolean", selector: { boolean: {} } }, |  | ||||||
|   { name: "Time", selector: { time: {} } }, |  | ||||||
|   { name: "Action", selector: { action: {} } }, |  | ||||||
|   { name: "Text", selector: { text: { multiline: false } } }, |  | ||||||
|   { name: "Text Multiline", selector: { text: { multiline: true } } }, |  | ||||||
|   { name: "Object", selector: { object: {} } }, |  | ||||||
|   { |  | ||||||
|     name: "Select", |  | ||||||
|     selector: { |  | ||||||
|       select: { |  | ||||||
|         options: ["Everyone Home", "Some Home", "All gone"], |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| @customElement("demo-automation-selectors") |  | ||||||
| class DemoHaSelector extends LitElement { |  | ||||||
|   @state() private hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   private data: any = SCHEMAS.map(() => undefined); |  | ||||||
|  |  | ||||||
|   constructor() { |  | ||||||
|     super(); |  | ||||||
|     const hass = provideHass(this); |  | ||||||
|     hass.updateTranslations(null, "en"); |  | ||||||
|     hass.updateTranslations("config", "en"); |  | ||||||
|     mockEntityRegistry(hass); |  | ||||||
|     mockDeviceRegistry(hass); |  | ||||||
|     mockAreaRegistry(hass); |  | ||||||
|     mockHassioSupervisor(hass); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     const valueChanged = (ev) => { |  | ||||||
|       const sampleIdx = ev.target.sampleIdx; |  | ||||||
|       this.data[sampleIdx] = ev.detail.value; |  | ||||||
|       this.requestUpdate(); |  | ||||||
|     }; |  | ||||||
|     return html` |  | ||||||
|       ${SCHEMAS.map( |  | ||||||
|         (info, sampleIdx) => html` |  | ||||||
|           <demo-black-white-row |  | ||||||
|             .title=${info.name} |  | ||||||
|             .value=${{ selector: info.selector, data: this.data[sampleIdx] }} |  | ||||||
|           > |  | ||||||
|             ${["light", "dark"].map( |  | ||||||
|               (slot) => |  | ||||||
|                 html` |  | ||||||
|                   <ha-selector |  | ||||||
|                     slot=${slot} |  | ||||||
|                     .hass=${this.hass} |  | ||||||
|                     .selector=${info.selector} |  | ||||||
|                     .label=${info.name} |  | ||||||
|                     .value=${this.data[sampleIdx]} |  | ||||||
|                     .sampleIdx=${sampleIdx} |  | ||||||
|                     @value-changed=${valueChanged} |  | ||||||
|                   ></ha-selector> |  | ||||||
|                 ` |  | ||||||
|             )} |  | ||||||
|           </demo-black-white-row> |  | ||||||
|         ` |  | ||||||
|       )} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "demo-automation-selectors": DemoHaSelector; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,11 +1,17 @@ | |||||||
| /* eslint-disable lit/no-template-arrow */ | /* eslint-disable lit/no-template-arrow */ | ||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import { LitElement, TemplateResult, html } from "lit"; | import { LitElement, TemplateResult, html } from "lit"; | ||||||
| import { customElement } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; | import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; | ||||||
| import type { HaFormSchema } from "../../../../src/components/ha-form/types"; | import type { HaFormSchema } from "../../../../src/components/ha-form/types"; | ||||||
| import "../../../../src/components/ha-form/ha-form"; | import "../../../../src/components/ha-form/ha-form"; | ||||||
| import "../../components/demo-black-white-row"; | import "../../components/demo-black-white-row"; | ||||||
|  | import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | ||||||
|  | import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | ||||||
|  | import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; | ||||||
|  | import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | ||||||
|  | import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||||
|  | import { HomeAssistant } from "../../../../src/types"; | ||||||
|  |  | ||||||
| const SCHEMAS: { | const SCHEMAS: { | ||||||
|   title: string; |   title: string; | ||||||
| @@ -14,6 +20,63 @@ const SCHEMAS: { | |||||||
|   schema: HaFormSchema[]; |   schema: HaFormSchema[]; | ||||||
|   data?: Record<string, any>; |   data?: Record<string, any>; | ||||||
| }[] = [ | }[] = [ | ||||||
|  |   { | ||||||
|  |     title: "Selectors", | ||||||
|  |     translations: { | ||||||
|  |       addon: "Addon", | ||||||
|  |       entity: "Entity", | ||||||
|  |       device: "Device", | ||||||
|  |       area: "Area", | ||||||
|  |       target: "Target", | ||||||
|  |       number: "Number", | ||||||
|  |       boolean: "Boolean", | ||||||
|  |       time: "Time", | ||||||
|  |       action: "Action", | ||||||
|  |       text: "Text", | ||||||
|  |       text_multiline: "Text Multiline", | ||||||
|  |       object: "Object", | ||||||
|  |       select: "Select", | ||||||
|  |       icon: "Icon", | ||||||
|  |       media: "Media", | ||||||
|  |     }, | ||||||
|  |     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"] }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: "icon", | ||||||
|  |         selector: { | ||||||
|  |           icon: {}, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: "media", | ||||||
|  |         selector: { | ||||||
|  |           media: {}, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     title: "Authentication", |     title: "Authentication", | ||||||
|     translations: { |     translations: { | ||||||
| @@ -50,13 +113,11 @@ const SCHEMAS: { | |||||||
|       { |       { | ||||||
|         type: "boolean", |         type: "boolean", | ||||||
|         name: "bool", |         name: "bool", | ||||||
|         optional: true, |  | ||||||
|         default: false, |         default: false, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         type: "integer", |         type: "integer", | ||||||
|         name: "int", |         name: "int", | ||||||
|         optional: true, |  | ||||||
|         default: 10, |         default: 10, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -67,7 +128,6 @@ const SCHEMAS: { | |||||||
|       { |       { | ||||||
|         type: "string", |         type: "string", | ||||||
|         name: "string", |         name: "string", | ||||||
|         optional: true, |  | ||||||
|         default: "Default", |         default: "Default", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -77,7 +137,6 @@ const SCHEMAS: { | |||||||
|           ["other", "other"], |           ["other", "other"], | ||||||
|         ], |         ], | ||||||
|         name: "select", |         name: "select", | ||||||
|         optional: true, |  | ||||||
|         default: "default", |         default: "default", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -87,7 +146,6 @@ const SCHEMAS: { | |||||||
|           other: "Other", |           other: "Other", | ||||||
|         }, |         }, | ||||||
|         name: "multi", |         name: "multi", | ||||||
|         optional: true, |  | ||||||
|         default: ["default"], |         default: ["default"], | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -108,7 +166,6 @@ const SCHEMAS: { | |||||||
|       { |       { | ||||||
|         type: "integer", |         type: "integer", | ||||||
|         name: "int with default", |         name: "int with default", | ||||||
|         optional: true, |  | ||||||
|         default: 10, |         default: 10, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -122,7 +179,6 @@ const SCHEMAS: { | |||||||
|       { |       { | ||||||
|         type: "integer", |         type: "integer", | ||||||
|         name: "int range optional", |         name: "int range optional", | ||||||
|         optional: true, |  | ||||||
|         valueMin: 0, |         valueMin: 0, | ||||||
|         valueMax: 10, |         valueMax: 10, | ||||||
|       }, |       }, | ||||||
| @@ -148,7 +204,6 @@ const SCHEMAS: { | |||||||
|           ["other", "Other"], |           ["other", "Other"], | ||||||
|         ], |         ], | ||||||
|         name: "select optional", |         name: "select optional", | ||||||
|         optional: true, |  | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         type: "select", |         type: "select", | ||||||
| @@ -161,7 +216,6 @@ const SCHEMAS: { | |||||||
|           ["option", "1000"], |           ["option", "1000"], | ||||||
|         ], |         ], | ||||||
|         name: "select many otions", |         name: "select many otions", | ||||||
|         optional: true, |  | ||||||
|         default: "default", |         default: "default", | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
| @@ -190,7 +244,6 @@ const SCHEMAS: { | |||||||
|           option: "1000", |           option: "1000", | ||||||
|         }, |         }, | ||||||
|         name: "multi many otions", |         name: "multi many otions", | ||||||
|         optional: true, |  | ||||||
|         default: ["default"], |         default: ["default"], | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
| @@ -239,23 +292,35 @@ const SCHEMAS: { | |||||||
|         valueMin: 1, |         valueMin: 1, | ||||||
|         valueMax: 65535, |         valueMax: 65535, | ||||||
|         name: "port", |         name: "port", | ||||||
|         optional: true, |  | ||||||
|         default: 80, |         default: 80, | ||||||
|       }, |       }, | ||||||
|       { type: "string", name: "path", optional: true, default: "/" }, |       { type: "string", name: "path", default: "/" }, | ||||||
|       { type: "boolean", name: "ssl", optional: true, default: false }, |       { type: "boolean", name: "ssl", default: false }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-components-ha-form") | @customElement("demo-components-ha-form") | ||||||
| class DemoHaForm extends LitElement { | class DemoHaForm extends LitElement { | ||||||
|  |   @state() private hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   private data = SCHEMAS.map( |   private data = SCHEMAS.map( | ||||||
|     ({ schema, data }) => data || computeInitialHaFormData(schema) |     ({ schema, data }) => data || computeInitialHaFormData(schema) | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private disabled = SCHEMAS.map(() => false); |   private disabled = SCHEMAS.map(() => false); | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |     const hass = provideHass(this); | ||||||
|  |     hass.updateTranslations(null, "en"); | ||||||
|  |     hass.updateTranslations("config", "en"); | ||||||
|  |     mockEntityRegistry(hass); | ||||||
|  |     mockDeviceRegistry(hass); | ||||||
|  |     mockAreaRegistry(hass); | ||||||
|  |     mockHassioSupervisor(hass); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${SCHEMAS.map((info, idx) => { |       ${SCHEMAS.map((info, idx) => { | ||||||
| @@ -278,6 +343,7 @@ class DemoHaForm extends LitElement { | |||||||
|               (slot) => html` |               (slot) => html` | ||||||
|                 <ha-form |                 <ha-form | ||||||
|                   slot=${slot} |                   slot=${slot} | ||||||
|  |                   .hass=${this.hass} | ||||||
|                   .data=${this.data[idx]} |                   .data=${this.data[idx]} | ||||||
|                   .schema=${info.schema} |                   .schema=${info.schema} | ||||||
|                   .error=${info.error} |                   .error=${info.error} | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| --- | --- | ||||||
| title: Target Selectors | title: Selectors | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/). | ||||||
|   | |||||||
| @@ -12,6 +12,100 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; | |||||||
| import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | ||||||
| import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | ||||||
| import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | ||||||
|  | import { getEntity } from "../../../../src/fake_data/entity"; | ||||||
|  | import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; | ||||||
|  | import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; | ||||||
|  |  | ||||||
|  | const ENTITIES = [ | ||||||
|  |   getEntity("alarm_control_panel", "alarm", "disarmed", { | ||||||
|  |     friendly_name: "Alarm", | ||||||
|  |   }), | ||||||
|  |   getEntity("media_player", "livingroom", "playing", { | ||||||
|  |     friendly_name: "Livingroom", | ||||||
|  |   }), | ||||||
|  |   getEntity("media_player", "lounge", "idle", { | ||||||
|  |     friendly_name: "Lounge", | ||||||
|  |     supported_features: 444983, | ||||||
|  |   }), | ||||||
|  |   getEntity("light", "bedroom", "on", { | ||||||
|  |     friendly_name: "Bedroom", | ||||||
|  |   }), | ||||||
|  |   getEntity("switch", "coffee", "off", { | ||||||
|  |     friendly_name: "Coffee", | ||||||
|  |   }), | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const DEVICES = [ | ||||||
|  |   { | ||||||
|  |     area_id: "bedroom", | ||||||
|  |     configuration_url: null, | ||||||
|  |     config_entries: ["config_entry_1"], | ||||||
|  |     connections: [], | ||||||
|  |     disabled_by: null, | ||||||
|  |     entry_type: null, | ||||||
|  |     id: "device_1", | ||||||
|  |     identifiers: [["demo", "volume1"] as [string, string]], | ||||||
|  |     manufacturer: null, | ||||||
|  |     model: null, | ||||||
|  |     name_by_user: null, | ||||||
|  |     name: "Dishwasher", | ||||||
|  |     sw_version: null, | ||||||
|  |     hw_version: null, | ||||||
|  |     via_device_id: null, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     area_id: "backyard", | ||||||
|  |     configuration_url: null, | ||||||
|  |     config_entries: ["config_entry_2"], | ||||||
|  |     connections: [], | ||||||
|  |     disabled_by: null, | ||||||
|  |     entry_type: null, | ||||||
|  |     id: "device_2", | ||||||
|  |     identifiers: [["demo", "pwm1"] as [string, string]], | ||||||
|  |     manufacturer: null, | ||||||
|  |     model: null, | ||||||
|  |     name_by_user: null, | ||||||
|  |     name: "Lamp", | ||||||
|  |     sw_version: null, | ||||||
|  |     hw_version: null, | ||||||
|  |     via_device_id: null, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     area_id: null, | ||||||
|  |     configuration_url: null, | ||||||
|  |     config_entries: ["config_entry_3"], | ||||||
|  |     connections: [], | ||||||
|  |     disabled_by: null, | ||||||
|  |     entry_type: null, | ||||||
|  |     id: "device_3", | ||||||
|  |     identifiers: [["demo", "pwm1"] as [string, string]], | ||||||
|  |     manufacturer: null, | ||||||
|  |     model: null, | ||||||
|  |     name_by_user: "User name", | ||||||
|  |     name: "Technical name", | ||||||
|  |     sw_version: null, | ||||||
|  |     hw_version: null, | ||||||
|  |     via_device_id: null, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const AREAS = [ | ||||||
|  |   { | ||||||
|  |     area_id: "backyard", | ||||||
|  |     name: "Backyard", | ||||||
|  |     picture: null, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     area_id: "bedroom", | ||||||
|  |     name: "Bedroom", | ||||||
|  |     picture: null, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     area_id: "livingroom", | ||||||
|  |     name: "Livingroom", | ||||||
|  |     picture: null, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
| const SCHEMAS: { | const SCHEMAS: { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -21,7 +115,12 @@ const SCHEMAS: { | |||||||
|     name: "One of each", |     name: "One of each", | ||||||
|     input: { |     input: { | ||||||
|       entity: { name: "Entity", selector: { entity: {} } }, |       entity: { name: "Entity", selector: { entity: {} } }, | ||||||
|  |       attribute: { | ||||||
|  |         name: "Attribute", | ||||||
|  |         selector: { attribute: { entity_id: "" } }, | ||||||
|  |       }, | ||||||
|       device: { name: "Device", selector: { device: {} } }, |       device: { name: "Device", selector: { device: {} } }, | ||||||
|  |       duration: { name: "Duration", selector: { duration: {} } }, | ||||||
|       addon: { name: "Addon", selector: { addon: {} } }, |       addon: { name: "Addon", selector: { addon: {} } }, | ||||||
|       area: { name: "Area", selector: { area: {} } }, |       area: { name: "Area", selector: { area: {} } }, | ||||||
|       target: { name: "Target", selector: { target: {} } }, |       target: { name: "Target", selector: { target: {} } }, | ||||||
| @@ -48,23 +147,34 @@ const SCHEMAS: { | |||||||
|       boolean: { name: "Boolean", selector: { boolean: {} } }, |       boolean: { name: "Boolean", selector: { boolean: {} } }, | ||||||
|       time: { name: "Time", selector: { time: {} } }, |       time: { name: "Time", selector: { time: {} } }, | ||||||
|       action: { name: "Action", selector: { action: {} } }, |       action: { name: "Action", selector: { action: {} } }, | ||||||
|       text: { name: "Text", selector: { text: { multiline: false } } }, |       text: { | ||||||
|  |         name: "Text", | ||||||
|  |         selector: { text: {} }, | ||||||
|  |       }, | ||||||
|  |       password: { | ||||||
|  |         name: "Password", | ||||||
|  |         selector: { text: { type: "password" } }, | ||||||
|  |       }, | ||||||
|       text_multiline: { |       text_multiline: { | ||||||
|         name: "Text multiline", |         name: "Text multiline", | ||||||
|         selector: { text: { multiline: true } }, |         selector: { | ||||||
|  |           text: { multiline: true }, | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|       object: { name: "Object", selector: { object: {} } }, |       object: { name: "Object", selector: { object: {} } }, | ||||||
|       select: { |       select: { | ||||||
|         name: "Select", |         name: "Select", | ||||||
|         selector: { select: { options: ["Option 1", "Option 2"] } }, |         selector: { select: { options: ["Option 1", "Option 2"] } }, | ||||||
|       }, |       }, | ||||||
|  |       icon: { name: "Icon", selector: { icon: {} } }, | ||||||
|  |       media: { name: "Media", selector: { media: {} } }, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-components-ha-selector") | @customElement("demo-components-ha-selector") | ||||||
| class DemoHaSelector extends LitElement { | class DemoHaSelector extends LitElement implements ProvideHassElement { | ||||||
|   @state() private hass!: HomeAssistant; |   @state() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   private data = SCHEMAS.map(() => ({})); |   private data = SCHEMAS.map(() => ({})); | ||||||
|  |  | ||||||
| @@ -73,12 +183,130 @@ class DemoHaSelector extends LitElement { | |||||||
|     const hass = provideHass(this); |     const hass = provideHass(this); | ||||||
|     hass.updateTranslations(null, "en"); |     hass.updateTranslations(null, "en"); | ||||||
|     hass.updateTranslations("config", "en"); |     hass.updateTranslations("config", "en"); | ||||||
|  |     hass.addEntities(ENTITIES); | ||||||
|     mockEntityRegistry(hass); |     mockEntityRegistry(hass); | ||||||
|     mockDeviceRegistry(hass); |     mockDeviceRegistry(hass, DEVICES); | ||||||
|     mockAreaRegistry(hass); |     mockAreaRegistry(hass, AREAS); | ||||||
|     mockHassioSupervisor(hass); |     mockHassioSupervisor(hass); | ||||||
|  |     hass.mockWS("auth/sign_path", (params) => params); | ||||||
|  |     hass.mockWS("media_player/browse_media", this._browseMedia); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public provideHass(el) { | ||||||
|  |     el.hass = this.hass; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |     this.addEventListener("show-dialog", this._dialogManager); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public disconnectedCallback() { | ||||||
|  |     super.disconnectedCallback(); | ||||||
|  |     this.removeEventListener("show-dialog", this._dialogManager); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _browseMedia = ({ media_content_id }) => { | ||||||
|  |     if (media_content_id === undefined) { | ||||||
|  |       return { | ||||||
|  |         title: "Media", | ||||||
|  |         media_class: "directory", | ||||||
|  |         media_content_type: "", | ||||||
|  |         media_content_id: "media-source://media_source/local/.", | ||||||
|  |         can_play: false, | ||||||
|  |         can_expand: true, | ||||||
|  |         children_media_class: "directory", | ||||||
|  |         thumbnail: null, | ||||||
|  |         children: [ | ||||||
|  |           { | ||||||
|  |             title: "Misc", | ||||||
|  |             media_class: "directory", | ||||||
|  |             media_content_type: "", | ||||||
|  |             media_content_id: "media-source://media_source/local/misc", | ||||||
|  |             can_play: false, | ||||||
|  |             can_expand: true, | ||||||
|  |             children_media_class: null, | ||||||
|  |             thumbnail: null, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: "Movies", | ||||||
|  |             media_class: "directory", | ||||||
|  |             media_content_type: "", | ||||||
|  |             media_content_id: "media-source://media_source/local/movies", | ||||||
|  |             can_play: true, | ||||||
|  |             can_expand: true, | ||||||
|  |             children_media_class: "movie", | ||||||
|  |             thumbnail: null, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: "Music", | ||||||
|  |             media_class: "album", | ||||||
|  |             media_content_type: "", | ||||||
|  |             media_content_id: "media-source://media_source/local/music", | ||||||
|  |             can_play: false, | ||||||
|  |             can_expand: true, | ||||||
|  |             children_media_class: "music", | ||||||
|  |             thumbnail: "/images/album_cover_2.jpg", | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       title: "Subfolder", | ||||||
|  |       media_class: "directory", | ||||||
|  |       media_content_type: "", | ||||||
|  |       media_content_id: "media-source://media_source/local/sub", | ||||||
|  |       can_play: false, | ||||||
|  |       can_expand: true, | ||||||
|  |       children_media_class: "directory", | ||||||
|  |       thumbnail: null, | ||||||
|  |       children: [ | ||||||
|  |         { | ||||||
|  |           title: "audio.mp3", | ||||||
|  |           media_class: "music", | ||||||
|  |           media_content_type: "audio/mpeg", | ||||||
|  |           media_content_id: "media-source://media_source/local/audio.mp3", | ||||||
|  |           can_play: true, | ||||||
|  |           can_expand: false, | ||||||
|  |           children_media_class: null, | ||||||
|  |           thumbnail: "/images/album_cover.jpg", | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           title: "image.jpg", | ||||||
|  |           media_class: "image", | ||||||
|  |           media_content_type: "image/jpeg", | ||||||
|  |           media_content_id: "media-source://media_source/local/image.jpg", | ||||||
|  |           can_play: true, | ||||||
|  |           can_expand: false, | ||||||
|  |           children_media_class: null, | ||||||
|  |           thumbnail: null, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           title: "movie.mp4", | ||||||
|  |           media_class: "movie", | ||||||
|  |           media_content_type: "image/jpeg", | ||||||
|  |           media_content_id: "media-source://media_source/local/movie.mp4", | ||||||
|  |           can_play: true, | ||||||
|  |           can_expand: false, | ||||||
|  |           children_media_class: null, | ||||||
|  |           thumbnail: null, | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _dialogManager = (e) => { | ||||||
|  |     const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; | ||||||
|  |     showDialog( | ||||||
|  |       this, | ||||||
|  |       this.shadowRoot!, | ||||||
|  |       dialogTag, | ||||||
|  |       dialogParams, | ||||||
|  |       dialogImport, | ||||||
|  |       addHistory | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${SCHEMAS.map((info, idx) => { |       ${SCHEMAS.map((info, idx) => { | ||||||
| @@ -117,7 +345,6 @@ class DemoHaSelector extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|     paper-input, |  | ||||||
|     ha-selector { |     ha-selector { | ||||||
|       width: 60; |       width: 60; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ const createConfigEntry = ( | |||||||
|   source: "zeroconf", |   source: "zeroconf", | ||||||
|   state: "loaded", |   state: "loaded", | ||||||
|   supports_options: false, |   supports_options: false, | ||||||
|  |   supports_remove_device: false, | ||||||
|   supports_unload: true, |   supports_unload: true, | ||||||
|   disabled_by: null, |   disabled_by: null, | ||||||
|   pref_disable_new_entities: false, |   pref_disable_new_entities: false, | ||||||
|   | |||||||
| @@ -42,7 +42,9 @@ class HassioAddonRepositoryEl extends LitElement { | |||||||
|     const repo = this.repo; |     const repo = this.repo; | ||||||
|     let _addons = this.addons; |     let _addons = this.addons; | ||||||
|     if (!this.hass.userData?.showAdvanced) { |     if (!this.hass.userData?.showAdvanced) { | ||||||
|       _addons = _addons.filter((addon) => !addon.advanced); |       _addons = _addons.filter( | ||||||
|  |         (addon) => !addon.advanced && addon.stage === "stable" | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|     const addons = this._getAddons(_addons, this.filter); |     const addons = this._getAddons(_addons, this.filter); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import memoizeOne from "memoize-one"; | |||||||
| import { atLeastVersion } from "../../../src/common/config/version"; | import { atLeastVersion } from "../../../src/common/config/version"; | ||||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
| import { navigate } from "../../../src/common/navigate"; | import { navigate } from "../../../src/common/navigate"; | ||||||
| import "../../../src/common/search/search-input"; | import "../../../src/components/search-input"; | ||||||
| import { extractSearchParam } from "../../../src/common/url/search-params"; | import { extractSearchParam } from "../../../src/common/url/search-params"; | ||||||
| import "../../../src/components/ha-button-menu"; | import "../../../src/components/ha-button-menu"; | ||||||
| import "../../../src/components/ha-icon-button"; | import "../../../src/components/ha-icon-button"; | ||||||
| @@ -221,13 +221,14 @@ class HassioAddonStore extends LitElement { | |||||||
|         margin-top: 24px; |         margin-top: 24px; | ||||||
|       } |       } | ||||||
|       .search { |       .search { | ||||||
|         padding: 0 16px; |         position: sticky; | ||||||
|         background: var(--sidebar-background-color); |         top: 0; | ||||||
|         border-bottom: 1px solid var(--divider-color); |         z-index: 2; | ||||||
|       } |       } | ||||||
|       .search search-input { |       search-input { | ||||||
|         position: relative; |         display: block; | ||||||
|         top: 2px; |         --mdc-text-field-fill-color: var(--sidebar-background-color); | ||||||
|  |         --mdc-text-field-idle-line-color: var(--divider-color); | ||||||
|       } |       } | ||||||
|       .advanced { |       .advanced { | ||||||
|         padding: 12px; |         padding: 12px; | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -11,10 +9,11 @@ import { | |||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit"; | } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import "web-animations-js/web-animations-next-lite.min"; | import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; | ||||||
| import "../../../../src/components/buttons/ha-progress-button"; | import "../../../../src/components/buttons/ha-progress-button"; | ||||||
| import "../../../../src/components/ha-alert"; | import "../../../../src/components/ha-alert"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
|  | import "../../../../src/components/ha-select"; | ||||||
| import { | import { | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
|   HassioAddonSetOptionParams, |   HassioAddonSetOptionParams, | ||||||
| @@ -57,49 +56,44 @@ class HassioAddonAudio extends LitElement { | |||||||
|           ${this._error |           ${this._error | ||||||
|             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|             : ""} |             : ""} | ||||||
|  |           ${this._inputDevices && | ||||||
|           <paper-dropdown-menu |           html`<ha-select | ||||||
|             .label=${this.supervisor.localize( |             .label=${this.supervisor.localize( | ||||||
|               "addon.configuration.audio.input" |               "addon.configuration.audio.input" | ||||||
|             )} |             )} | ||||||
|             @iron-select=${this._setInputDevice} |             @selected=${this._setInputDevice} | ||||||
|  |             @closed=${stopPropagation} | ||||||
|  |             fixedMenuPosition | ||||||
|  |             naturalMenuWidth | ||||||
|  |             .value=${this._selectedInput!} | ||||||
|           > |           > | ||||||
|             <paper-listbox |             ${this._inputDevices.map( | ||||||
|               slot="dropdown-content" |  | ||||||
|               attr-for-selected="device" |  | ||||||
|               .selected=${this._selectedInput!} |  | ||||||
|             > |  | ||||||
|               ${this._inputDevices && |  | ||||||
|               this._inputDevices.map( |  | ||||||
|               (item) => html` |               (item) => html` | ||||||
|                   <paper-item device=${item.device || ""}> |                 <mwc-list-item .value=${item.device || ""}> | ||||||
|                   ${item.name} |                   ${item.name} | ||||||
|                   </paper-item> |                 </mwc-list-item> | ||||||
|               ` |               ` | ||||||
|             )} |             )} | ||||||
|             </paper-listbox> |           </ha-select>`} | ||||||
|           </paper-dropdown-menu> |           ${this._outputDevices && | ||||||
|           <paper-dropdown-menu |           html`<ha-select | ||||||
|             .label=${this.supervisor.localize( |             .label=${this.supervisor.localize( | ||||||
|               "addon.configuration.audio.output" |               "addon.configuration.audio.output" | ||||||
|             )} |             )} | ||||||
|             @iron-select=${this._setOutputDevice} |             @selected=${this._setOutputDevice} | ||||||
|  |             @closed=${stopPropagation} | ||||||
|  |             fixedMenuPosition | ||||||
|  |             naturalMenuWidth | ||||||
|  |             .value=${this._selectedOutput!} | ||||||
|           > |           > | ||||||
|             <paper-listbox |             ${this._outputDevices.map( | ||||||
|               slot="dropdown-content" |  | ||||||
|               attr-for-selected="device" |  | ||||||
|               .selected=${this._selectedOutput!} |  | ||||||
|             > |  | ||||||
|               ${this._outputDevices && |  | ||||||
|               this._outputDevices.map( |  | ||||||
|               (item) => html` |               (item) => html` | ||||||
|                   <paper-item device=${item.device || ""} |                 <mwc-list-item .value=${item.device || ""} | ||||||
|                     >${item.name}</paper-item |                   >${item.name}</mwc-list-item | ||||||
|                 > |                 > | ||||||
|               ` |               ` | ||||||
|             )} |             )} | ||||||
|             </paper-listbox> |           </ha-select>`} | ||||||
|           </paper-dropdown-menu> |  | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button @click=${this._saveSettings}> |           <ha-progress-button @click=${this._saveSettings}> | ||||||
| @@ -116,8 +110,7 @@ class HassioAddonAudio extends LitElement { | |||||||
|       hassioStyle, |       hassioStyle, | ||||||
|       css` |       css` | ||||||
|         :host, |         :host, | ||||||
|         ha-card, |         ha-card { | ||||||
|         paper-dropdown-menu { |  | ||||||
|           display: block; |           display: block; | ||||||
|         } |         } | ||||||
|         paper-item { |         paper-item { | ||||||
| @@ -126,24 +119,30 @@ class HassioAddonAudio extends LitElement { | |||||||
|         .card-actions { |         .card-actions { | ||||||
|           text-align: right; |           text-align: right; | ||||||
|         } |         } | ||||||
|  |         ha-select { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |         ha-select:last-child { | ||||||
|  |           margin-top: 8px; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected update(changedProperties: PropertyValues): void { |   protected willUpdate(changedProperties: PropertyValues): void { | ||||||
|     super.update(changedProperties); |     super.willUpdate(changedProperties); | ||||||
|     if (changedProperties.has("addon")) { |     if (changedProperties.has("addon")) { | ||||||
|       this._addonChanged(); |       this._addonChanged(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _setInputDevice(ev): void { |   private _setInputDevice(ev): void { | ||||||
|     const device = ev.detail.item.getAttribute("device"); |     const device = ev.target.value; | ||||||
|     this._selectedInput = device; |     this._selectedInput = device; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _setOutputDevice(ev): void { |   private _setOutputDevice(ev): void { | ||||||
|     const device = ev.detail.item.getAttribute("device"); |     const device = ev.target.value; | ||||||
|     this._selectedOutput = device; |     this._selectedOutput = device; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import { | |||||||
|   mdiFlask, |   mdiFlask, | ||||||
|   mdiHomeAssistant, |   mdiHomeAssistant, | ||||||
|   mdiKey, |   mdiKey, | ||||||
|  |   mdiLinkLock, | ||||||
|   mdiNetwork, |   mdiNetwork, | ||||||
|   mdiNumeric1, |   mdiNumeric1, | ||||||
|   mdiNumeric2, |   mdiNumeric2, | ||||||
| @@ -16,6 +17,8 @@ import { | |||||||
|   mdiNumeric4, |   mdiNumeric4, | ||||||
|   mdiNumeric5, |   mdiNumeric5, | ||||||
|   mdiNumeric6, |   mdiNumeric6, | ||||||
|  |   mdiNumeric7, | ||||||
|  |   mdiNumeric8, | ||||||
|   mdiPound, |   mdiPound, | ||||||
|   mdiShield, |   mdiShield, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| @@ -31,6 +34,7 @@ import "../../../../src/components/buttons/ha-progress-button"; | |||||||
| import "../../../../src/components/ha-alert"; | import "../../../../src/components/ha-alert"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-chip"; | import "../../../../src/components/ha-chip"; | ||||||
|  | import "../../../../src/components/ha-chip-set"; | ||||||
| import "../../../../src/components/ha-markdown"; | import "../../../../src/components/ha-markdown"; | ||||||
| import "../../../../src/components/ha-settings-row"; | import "../../../../src/components/ha-settings-row"; | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-svg-icon"; | ||||||
| @@ -84,6 +88,8 @@ const RATING_ICON = { | |||||||
|   4: mdiNumeric4, |   4: mdiNumeric4, | ||||||
|   5: mdiNumeric5, |   5: mdiNumeric5, | ||||||
|   6: mdiNumeric6, |   6: mdiNumeric6, | ||||||
|  |   7: mdiNumeric7, | ||||||
|  |   8: mdiNumeric8, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @customElement("hassio-addon-info") | @customElement("hassio-addon-info") | ||||||
| @@ -209,7 +215,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|                 >`} |                 >`} | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           <div class="capabilities"> |           <ha-chip-set class="capabilities"> | ||||||
|             ${this.addon.stage !== "stable" |             ${this.addon.stage !== "stable" | ||||||
|               ? html` <ha-chip |               ? html` <ha-chip | ||||||
|                   hasIcon |                   hasIcon | ||||||
| @@ -234,9 +240,9 @@ class HassioAddonInfo extends LitElement { | |||||||
|             <ha-chip |             <ha-chip | ||||||
|               hasIcon |               hasIcon | ||||||
|               class=${classMap({ |               class=${classMap({ | ||||||
|                 green: [5, 6].includes(Number(this.addon.rating)), |                 green: Number(this.addon.rating) >= 6, | ||||||
|                 yellow: [3, 4].includes(Number(this.addon.rating)), |                 yellow: [3, 4, 5].includes(Number(this.addon.rating)), | ||||||
|                 red: [1, 2].includes(Number(this.addon.rating)), |                 red: Number(this.addon.rating) >= 2, | ||||||
|               })} |               })} | ||||||
|               @click=${this._showMoreInfo} |               @click=${this._showMoreInfo} | ||||||
|               id="rating" |               id="rating" | ||||||
| @@ -364,7 +370,17 @@ class HassioAddonInfo extends LitElement { | |||||||
|                   </ha-chip> |                   </ha-chip> | ||||||
|                 ` |                 ` | ||||||
|               : ""} |               : ""} | ||||||
|           </div> |             ${this.addon.signed | ||||||
|  |               ? html` | ||||||
|  |                   <ha-chip hasIcon @click=${this._showMoreInfo} id="signed"> | ||||||
|  |                     <ha-svg-icon slot="icon" .path=${mdiLinkLock}></ha-svg-icon> | ||||||
|  |                     ${this.supervisor.localize( | ||||||
|  |                       "addon.dashboard.capability.label.signed" | ||||||
|  |                     )} | ||||||
|  |                   </ha-chip> | ||||||
|  |                 ` | ||||||
|  |               : ""} | ||||||
|  |           </ha-chip-set> | ||||||
|  |  | ||||||
|           <div class="description light-color"> |           <div class="description light-color"> | ||||||
|             ${this.addon.description}.<br /> |             ${this.addon.description}.<br /> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; | import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; | ||||||
| import { PaperInputElement } from "@polymer/paper-input/paper-input"; | import { PaperInputElement } from "@polymer/paper-input/paper-input"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { atLeastVersion } from "../../../src/common/config/version"; | import { atLeastVersion } from "../../../src/common/config/version"; | ||||||
| import { formatDate } from "../../../src/common/datetime/format_date"; | import { formatDate } from "../../../src/common/datetime/format_date"; | ||||||
| import { formatDateTime } from "../../../src/common/datetime/format_date_time"; | import { formatDateTime } from "../../../src/common/datetime/format_date_time"; | ||||||
| @@ -92,6 +92,8 @@ export class SupervisorBackupContent extends LitElement { | |||||||
|  |  | ||||||
|   @property() public confirmBackupPassword = ""; |   @property() public confirmBackupPassword = ""; | ||||||
|  |  | ||||||
|  |   @query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget; | ||||||
|  |  | ||||||
|   public willUpdate(changedProps) { |   public willUpdate(changedProps) { | ||||||
|     super.willUpdate(changedProps); |     super.willUpdate(changedProps); | ||||||
|     if (!this.hasUpdated) { |     if (!this.hasUpdated) { | ||||||
| @@ -109,6 +111,10 @@ export class SupervisorBackupContent extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public override focus() { | ||||||
|  |     this._focusTarget?.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _localize = (string: string) => |   private _localize = (string: string) => | ||||||
|     this.supervisor?.localize(`backup.${string}`) || |     this.supervisor?.localize(`backup.${string}`) || | ||||||
|     this.localize!(`ui.panel.page-onboarding.restore.${string}`); |     this.localize!(`ui.panel.page-onboarding.restore.${string}`); | ||||||
| @@ -169,13 +175,13 @@ export class SupervisorBackupContent extends LitElement { | |||||||
|         : ""} |         : ""} | ||||||
|       ${this.backupType === "partial" |       ${this.backupType === "partial" | ||||||
|         ? html`<div class="partial-picker"> |         ? html`<div class="partial-picker"> | ||||||
|             ${this.backup && this.backup.homeassistant |  | ||||||
|               ? html` |  | ||||||
|             <ha-formfield |             <ha-formfield | ||||||
|               .label=${html`<supervisor-formfield-label |               .label=${html`<supervisor-formfield-label | ||||||
|                 label="Home Assistant" |                 label="Home Assistant" | ||||||
|                 .iconPath=${mdiHomeAssistant} |                 .iconPath=${mdiHomeAssistant} | ||||||
|                       .version=${this.backup.homeassistant} |                 .version=${this.backup | ||||||
|  |                   ? this.backup.homeassistant | ||||||
|  |                   : this.hass.config.version} | ||||||
|               > |               > | ||||||
|               </supervisor-formfield-label>`} |               </supervisor-formfield-label>`} | ||||||
|             > |             > | ||||||
| @@ -185,8 +191,7 @@ export class SupervisorBackupContent extends LitElement { | |||||||
|               > |               > | ||||||
|               </ha-checkbox> |               </ha-checkbox> | ||||||
|             </ha-formfield> |             </ha-formfield> | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|             ${foldersSection?.templates.length |             ${foldersSection?.templates.length | ||||||
|               ? html` |               ? html` | ||||||
|                   <ha-formfield |                   <ha-formfield | ||||||
|   | |||||||
| @@ -148,7 +148,6 @@ export class HassioUpdate extends LitElement { | |||||||
|         } |         } | ||||||
|         ha-settings-row { |         ha-settings-row { | ||||||
|           padding: 0; |           padding: 0; | ||||||
|           --paper-item-body-two-line-min-height: 32px; |  | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -64,6 +64,7 @@ export class DialogHassioBackupUpload | |||||||
|               .path=${mdiClose} |               .path=${mdiClose} | ||||||
|               slot="actionItems" |               slot="actionItems" | ||||||
|               dialogAction="cancel" |               dialogAction="cancel" | ||||||
|  |               dialogInitialFocus | ||||||
|             ></ha-icon-button> |             ></ha-icon-button> | ||||||
|           </ha-header-bar> |           </ha-header-bar> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ class HassioBackupDialog | |||||||
|               .backup=${this._backup} |               .backup=${this._backup} | ||||||
|               .onboarding=${this._dialogParams.onboarding || false} |               .onboarding=${this._dialogParams.onboarding || false} | ||||||
|               .localize=${this._dialogParams.localize} |               .localize=${this._dialogParams.localize} | ||||||
|  |               dialogInitialFocus | ||||||
|             > |             > | ||||||
|             </supervisor-backup-content>`} |             </supervisor-backup-content>`} | ||||||
|         ${this._error |         ${this._error | ||||||
|   | |||||||
| @@ -61,6 +61,7 @@ class HassioCreateBackupDialog extends LitElement { | |||||||
|           : html`<supervisor-backup-content |           : html`<supervisor-backup-content | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|               .supervisor=${this._dialogParams.supervisor} |               .supervisor=${this._dialogParams.supervisor} | ||||||
|  |               dialogInitialFocus | ||||||
|             > |             > | ||||||
|             </supervisor-backup-content>`} |             </supervisor-backup-content>`} | ||||||
|         ${this._error |         ${this._error | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import "../../../../src/components/ha-circular-progress"; | import "../../../../src/components/ha-circular-progress"; | ||||||
| import "../../../../src/components/ha-markdown"; | import "../../../../src/components/ha-markdown"; | ||||||
|  | import "../../../../src/components/ha-select"; | ||||||
| import { | import { | ||||||
|   extractApiErrorMessage, |   extractApiErrorMessage, | ||||||
|   ignoreSupervisorError, |   ignoreSupervisorError, | ||||||
| @@ -90,18 +89,20 @@ class HassioDatadiskDialog extends LitElement { | |||||||
|                     )} |                     )} | ||||||
|                     <br /><br /> |                     <br /><br /> | ||||||
|  |  | ||||||
|                     <paper-dropdown-menu |                     <ha-select | ||||||
|                       .label=${this.dialogParams.supervisor.localize( |                       .label=${this.dialogParams.supervisor.localize( | ||||||
|                         "dialog.datadisk_move.select_device" |                         "dialog.datadisk_move.select_device" | ||||||
|                       )} |                       )} | ||||||
|                       @value-changed=${this._select_device} |                       @selected=${this._select_device} | ||||||
|  |                       dialogInitialFocus | ||||||
|                     > |                     > | ||||||
|                       <paper-listbox slot="dropdown-content"> |  | ||||||
|                       ${this.devices.map( |                       ${this.devices.map( | ||||||
|                           (device) => html`<paper-item>${device}</paper-item>` |                         (device) => | ||||||
|  |                           html`<mwc-list-item .value=${device} | ||||||
|  |                             >${device}</mwc-list-item | ||||||
|  |                           >` | ||||||
|                       )} |                       )} | ||||||
|                       </paper-listbox> |                     </ha-select> | ||||||
|                     </paper-dropdown-menu> |  | ||||||
|                   ` |                   ` | ||||||
|                 : this.devices === undefined |                 : this.devices === undefined | ||||||
|                 ? this.dialogParams.supervisor.localize( |                 ? this.dialogParams.supervisor.localize( | ||||||
| @@ -111,7 +112,11 @@ class HassioDatadiskDialog extends LitElement { | |||||||
|                     "dialog.datadisk_move.no_devices" |                     "dialog.datadisk_move.no_devices" | ||||||
|                   )} |                   )} | ||||||
|  |  | ||||||
|               <mwc-button slot="secondaryAction" @click=${this.closeDialog}> |               <mwc-button | ||||||
|  |                 slot="secondaryAction" | ||||||
|  |                 @click=${this.closeDialog} | ||||||
|  |                 dialogInitialFocus | ||||||
|  |               > | ||||||
|                 ${this.dialogParams.supervisor.localize( |                 ${this.dialogParams.supervisor.localize( | ||||||
|                   "dialog.datadisk_move.cancel" |                   "dialog.datadisk_move.cancel" | ||||||
|                 )} |                 )} | ||||||
| @@ -130,8 +135,8 @@ class HassioDatadiskDialog extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _select_device(event) { |   private _select_device(ev) { | ||||||
|     this.selectedDevice = event.detail.value; |     this.selectedDevice = ev.target.value; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _moveDatadisk() { |   private async _moveDatadisk() { | ||||||
| @@ -156,7 +161,7 @@ class HassioDatadiskDialog extends LitElement { | |||||||
|       haStyle, |       haStyle, | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         paper-dropdown-menu { |         ha-select { | ||||||
|           width: 100%; |           width: 100%; | ||||||
|         } |         } | ||||||
|         ha-circular-progress { |         ha-circular-progress { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import "../../../../src/common/search/search-input"; | import "../../../../src/components/search-input"; | ||||||
| import { stringCompare } from "../../../../src/common/string/compare"; | import { stringCompare } from "../../../../src/common/string/compare"; | ||||||
| import "../../../../src/components/ha-dialog"; | import "../../../../src/components/ha-dialog"; | ||||||
| import "../../../../src/components/ha-expansion-panel"; | import "../../../../src/components/ha-expansion-panel"; | ||||||
| @@ -80,7 +80,7 @@ class HassioHardwareDialog extends LitElement { | |||||||
|           ></ha-icon-button> |           ></ha-icon-button> | ||||||
|           <search-input |           <search-input | ||||||
|             .hass=${this.hass} |             .hass=${this.hass} | ||||||
|             autofocus |             dialogInitialFocus | ||||||
|             no-label-float |             no-label-float | ||||||
|             .filter=${this._filter} |             .filter=${this._filter} | ||||||
|             @value-changed=${this._handleSearchChange} |             @value-changed=${this._handleSearchChange} | ||||||
| @@ -178,7 +178,7 @@ class HassioHardwareDialog extends LitElement { | |||||||
|           padding: 0.2em 0.4em; |           padding: 0.2em 0.4em; | ||||||
|         } |         } | ||||||
|         search-input { |         search-input { | ||||||
|           margin: 0 16px; |           margin: 8px 16px 0; | ||||||
|           display: block; |           display: block; | ||||||
|         } |         } | ||||||
|         .device-property { |         .device-property { | ||||||
|   | |||||||
| @@ -37,7 +37,10 @@ class HassioMarkdownDialog extends LitElement { | |||||||
|         @closed=${this.closeDialog} |         @closed=${this.closeDialog} | ||||||
|         .heading=${createCloseHeading(this.hass, this.title)} |         .heading=${createCloseHeading(this.hass, this.title)} | ||||||
|       > |       > | ||||||
|         <ha-markdown .content=${this.content || ""}></ha-markdown> |         <ha-markdown | ||||||
|  |           .content=${this.content || ""} | ||||||
|  |           dialogInitialFocus | ||||||
|  |         ></ha-markdown> | ||||||
|       </ha-dialog> |       </ha-dialog> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -119,6 +119,7 @@ export class DialogHassioNetwork | |||||||
|                     html`<mwc-tab |                     html`<mwc-tab | ||||||
|                       .id=${device.interface} |                       .id=${device.interface} | ||||||
|                       .label=${device.interface} |                       .label=${device.interface} | ||||||
|  |                       dialogInitialFocus | ||||||
|                     > |                     > | ||||||
|                     </mwc-tab>` |                     </mwc-tab>` | ||||||
|                 )} |                 )} | ||||||
| @@ -315,6 +316,7 @@ export class DialogHassioNetwork | |||||||
|               value="auto" |               value="auto" | ||||||
|               name="${version}method" |               name="${version}method" | ||||||
|               .checked=${this._interface![version]?.method === "auto"} |               .checked=${this._interface![version]?.method === "auto"} | ||||||
|  |               dialogInitialFocus | ||||||
|             > |             > | ||||||
|             </ha-radio> |             </ha-radio> | ||||||
|           </ha-formfield> |           </ha-formfield> | ||||||
|   | |||||||
| @@ -19,22 +19,21 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; | |||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| import { RegistriesDialogParams } from "./show-dialog-registries"; | import { RegistriesDialogParams } from "./show-dialog-registries"; | ||||||
|  |  | ||||||
| const SCHEMA = [ | const SCHEMA: HaFormSchema[] = [ | ||||||
|   { |   { | ||||||
|     type: "string", |  | ||||||
|     name: "registry", |     name: "registry", | ||||||
|     required: true, |     required: true, | ||||||
|  |     selector: { text: {} }, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     type: "string", |  | ||||||
|     name: "username", |     name: "username", | ||||||
|     required: true, |     required: true, | ||||||
|  |     selector: { text: {} }, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     type: "string", |  | ||||||
|     name: "password", |     name: "password", | ||||||
|     required: true, |     required: true, | ||||||
|     format: "password", |     selector: { text: { type: "password" } }, | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @@ -81,6 +80,7 @@ class HassioRegistriesDialog extends LitElement { | |||||||
|                 .schema=${SCHEMA} |                 .schema=${SCHEMA} | ||||||
|                 @value-changed=${this._valueChanged} |                 @value-changed=${this._valueChanged} | ||||||
|                 .computeLabel=${this._computeLabel} |                 .computeLabel=${this._computeLabel} | ||||||
|  |                 dialogInitialFocus | ||||||
|               ></ha-form> |               ></ha-form> | ||||||
|               <div class="action"> |               <div class="action"> | ||||||
|                 <mwc-button |                 <mwc-button | ||||||
| @@ -125,7 +125,7 @@ class HassioRegistriesDialog extends LitElement { | |||||||
|                     </ha-alert> |                     </ha-alert> | ||||||
|                   `} |                   `} | ||||||
|               <div class="action"> |               <div class="action"> | ||||||
|                 <mwc-button @click=${this._addRegistry}> |                 <mwc-button @click=${this._addRegistry} dialogInitialFocus> | ||||||
|                   ${this.supervisor.localize( |                   ${this.supervisor.localize( | ||||||
|                     "dialog.registries.add_new_registry" |                     "dialog.registries.add_new_registry" | ||||||
|                   )} |                   )} | ||||||
|   | |||||||
| @@ -139,6 +139,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|                 "dialog.repositories.add" |                 "dialog.repositories.add" | ||||||
|               )} |               )} | ||||||
|               @keydown=${this._handleKeyAdd} |               @keydown=${this._handleKeyAdd} | ||||||
|  |               dialogInitialFocus | ||||||
|             ></paper-input> |             ></paper-input> | ||||||
|             <mwc-button @click=${this._addRepository}> |             <mwc-button @click=${this._addRepository}> | ||||||
|               ${this._processing |               ${this._processing | ||||||
|   | |||||||
| @@ -205,16 +205,6 @@ class HassioCoreInfo extends LitElement { | |||||||
|           color: var(--secondary-text-color); |           color: var(--secondary-text-color); | ||||||
|           --mdc-menu-min-width: 200px; |           --mdc-menu-min-width: 200px; | ||||||
|         } |         } | ||||||
|         @media (min-width: 563px) { |  | ||||||
|           paper-listbox { |  | ||||||
|             max-height: 150px; |  | ||||||
|             overflow: auto; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         paper-item { |  | ||||||
|           cursor: pointer; |  | ||||||
|           min-height: 35px; |  | ||||||
|         } |  | ||||||
|         mwc-list-item ha-svg-icon { |         mwc-list-item ha-svg-icon { | ||||||
|           color: var(--secondary-text-color); |           color: var(--secondary-text-color); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -440,16 +440,6 @@ class HassioHostInfo extends LitElement { | |||||||
|           color: var(--secondary-text-color); |           color: var(--secondary-text-color); | ||||||
|           --mdc-menu-min-width: 200px; |           --mdc-menu-min-width: 200px; | ||||||
|         } |         } | ||||||
|         @media (min-width: 563px) { |  | ||||||
|           paper-listbox { |  | ||||||
|             max-height: 150px; |  | ||||||
|             overflow: auto; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         paper-item { |  | ||||||
|           cursor: pointer; |  | ||||||
|           min-height: 35px; |  | ||||||
|         } |  | ||||||
|         mwc-list-item ha-svg-icon { |         mwc-list-item ha-svg-icon { | ||||||
|           color: var(--secondary-text-color); |           color: var(--secondary-text-color); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; |  | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import "../../../src/components/buttons/ha-progress-button"; | import "../../../src/components/buttons/ha-progress-button"; | ||||||
| import "../../../src/components/ha-alert"; | import "../../../src/components/ha-alert"; | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
|  | import "../../../src/components/ha-select"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||||
| import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; | import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; | ||||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||||
| @@ -73,24 +71,19 @@ class HassioSupervisorLog extends LitElement { | |||||||
|           : ""} |           : ""} | ||||||
|         ${this.hass.userData?.showAdvanced |         ${this.hass.userData?.showAdvanced | ||||||
|           ? html` |           ? html` | ||||||
|               <paper-dropdown-menu |               <ha-select | ||||||
|                 .label=${this.supervisor.localize("system.log.log_provider")} |                 .label=${this.supervisor.localize("system.log.log_provider")} | ||||||
|                 @iron-select=${this._setLogProvider} |                 @selected=${this._setLogProvider} | ||||||
|               > |                 .value=${this._selectedLogProvider} | ||||||
|                 <paper-listbox |  | ||||||
|                   slot="dropdown-content" |  | ||||||
|                   attr-for-selected="provider" |  | ||||||
|                   .selected=${this._selectedLogProvider} |  | ||||||
|               > |               > | ||||||
|                 ${logProviders.map( |                 ${logProviders.map( | ||||||
|                   (provider) => html` |                   (provider) => html` | ||||||
|                       <paper-item provider=${provider.key}> |                     <mwc-list-item .value=${provider.key}> | ||||||
|                       ${provider.name} |                       ${provider.name} | ||||||
|                       </paper-item> |                     </mwc-list-item> | ||||||
|                   ` |                   ` | ||||||
|                 )} |                 )} | ||||||
|                 </paper-listbox> |               </ha-select> | ||||||
|               </paper-dropdown-menu> |  | ||||||
|             ` |             ` | ||||||
|           : ""} |           : ""} | ||||||
|  |  | ||||||
| @@ -110,7 +103,7 @@ class HassioSupervisorLog extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _setLogProvider(ev): Promise<void> { |   private async _setLogProvider(ev): Promise<void> { | ||||||
|     const provider = ev.detail.item.getAttribute("provider"); |     const provider = ev.target.value; | ||||||
|     this._selectedLogProvider = provider; |     this._selectedLogProvider = provider; | ||||||
|     this._loadData(); |     this._loadData(); | ||||||
|   } |   } | ||||||
| @@ -153,9 +146,9 @@ class HassioSupervisorLog extends LitElement { | |||||||
|         pre { |         pre { | ||||||
|           white-space: pre-wrap; |           white-space: pre-wrap; | ||||||
|         } |         } | ||||||
|         paper-dropdown-menu { |         ha-select { | ||||||
|           padding: 0 2%; |           width: 100%; | ||||||
|           width: 96%; |           margin-bottom: 4px; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import { | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
| import "../../../src/common/search/search-input"; |  | ||||||
| import "../../../src/components/buttons/ha-progress-button"; | import "../../../src/components/buttons/ha-progress-button"; | ||||||
| import "../../../src/components/ha-alert"; | import "../../../src/components/ha-alert"; | ||||||
| import "../../../src/components/ha-button-menu"; | import "../../../src/components/ha-button-menu"; | ||||||
| @@ -192,13 +191,7 @@ class UpdateAvailableCard extends LitElement { | |||||||
|                     </a>` |                     </a>` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 <span></span> |                 <span></span> | ||||||
|                 <ha-progress-button |                 <ha-progress-button @click=${this._update} raised> | ||||||
|                   .disabled=${!this._version || |  | ||||||
|                   (this._shouldCreateBackup && |  | ||||||
|                     this.supervisor.info?.state !== "running")} |  | ||||||
|                   @click=${this._update} |  | ||||||
|                   raised |  | ||||||
|                 > |  | ||||||
|                   ${this.supervisor.localize("common.update")} |                   ${this.supervisor.localize("common.update")} | ||||||
|                 </ha-progress-button> |                 </ha-progress-button> | ||||||
|               </div> |               </div> | ||||||
| @@ -360,8 +353,14 @@ class UpdateAvailableCard extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _update() { |   private async _update() { | ||||||
|  |     if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") { | ||||||
|  |       this._error = this.supervisor.localize("backup.backup_already_running"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._updating = true; |     this._updating = true; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       if (this._updateType === "addon") { |       if (this._updateType === "addon") { | ||||||
|         await updateHassioAddon( |         await updateHassioAddon( | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|   "description": "A frontend for Home Assistant using the Polymer framework", |   "description": "A frontend for Home Assistant", | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "type": "git", | ||||||
|     "url": "https://github.com/home-assistant/home-assistant-polymer" |     "url": "https://github.com/home-assistant/frontend" | ||||||
|   }, |   }, | ||||||
|   "name": "home-assistant-frontend", |   "name": "home-assistant-frontend", | ||||||
|   "version": "1.0.0", |   "version": "1.0.0", | ||||||
| @@ -22,17 +22,18 @@ | |||||||
|   "license": "Apache-2.0", |   "license": "Apache-2.0", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@braintree/sanitize-url": "^5.0.2", |     "@braintree/sanitize-url": "^5.0.2", | ||||||
|     "@codemirror/commands": "^0.19.5", |     "@codemirror/autocomplete": "^0.19.12", | ||||||
|     "@codemirror/gutter": "^0.19.4", |     "@codemirror/commands": "^0.19.8", | ||||||
|     "@codemirror/highlight": "^0.19.6", |     "@codemirror/gutter": "^0.19.9", | ||||||
|     "@codemirror/history": "^0.19.0", |     "@codemirror/highlight": "^0.19.7", | ||||||
|  |     "@codemirror/history": "^0.19.2", | ||||||
|     "@codemirror/legacy-modes": "^0.19.0", |     "@codemirror/legacy-modes": "^0.19.0", | ||||||
|     "@codemirror/rectangular-selection": "^0.19.1", |     "@codemirror/rectangular-selection": "^0.19.1", | ||||||
|     "@codemirror/search": "^0.19.2", |     "@codemirror/search": "^0.19.6", | ||||||
|     "@codemirror/state": "^0.19.4", |     "@codemirror/state": "^0.19.6", | ||||||
|     "@codemirror/stream-parser": "^0.19.2", |     "@codemirror/stream-parser": "^0.19.5", | ||||||
|     "@codemirror/text": "^0.19.5", |     "@codemirror/text": "^0.19.6", | ||||||
|     "@codemirror/view": "^0.19.15", |     "@codemirror/view": "^0.19.40", | ||||||
|     "@formatjs/intl-datetimeformat": "^4.2.5", |     "@formatjs/intl-datetimeformat": "^4.2.5", | ||||||
|     "@formatjs/intl-getcanonicallocales": "^1.8.0", |     "@formatjs/intl-getcanonicallocales": "^1.8.0", | ||||||
|     "@formatjs/intl-locale": "^2.4.40", |     "@formatjs/intl-locale": "^2.4.40", | ||||||
| @@ -45,7 +46,8 @@ | |||||||
|     "@fullcalendar/daygrid": "5.9.0", |     "@fullcalendar/daygrid": "5.9.0", | ||||||
|     "@fullcalendar/interaction": "5.9.0", |     "@fullcalendar/interaction": "5.9.0", | ||||||
|     "@fullcalendar/list": "5.9.0", |     "@fullcalendar/list": "5.9.0", | ||||||
|     "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch", |     "@lit-labs/motion": "^1.0.2", | ||||||
|  |     "@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/chips": "14.0.0-canary.261f2db59.0", | ||||||
|     "@material/data-table": "14.0.0-canary.261f2db59.0", |     "@material/data-table": "14.0.0-canary.261f2db59.0", | ||||||
|     "@material/mwc-button": "0.25.3", |     "@material/mwc-button": "0.25.3", | ||||||
| @@ -57,7 +59,7 @@ | |||||||
|     "@material/mwc-formfield": "0.25.3", |     "@material/mwc-formfield": "0.25.3", | ||||||
|     "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", |     "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", | ||||||
|     "@material/mwc-linear-progress": "0.25.3", |     "@material/mwc-linear-progress": "0.25.3", | ||||||
|     "@material/mwc-list": "0.25.3", |     "@material/mwc-list": "^0.25.3", | ||||||
|     "@material/mwc-menu": "0.25.3", |     "@material/mwc-menu": "0.25.3", | ||||||
|     "@material/mwc-radio": "0.25.3", |     "@material/mwc-radio": "0.25.3", | ||||||
|     "@material/mwc-ripple": "0.25.3", |     "@material/mwc-ripple": "0.25.3", | ||||||
| @@ -66,6 +68,7 @@ | |||||||
|     "@material/mwc-switch": "0.25.3", |     "@material/mwc-switch": "0.25.3", | ||||||
|     "@material/mwc-tab": "0.25.3", |     "@material/mwc-tab": "0.25.3", | ||||||
|     "@material/mwc-tab-bar": "0.25.3", |     "@material/mwc-tab-bar": "0.25.3", | ||||||
|  |     "@material/mwc-textarea": "^0.25.3", | ||||||
|     "@material/mwc-textfield": "0.25.3", |     "@material/mwc-textfield": "0.25.3", | ||||||
|     "@material/mwc-top-app-bar-fixed": "^0.25.3", |     "@material/mwc-top-app-bar-fixed": "^0.25.3", | ||||||
|     "@material/top-app-bar": "14.0.0-canary.261f2db59.0", |     "@material/top-app-bar": "14.0.0-canary.261f2db59.0", | ||||||
| @@ -87,13 +90,15 @@ | |||||||
|     "@polymer/paper-tooltip": "^3.0.1", |     "@polymer/paper-tooltip": "^3.0.1", | ||||||
|     "@polymer/polymer": "3.4.1", |     "@polymer/polymer": "3.4.1", | ||||||
|     "@thomasloven/round-slider": "0.5.4", |     "@thomasloven/round-slider": "0.5.4", | ||||||
|     "@vaadin/vaadin-combo-box": "^21.0.2", |     "@vaadin/combo-box": "^22.0.4", | ||||||
|     "@vaadin/vaadin-date-picker": "^21.0.2", |     "@vaadin/vaadin-themable-mixin": "^22.0.4", | ||||||
|     "@vibrant/color": "^3.2.1-alpha.1", |     "@vibrant/color": "^3.2.1-alpha.1", | ||||||
|     "@vibrant/core": "^3.2.1-alpha.1", |     "@vibrant/core": "^3.2.1-alpha.1", | ||||||
|     "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", |     "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", | ||||||
|     "@vue/web-component-wrapper": "^1.2.0", |     "@vue/web-component-wrapper": "^1.2.0", | ||||||
|  |     "@webcomponents/scoped-custom-element-registry": "^0.0.5", | ||||||
|     "@webcomponents/webcomponentsjs": "^2.2.10", |     "@webcomponents/webcomponentsjs": "^2.2.10", | ||||||
|  |     "app-datepicker": "^5.0.1", | ||||||
|     "chart.js": "^3.3.2", |     "chart.js": "^3.3.2", | ||||||
|     "comlink": "^4.3.1", |     "comlink": "^4.3.1", | ||||||
|     "core-js": "^3.15.2", |     "core-js": "^3.15.2", | ||||||
| @@ -103,7 +108,7 @@ | |||||||
|     "deep-freeze": "^0.0.1", |     "deep-freeze": "^0.0.1", | ||||||
|     "fuse.js": "^6.0.0", |     "fuse.js": "^6.0.0", | ||||||
|     "google-timezones-json": "^1.0.2", |     "google-timezones-json": "^1.0.2", | ||||||
|     "hls.js": "^1.0.11", |     "hls.js": "^1.1.5", | ||||||
|     "home-assistant-js-websocket": "^6.0.1", |     "home-assistant-js-websocket": "^6.0.1", | ||||||
|     "idb-keyval": "^5.1.3", |     "idb-keyval": "^5.1.3", | ||||||
|     "intl-messageformat": "^9.9.1", |     "intl-messageformat": "^9.9.1", | ||||||
| @@ -111,8 +116,8 @@ | |||||||
|     "leaflet": "^1.7.1", |     "leaflet": "^1.7.1", | ||||||
|     "leaflet-draw": "^1.0.4", |     "leaflet-draw": "^1.0.4", | ||||||
|     "lit": "^2.1.2", |     "lit": "^2.1.2", | ||||||
|     "lit-vaadin-helpers": "^0.2.1", |     "lit-vaadin-helpers": "^0.3.0", | ||||||
|     "marked": "^3.0.2", |     "marked": "^4.0.12", | ||||||
|     "memoize-one": "^5.2.1", |     "memoize-one": "^5.2.1", | ||||||
|     "node-vibrant": "3.2.1-alpha.1", |     "node-vibrant": "3.2.1-alpha.1", | ||||||
|     "proxy-polyfill": "^0.3.2", |     "proxy-polyfill": "^0.3.2", | ||||||
| @@ -132,12 +137,12 @@ | |||||||
|     "vue": "^2.6.12", |     "vue": "^2.6.12", | ||||||
|     "vue2-daterange-picker": "^0.5.1", |     "vue2-daterange-picker": "^0.5.1", | ||||||
|     "web-animations-js": "^2.3.2", |     "web-animations-js": "^2.3.2", | ||||||
|     "workbox-cacheable-response": "^6.1.5", |     "workbox-cacheable-response": "^6.4.2", | ||||||
|     "workbox-core": "^6.1.5", |     "workbox-core": "^6.4.2", | ||||||
|     "workbox-expiration": "^6.1.5", |     "workbox-expiration": "^6.4.2", | ||||||
|     "workbox-precaching": "^6.1.5", |     "workbox-precaching": "^6.4.2", | ||||||
|     "workbox-routing": "^6.1.5", |     "workbox-routing": "^6.4.2", | ||||||
|     "workbox-strategies": "^6.1.5", |     "workbox-strategies": "^6.4.2", | ||||||
|     "xss": "^1.0.9" |     "xss": "^1.0.9" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
| @@ -166,7 +171,7 @@ | |||||||
|     "@types/js-yaml": "^4", |     "@types/js-yaml": "^4", | ||||||
|     "@types/leaflet": "^1", |     "@types/leaflet": "^1", | ||||||
|     "@types/leaflet-draw": "^1", |     "@types/leaflet-draw": "^1", | ||||||
|     "@types/marked": "^2", |     "@types/marked": "^4", | ||||||
|     "@types/mocha": "^8", |     "@types/mocha": "^8", | ||||||
|     "@types/qrcode": "^1.4.2", |     "@types/qrcode": "^1.4.2", | ||||||
|     "@types/sortablejs": "^1", |     "@types/sortablejs": "^1", | ||||||
| @@ -193,7 +198,7 @@ | |||||||
|     "fs-extra": "^7.0.1", |     "fs-extra": "^7.0.1", | ||||||
|     "glob": "^7.2.0", |     "glob": "^7.2.0", | ||||||
|     "gulp": "^4.0.2", |     "gulp": "^4.0.2", | ||||||
|     "gulp-foreach": "^0.1.0", |     "gulp-flatmap": "^1.0.2", | ||||||
|     "gulp-json-transform": "^0.4.6", |     "gulp-json-transform": "^0.4.6", | ||||||
|     "gulp-merge-json": "^1.3.1", |     "gulp-merge-json": "^1.3.1", | ||||||
|     "gulp-rename": "^2.0.0", |     "gulp-rename": "^2.0.0", | ||||||
| @@ -230,7 +235,7 @@ | |||||||
|     "webpack-dev-server": "^4.3.0", |     "webpack-dev-server": "^4.3.0", | ||||||
|     "webpack-manifest-plugin": "^4.0.2", |     "webpack-manifest-plugin": "^4.0.2", | ||||||
|     "webpackbar": "^5.0.0-3", |     "webpackbar": "^5.0.0-3", | ||||||
|     "workbox-build": "^6.1.5" |     "workbox-build": "^6.4.2" | ||||||
|   }, |   }, | ||||||
|   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", |   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", | ||||||
|   "resolutions": { |   "resolutions": { | ||||||
| @@ -250,5 +255,6 @@ | |||||||
|   "prettier": { |   "prettier": { | ||||||
|     "trailingComma": "es5", |     "trailingComma": "es5", | ||||||
|     "arrowParens": "always" |     "arrowParens": "always" | ||||||
|   } |   }, | ||||||
|  |   "packageManager": "yarn@3.2.0" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,6 @@ | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
|  |  | ||||||
| def where(): | def where() -> Path: | ||||||
|     """Return path to the frontend.""" |     """Return path to the frontend.""" | ||||||
|     return Path(__file__).parent |     return Path(__file__).parent | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								public/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								public/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										18
									
								
								script/core
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								script/core
									
									
									
									
									
								
							| @@ -4,6 +4,8 @@ | |||||||
| # Stop on errors | # Stop on errors | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
|  | WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}" | ||||||
|  |  | ||||||
| if [ -z "${DEVCONTAINER}" ]; then | if [ -z "${DEVCONTAINER}" ]; then | ||||||
|   echo "This task should only run inside a devcontainer, for local install HA Core in a venv." |   echo "This task should only run inside a devcontainer, for local install HA Core in a venv." | ||||||
|   exit 1 |   exit 1 | ||||||
| @@ -16,9 +18,9 @@ if [ -z $(which hass) ]; then | |||||||
|     git+git://github.com/home-assistant/home-assistant.git@dev |     git+git://github.com/home-assistant/home-assistant.git@dev | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [ ! -d "/workspaces/frontend/config" ]; then | if [ ! -d "${WD}/config" ]; then | ||||||
|   echo "Creating default configuration." |   echo "Creating default configuration." | ||||||
|   mkdir -p "/workspaces/frontend/config"; |   mkdir -p "${WD}/config"; | ||||||
|   hass --script ensure_config -c config |   hass --script ensure_config -c config | ||||||
|   echo "demo: |   echo "demo: | ||||||
|  |  | ||||||
| @@ -26,24 +28,24 @@ logger: | |||||||
|   default: info |   default: info | ||||||
|   logs: |   logs: | ||||||
|     homeassistant.components.frontend: debug |     homeassistant.components.frontend: debug | ||||||
| " >> /workspaces/frontend/config/configuration.yaml | " >> "${WD}/config/configuration.yaml" | ||||||
|  |  | ||||||
|   if [ ! -z "${HASSIO}" ]; then |   if [ ! -z "${HASSIO}" ]; then | ||||||
|   echo " |   echo " | ||||||
| # frontend: | # frontend: | ||||||
| #   development_repo: /workspaces/frontend | #   development_repo: ${WD} | ||||||
|  |  | ||||||
| hassio: | hassio: | ||||||
|   development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml |   development_repo: ${WD}" >> "${WD}/config/configuration.yaml" | ||||||
|   else |   else | ||||||
|   echo " |   echo " | ||||||
| frontend: | frontend: | ||||||
|   development_repo: /workspaces/frontend |   development_repo: ${WD} | ||||||
|  |  | ||||||
| # hassio: | # hassio: | ||||||
| #   development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml | #   development_repo: ${WD}" >> "${WD}/config/configuration.yaml" | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
| fi | fi | ||||||
|  |  | ||||||
| hass -c /workspaces/frontend/config | hass -c "${WD}/config" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [metadata] | [metadata] | ||||||
| name         = home-assistant-frontend | name         = home-assistant-frontend | ||||||
| version      = 20220203.1 | version      = 20220226.0 | ||||||
| author       = The Home Assistant Authors | author       = The Home Assistant Authors | ||||||
| author_email = hello@home-assistant.io | author_email = hello@home-assistant.io | ||||||
| license      = Apache-2.0 | license      = Apache-2.0 | ||||||
| @@ -19,3 +19,8 @@ python_requires = >= 3.4.0 | |||||||
| [options.packages.find] | [options.packages.find] | ||||||
| include = | include = | ||||||
|     hass_frontend* |     hass_frontend* | ||||||
|  |  | ||||||
|  | [mypy] | ||||||
|  | python_version = 3.4 | ||||||
|  | show_error_codes = True | ||||||
|  | strict = True | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { HaDurationData } from "../../components/ha-duration-input"; | import type { HaDurationData } from "../../components/ha-duration-input"; | ||||||
| import { ForDict } from "../../data/automation"; | import type { ForDict } from "../../data/automation"; | ||||||
|  |  | ||||||
| export const createDurationData = ( | export const createDurationData = ( | ||||||
|   duration: string | number | ForDict | undefined |   duration: string | number | ForDict | undefined | ||||||
| ): HaDurationData => { | ): HaDurationData | undefined => { | ||||||
|   if (duration === undefined) { |   if (duration === undefined) { | ||||||
|     return {}; |     return undefined; | ||||||
|   } |   } | ||||||
|   if (typeof duration !== "object") { |   if (typeof duration !== "object") { | ||||||
|     if (typeof duration === "string" || isNaN(duration)) { |     if (typeof duration === "string" || isNaN(duration)) { | ||||||
| @@ -19,6 +19,9 @@ export const createDurationData = ( | |||||||
|     } |     } | ||||||
|     return { seconds: duration }; |     return { seconds: duration }; | ||||||
|   } |   } | ||||||
|  |   if (!("days" in duration)) { | ||||||
|  |     return duration; | ||||||
|  |   } | ||||||
|   const { days, minutes, seconds, milliseconds } = duration; |   const { days, minutes, seconds, milliseconds } = duration; | ||||||
|   let hours = duration.hours || 0; |   let hours = duration.hours || 0; | ||||||
|   hours = (hours || 0) + (days || 0) * 24; |   hours = (hours || 0) + (days || 0) * 24; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
|  |  | ||||||
| export const canToggleDomain = (hass: HomeAssistant, domain: string) => { | export const canToggleDomain = (hass: HomeAssistant, domain: string) => { | ||||||
|   const services = hass.services[domain]; |   const services = hass.services[domain]; | ||||||
|   | |||||||
| @@ -1,14 +1,30 @@ | |||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import type { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { canToggleDomain } from "./can_toggle_domain"; | import { canToggleDomain } from "./can_toggle_domain"; | ||||||
| import { computeStateDomain } from "./compute_state_domain"; | import { computeStateDomain } from "./compute_state_domain"; | ||||||
| import { supportsFeature } from "./supports-feature"; | import { supportsFeature } from "./supports-feature"; | ||||||
|  |  | ||||||
| export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => { | export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => { | ||||||
|   const domain = computeStateDomain(stateObj); |   const domain = computeStateDomain(stateObj); | ||||||
|  |  | ||||||
|   if (domain === "group") { |   if (domain === "group") { | ||||||
|  |     if ( | ||||||
|  |       stateObj.attributes?.entity_id?.some((entity) => { | ||||||
|  |         const entityStateObj = hass.states[entity]; | ||||||
|  |         if (!entityStateObj) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const entityDomain = computeStateDomain(entityStateObj); | ||||||
|  |         return canToggleDomain(hass, entityDomain); | ||||||
|  |       }) | ||||||
|  |     ) { | ||||||
|       return stateObj.state === "on" || stateObj.state === "off"; |       return stateObj.state === "on" || stateObj.state === "off"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (domain === "climate") { |   if (domain === "climate") { | ||||||
|     return supportsFeature(stateObj, 4096); |     return supportsFeature(stateObj, 4096); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -120,6 +120,7 @@ export const computeOpenIcon = (stateObj: HassEntity): string => { | |||||||
|     case "awning": |     case "awning": | ||||||
|     case "door": |     case "door": | ||||||
|     case "gate": |     case "gate": | ||||||
|  |     case "curtain": | ||||||
|       return mdiArrowExpandHorizontal; |       return mdiArrowExpandHorizontal; | ||||||
|     default: |     default: | ||||||
|       return mdiArrowUp; |       return mdiArrowUp; | ||||||
| @@ -131,6 +132,7 @@ export const computeCloseIcon = (stateObj: HassEntity): string => { | |||||||
|     case "awning": |     case "awning": | ||||||
|     case "door": |     case "door": | ||||||
|     case "gate": |     case "gate": | ||||||
|  |     case "curtain": | ||||||
|       return mdiArrowCollapseHorizontal; |       return mdiArrowCollapseHorizontal; | ||||||
|     default: |     default: | ||||||
|       return mdiArrowDown; |       return mdiArrowDown; | ||||||
|   | |||||||
| @@ -1,24 +1,32 @@ | |||||||
|  | const SUFFIXES = [" ", ": "]; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Strips a device name from an entity name. |  * Strips a device name from an entity name. | ||||||
|  * @param entityName the entity name |  * @param entityName the entity name | ||||||
|  * @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix |  * @param lowerCasedPrefix the prefix to strip, lower cased | ||||||
|  * @returns |  * @returns | ||||||
|  */ |  */ | ||||||
| export const stripPrefixFromEntityName = ( | export const stripPrefixFromEntityName = ( | ||||||
|   entityName: string, |   entityName: string, | ||||||
|   lowerCasedPrefixWithSpaceSuffix: string |   lowerCasedPrefix: string | ||||||
| ) => { | ) => { | ||||||
|   if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) { |   const lowerCasedEntityName = entityName.toLowerCase(); | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length); |   for (const suffix of SUFFIXES) { | ||||||
|  |     const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`; | ||||||
|  |  | ||||||
|  |     if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) { | ||||||
|  |       const newName = entityName.substring(lowerCasedPrefixWithSuffix.length); | ||||||
|  |  | ||||||
|       // If first word already has an upper case letter (e.g. from brand name) |       // If first word already has an upper case letter (e.g. from brand name) | ||||||
|       // leave as-is, otherwise capitalize the first word. |       // leave as-is, otherwise capitalize the first word. | ||||||
|       return hasUpperCase(newName.substr(0, newName.indexOf(" "))) |       return hasUpperCase(newName.substr(0, newName.indexOf(" "))) | ||||||
|         ? newName |         ? newName | ||||||
|         : newName[0].toUpperCase() + newName.slice(1); |         : newName[0].toUpperCase() + newName.slice(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return undefined; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str; | const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str; | ||||||
|   | |||||||
| @@ -1,112 +0,0 @@ | |||||||
| 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 { customElement, property, query } from "lit/decorators"; |  | ||||||
| import "../../components/ha-icon-button"; |  | ||||||
| import "../../components/ha-svg-icon"; |  | ||||||
| import { HomeAssistant } from "../../types"; |  | ||||||
| import { fireEvent } from "../dom/fire_event"; |  | ||||||
|  |  | ||||||
| @customElement("search-input") |  | ||||||
| class SearchInput extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @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; |  | ||||||
|  |  | ||||||
|   @property({ type: String }) |  | ||||||
|   public label?: string; |  | ||||||
|  |  | ||||||
|   public focus() { |  | ||||||
|     this.shadowRoot!.querySelector("paper-input")!.focus(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @query("paper-input", true) private _input!: PaperInputElement; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       <paper-input |  | ||||||
|         .autofocus=${this.autofocus} |  | ||||||
|         .label=${this.label || "Search"} |  | ||||||
|         .value=${this.filter} |  | ||||||
|         @value-changed=${this._filterInputChanged} |  | ||||||
|         .noLabelFloat=${this.noLabelFloat} |  | ||||||
|       > |  | ||||||
|         <slot name="prefix" slot="prefix"> |  | ||||||
|           <ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon> |  | ||||||
|         </slot> |  | ||||||
|         ${this.filter && |  | ||||||
|         html` |  | ||||||
|           <ha-icon-button |  | ||||||
|             slot="suffix" |  | ||||||
|             @click=${this._clearSearch} |  | ||||||
|             .label=${this.hass.localize("ui.common.clear")} |  | ||||||
|             .path=${mdiClose} |  | ||||||
|           ></ha-icon-button> |  | ||||||
|         `} |  | ||||||
|       </paper-input> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues) { |  | ||||||
|     if ( |  | ||||||
|       changedProps.has("noUnderline") && |  | ||||||
|       (this.noUnderline || changedProps.get("noUnderline") !== undefined) |  | ||||||
|     ) { |  | ||||||
|       ( |  | ||||||
|         this._input.inputElement!.parentElement!.shadowRoot!.querySelector( |  | ||||||
|           "div.unfocused-line" |  | ||||||
|         ) as HTMLElement |  | ||||||
|       ).style.display = this.noUnderline ? "none" : "block"; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _filterChanged(value: string) { |  | ||||||
|     fireEvent(this, "value-changed", { value: String(value) }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _filterInputChanged(e) { |  | ||||||
|     this._filterChanged(e.target.value); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _clearSearch() { |  | ||||||
|     this._filterChanged(""); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       ha-svg-icon, |  | ||||||
|       ha-icon-button { |  | ||||||
|         color: var(--primary-text-color); |  | ||||||
|       } |  | ||||||
|       ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|       } |  | ||||||
|       ha-svg-icon.prefix { |  | ||||||
|         margin: 8px; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "search-input": SearchInput; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -15,6 +15,7 @@ export const iconColorCSS = css` | |||||||
|   ha-state-icon[data-domain="media_player"][data-state="on"], |   ha-state-icon[data-domain="media_player"][data-state="on"], | ||||||
|   ha-state-icon[data-domain="media_player"][data-state="paused"], |   ha-state-icon[data-domain="media_player"][data-state="paused"], | ||||||
|   ha-state-icon[data-domain="media_player"][data-state="playing"], |   ha-state-icon[data-domain="media_player"][data-state="playing"], | ||||||
|  |   ha-state-icon[data-domain="remote"][data-state="on"], | ||||||
|   ha-state-icon[data-domain="script"][data-state="on"], |   ha-state-icon[data-domain="script"][data-state="on"], | ||||||
|   ha-state-icon[data-domain="sun"][data-state="above_horizon"], |   ha-state-icon[data-domain="sun"][data-state="above_horizon"], | ||||||
|   ha-state-icon[data-domain="switch"][data-state="on"], |   ha-state-icon[data-domain="switch"][data-state="on"], | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export const debounce = <T extends any[]>( | |||||||
|   immediate = false |   immediate = false | ||||||
| ) => { | ) => { | ||||||
|   let timeout: number | undefined; |   let timeout: number | undefined; | ||||||
|   return (...args: T): void => { |   const debouncedFunc = (...args: T): void => { | ||||||
|     const later = () => { |     const later = () => { | ||||||
|       timeout = undefined; |       timeout = undefined; | ||||||
|       if (!immediate) { |       if (!immediate) { | ||||||
| @@ -25,4 +25,8 @@ export const debounce = <T extends any[]>( | |||||||
|       func(...args); |       func(...args); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |   debouncedFunc.cancel = () => { | ||||||
|  |     clearTimeout(timeout); | ||||||
|  |   }; | ||||||
|  |   return debouncedFunc; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { Layout1d, scroll } from "@lit-labs/virtualizer"; |  | ||||||
| import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; | import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; | ||||||
| import deepClone from "deep-clone-simple"; | import deepClone from "deep-clone-simple"; | ||||||
| import { | import { | ||||||
| @@ -22,7 +21,7 @@ import { styleMap } from "lit/directives/style-map"; | |||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { restoreScroll } from "../../common/decorators/restore-scroll"; | import { restoreScroll } from "../../common/decorators/restore-scroll"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../../common/search/search-input"; | import "../search-input"; | ||||||
| import { debounce } from "../../common/util/debounce"; | import { debounce } from "../../common/util/debounce"; | ||||||
| import { nextRender } from "../../common/util/render-status"; | import { nextRender } from "../../common/util/render-status"; | ||||||
| import { haStyleScrollbar } from "../../resources/styles"; | import { haStyleScrollbar } from "../../resources/styles"; | ||||||
| @@ -31,6 +30,7 @@ import type { HaCheckbox } from "../ha-checkbox"; | |||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import { filterData, sortData } from "./sort-filter"; | import { filterData, sortData } from "./sort-filter"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
|  | import "@lit-labs/virtualizer"; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   // for fire event |   // for fire event | ||||||
| @@ -70,6 +70,7 @@ export interface DataTableSortColumnData { | |||||||
|  |  | ||||||
| export interface DataTableColumnData<T = any> extends DataTableSortColumnData { | export interface DataTableColumnData<T = any> extends DataTableSortColumnData { | ||||||
|   title: TemplateResult | string; |   title: TemplateResult | string; | ||||||
|  |   label?: TemplateResult | string; | ||||||
|   type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; |   type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; | ||||||
|   template?: (data: any, row: T) => TemplateResult | string; |   template?: (data: any, row: T) => TemplateResult | string; | ||||||
|   width?: string; |   width?: string; | ||||||
| @@ -294,6 +295,7 @@ export class HaDataTable extends LitElement { | |||||||
|               }; |               }; | ||||||
|               return html` |               return html` | ||||||
|                 <div |                 <div | ||||||
|  |                   aria-label=${column.label} | ||||||
|                   class="mdc-data-table__header-cell ${classMap(classes)}" |                   class="mdc-data-table__header-cell ${classMap(classes)}" | ||||||
|                   style=${column.width |                   style=${column.width | ||||||
|                     ? styleMap({ |                     ? styleMap({ | ||||||
| @@ -337,41 +339,47 @@ export class HaDataTable extends LitElement { | |||||||
|                 </div> |                 </div> | ||||||
|               ` |               ` | ||||||
|             : html` |             : html` | ||||||
|                 <div |                 <lit-virtualizer | ||||||
|  |                   scroller | ||||||
|                   class="mdc-data-table__content scroller ha-scrollbar" |                   class="mdc-data-table__content scroller ha-scrollbar" | ||||||
|                   @scroll=${this._saveScrollPos} |                   @scroll=${this._saveScrollPos} | ||||||
|                 > |                   .items=${this._items} | ||||||
|                   ${scroll({ |                   .renderItem=${this._renderRow} | ||||||
|                     items: this._items, |                 ></lit-virtualizer> | ||||||
|                     layout: Layout1d, |               `} | ||||||
|                     renderItem: (row: DataTableRowData, index) => { |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _renderRow = ( | ||||||
|  |     row: DataTableRowData, | ||||||
|  |     index: number | ||||||
|  |   ): TemplateResult => { | ||||||
|     // not sure how this happens... |     // not sure how this happens... | ||||||
|     if (!row) { |     if (!row) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     if (row.append) { |     if (row.append) { | ||||||
|                         return html` |       return html` <div class="mdc-data-table__row">${row.content}</div> `; | ||||||
|                           <div class="mdc-data-table__row">${row.content}</div> |  | ||||||
|                         `; |  | ||||||
|     } |     } | ||||||
|     if (row.empty) { |     if (row.empty) { | ||||||
|       return html` <div class="mdc-data-table__row"></div> `; |       return html` <div class="mdc-data-table__row"></div> `; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <div |       <div | ||||||
|                           aria-rowindex=${index! + 2} |         aria-rowindex=${index + 2} | ||||||
|         role="row" |         role="row" | ||||||
|         .rowId=${row[this.id]} |         .rowId=${row[this.id]} | ||||||
|         @click=${this._handleRowClick} |         @click=${this._handleRowClick} | ||||||
|         class="mdc-data-table__row ${classMap({ |         class="mdc-data-table__row ${classMap({ | ||||||
|                             "mdc-data-table__row--selected": |           "mdc-data-table__row--selected": this._checkedRows.includes( | ||||||
|                               this._checkedRows.includes(String(row[this.id])), |             String(row[this.id]) | ||||||
|  |           ), | ||||||
|           clickable: this.clickable, |           clickable: this.clickable, | ||||||
|         })}" |         })}" | ||||||
|         aria-selected=${ifDefined( |         aria-selected=${ifDefined( | ||||||
|                             this._checkedRows.includes(String(row[this.id])) |           this._checkedRows.includes(String(row[this.id])) ? true : undefined | ||||||
|                               ? true |  | ||||||
|                               : undefined |  | ||||||
|         )} |         )} | ||||||
|         .selectable=${row.selectable !== false} |         .selectable=${row.selectable !== false} | ||||||
|       > |       > | ||||||
| @@ -386,16 +394,13 @@ export class HaDataTable extends LitElement { | |||||||
|                   @change=${this._handleRowCheckboxClick} |                   @change=${this._handleRowCheckboxClick} | ||||||
|                   .rowId=${row[this.id]} |                   .rowId=${row[this.id]} | ||||||
|                   .disabled=${row.selectable === false} |                   .disabled=${row.selectable === false} | ||||||
|                                     .checked=${this._checkedRows.includes( |                   .checked=${this._checkedRows.includes(String(row[this.id]))} | ||||||
|                                       String(row[this.id]) |  | ||||||
|                                     )} |  | ||||||
|                 > |                 > | ||||||
|                 </ha-checkbox> |                 </ha-checkbox> | ||||||
|               </div> |               </div> | ||||||
|             ` |             ` | ||||||
|           : ""} |           : ""} | ||||||
|                           ${Object.entries(this.columns).map( |         ${Object.entries(this.columns).map(([key, column]) => { | ||||||
|                             ([key, column]) => { |  | ||||||
|           if (column.hidden) { |           if (column.hidden) { | ||||||
|             return ""; |             return ""; | ||||||
|           } |           } | ||||||
| @@ -403,10 +408,8 @@ export class HaDataTable extends LitElement { | |||||||
|             <div |             <div | ||||||
|               role="cell" |               role="cell" | ||||||
|               class="mdc-data-table__cell ${classMap({ |               class="mdc-data-table__cell ${classMap({ | ||||||
|                                     "mdc-data-table__cell--numeric": |                 "mdc-data-table__cell--numeric": column.type === "numeric", | ||||||
|                                       column.type === "numeric", |                 "mdc-data-table__cell--icon": column.type === "icon", | ||||||
|                                     "mdc-data-table__cell--icon": |  | ||||||
|                                       column.type === "icon", |  | ||||||
|                 "mdc-data-table__cell--icon-button": |                 "mdc-data-table__cell--icon-button": | ||||||
|                   column.type === "icon-button", |                   column.type === "icon-button", | ||||||
|                 "mdc-data-table__cell--overflow-menu": |                 "mdc-data-table__cell--overflow-menu": | ||||||
| @@ -416,31 +419,18 @@ export class HaDataTable extends LitElement { | |||||||
|               })}" |               })}" | ||||||
|               style=${column.width |               style=${column.width | ||||||
|                 ? styleMap({ |                 ? styleMap({ | ||||||
|                                         [column.grows ? "minWidth" : "width"]: |                     [column.grows ? "minWidth" : "width"]: column.width, | ||||||
|                                           column.width, |                     maxWidth: column.maxWidth ? column.maxWidth : "", | ||||||
|                                         maxWidth: column.maxWidth |  | ||||||
|                                           ? column.maxWidth |  | ||||||
|                                           : "", |  | ||||||
|                   }) |                   }) | ||||||
|                 : ""} |                 : ""} | ||||||
|             > |             > | ||||||
|                                   ${column.template |               ${column.template ? column.template(row[key], row) : row[key]} | ||||||
|                                     ? column.template(row[key], row) |  | ||||||
|                                     : row[key]} |  | ||||||
|             </div> |             </div> | ||||||
|           `; |           `; | ||||||
|                             } |  | ||||||
|                           )} |  | ||||||
|                         </div> |  | ||||||
|                       `; |  | ||||||
|                     }, |  | ||||||
|         })} |         })} | ||||||
|       </div> |       </div> | ||||||
|               `} |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|   private async _sortFilterData() { |   private async _sortFilterData() { | ||||||
|     const startTime = new Date().getTime(); |     const startTime = new Date().getTime(); | ||||||
| @@ -536,7 +526,7 @@ export class HaDataTable extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleRowCheckboxClick(ev: Event) { |   private _handleRowCheckboxClick = (ev: Event) => { | ||||||
|     const checkbox = ev.currentTarget as HaCheckbox; |     const checkbox = ev.currentTarget as HaCheckbox; | ||||||
|     const rowId = (checkbox as any).rowId; |     const rowId = (checkbox as any).rowId; | ||||||
|  |  | ||||||
| @@ -549,16 +539,16 @@ export class HaDataTable extends LitElement { | |||||||
|       this._checkedRows = this._checkedRows.filter((row) => row !== rowId); |       this._checkedRows = this._checkedRows.filter((row) => row !== rowId); | ||||||
|     } |     } | ||||||
|     this._checkedRowsChanged(); |     this._checkedRowsChanged(); | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|   private _handleRowClick(ev: Event) { |   private _handleRowClick = (ev: Event) => { | ||||||
|     const target = ev.target as HTMLElement; |     const target = ev.target as HTMLElement; | ||||||
|     if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) { |     if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const rowId = (ev.currentTarget as any).rowId; |     const rowId = (ev.currentTarget as any).rowId; | ||||||
|     fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); |     fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|   private _checkedRowsChanged() { |   private _checkedRowsChanged() { | ||||||
|     // force scroller to update, change it's items |     // force scroller to update, change it's items | ||||||
| @@ -571,6 +561,9 @@ export class HaDataTable extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleSearchChange(ev: CustomEvent): void { |   private _handleSearchChange(ev: CustomEvent): void { | ||||||
|  |     if (this.filter) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this._debounceSearch(ev.detail.value); |     this._debounceSearch(ev.detail.value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -935,11 +928,10 @@ export class HaDataTable extends LitElement { | |||||||
|         } |         } | ||||||
|         .table-header { |         .table-header { | ||||||
|           border-bottom: 1px solid var(--divider-color); |           border-bottom: 1px solid var(--divider-color); | ||||||
|           padding: 0 16px; |  | ||||||
|         } |         } | ||||||
|         search-input { |         search-input { | ||||||
|           position: relative; |           display: block; | ||||||
|           top: 2px; |           flex: 1; | ||||||
|         } |         } | ||||||
|         slot[name="header"] { |         slot[name="header"] { | ||||||
|           display: block; |           display: block; | ||||||
| @@ -952,6 +944,7 @@ export class HaDataTable extends LitElement { | |||||||
|         } |         } | ||||||
|         .scroller { |         .scroller { | ||||||
|           height: calc(100% - 57px); |           height: calc(100% - 57px); | ||||||
|  |           overflow: overlay !important; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .mdc-data-table__table.auto-height .scroller { |         .mdc-data-table__table.auto-height .scroller { | ||||||
| @@ -967,6 +960,9 @@ export class HaDataTable extends LitElement { | |||||||
|         .clickable { |         .clickable { | ||||||
|           cursor: pointer; |           cursor: pointer; | ||||||
|         } |         } | ||||||
|  |         lit-virtualizer { | ||||||
|  |           contain: size layout !important; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,20 +1,7 @@ | |||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; |  | ||||||
| import "@polymer/paper-input/paper-input"; |  | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-item/paper-item-body"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; |  | ||||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, | import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | ||||||
|   CSSResultGroup, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; |  | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| @@ -50,36 +37,12 @@ interface AreaDevices { | |||||||
|   devices: string[]; |   devices: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | const rowRenderer: ComboBoxLitRenderer<AreaDevices> = ( | ||||||
| const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style> |   item | ||||||
|     paper-item { | ) => html`<mwc-list-item twoline> | ||||||
|       padding: 0; |   <span>${item.name}</span> | ||||||
|       margin: -10px; |   <span slot="secondary">${item.devices.length} devices</span> | ||||||
|       margin-left: 0; | </mwc-list-item>`; | ||||||
|     } |  | ||||||
|     #content { |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       margin-right: -2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-item { |  | ||||||
|       margin-left: 10px; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-item> |  | ||||||
|     <paper-item-body two-line=""> |  | ||||||
|       <div class="name">${item.name}</div> |  | ||||||
|       <div secondary>${item.devices.length} devices</div> |  | ||||||
|     </paper-item-body> |  | ||||||
|   </paper-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-area-devices-picker") | @customElement("ha-area-devices-picker") | ||||||
| export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||||
| @@ -117,9 +80,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | |||||||
|   @property({ type: Array, attribute: "include-device-classes" }) |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|   public includeDeviceClasses?: string[]; |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) |  | ||||||
|   private _opened?: boolean; |  | ||||||
|  |  | ||||||
|   @state() private _areaPicker = true; |   @state() private _areaPicker = true; | ||||||
|  |  | ||||||
|   @state() private _devices?: DeviceRegistryEntry[]; |   @state() private _devices?: DeviceRegistryEntry[]; | ||||||
| @@ -302,71 +262,30 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <ha-combo-box | ||||||
|  |         .hass=${this.hass} | ||||||
|         item-value-path="id" |         item-value-path="id" | ||||||
|         item-id-path="id" |         item-id-path="id" | ||||||
|         item-label-path="name" |         item-label-path="name" | ||||||
|         .items=${areas} |         .items=${areas} | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|         ${comboBoxRenderer(rowRenderer)} |         .renderer=${rowRenderer} | ||||||
|         @opened-changed=${this._openedChanged} |  | ||||||
|         @value-changed=${this._areaPicked} |  | ||||||
|       > |  | ||||||
|         <paper-input |  | ||||||
|         .label=${this.label === undefined && this.hass |         .label=${this.label === undefined && this.hass | ||||||
|           ? this.hass.localize("ui.components.device-picker.device") |           ? this.hass.localize("ui.components.device-picker.device") | ||||||
|           : `${this.label} in area`} |           : `${this.label} in area`} | ||||||
|           class="input" |         @value-changed=${this._areaPicked} | ||||||
|           autocapitalize="none" |  | ||||||
|           autocomplete="off" |  | ||||||
|           autocorrect="off" |  | ||||||
|           spellcheck="false" |  | ||||||
|         > |  | ||||||
|           <div class="suffix" slot="suffix"> |  | ||||||
|             ${this.value |  | ||||||
|               ? html`<ha-icon-button |  | ||||||
|                   class="clear-button" |  | ||||||
|                   .label=${this.hass.localize( |  | ||||||
|                     "ui.components.device-picker.clear" |  | ||||||
|                   )} |  | ||||||
|                   .path=${mdiClose} |  | ||||||
|                   @click=${this._clearValue} |  | ||||||
|                   no-ripple |  | ||||||
|                 ></ha-icon-button> ` |  | ||||||
|               : ""} |  | ||||||
|             ${areas.length > 0 |  | ||||||
|               ? html` |  | ||||||
|                   <ha-icon-button |  | ||||||
|                     .label=${this.hass.localize( |  | ||||||
|                       "ui.components.device-picker.show_devices" |  | ||||||
|                     )} |  | ||||||
|                     .path=${this._opened ? mdiMenuUp : mdiMenuDown} |  | ||||||
|                     class="toggle-button" |  | ||||||
|                   ></ha-icon-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|           </div> |  | ||||||
|         </paper-input> |  | ||||||
|       </vaadin-combo-box-light> |  | ||||||
|       <mwc-button @click=${this._switchPicker} |  | ||||||
|         >Choose individual devices</mwc-button |  | ||||||
|       > |       > | ||||||
|  |       </ha-combo-box> | ||||||
|  |       <mwc-button @click=${this._switchPicker}> | ||||||
|  |         Choose individual devices | ||||||
|  |       </mwc-button> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._setValue([]); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private get _value() { |   private get _value() { | ||||||
|     return this.value || []; |     return this.value || []; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _openedChanged(ev: PolymerChangedEvent<boolean>) { |  | ||||||
|     this._opened = ev.detail.value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _switchPicker() { |   private async _switchPicker() { | ||||||
|     this._areaPicker = !this._areaPicker; |     this._areaPicker = !this._areaPicker; | ||||||
|   } |   } | ||||||
| @@ -398,22 +317,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | |||||||
|       fireEvent(this, "change"); |       fireEvent(this, "change"); | ||||||
|     }, 0); |     }, 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       .suffix { |  | ||||||
|         display: flex; |  | ||||||
|       } |  | ||||||
|       ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 0px 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,7 +1,4 @@ | |||||||
| import "@polymer/paper-input/paper-input"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-item/paper-item-body"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { property, state } from "lit/decorators"; | import { property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| @@ -10,7 +7,7 @@ import { | |||||||
|   deviceAutomationsEqual, |   deviceAutomationsEqual, | ||||||
| } from "../../data/device_automation"; | } from "../../data/device_automation"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-paper-dropdown-menu"; | import "../ha-select"; | ||||||
|  |  | ||||||
| const NO_AUTOMATION_KEY = "NO_AUTOMATION"; | const NO_AUTOMATION_KEY = "NO_AUTOMATION"; | ||||||
| const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; | const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; | ||||||
| @@ -67,14 +64,12 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|     this._createNoAutomation = createNoAutomation; |     this._createNoAutomation = createNoAutomation; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private get _key() { |   private get _value() { | ||||||
|     if ( |     if (!this.value) { | ||||||
|       !this.value || |       return ""; | ||||||
|       deviceAutomationsEqual( |     } | ||||||
|         this._createNoAutomation(this.deviceId), |  | ||||||
|         this.value |     if (!this._automations.length) { | ||||||
|       ) |  | ||||||
|     ) { |  | ||||||
|       return NO_AUTOMATION_KEY; |       return NO_AUTOMATION_KEY; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -93,42 +88,32 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|     if (this._renderEmpty) { |     if (this._renderEmpty) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|  |     const value = this._value; | ||||||
|     return html` |     return html` | ||||||
|       <ha-paper-dropdown-menu |       <ha-select | ||||||
|         .label=${this.label} |         .label=${this.label} | ||||||
|         .value=${this.value |         .value=${value} | ||||||
|           ? this._localizeDeviceAutomation(this.hass, this.value) |         @selected=${this._automationChanged} | ||||||
|           : ""} |         .disabled=${this._automations.length === 0} | ||||||
|         ?disabled=${this._automations.length === 0} |  | ||||||
|       > |  | ||||||
|         <paper-listbox |  | ||||||
|           slot="dropdown-content" |  | ||||||
|           .selected=${this._key} |  | ||||||
|           attr-for-selected="key" |  | ||||||
|           @iron-select=${this._automationChanged} |  | ||||||
|         > |  | ||||||
|           <paper-item |  | ||||||
|             key=${NO_AUTOMATION_KEY} |  | ||||||
|             .automation=${this._createNoAutomation(this.deviceId)} |  | ||||||
|             hidden |  | ||||||
|       > |       > | ||||||
|  |         ${value === NO_AUTOMATION_KEY | ||||||
|  |           ? html`<mwc-list-item .value=${NO_AUTOMATION_KEY}> | ||||||
|               ${this.NO_AUTOMATION_TEXT} |               ${this.NO_AUTOMATION_TEXT} | ||||||
|           </paper-item> |             </mwc-list-item>` | ||||||
|           <paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden> |           : ""} | ||||||
|  |         ${value === UNKNOWN_AUTOMATION_KEY | ||||||
|  |           ? html`<mwc-list-item .value=${UNKNOWN_AUTOMATION_KEY}> | ||||||
|               ${this.UNKNOWN_AUTOMATION_TEXT} |               ${this.UNKNOWN_AUTOMATION_TEXT} | ||||||
|           </paper-item> |             </mwc-list-item>` | ||||||
|  |           : ""} | ||||||
|         ${this._automations.map( |         ${this._automations.map( | ||||||
|           (automation, idx) => html` |           (automation, idx) => html` | ||||||
|               <paper-item |             <mwc-list-item .value=${`${automation.device_id}_${idx}`}> | ||||||
|                 key=${`${this.deviceId}_${idx}`} |  | ||||||
|                 .automation=${automation} |  | ||||||
|               > |  | ||||||
|               ${this._localizeDeviceAutomation(this.hass, automation)} |               ${this._localizeDeviceAutomation(this.hass, automation)} | ||||||
|               </paper-item> |             </mwc-list-item> | ||||||
|           ` |           ` | ||||||
|         )} |         )} | ||||||
|         </paper-listbox> |       </ha-select> | ||||||
|       </ha-paper-dropdown-menu> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -138,14 +123,6 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|     if (changedProps.has("deviceId")) { |     if (changedProps.has("deviceId")) { | ||||||
|       this._updateDeviceInfo(); |       this._updateDeviceInfo(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // The value has changed, force the listbox to update |  | ||||||
|     if (changedProps.has("value") || changedProps.has("_renderEmpty")) { |  | ||||||
|       const listbox = this.shadowRoot!.querySelector("paper-listbox")!; |  | ||||||
|       if (listbox) { |  | ||||||
|         listbox._selectSelected(this._key); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _updateDeviceInfo() { |   private async _updateDeviceInfo() { | ||||||
| @@ -168,9 +145,16 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _automationChanged(ev) { |   private _automationChanged(ev) { | ||||||
|     if (ev.detail.item.automation) { |     const value = ev.target.value; | ||||||
|       this._setValue(ev.detail.item.automation); |     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) { |   private _setValue(automation: T) { | ||||||
| @@ -183,14 +167,9 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       ha-paper-dropdown-menu { |       ha-select { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|       } |         margin-top: 4px; | ||||||
|       paper-listbox { |  | ||||||
|         min-width: 200px; |  | ||||||
|       } |  | ||||||
|       paper-item { |  | ||||||
|         cursor: pointer; |  | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,18 +1,9 @@ | |||||||
| import "@polymer/paper-item/paper-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; |  | ||||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, |  | ||||||
|   CSSResultGroup, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { customElement, property, state, query } from "lit/decorators"; |  | ||||||
| import memoizeOne from "memoize-one"; |  | ||||||
| import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | 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 { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; | import { computeDomain } from "../../common/entity/compute_domain"; | ||||||
| import { stringCompare } from "../../common/string/compare"; | import { stringCompare } from "../../common/string/compare"; | ||||||
| @@ -46,36 +37,12 @@ export type HaDevicePickerDeviceFilterFunc = ( | |||||||
|   device: DeviceRegistryEntry |   device: DeviceRegistryEntry | ||||||
| ) => boolean; | ) => boolean; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item | ||||||
| const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style> |   .twoline=${!!item.area} | ||||||
|     paper-item { | > | ||||||
|       padding: 0; |   <span>${item.name}</span> | ||||||
|       margin: -10px; |   <span slot="secondary">${item.area}</span> | ||||||
|       margin-left: 0; | </mwc-list-item>`; | ||||||
|     } |  | ||||||
|     #content { |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       margin-right: -2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-item { |  | ||||||
|       margin-left: 10px; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-item> |  | ||||||
|     <paper-item-body two-line> |  | ||||||
|       ${item.name} |  | ||||||
|       <span secondary>${item.area}</span> |  | ||||||
|     </paper-item-body> |  | ||||||
|   </paper-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-device-picker") | @customElement("ha-device-picker") | ||||||
| export class HaDevicePicker extends SubscribeMixin(LitElement) { | export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||||
| @@ -138,7 +105,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|       if (!devices.length) { |       if (!devices.length) { | ||||||
|         return [ |         return [ | ||||||
|           { |           { | ||||||
|             id: "", |             id: "no_devices", | ||||||
|             area: "", |             area: "", | ||||||
|             name: this.hass.localize("ui.components.device-picker.no_devices"), |             name: this.hass.localize("ui.components.device-picker.no_devices"), | ||||||
|           }, |           }, | ||||||
| @@ -234,7 +201,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|       if (!outputDevices.length) { |       if (!outputDevices.length) { | ||||||
|         return [ |         return [ | ||||||
|           { |           { | ||||||
|             id: "", |             id: "no_devices", | ||||||
|             area: "", |             area: "", | ||||||
|             name: this.hass.localize("ui.components.device-picker.no_match"), |             name: this.hass.localize("ui.components.device-picker.no_match"), | ||||||
|           }, |           }, | ||||||
| @@ -303,7 +270,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|         .renderer=${rowRenderer} |         .renderer=${rowRenderer} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         item-value-path="id" |         item-value-path="id" | ||||||
|         item-id-path="id" |  | ||||||
|         item-label-path="name" |         item-label-path="name" | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
|         @value-changed=${this._deviceChanged} |         @value-changed=${this._deviceChanged} | ||||||
| @@ -317,7 +283,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   private _deviceChanged(ev: PolymerChangedEvent<string>) { |   private _deviceChanged(ev: PolymerChangedEvent<string>) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     const newValue = ev.detail.value; |     let newValue = ev.detail.value; | ||||||
|  |  | ||||||
|  |     if (newValue === "no_devices") { | ||||||
|  |       newValue = ""; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (newValue !== this._value) { |     if (newValue !== this._value) { | ||||||
|       this._setValue(newValue); |       this._setValue(newValue); | ||||||
| @@ -335,19 +305,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|       fireEvent(this, "change"); |       fireEvent(this, "change"); | ||||||
|     }, 0); |     }, 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       paper-input > ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; | 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 { customElement, property } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { isValidEntityId } from "../../common/entity/valid_entity_id"; | import { isValidEntityId } from "../../common/entity/valid_entity_id"; | ||||||
| @@ -114,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|     const newValue = event.detail.value; |     const newValue = event.detail.value; | ||||||
|     if ( |     if ( | ||||||
|       newValue === curValue || |       newValue === curValue || | ||||||
|       (newValue !== "" && !isValidEntityId(newValue)) |       (newValue !== undefined && !isValidEntityId(newValue)) | ||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -145,6 +145,12 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|  |  | ||||||
|     this._updateEntities([...currentEntities, toAdd]); |     this._updateEntities([...currentEntities, toAdd]); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static override styles = css` | ||||||
|  |     div { | ||||||
|  |       margin-top: 8px; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -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 { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, |  | ||||||
|   CSSResultGroup, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; |  | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import { formatAttributeName } from "../../data/entity_attributes"; | import { formatAttributeName } from "../../data/entity_attributes"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-icon-button"; | import "../ha-combo-box"; | ||||||
| import "../ha-svg-icon"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
| import "./state-badge"; |  | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles |  | ||||||
| const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> |  | ||||||
|     paper-item { |  | ||||||
|       padding: 0; |  | ||||||
|       margin: -10px; |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|     #content { |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       margin-right: -2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-item { |  | ||||||
|       margin-left: 10px; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-item>${formatAttributeName(item)}</paper-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-entity-attribute-picker") | @customElement("ha-entity-attribute-picker") | ||||||
| class HaEntityAttributePicker extends LitElement { | class HaEntityAttributePicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -68,7 +28,7 @@ class HaEntityAttributePicker extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) private _opened = false; |   @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) { |   protected shouldUpdate(changedProps: PropertyValues) { | ||||||
|     return !(!changedProps.has("_opened") && this._opened); |     return !(!changedProps.has("_opened") && this._opened); | ||||||
| @@ -78,7 +38,10 @@ class HaEntityAttributePicker extends LitElement { | |||||||
|     if (changedProps.has("_opened") && this._opened) { |     if (changedProps.has("_opened") && this._opened) { | ||||||
|       const state = this.entityId ? this.hass.states[this.entityId] : undefined; |       const state = this.entityId ? this.hass.states[this.entityId] : undefined; | ||||||
|       (this._comboBox as any).items = state |       (this._comboBox as any).items = state | ||||||
|         ? Object.keys(state.attributes) |         ? Object.keys(state.attributes).map((key) => ({ | ||||||
|  |             value: key, | ||||||
|  |             label: formatAttributeName(key), | ||||||
|  |           })) | ||||||
|         : []; |         : []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -89,100 +52,31 @@ class HaEntityAttributePicker extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <ha-combo-box | ||||||
|         .value=${this._value} |         .hass=${this.hass} | ||||||
|         .allowCustomValue=${this.allowCustomValue} |         .value=${this.value || ""} | ||||||
|         attr-for-value="bind-value" |  | ||||||
|         ${comboBoxRenderer(rowRenderer)} |  | ||||||
|         @opened-changed=${this._openedChanged} |  | ||||||
|         @value-changed=${this._valueChanged} |  | ||||||
|       > |  | ||||||
|         <paper-input |  | ||||||
|         .autofocus=${this.autofocus} |         .autofocus=${this.autofocus} | ||||||
|         .label=${this.label ?? |         .label=${this.label ?? | ||||||
|         this.hass.localize( |         this.hass.localize( | ||||||
|           "ui.components.entity.entity-attribute-picker.attribute" |           "ui.components.entity.entity-attribute-picker.attribute" | ||||||
|         )} |         )} | ||||||
|           .value=${this._value ? formatAttributeName(this._value) : ""} |  | ||||||
|         .disabled=${this.disabled || !this.entityId} |         .disabled=${this.disabled || !this.entityId} | ||||||
|           class="input" |         .allowCustomValue=${this.allowCustomValue} | ||||||
|           autocapitalize="none" |         item-value-path="value" | ||||||
|           autocomplete="off" |         item-label-path="label" | ||||||
|           autocorrect="off" |         @opened-changed=${this._openedChanged} | ||||||
|           spellcheck="false" |         @value-changed=${this._valueChanged} | ||||||
|       > |       > | ||||||
|           <div class="suffix" slot="suffix"> |       </ha-combo-box> | ||||||
|             ${this.value |  | ||||||
|               ? html` |  | ||||||
|                   <ha-icon-button |  | ||||||
|                     .label=${this.hass.localize( |  | ||||||
|                       "ui.components.entity.entity-picker.clear" |  | ||||||
|                     )} |  | ||||||
|                     .path=${mdiClose} |  | ||||||
|                     class="clear-button" |  | ||||||
|                     tabindex="-1" |  | ||||||
|                     @click=${this._clearValue} |  | ||||||
|                     no-ripple |  | ||||||
|                   ></ha-icon-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|  |  | ||||||
|             <ha-icon-button |  | ||||||
|               .label=${this.hass.localize( |  | ||||||
|                 "ui.components.entity.entity-attribute-picker.show_attributes" |  | ||||||
|               )} |  | ||||||
|               .path=${this._opened ? mdiMenuUp : mdiMenuDown} |  | ||||||
|               class="toggle-button" |  | ||||||
|               tabindex="-1" |  | ||||||
|             ></ha-icon-button> |  | ||||||
|           </div> |  | ||||||
|         </paper-input> |  | ||||||
|       </vaadin-combo-box-light> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._setValue(""); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private get _value() { |  | ||||||
|     return this.value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _openedChanged(ev: PolymerChangedEvent<boolean>) { |   private _openedChanged(ev: PolymerChangedEvent<boolean>) { | ||||||
|     this._opened = ev.detail.value; |     this._opened = ev.detail.value; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: PolymerChangedEvent<string>) { |   private _valueChanged(ev: PolymerChangedEvent<string>) { | ||||||
|     const newValue = ev.detail.value; |     this.value = 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; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,25 +1,16 @@ | |||||||
| import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-input/paper-input"; |  | ||||||
| import "@polymer/paper-item/paper-icon-item"; |  | ||||||
| import "@polymer/paper-item/paper-item-body"; |  | ||||||
| import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; |  | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, | import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | ||||||
|   CSSResultGroup, | import { customElement, property, query, state } from "lit/decorators"; | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; |  | ||||||
| import { customElement, property, query } from "lit/decorators"; |  | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; | import { computeDomain } from "../../common/entity/compute_domain"; | ||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | import { computeStateName } from "../../common/entity/compute_state_name"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
|  | import "../ha-combo-box"; | ||||||
|  | import type { HaComboBox } from "../ha-combo-box"; | ||||||
| import "../ha-icon-button"; | import "../ha-icon-button"; | ||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
| @@ -27,35 +18,15 @@ import "./state-badge"; | |||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | // eslint-disable-next-line lit/prefer-static-styles | ||||||
| const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style> | const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> = | ||||||
|     paper-icon-item { |   (item) => | ||||||
|       padding: 0; |     html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}> | ||||||
|       margin: -8px; |       ${item.state | ||||||
|     } |         ? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>` | ||||||
|     #content { |         : ""} | ||||||
|       display: flex; |       <span>${item.friendly_name}</span> | ||||||
|       align-items: center; |       <span slot="secondary">${item.entity_id}</span> | ||||||
|     } |     </mwc-list-item>`; | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-icon-item { |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-icon-item> |  | ||||||
|     <state-badge slot="item-icon" .stateObj=${item}></state-badge> |  | ||||||
|     <paper-item-body two-line=""> |  | ||||||
|       ${computeStateName(item)} |  | ||||||
|       <span secondary>${item.entity_id}</span> |  | ||||||
|     </paper-item-body> |  | ||||||
|   </paper-icon-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-entity-picker") | @customElement("ha-entity-picker") | ||||||
| export class HaEntityPicker extends LitElement { | export class HaEntityPicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -107,19 +78,19 @@ export class HaEntityPicker extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public hideClearIcon = false; |   @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() { |   public open() { | ||||||
|     this.updateComplete.then(() => { |     this.updateComplete.then(() => { | ||||||
|       (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); |       this.comboBox?.open(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     this.updateComplete.then(() => { |     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); |       let entityIds = Object.keys(hass.states); | ||||||
|  |  | ||||||
|  |       if (!entityIds.length) { | ||||||
|  |         return [ | ||||||
|  |           { | ||||||
|  |             entity_id: "", | ||||||
|  |             state: "", | ||||||
|  |             last_changed: "", | ||||||
|  |             last_updated: "", | ||||||
|  |             context: { id: "", user_id: null }, | ||||||
|  |             friendly_name: this.hass!.localize( | ||||||
|  |               "ui.components.entity.entity-picker.no_entities" | ||||||
|  |             ), | ||||||
|  |             attributes: { | ||||||
|  |               friendly_name: this.hass!.localize( | ||||||
|  |                 "ui.components.entity.entity-picker.no_entities" | ||||||
|  |               ), | ||||||
|  |               icon: "mdi:magnify", | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (includeDomains) { |       if (includeDomains) { | ||||||
|         entityIds = entityIds.filter((eid) => |         entityIds = entityIds.filter((eid) => | ||||||
|           includeDomains.includes(computeDomain(eid)) |           includeDomains.includes(computeDomain(eid)) | ||||||
| @@ -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) { |       if (includeDeviceClasses) { | ||||||
|         states = states.filter( |         states = states.filter( | ||||||
| @@ -196,6 +191,9 @@ export class HaEntityPicker extends LitElement { | |||||||
|             last_changed: "", |             last_changed: "", | ||||||
|             last_updated: "", |             last_updated: "", | ||||||
|             context: { id: "", user_id: null }, |             context: { id: "", user_id: null }, | ||||||
|  |             friendly_name: this.hass!.localize( | ||||||
|  |               "ui.components.entity.entity-picker.no_match" | ||||||
|  |             ), | ||||||
|             attributes: { |             attributes: { | ||||||
|               friendly_name: this.hass!.localize( |               friendly_name: this.hass!.localize( | ||||||
|                 "ui.components.entity.entity-picker.no_match" |                 "ui.components.entity.entity-picker.no_match" | ||||||
| @@ -241,64 +239,25 @@ export class HaEntityPicker extends LitElement { | |||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <ha-combo-box | ||||||
|         item-value-path="entity_id" |         item-value-path="entity_id" | ||||||
|         item-label-path="entity_id" |         item-label-path="friendly_name" | ||||||
|  |         .hass=${this.hass} | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|  |         .label=${this.label === undefined | ||||||
|  |           ? this.hass.localize("ui.components.entity.entity-picker.entity") | ||||||
|  |           : this.label} | ||||||
|         .allowCustomValue=${this.allowCustomEntity} |         .allowCustomValue=${this.allowCustomEntity} | ||||||
|         .filteredItems=${this._states} |         .filteredItems=${this._states} | ||||||
|         ${comboBoxRenderer(rowRenderer)} |         .renderer=${rowRenderer} | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|         @filter-changed=${this._filterChanged} |         @filter-changed=${this._filterChanged} | ||||||
|       > |       > | ||||||
|         <paper-input |       </ha-combo-box> | ||||||
|           .autofocus=${this.autofocus} |  | ||||||
|           .label=${this.label === undefined |  | ||||||
|             ? this.hass.localize("ui.components.entity.entity-picker.entity") |  | ||||||
|             : this.label} |  | ||||||
|           .disabled=${this.disabled} |  | ||||||
|           class="input" |  | ||||||
|           autocapitalize="none" |  | ||||||
|           autocomplete="off" |  | ||||||
|           autocorrect="off" |  | ||||||
|           spellcheck="false" |  | ||||||
|         > |  | ||||||
|           <div class="suffix" slot="suffix"> |  | ||||||
|             ${this.value && !this.hideClearIcon |  | ||||||
|               ? html` |  | ||||||
|                   <ha-icon-button |  | ||||||
|                     .label=${this.hass.localize( |  | ||||||
|                       "ui.components.entity.entity-picker.clear" |  | ||||||
|                     )} |  | ||||||
|                     .path=${mdiClose} |  | ||||||
|                     class="clear-button" |  | ||||||
|                     tabindex="-1" |  | ||||||
|                     @click=${this._clearValue} |  | ||||||
|                     no-ripple |  | ||||||
|                   ></ha-icon-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|  |  | ||||||
|             <ha-icon-button |  | ||||||
|               .label=${this.hass.localize( |  | ||||||
|                 "ui.components.entity.entity-picker.show_entities" |  | ||||||
|               )} |  | ||||||
|               .path=${this._opened ? mdiMenuUp : mdiMenuDown} |  | ||||||
|               class="toggle-button" |  | ||||||
|               tabindex="-1" |  | ||||||
|             ></ha-icon-button> |  | ||||||
|           </div> |  | ||||||
|         </paper-input> |  | ||||||
|       </vaadin-combo-box-light> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._setValue(""); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private get _value() { |   private get _value() { | ||||||
|     return this.value || ""; |     return this.value || ""; | ||||||
|   } |   } | ||||||
| @@ -308,6 +267,7 @@ export class HaEntityPicker extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: PolymerChangedEvent<string>) { |   private _valueChanged(ev: PolymerChangedEvent<string>) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     const newValue = ev.detail.value; |     const newValue = ev.detail.value; | ||||||
|     if (newValue !== this._value) { |     if (newValue !== this._value) { | ||||||
|       this._setValue(newValue); |       this._setValue(newValue); | ||||||
| @@ -317,9 +277,9 @@ export class HaEntityPicker extends LitElement { | |||||||
|   private _filterChanged(ev: CustomEvent): void { |   private _filterChanged(ev: CustomEvent): void { | ||||||
|     const filterString = ev.detail.value.toLowerCase(); |     const filterString = ev.detail.value.toLowerCase(); | ||||||
|     (this.comboBox as any).filteredItems = this._states.filter( |     (this.comboBox as any).filteredItems = this._states.filter( | ||||||
|       (state) => |       (entityState) => | ||||||
|         state.entity_id.toLowerCase().includes(filterString) || |         entityState.entity_id.toLowerCase().includes(filterString) || | ||||||
|         computeStateName(state).toLowerCase().includes(filterString) |         computeStateName(entityState).toLowerCase().includes(filterString) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -330,22 +290,6 @@ export class HaEntityPicker extends LitElement { | |||||||
|       fireEvent(this, "change"); |       fireEvent(this, "change"); | ||||||
|     }, 0); |     }, 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       .suffix { |  | ||||||
|         display: flex; |  | ||||||
|       } |  | ||||||
|       ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 0px 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,17 +1,5 @@ | |||||||
| 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 { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, |  | ||||||
|   CSSResultGroup, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| @@ -76,41 +64,12 @@ export class HaStatisticPicker extends LitElement { | |||||||
|     id: string; |     id: string; | ||||||
|     name: string; |     name: string; | ||||||
|     state?: HassEntity; |     state?: HassEntity; | ||||||
|     // eslint-disable-next-line lit/prefer-static-styles |   }> = (item) => html`<mwc-list-item graphic="avatar" twoline> | ||||||
|   }> = (item) => html`<style> |  | ||||||
|       paper-icon-item { |  | ||||||
|         padding: 0; |  | ||||||
|         margin: -8px; |  | ||||||
|       } |  | ||||||
|       #content { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|       } |  | ||||||
|       ha-svg-icon { |  | ||||||
|         padding-left: 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       :host(:not([selected])) ha-svg-icon { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|       :host([selected]) paper-icon-item { |  | ||||||
|         margin-left: 0; |  | ||||||
|       } |  | ||||||
|       a { |  | ||||||
|         color: var(--primary-color); |  | ||||||
|       } |  | ||||||
|     </style> |  | ||||||
|     <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|     <paper-icon-item> |  | ||||||
|     ${item.state |     ${item.state | ||||||
|         ? html`<state-badge |       ? html`<state-badge slot="graphic" .stateObj=${item.state}></state-badge>` | ||||||
|             slot="item-icon" |  | ||||||
|             .stateObj=${item.state} |  | ||||||
|           ></state-badge>` |  | ||||||
|       : ""} |       : ""} | ||||||
|       <paper-item-body two-line=""> |     <span>${item.name}</span> | ||||||
|         ${item.name} |     <span slot="secondary" | ||||||
|         <span secondary |  | ||||||
|       >${item.id === "" || item.id === "__missing" |       >${item.id === "" || item.id === "__missing" | ||||||
|         ? html`<a |         ? html`<a | ||||||
|             target="_blank" |             target="_blank" | ||||||
| @@ -122,8 +81,7 @@ export class HaStatisticPicker extends LitElement { | |||||||
|           >` |           >` | ||||||
|         : item.id}</span |         : item.id}</span | ||||||
|     > |     > | ||||||
|       </paper-item-body> |   </mwc-list-item>`; | ||||||
|     </paper-icon-item>`; |  | ||||||
|  |  | ||||||
|   private _getStatistics = memoizeOne( |   private _getStatistics = memoizeOne( | ||||||
|     ( |     ( | ||||||
| @@ -293,19 +251,6 @@ export class HaStatisticPicker extends LitElement { | |||||||
|       fireEvent(this, "change"); |       fireEvent(this, "change"); | ||||||
|     }, 0); |     }, 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       paper-input > ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -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 { customElement, property } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import type { PolymerChangedEvent } from "../../polymer-types"; | import type { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| @@ -103,6 +103,20 @@ class HaStatisticsPicker extends LitElement { | |||||||
|  |  | ||||||
|     this._updateStatistics([...currentEntities, toAdd]); |     this._updateStatistics([...currentEntities, toAdd]); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|  |       :host { | ||||||
|  |         width: 200px; | ||||||
|  |         display: block; | ||||||
|  |       } | ||||||
|  |       ha-statistic-picker { | ||||||
|  |         display: block; | ||||||
|  |         width: 100%; | ||||||
|  |         margin-top: 8px; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { mdiCheck } from "@mdi/js"; |  | ||||||
| import { html, LitElement, TemplateResult } from "lit"; | import { html, LitElement, TemplateResult } from "lit"; | ||||||
| import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| @@ -12,39 +11,12 @@ import { PolymerChangedEvent } from "../polymer-types"; | |||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { HaComboBox } from "./ha-combo-box"; | import { HaComboBox } from "./ha-combo-box"; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = ( | ||||||
| const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style> |   item | ||||||
|     paper-item { | ) => html`<mwc-list-item twoline> | ||||||
|       padding: 0; |   <span>${item.name}</span> | ||||||
|       margin: -10px; |   <span slot="secondary">${item.slug}</span> | ||||||
|       margin-left: 0px; | </mwc-list-item>`; | ||||||
|     } |  | ||||||
|     #content { |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-item { |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       margin-right: -2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-icon-item { |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-item> |  | ||||||
|     <paper-item-body two-line> |  | ||||||
|       ${item.name} |  | ||||||
|       <span secondary>${item.slug}</span> |  | ||||||
|     </paper-item-body> |  | ||||||
|   </paper-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-addon-picker") | @customElement("ha-addon-picker") | ||||||
| class HaAddonPicker extends LitElement { | class HaAddonPicker extends LitElement { | ||||||
|   | |||||||
| @@ -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 { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { | import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
|   css, | import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; | ||||||
|   CSSResultGroup, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit"; |  | ||||||
| import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; |  | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| @@ -41,38 +28,18 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; | |||||||
| import { PolymerChangedEvent } from "../polymer-types"; | import { PolymerChangedEvent } from "../polymer-types"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
|  | import type { HaComboBox } from "./ha-combo-box"; | ||||||
|  | import "./ha-combo-box"; | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
|  |  | ||||||
| const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( | const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( | ||||||
|   item |   item | ||||||
|   // eslint-disable-next-line lit/prefer-static-styles | ) => html`<mwc-list-item | ||||||
| ) => html`<style> |   class=${classMap({ "add-new": item.area_id === "add_new" })} | ||||||
|     paper-item { | > | ||||||
|       padding: 0; |   ${item.name} | ||||||
|       margin: -10px; | </mwc-list-item>`; | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|     #content { |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       margin-right: -2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-item { |  | ||||||
|       margin-left: 10px; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}> |  | ||||||
|     <paper-item-body two-line>${item.name}</paper-item-body> |  | ||||||
|   </paper-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-area-picker") | @customElement("ha-area-picker") | ||||||
| export class HaAreaPicker extends SubscribeMixin(LitElement) { | export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||||
| @@ -125,7 +92,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _opened?: boolean; |   @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; |   private _init = false; | ||||||
|  |  | ||||||
| @@ -145,13 +114,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   public open() { |   public open() { | ||||||
|     this.updateComplete.then(() => { |     this.updateComplete.then(() => { | ||||||
|       (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); |       this.comboBox?.open(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     this.updateComplete.then(() => { |     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) { |       if (!areas.length) { | ||||||
|         return [ |         return [ | ||||||
|           { |           { | ||||||
|             area_id: "", |             area_id: "no_areas", | ||||||
|             name: this.hass.localize("ui.components.area-picker.no_areas"), |             name: this.hass.localize("ui.components.area-picker.no_areas"), | ||||||
|             picture: null, |             picture: null, | ||||||
|           }, |           }, | ||||||
| @@ -294,7 +263,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|       if (!outputAreas.length) { |       if (!outputAreas.length) { | ||||||
|         outputAreas = [ |         outputAreas = [ | ||||||
|           { |           { | ||||||
|             area_id: "", |             area_id: "no_areas", | ||||||
|             name: this.hass.localize("ui.components.area-picker.no_match"), |             name: this.hass.localize("ui.components.area-picker.no_match"), | ||||||
|             picture: null, |             picture: null, | ||||||
|           }, |           }, | ||||||
| @@ -339,52 +308,25 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <ha-combo-box | ||||||
|  |         .hass=${this.hass} | ||||||
|         item-value-path="area_id" |         item-value-path="area_id" | ||||||
|         item-id-path="area_id" |         item-id-path="area_id" | ||||||
|         item-label-path="name" |         item-label-path="name" | ||||||
|         .value=${this.value} |         .value=${this.value} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         ${comboBoxRenderer(rowRenderer)} |  | ||||||
|         @opened-changed=${this._openedChanged} |  | ||||||
|         @value-changed=${this._areaChanged} |  | ||||||
|       > |  | ||||||
|         <paper-input |  | ||||||
|         .label=${this.label === undefined && this.hass |         .label=${this.label === undefined && this.hass | ||||||
|           ? this.hass.localize("ui.components.area-picker.area") |           ? this.hass.localize("ui.components.area-picker.area") | ||||||
|           : this.label} |           : this.label} | ||||||
|         .placeholder=${this.placeholder |         .placeholder=${this.placeholder | ||||||
|           ? this._area(this.placeholder)?.name |           ? this._area(this.placeholder)?.name | ||||||
|           : undefined} |           : undefined} | ||||||
|           .disabled=${this.disabled} |         .renderer=${rowRenderer} | ||||||
|           class="input" |         @filter-changed=${this._filterChanged} | ||||||
|           autocapitalize="none" |         @opened-changed=${this._openedChanged} | ||||||
|           autocomplete="off" |         @value-changed=${this._areaChanged} | ||||||
|           autocorrect="off" |  | ||||||
|           spellcheck="false" |  | ||||||
|       > |       > | ||||||
|           ${this.value |       </ha-combo-box> | ||||||
|             ? 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> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -392,9 +334,29 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|     this._areas?.find((area) => area.area_id === areaId) |     this._areas?.find((area) => area.area_id === areaId) | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |   private _filterChanged(ev: CustomEvent): void { | ||||||
|     ev.stopPropagation(); |     this._filter = ev.detail.value; | ||||||
|     this._setValue(""); |     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() { |   private get _value() { | ||||||
| @@ -406,9 +368,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _areaChanged(ev: PolymerChangedEvent<string>) { |   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) { |       if (newValue !== this._value) { | ||||||
|         this._setValue(newValue); |         this._setValue(newValue); | ||||||
|       } |       } | ||||||
| @@ -425,6 +392,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|       inputLabel: this.hass.localize( |       inputLabel: this.hass.localize( | ||||||
|         "ui.components.area-picker.add_dialog.name" |         "ui.components.area-picker.add_dialog.name" | ||||||
|       ), |       ), | ||||||
|  |       defaultValue: | ||||||
|  |         newValue === "add_new_suggestion" ? this._filter : undefined, | ||||||
|       confirm: async (name) => { |       confirm: async (name) => { | ||||||
|         if (!name) { |         if (!name) { | ||||||
|           return; |           return; | ||||||
| @@ -445,6 +414,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|             this.entityFilter, |             this.entityFilter, | ||||||
|             this.noAdd |             this.noAdd | ||||||
|           ); |           ); | ||||||
|  |           await this.updateComplete; | ||||||
|  |           await this.comboBox.updateComplete; | ||||||
|           this._setValue(area.area_id); |           this._setValue(area.area_id); | ||||||
|         } catch (err: any) { |         } catch (err: any) { | ||||||
|           showAlertDialog(this, { |           showAlertDialog(this, { | ||||||
| @@ -465,19 +436,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|       fireEvent(this, "change"); |       fireEvent(this, "change"); | ||||||
|     }, 0); |     }, 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return css` |  | ||||||
|       paper-input > ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
							
								
								
									
										313
									
								
								src/components/ha-base-time-input.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								src/components/ha-base-time-input.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | |||||||
|  | import { LitElement, html, TemplateResult, css } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import "./ha-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`<ha-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> | ||||||
|  |             </ha-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); | ||||||
|  |     } | ||||||
|  |     ha-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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "./ha-select"; | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  | import { stopPropagation } from "../common/dom/stop_propagation"; | ||||||
| import { stringCompare } from "../common/string/compare"; | import { stringCompare } from "../common/string/compare"; | ||||||
| import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint"; | import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| @@ -24,7 +24,11 @@ class HaBluePrintPicker extends LitElement { | |||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|   public open() { |   public open() { | ||||||
|     this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open(); |     const select = this.shadowRoot?.querySelector("ha-select"); | ||||||
|  |     if (select) { | ||||||
|  |       // @ts-expect-error | ||||||
|  |       select.menuOpen = true; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { |   private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { | ||||||
| @@ -45,32 +49,29 @@ class HaBluePrintPicker extends LitElement { | |||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <paper-dropdown-menu-light |       <ha-select | ||||||
|         .label=${this.label || |         .label=${this.label || | ||||||
|         this.hass.localize("ui.components.blueprint-picker.label")} |         this.hass.localize("ui.components.blueprint-picker.label")} | ||||||
|  |         fixedMenuPosition | ||||||
|  |         naturalMenuWidth | ||||||
|  |         .value=${this.value} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         horizontal-align="left" |         @selected=${this._blueprintChanged} | ||||||
|  |         @closed=${stopPropagation} | ||||||
|       > |       > | ||||||
|         <paper-listbox |         <mwc-list-item value=""> | ||||||
|           slot="dropdown-content" |  | ||||||
|           .selected=${this.value} |  | ||||||
|           attr-for-selected="data-blueprint-path" |  | ||||||
|           @iron-select=${this._blueprintChanged} |  | ||||||
|         > |  | ||||||
|           <paper-item data-blueprint-path=""> |  | ||||||
|           ${this.hass.localize( |           ${this.hass.localize( | ||||||
|             "ui.components.blueprint-picker.select_blueprint" |             "ui.components.blueprint-picker.select_blueprint" | ||||||
|           )} |           )} | ||||||
|           </paper-item> |         </mwc-list-item> | ||||||
|         ${this._processedBlueprints(this.blueprints).map( |         ${this._processedBlueprints(this.blueprints).map( | ||||||
|           (blueprint) => html` |           (blueprint) => html` | ||||||
|               <paper-item data-blueprint-path=${blueprint.path}> |             <mwc-list-item .value=${blueprint.path}> | ||||||
|               ${blueprint.name} |               ${blueprint.name} | ||||||
|               </paper-item> |             </mwc-list-item> | ||||||
|           ` |           ` | ||||||
|         )} |         )} | ||||||
|         </paper-listbox> |       </ha-select> | ||||||
|       </paper-dropdown-menu-light> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -84,10 +85,10 @@ class HaBluePrintPicker extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _blueprintChanged(ev) { |   private _blueprintChanged(ev) { | ||||||
|     const newValue = ev.detail.item.dataset.blueprintPath; |     const newValue = ev.target.value; | ||||||
|  |  | ||||||
|     if (newValue !== this.value) { |     if (newValue !== this.value) { | ||||||
|       this.value = ev.detail.value; |       this.value = newValue; | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         fireEvent(this, "value-changed", { value: newValue }); |         fireEvent(this, "value-changed", { value: newValue }); | ||||||
|         fireEvent(this, "change"); |         fireEvent(this, "change"); | ||||||
| @@ -100,15 +101,11 @@ class HaBluePrintPicker extends LitElement { | |||||||
|       :host { |       :host { | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|       } |       } | ||||||
|       paper-dropdown-menu-light { |       ha-select { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         min-width: 200px; |         min-width: 200px; | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|       paper-item { |  | ||||||
|         cursor: pointer; |  | ||||||
|         min-width: 200px; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import "@material/mwc-menu"; | import "@material/mwc-menu"; | ||||||
| import type { Corner, Menu } from "@material/mwc-menu"; | import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
|  |  | ||||||
| @@ -7,6 +7,12 @@ import { customElement, property, query } from "lit/decorators"; | |||||||
| export class HaButtonMenu extends LitElement { | export class HaButtonMenu extends LitElement { | ||||||
|   @property() public corner: Corner = "TOP_START"; |   @property() public corner: Corner = "TOP_START"; | ||||||
|  |  | ||||||
|  |   @property() public menuCorner: MenuCorner = "START"; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) public x?: number; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) public y?: number; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public multi = false; |   @property({ type: Boolean }) public multi = false; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public activatable = false; |   @property({ type: Boolean }) public activatable = false; | ||||||
| @@ -32,9 +38,12 @@ export class HaButtonMenu extends LitElement { | |||||||
|       </div> |       </div> | ||||||
|       <mwc-menu |       <mwc-menu | ||||||
|         .corner=${this.corner} |         .corner=${this.corner} | ||||||
|  |         .menuCorner=${this.menuCorner} | ||||||
|         .fixed=${this.fixed} |         .fixed=${this.fixed} | ||||||
|         .multi=${this.multi} |         .multi=${this.multi} | ||||||
|         .activatable=${this.activatable} |         .activatable=${this.activatable} | ||||||
|  |         .y=${this.y} | ||||||
|  |         .x=${this.x} | ||||||
|       > |       > | ||||||
|         <slot></slot> |         <slot></slot> | ||||||
|       </mwc-menu> |       </mwc-menu> | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import { mdiFilterVariant } from "@mdi/js"; | |||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  | import { stopPropagation } from "../common/dom/stop_propagation"; | ||||||
| import { computeStateName } from "../common/entity/compute_state_name"; | import { computeStateName } from "../common/entity/compute_state_name"; | ||||||
| import { computeDeviceName } from "../data/device_registry"; | import { computeDeviceName } from "../data/device_registry"; | ||||||
| import { findRelated, RelatedResult } from "../data/search"; | import { findRelated, RelatedResult } from "../data/search"; | ||||||
| @@ -65,6 +66,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|         .fullwidth=${this.narrow} |         .fullwidth=${this.narrow} | ||||||
|         .corner=${this.corner} |         .corner=${this.corner} | ||||||
|         @closed=${this._onClosed} |         @closed=${this._onClosed} | ||||||
|  |         @input=${stopPropagation} | ||||||
|       > |       > | ||||||
|         <ha-area-picker |         <ha-area-picker | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
| @@ -74,6 +76,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|           .value=${this.value?.area} |           .value=${this.value?.area} | ||||||
|           no-add |           no-add | ||||||
|           @value-changed=${this._areaPicked} |           @value-changed=${this._areaPicked} | ||||||
|  |           @click=${this._preventDefault} | ||||||
|         ></ha-area-picker> |         ></ha-area-picker> | ||||||
|         <ha-device-picker |         <ha-device-picker | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
| @@ -82,6 +85,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
|           .value=${this.value?.device} |           .value=${this.value?.device} | ||||||
|           @value-changed=${this._devicePicked} |           @value-changed=${this._devicePicked} | ||||||
|  |           @click=${this._preventDefault} | ||||||
|         ></ha-device-picker> |         ></ha-device-picker> | ||||||
|         <ha-entity-picker |         <ha-entity-picker | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
| @@ -91,6 +95,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|           .value=${this.value?.entity} |           .value=${this.value?.entity} | ||||||
|           .excludeDomains=${this.excludeDomains} |           .excludeDomains=${this.excludeDomains} | ||||||
|           @value-changed=${this._entityPicked} |           @value-changed=${this._entityPicked} | ||||||
|  |           @click=${this._preventDefault} | ||||||
|         ></ha-entity-picker> |         ></ha-entity-picker> | ||||||
|       </mwc-menu-surface> |       </mwc-menu-surface> | ||||||
|     `; |     `; | ||||||
| @@ -103,11 +108,17 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|     this._open = true; |     this._open = true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _onClosed(): void { |   private _onClosed(ev): void { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     this._open = false; |     this._open = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _preventDefault(ev) { | ||||||
|  |     ev.preventDefault(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async _entityPicked(ev: CustomEvent) { |   private async _entityPicked(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     const entityId = ev.detail.value; |     const entityId = ev.detail.value; | ||||||
|     if (!entityId) { |     if (!entityId) { | ||||||
|       fireEvent(this, "related-changed", { value: undefined }); |       fireEvent(this, "related-changed", { value: undefined }); | ||||||
| @@ -127,6 +138,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _devicePicked(ev: CustomEvent) { |   private async _devicePicked(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     const deviceId = ev.detail.value; |     const deviceId = ev.detail.value; | ||||||
|     if (!deviceId) { |     if (!deviceId) { | ||||||
|       fireEvent(this, "related-changed", { value: undefined }); |       fireEvent(this, "related-changed", { value: undefined }); | ||||||
| @@ -150,6 +162,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _areaPicked(ev: CustomEvent) { |   private async _areaPicked(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     const areaId = ev.detail.value; |     const areaId = ev.detail.value; | ||||||
|     if (!areaId) { |     if (!areaId) { | ||||||
|       fireEvent(this, "related-changed", { value: undefined }); |       fireEvent(this, "related-changed", { value: undefined }); | ||||||
| @@ -173,9 +186,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|       :host { |       :host { | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|         position: relative; |         position: relative; | ||||||
|       } |         --mdc-menu-min-width: 250px; | ||||||
|       :host([narrow]) { |  | ||||||
|         position: static; |  | ||||||
|       } |       } | ||||||
|       ha-area-picker, |       ha-area-picker, | ||||||
|       ha-device-picker, |       ha-device-picker, | ||||||
| @@ -185,8 +196,15 @@ export class HaRelatedFilterButtonMenu extends LitElement { | |||||||
|         padding: 4px 16px; |         padding: 4px 16px; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|       } |       } | ||||||
|  |       ha-area-picker { | ||||||
|  |         padding-top: 16px; | ||||||
|  |       } | ||||||
|  |       ha-entity-picker { | ||||||
|  |         padding-bottom: 16px; | ||||||
|  |       } | ||||||
|       :host([narrow]) ha-area-picker, |       :host([narrow]) ha-area-picker, | ||||||
|       :host([narrow]) ha-device-picker { |       :host([narrow]) ha-device-picker, | ||||||
|  |       :host([narrow]) ha-entity-picker { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								src/components/ha-check-list-item.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/components/ha-check-list-item.ts
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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"; | import { customElement } from "lit/decorators"; | ||||||
|  |  | ||||||
| @customElement("ha-checkbox") | @customElement("ha-checkbox") | ||||||
| export class HaCheckbox extends Checkbox { | export class HaCheckbox extends CheckboxBase { | ||||||
|   public firstUpdated() { |   static override styles = [ | ||||||
|     super.firstUpdated(); |     styles, | ||||||
|     this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); |     css` | ||||||
|  |       :host { | ||||||
|  |         --mdc-theme-secondary: var(--primary-color); | ||||||
|       } |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,8 +1,16 @@ | |||||||
|  | import type { | ||||||
|  |   Completion, | ||||||
|  |   CompletionContext, | ||||||
|  |   CompletionResult, | ||||||
|  | } from "@codemirror/autocomplete"; | ||||||
| import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; | import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; | ||||||
|  | import { HassEntities } from "home-assistant-js-websocket"; | ||||||
| import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; | import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { loadCodeMirror } from "../resources/codemirror.ondemand"; | import { loadCodeMirror } from "../resources/codemirror.ondemand"; | ||||||
|  | import { HomeAssistant } from "../types"; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HASSDomEvents { |   interface HASSDomEvents { | ||||||
| @@ -24,10 +32,15 @@ export class HaCodeEditor extends ReactiveElement { | |||||||
|  |  | ||||||
|   @property() public mode = "yaml"; |   @property() public mode = "yaml"; | ||||||
|  |  | ||||||
|  |   public hass?: HomeAssistant; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public autofocus = false; |   @property({ type: Boolean }) public autofocus = false; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public readOnly = false; |   @property({ type: Boolean }) public readOnly = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "autocomplete-entities" }) | ||||||
|  |   public autocompleteEntities = false; | ||||||
|  |  | ||||||
|   @property() public error = false; |   @property() public error = false; | ||||||
|  |  | ||||||
|   @state() private _value = ""; |   @state() private _value = ""; | ||||||
| @@ -110,11 +123,7 @@ export class HaCodeEditor extends ReactiveElement { | |||||||
|  |  | ||||||
|   private async _load(): Promise<void> { |   private async _load(): Promise<void> { | ||||||
|     this._loadedCodeMirror = await loadCodeMirror(); |     this._loadedCodeMirror = await loadCodeMirror(); | ||||||
|  |     const extensions = [ | ||||||
|     this.codemirror = new this._loadedCodeMirror.EditorView({ |  | ||||||
|       state: this._loadedCodeMirror.EditorState.create({ |  | ||||||
|         doc: this._value, |  | ||||||
|         extensions: [ |  | ||||||
|       this._loadedCodeMirror.lineNumbers(), |       this._loadedCodeMirror.lineNumbers(), | ||||||
|       this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), |       this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), | ||||||
|       this._loadedCodeMirror.history(), |       this._loadedCodeMirror.history(), | ||||||
| @@ -140,13 +149,66 @@ export class HaCodeEditor extends ReactiveElement { | |||||||
|       this._loadedCodeMirror.EditorView.updateListener.of((update) => |       this._loadedCodeMirror.EditorView.updateListener.of((update) => | ||||||
|         this._onUpdate(update) |         this._onUpdate(update) | ||||||
|       ), |       ), | ||||||
|         ], |     ]; | ||||||
|  |  | ||||||
|  |     if (!this.readOnly && this.autocompleteEntities && this.hass) { | ||||||
|  |       extensions.push( | ||||||
|  |         this._loadedCodeMirror.autocompletion({ | ||||||
|  |           override: [this._entityCompletions.bind(this)], | ||||||
|  |           maxRenderedOptions: 10, | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.codemirror = new this._loadedCodeMirror.EditorView({ | ||||||
|  |       state: this._loadedCodeMirror.EditorState.create({ | ||||||
|  |         doc: this._value, | ||||||
|  |         extensions, | ||||||
|       }), |       }), | ||||||
|       root: this.shadowRoot!, |       root: this.shadowRoot!, | ||||||
|       parent: this.shadowRoot!, |       parent: this.shadowRoot!, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _getStates = memoizeOne((states: HassEntities): Completion[] => { | ||||||
|  |     if (!states) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |     const options = Object.keys(states).map((key) => ({ | ||||||
|  |       type: "variable", | ||||||
|  |       label: key, | ||||||
|  |       detail: states[key].attributes.friendly_name, | ||||||
|  |       info: `State: ${states[key].state}`, | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     return options; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _entityCompletions( | ||||||
|  |     context: CompletionContext | ||||||
|  |   ): CompletionResult | null | Promise<CompletionResult | null> { | ||||||
|  |     const entityWord = context.matchBefore(/[a-z_]{3,}\./); | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       !entityWord || | ||||||
|  |       (entityWord.from === entityWord.to && !context.explicit) | ||||||
|  |     ) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const states = this._getStates(this.hass!.states); | ||||||
|  |  | ||||||
|  |     if (!states || !states.length) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       from: Number(entityWord.from), | ||||||
|  |       options: states, | ||||||
|  |       span: /^\w*.\w*$/, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _blockKeyboardShortcuts() { |   private _blockKeyboardShortcuts() { | ||||||
|     this.addEventListener("keydown", (ev) => ev.stopPropagation()); |     this.addEventListener("keydown", (ev) => ev.stopPropagation()); | ||||||
|   } |   } | ||||||
| @@ -163,10 +225,9 @@ export class HaCodeEditor extends ReactiveElement { | |||||||
|     fireEvent(this, "value-changed", { value: this._value }); |     fireEvent(this, "value-changed", { value: this._value }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Only Lit 2.0 will use this |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       :host(.error-state) div.cm-wrap .cm-gutters { |       :host(.error-state) .cm-gutters { | ||||||
|         border-color: var(--error-state-color, red); |         border-color: var(--error-state-color, red); | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,37 +1,78 @@ | |||||||
|  | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | ||||||
| import "@polymer/paper-input/paper-input"; | import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; | ||||||
| import "@polymer/paper-item/paper-item"; | import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; |  | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; | import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { PolymerChangedEvent } from "../polymer-types"; | import { PolymerChangedEvent } from "../polymer-types"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
|  | import "./ha-textfield"; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | registerStyles( | ||||||
| const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> |   "vaadin-combo-box-item", | ||||||
|     paper-item { |   css` | ||||||
|       margin: -5px -10px; |     :host { | ||||||
|       padding: 0; |       padding: 0; | ||||||
|     } |     } | ||||||
|   </style> |     :host([focused]:not([disabled])) { | ||||||
|   <paper-item>${item}</paper-item>`; |       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") | @customElement("ha-combo-box") | ||||||
| export class HaComboBox extends LitElement { | export class HaComboBox extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass?: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
|  |  | ||||||
|   @property() public value?: string; |   @property() public value?: string; | ||||||
|  |  | ||||||
|   @property() public 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 }) |   @property({ attribute: "allow-custom-value", type: Boolean }) | ||||||
|   public allowCustomValue?: boolean; |   public allowCustomValue?: boolean; | ||||||
| @@ -46,24 +87,25 @@ export class HaComboBox extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled?: boolean; |   @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() { |   public open() { | ||||||
|     this.updateComplete.then(() => { |     this.updateComplete.then(() => { | ||||||
|       (this._comboBox as any)?.open(); |       this._comboBox?.open(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     this.updateComplete.then(() => { |     this.updateComplete.then(() => { | ||||||
|       this.shadowRoot?.querySelector("paper-input")?.focus(); |       this._comboBox?.inputElement?.focus(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public get selectedItem() { |   public get selectedItem() { | ||||||
|     return (this._comboBox as any).selectedItem; |     return this._comboBox.selectedItem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
| @@ -72,55 +114,78 @@ export class HaComboBox extends LitElement { | |||||||
|         .itemValuePath=${this.itemValuePath} |         .itemValuePath=${this.itemValuePath} | ||||||
|         .itemIdPath=${this.itemIdPath} |         .itemIdPath=${this.itemIdPath} | ||||||
|         .itemLabelPath=${this.itemLabelPath} |         .itemLabelPath=${this.itemLabelPath} | ||||||
|         .value=${this.value} |         .value=${this.value || ""} | ||||||
|         .items=${this.items} |         .items=${this.items} | ||||||
|         .filteredItems=${this.filteredItems} |         .filteredItems=${this.filteredItems} | ||||||
|         .allowCustomValue=${this.allowCustomValue} |         .allowCustomValue=${this.allowCustomValue} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         ${comboBoxRenderer(this.renderer || defaultRowRenderer)} |         ${comboBoxRenderer(this.renderer || this._defaultRowRenderer)} | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
|         @filter-changed=${this._filterChanged} |         @filter-changed=${this._filterChanged} | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|  |         attr-for-value="value" | ||||||
|       > |       > | ||||||
|         <paper-input |         <ha-textfield | ||||||
|           .label=${this.label} |           .label=${this.label} | ||||||
|  |           .placeholder=${this.placeholder} | ||||||
|           .disabled=${this.disabled} |           .disabled=${this.disabled} | ||||||
|  |           .validationMessage=${this.validationMessage} | ||||||
|  |           .errorMessage=${this.errorMessage} | ||||||
|           class="input" |           class="input" | ||||||
|           autocapitalize="none" |           autocapitalize="none" | ||||||
|           autocomplete="off" |           autocomplete="off" | ||||||
|           autocorrect="off" |           autocorrect="off" | ||||||
|           spellcheck="false" |           spellcheck="false" | ||||||
|  |           .suffix=${html`<div style="width: 28px;"></div>`} | ||||||
|  |           .icon=${this.icon} | ||||||
|  |           .invalid=${this.invalid} | ||||||
|         > |         > | ||||||
|  |           <slot name="icon" slot="leadingIcon"></slot> | ||||||
|  |         </ha-textfield> | ||||||
|         ${this.value |         ${this.value | ||||||
|             ? html` |           ? html`<ha-svg-icon | ||||||
|                 <ha-icon-button |               aria-label=${this.hass?.localize("ui.components.combo-box.clear")} | ||||||
|                   .label=${this.hass.localize("ui.components.combo-box.clear")} |  | ||||||
|                   .path=${mdiClose} |  | ||||||
|                   slot="suffix" |  | ||||||
|               class="clear-button" |               class="clear-button" | ||||||
|  |               .path=${mdiClose} | ||||||
|               @click=${this._clearValue} |               @click=${this._clearValue} | ||||||
|                 ></ha-icon-button> |             ></ha-svg-icon>` | ||||||
|               ` |  | ||||||
|           : ""} |           : ""} | ||||||
|  |         <ha-svg-icon | ||||||
|           <ha-icon-button |           aria-label=${this.hass?.localize("ui.components.combo-box.show")} | ||||||
|             .label=${this.hass.localize("ui.components.combo-box.show")} |  | ||||||
|             .path=${this._opened ? mdiMenuUp : mdiMenuDown} |  | ||||||
|             slot="suffix" |  | ||||||
|           class="toggle-button" |           class="toggle-button" | ||||||
|           ></ha-icon-button> |           .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||||
|         </paper-input> |           @click=${this._toggleOpen} | ||||||
|  |         ></ha-svg-icon> | ||||||
|       </vaadin-combo-box-light> |       </vaadin-combo-box-light> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _defaultRowRenderer: ComboBoxLitRenderer< | ||||||
|  |     string | Record<string, any> | ||||||
|  |   > = (item) => | ||||||
|  |     html`<mwc-list-item> | ||||||
|  |       ${this.itemLabelPath ? item[this.itemLabelPath] : item} | ||||||
|  |     </mwc-list-item>`; | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |   private _clearValue(ev: Event) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     fireEvent(this, "value-changed", { value: undefined }); |     fireEvent(this, "value-changed", { value: undefined }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _toggleOpen(ev: Event) { | ||||||
|  |     if (this._opened) { | ||||||
|  |       this._comboBox?.close(); | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |     } else { | ||||||
|  |       this._comboBox?.inputElement.focus(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _openedChanged(ev: PolymerChangedEvent<boolean>) { |   private _openedChanged(ev: PolymerChangedEvent<boolean>) { | ||||||
|  |     // delay this so we can handle click event before setting _opened | ||||||
|  |     setTimeout(() => { | ||||||
|       this._opened = ev.detail.value; |       this._opened = ev.detail.value; | ||||||
|  |     }, 0); | ||||||
|     // @ts-ignore |     // @ts-ignore | ||||||
|     fireEvent(this, ev.type, ev.detail); |     fireEvent(this, ev.type, ev.detail); | ||||||
|   } |   } | ||||||
| @@ -141,11 +206,38 @@ export class HaComboBox extends LitElement { | |||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     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; |         --mdc-icon-button-size: 24px; | ||||||
|         padding: 2px; |         padding: 2px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
|  |       ha-svg-icon { | ||||||
|  |         color: var(--input-dropdown-icon-color); | ||||||
|  |         position: absolute; | ||||||
|  |         cursor: pointer; | ||||||
|  |       } | ||||||
|  |       .toggle-button { | ||||||
|  |         right: 12px; | ||||||
|  |         top: -10px; | ||||||
|  |       } | ||||||
|  |       :host([opened]) .toggle-button { | ||||||
|  |         color: var(--primary-color); | ||||||
|  |       } | ||||||
|  |       .clear-button { | ||||||
|  |         --mdc-icon-size: 20px; | ||||||
|  |         top: -7px; | ||||||
|  |         right: 36px; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,140 +1,78 @@ | |||||||
| import { mdiCalendar } from "@mdi/js"; | import { mdiCalendar } from "@mdi/js"; | ||||||
| import "@polymer/paper-input/paper-input"; | import { css, CSSResultGroup, html, LitElement } from "lit"; | ||||||
| import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; | import { formatDateNumeric } from "../common/datetime/format_date"; | ||||||
| import { customElement, property, query } from "lit/decorators"; |  | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  | import { HomeAssistant } from "../types"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
|  | import "./ha-textfield"; | ||||||
|  |  | ||||||
| const i18n = { | const loadDatePickerDialog = () => import("./ha-dialog-date-picker"); | ||||||
|   monthNames: [ |  | ||||||
|     "January", | export interface datePickerDialogParams { | ||||||
|     "February", |   value?: string; | ||||||
|     "March", |   min?: string; | ||||||
|     "April", |   max?: string; | ||||||
|     "May", |   locale?: string; | ||||||
|     "June", |   onChange: (value: string) => void; | ||||||
|     "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]); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|     if (date !== undefined) { | const showDatePickerDialog = ( | ||||||
|       return { day: date, month, year }; |   element: HTMLElement, | ||||||
|     } |   dialogParams: datePickerDialogParams | ||||||
|     return undefined; | ): void => { | ||||||
|   }, |   fireEvent(element, "show-dialog", { | ||||||
|  |     dialogTag: "ha-dialog-date-picker", | ||||||
|  |     dialogImport: loadDatePickerDialog, | ||||||
|  |     dialogParams, | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
| @customElement("ha-date-input") | @customElement("ha-date-input") | ||||||
| export class HaDateInput extends LitElement { | export class HaDateInput extends LitElement { | ||||||
|  |   @property({ attribute: false }) public locale!: HomeAssistant["locale"]; | ||||||
|  |  | ||||||
|   @property() public value?: string; |   @property() public value?: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
|  |  | ||||||
|   @query("vaadin-date-picker-light", true) private _datePicker; |  | ||||||
|  |  | ||||||
|   private _inited = false; |  | ||||||
|  |  | ||||||
|   updated(changedProps: PropertyValues) { |  | ||||||
|     if (changedProps.has("value")) { |  | ||||||
|       this._datePicker.value = this.value; |  | ||||||
|       this._inited = true; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   render() { |   render() { | ||||||
|     return html`<vaadin-date-picker-light |     return html`<ha-textfield | ||||||
|       .disabled=${this.disabled} |  | ||||||
|       @value-changed=${this._valueChanged} |  | ||||||
|       attr-for-value="value" |  | ||||||
|       .i18n=${i18n} |  | ||||||
|     > |  | ||||||
|       <paper-input |  | ||||||
|       .label=${this.label} |       .label=${this.label} | ||||||
|       .disabled=${this.disabled} |       .disabled=${this.disabled} | ||||||
|         no-label-float |       iconTrailing="calendar" | ||||||
|  |       @click=${this._openDialog} | ||||||
|  |       .value=${this.value | ||||||
|  |         ? formatDateNumeric(new Date(this.value), this.locale) | ||||||
|  |         : ""} | ||||||
|     > |     > | ||||||
|         <ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon> |       <ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon> | ||||||
|       </paper-input> |     </ha-textfield>`; | ||||||
|     </vaadin-date-picker-light>`; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: CustomEvent) { |   private _openDialog() { | ||||||
|     if ( |     if (this.disabled) { | ||||||
|       !this.value || |       return; | ||||||
|       (this._inited && !this._compareStringDates(ev.detail.value, this.value)) |     } | ||||||
|     ) { |     showDatePickerDialog(this, { | ||||||
|       this.value = ev.detail.value; |       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, "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 { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       paper-input { |  | ||||||
|         width: 110px; |  | ||||||
|       } |  | ||||||
|       ha-svg-icon { |       ha-svg-icon { | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list"; | |||||||
| import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; | import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiCalendar } from "@mdi/js"; | import { mdiCalendar } from "@mdi/js"; | ||||||
| import "@polymer/paper-input/paper-input"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -19,6 +18,7 @@ import { computeRTLDirection } from "../common/util/compute_rtl"; | |||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import "./date-range-picker"; | import "./date-range-picker"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
|  | import "./ha-textfield"; | ||||||
|  |  | ||||||
| export interface DateRangePickerRanges { | export interface DateRangePickerRanges { | ||||||
|   [key: string]: [Date, Date]; |   [key: string]: [Date, Date]; | ||||||
| @@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement { | |||||||
|       > |       > | ||||||
|         <div slot="input" class="date-range-inputs"> |         <div slot="input" class="date-range-inputs"> | ||||||
|           <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> |           <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> | ||||||
|           <paper-input |           <ha-textfield | ||||||
|             .value=${formatDateTime(this.startDate, this.hass.locale)} |             .value=${formatDateTime(this.startDate, this.hass.locale)} | ||||||
|             .label=${this.hass.localize( |             .label=${this.hass.localize( | ||||||
|               "ui.components.date-range-picker.start_date" |               "ui.components.date-range-picker.start_date" | ||||||
| @@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement { | |||||||
|             .disabled=${this.disabled} |             .disabled=${this.disabled} | ||||||
|             @click=${this._handleInputClick} |             @click=${this._handleInputClick} | ||||||
|             readonly |             readonly | ||||||
|           ></paper-input> |           ></ha-textfield> | ||||||
|           <paper-input |           <ha-textfield | ||||||
|             .value=${formatDateTime(this.endDate, this.hass.locale)} |             .value=${formatDateTime(this.endDate, this.hass.locale)} | ||||||
|             label=${this.hass.localize( |             .label=${this.hass.localize( | ||||||
|               "ui.components.date-range-picker.end_date" |               "ui.components.date-range-picker.end_date" | ||||||
|             )} |             )} | ||||||
|             .disabled=${this.disabled} |             .disabled=${this.disabled} | ||||||
|             @click=${this._handleInputClick} |             @click=${this._handleInputClick} | ||||||
|             readonly |             readonly | ||||||
|           ></paper-input> |           ></ha-textfield> | ||||||
|         </div> |         </div> | ||||||
|         ${this.ranges |         ${this.ranges | ||||||
|           ? html`<div |           ? html`<div | ||||||
| @@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement { | |||||||
|         border-top: 1px solid var(--divider-color); |         border-top: 1px solid var(--divider-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       paper-input { |       ha-textfield { | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|         max-width: 250px; |         max-width: 250px; | ||||||
|         min-width: 200px; |         min-width: 200px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       paper-input:last-child { |       ha-textfield:last-child { | ||||||
|         margin-left: 8px; |         margin-left: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       @media only screen and (max-width: 500px) { |       @media only screen and (max-width: 500px) { | ||||||
|         paper-input { |         ha-textfield { | ||||||
|           min-width: inherit; |           min-width: inherit; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								src/components/ha-dialog-date-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/components/ha-dialog-date-picker.ts
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 { mdiClose } from "@mdi/js"; | ||||||
| import { css, CSSResultGroup, html, TemplateResult } from "lit"; | import { css, html, TemplateResult } from "lit"; | ||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
| import { computeRTLDirection } from "../common/util/compute_rtl"; | import { computeRTLDirection } from "../common/util/compute_rtl"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
| @@ -21,8 +22,7 @@ export const createCloseHeading = ( | |||||||
| `; | `; | ||||||
|  |  | ||||||
| @customElement("ha-dialog") | @customElement("ha-dialog") | ||||||
| // @ts-expect-error | export class HaDialog extends DialogBase { | ||||||
| export class HaDialog extends Dialog { |  | ||||||
|   public scrollToPos(x: number, y: number) { |   public scrollToPos(x: number, y: number) { | ||||||
|     this.contentElement?.scrollTo(x, y); |     this.contentElement?.scrollTo(x, y); | ||||||
|   } |   } | ||||||
| @@ -31,9 +31,8 @@ export class HaDialog extends Dialog { | |||||||
|     return html`<slot name="heading"> ${super.renderHeading()} </slot>`; |     return html`<slot name="heading"> ${super.renderHeading()} </slot>`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected static get styles(): CSSResultGroup { |   static override styles = [ | ||||||
|     return [ |     styles, | ||||||
|       Dialog.styles, |  | ||||||
|     css` |     css` | ||||||
|       .mdc-dialog { |       .mdc-dialog { | ||||||
|         --mdc-dialog-scroll-divider-color: var(--divider-color); |         --mdc-dialog-scroll-divider-color: var(--divider-color); | ||||||
| @@ -102,7 +101,6 @@ export class HaDialog extends Dialog { | |||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { html, LitElement, TemplateResult } from "lit"; | import { html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import "./paper-time-input"; | import "./ha-base-time-input"; | ||||||
|  | import type { TimeChangedEvent } from "./ha-base-time-input"; | ||||||
|  |  | ||||||
| export interface HaDurationData { | export interface HaDurationData { | ||||||
|   hours?: number; |   hours?: number; | ||||||
| @@ -32,110 +33,69 @@ class HaDurationInput extends LitElement { | |||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <paper-time-input |       <ha-base-time-input | ||||||
|         .label=${this.label} |         .label=${this.label} | ||||||
|         .required=${this.required} |         .required=${this.required} | ||||||
|         .autoValidate=${this.required} |         .autoValidate=${this.required} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         error-message="Required" |         errorMessage="Required" | ||||||
|         enable-second |         enableSecond | ||||||
|         .enableMillisecond=${this.enableMillisecond} |         .enableMillisecond=${this.enableMillisecond} | ||||||
|         format="24" |         format="24" | ||||||
|         .hour=${this._parseDuration(this._hours)} |         .hours=${this._hours} | ||||||
|         .min=${this._parseDuration(this._minutes)} |         .minutes=${this._minutes} | ||||||
|         .sec=${this._parseDuration(this._seconds)} |         .seconds=${this._seconds} | ||||||
|         .millisec=${this._parseDurationMillisec(this._milliseconds)} |         .milliseconds=${this._milliseconds} | ||||||
|         @hour-changed=${this._hourChanged} |         @value-changed=${this._durationChanged} | ||||||
|         @min-changed=${this._minChanged} |         noHoursLimit | ||||||
|         @sec-changed=${this._secChanged} |         hourLabel="hh" | ||||||
|         @millisec-changed=${this._millisecChanged} |         minLabel="mm" | ||||||
|         float-input-labels |         secLabel="ss" | ||||||
|         no-hours-limit |         millisecLabel="ms" | ||||||
|         always-float-input-labels |       ></ha-base-time-input> | ||||||
|         hour-label="hh" |  | ||||||
|         min-label="mm" |  | ||||||
|         sec-label="ss" |  | ||||||
|         millisec-label="ms" |  | ||||||
|       ></paper-time-input> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private get _hours() { |   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() { |   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() { |   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() { |   private get _milliseconds() { | ||||||
|     return this.data && this.data.milliseconds |     return this.data?.milliseconds ? Number(this.data.milliseconds) : 0; | ||||||
|       ? Number(this.data.milliseconds) |  | ||||||
|       : 0; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _parseDuration(value) { |   private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { | ||||||
|     return value.toString().padStart(2, "0"); |     ev.stopPropagation(); | ||||||
|  |     const value = { ...ev.detail.value }; | ||||||
|  |  | ||||||
|  |     if (!this.enableMillisecond && !value.milliseconds) { | ||||||
|  |       // @ts-ignore | ||||||
|  |       delete value.milliseconds; | ||||||
|  |     } else if (value.milliseconds > 999) { | ||||||
|  |       value.seconds += Math.floor(value.milliseconds / 1000); | ||||||
|  |       value.milliseconds %= 1000; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   private _parseDurationMillisec(value) { |     if (value.seconds > 59) { | ||||||
|     return value.toString().padStart(3, "0"); |       value.minutes += Math.floor(value.seconds / 60); | ||||||
|  |       value.seconds %= 60; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   private _hourChanged(ev) { |     if (value.minutes > 59) { | ||||||
|     this._durationChanged(ev, "hours"); |       value.hours += Math.floor(value.minutes / 60); | ||||||
|  |       value.minutes %= 60; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   private _minChanged(ev) { |  | ||||||
|     this._durationChanged(ev, "minutes"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _secChanged(ev) { |  | ||||||
|     this._durationChanged(ev, "seconds"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _millisecChanged(ev) { |  | ||||||
|     this._durationChanged(ev, "milliseconds"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _durationChanged(ev, unit) { |  | ||||||
|     let value = Number(ev.detail.value); |  | ||||||
|  |  | ||||||
|     if (value === this[`_${unit}`]) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let hours = this._hours; |  | ||||||
|     let minutes = this._minutes; |  | ||||||
|  |  | ||||||
|     if (unit === "seconds" && value > 59) { |  | ||||||
|       minutes += Math.floor(value / 60); |  | ||||||
|       value %= 60; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (unit === "minutes" && value > 59) { |  | ||||||
|       hours += Math.floor(value / 60); |  | ||||||
|       value %= 60; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const newValue: HaDurationData = { |  | ||||||
|       hours, |  | ||||||
|       minutes, |  | ||||||
|       seconds: this._seconds, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     if (this.enableMillisecond || this._milliseconds) { |  | ||||||
|       newValue.milliseconds = this._milliseconds; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     newValue[unit] = value; |  | ||||||
|  |  | ||||||
|     fireEvent(this, "value-changed", { |     fireEvent(this, "value-changed", { | ||||||
|       value: newValue, |       value, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,21 @@ | |||||||
| import "@material/mwc-textfield"; |  | ||||||
| import type { TextField } from "@material/mwc-textfield"; |  | ||||||
| import { css, html, LitElement, TemplateResult, PropertyValues } from "lit"; | import { css, html, LitElement, TemplateResult, PropertyValues } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import type { HaTextField } from "../ha-textfield"; | ||||||
|  | import "../ha-textfield"; | ||||||
| import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types"; | import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types"; | ||||||
|  |  | ||||||
| @customElement("ha-form-float") | @customElement("ha-form-float") | ||||||
| export class HaFormFloat extends LitElement implements HaFormElement { | export class HaFormFloat extends LitElement implements HaFormElement { | ||||||
|   @property() public schema!: HaFormFloatSchema; |   @property({ attribute: false }) public schema!: HaFormFloatSchema; | ||||||
|  |  | ||||||
|   @property() public data!: HaFormFloatData; |   @property({ attribute: false }) public data!: HaFormFloatData; | ||||||
|  |  | ||||||
|   @property() public label!: string; |   @property() public label!: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|   @query("mwc-textfield") private _input?: HTMLElement; |   @query("ha-textfield") private _input?: HaTextField; | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     if (this._input) { |     if (this._input) { | ||||||
| @@ -25,7 +25,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <mwc-textfield |       <ha-textfield | ||||||
|         inputMode="decimal" |         inputMode="decimal" | ||||||
|         .label=${this.label} |         .label=${this.label} | ||||||
|         .value=${this.data !== undefined ? this.data : ""} |         .value=${this.data !== undefined ? this.data : ""} | ||||||
| @@ -35,7 +35,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { | |||||||
|         .suffix=${this.schema.description?.suffix} |         .suffix=${this.schema.description?.suffix} | ||||||
|         .validationMessage=${this.schema.required ? "Required" : undefined} |         .validationMessage=${this.schema.required ? "Required" : undefined} | ||||||
|         @input=${this._valueChanged} |         @input=${this._valueChanged} | ||||||
|       ></mwc-textfield> |       ></ha-textfield> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -46,7 +46,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: Event) { |   private _valueChanged(ev: Event) { | ||||||
|     const source = ev.target as TextField; |     const source = ev.target as HaTextField; | ||||||
|     const rawValue = source.value.replace(",", "."); |     const rawValue = source.value.replace(",", "."); | ||||||
|  |  | ||||||
|     let value: number | undefined; |     let value: number | undefined; | ||||||
| @@ -81,7 +81,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { | |||||||
|     :host([own-margin]) { |     :host([own-margin]) { | ||||||
|       margin-bottom: 5px; |       margin-bottom: 5px; | ||||||
|     } |     } | ||||||
|     mwc-textfield { |     ha-textfield { | ||||||
|       display: block; |       display: block; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
|   | |||||||
							
								
								
									
										95
									
								
								src/components/ha-form/ha-form-grid.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/components/ha-form/ha-form-grid.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | import "./ha-form"; | ||||||
|  | import { | ||||||
|  |   css, | ||||||
|  |   CSSResultGroup, | ||||||
|  |   html, | ||||||
|  |   LitElement, | ||||||
|  |   PropertyValues, | ||||||
|  |   TemplateResult, | ||||||
|  | } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import type { | ||||||
|  |   HaFormGridSchema, | ||||||
|  |   HaFormDataContainer, | ||||||
|  |   HaFormElement, | ||||||
|  |   HaFormSchema, | ||||||
|  | } from "./types"; | ||||||
|  | import type { HomeAssistant } from "../../types"; | ||||||
|  |  | ||||||
|  | @customElement("ha-form-grid") | ||||||
|  | export class HaFormGrid extends LitElement implements HaFormElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public data!: HaFormDataContainer; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public schema!: HaFormGridSchema; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @property() public computeLabel?: ( | ||||||
|  |     schema: HaFormSchema, | ||||||
|  |     data?: HaFormDataContainer | ||||||
|  |   ) => string; | ||||||
|  |  | ||||||
|  |   @property() public computeHelper?: (schema: HaFormSchema) => string; | ||||||
|  |  | ||||||
|  |   protected firstUpdated(changedProps: PropertyValues) { | ||||||
|  |     super.firstUpdated(changedProps); | ||||||
|  |     this.setAttribute("own-margin", ""); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProps: PropertyValues): void { | ||||||
|  |     super.updated(changedProps); | ||||||
|  |     if (changedProps.has("schema")) { | ||||||
|  |       if (this.schema.column_min_width) { | ||||||
|  |         this.style.setProperty( | ||||||
|  |           "--form-grid-min-width", | ||||||
|  |           this.schema.column_min_width | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         this.style.setProperty("--form-grid-min-width", ""); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render(): TemplateResult { | ||||||
|  |     return html` | ||||||
|  |       ${this.schema.schema.map( | ||||||
|  |         (item) => | ||||||
|  |           html` | ||||||
|  |             <ha-form | ||||||
|  |               .hass=${this.hass} | ||||||
|  |               .data=${this.data} | ||||||
|  |               .schema=${[item]} | ||||||
|  |               .disabled=${this.disabled} | ||||||
|  |               .computeLabel=${this.computeLabel} | ||||||
|  |               .computeHelper=${this.computeHelper} | ||||||
|  |             ></ha-form> | ||||||
|  |           ` | ||||||
|  |       )} | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|  |       :host { | ||||||
|  |         display: grid !important; | ||||||
|  |         grid-template-columns: repeat( | ||||||
|  |           var(--form-grid-column-count, auto-fit), | ||||||
|  |           minmax(var(--form-grid-min-width, 200px), 1fr) | ||||||
|  |         ); | ||||||
|  |         grid-gap: 8px; | ||||||
|  |       } | ||||||
|  |       :host > ha-form { | ||||||
|  |         display: block; | ||||||
|  |         margin-bottom: 24px; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-form-grid": HaFormGrid; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,3 @@ | |||||||
| import "@material/mwc-textfield"; |  | ||||||
| import type { TextField } from "@material/mwc-textfield"; |  | ||||||
| import type { Slider } from "@material/mwc-slider"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -14,18 +11,21 @@ import { fireEvent } from "../../common/dom/fire_event"; | |||||||
| import { HaCheckbox } from "../ha-checkbox"; | import { HaCheckbox } from "../ha-checkbox"; | ||||||
| import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; | import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; | ||||||
| import "../ha-slider"; | import "../ha-slider"; | ||||||
|  | import { HaTextField } from "../ha-textfield"; | ||||||
|  |  | ||||||
| @customElement("ha-form-integer") | @customElement("ha-form-integer") | ||||||
| export class HaFormInteger extends LitElement implements HaFormElement { | export class HaFormInteger extends LitElement implements HaFormElement { | ||||||
|   @property() public schema!: HaFormIntegerSchema; |   @property({ attribute: false }) public schema!: HaFormIntegerSchema; | ||||||
|  |  | ||||||
|   @property() public data?: HaFormIntegerData; |   @property({ attribute: false }) public data?: HaFormIntegerData; | ||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @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; |   private _lastValue?: HaFormIntegerData; | ||||||
|  |  | ||||||
| @@ -45,7 +45,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|         <div> |         <div> | ||||||
|           ${this.label} |           ${this.label} | ||||||
|           <div class="flex"> |           <div class="flex"> | ||||||
|             ${this.schema.optional |             ${!this.schema.required | ||||||
|               ? html` |               ? html` | ||||||
|                   <ha-checkbox |                   <ha-checkbox | ||||||
|                     @change=${this._handleCheckboxChange} |                     @change=${this._handleCheckboxChange} | ||||||
| @@ -61,7 +61,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|               .min=${this.schema.valueMin} |               .min=${this.schema.valueMin} | ||||||
|               .max=${this.schema.valueMax} |               .max=${this.schema.valueMax} | ||||||
|               .disabled=${this.disabled || |               .disabled=${this.disabled || | ||||||
|               (this.data === undefined && this.schema.optional)} |               (this.data === undefined && !this.schema.required)} | ||||||
|               @change=${this._valueChanged} |               @change=${this._valueChanged} | ||||||
|             ></ha-slider> |             ></ha-slider> | ||||||
|           </div> |           </div> | ||||||
| @@ -70,7 +70,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <mwc-textfield |       <ha-textfield | ||||||
|         type="number" |         type="number" | ||||||
|         inputMode="numeric" |         inputMode="numeric" | ||||||
|         .label=${this.label} |         .label=${this.label} | ||||||
| @@ -81,7 +81,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|         .suffix=${this.schema.description?.suffix} |         .suffix=${this.schema.description?.suffix} | ||||||
|         .validationMessage=${this.schema.required ? "Required" : undefined} |         .validationMessage=${this.schema.required ? "Required" : undefined} | ||||||
|         @input=${this._valueChanged} |         @input=${this._valueChanged} | ||||||
|       ></mwc-textfield> |       ></ha-textfield> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -100,7 +100,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|       return this.data; |       return this.data; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (this.schema.optional) { |     if (!this.schema.required) { | ||||||
|       return this.schema.valueMin || 0; |       return this.schema.valueMin || 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -138,7 +138,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: Event) { |   private _valueChanged(ev: Event) { | ||||||
|     const source = ev.target as TextField | Slider; |     const source = ev.target as HaTextField | HTMLInputElement; | ||||||
|     const rawValue = source.value; |     const rawValue = source.value; | ||||||
|  |  | ||||||
|     let value: number | undefined; |     let value: number | undefined; | ||||||
| @@ -172,7 +172,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|       ha-slider { |       ha-slider { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|       } |       } | ||||||
|       mwc-textfield { |       ha-textfield { | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,25 +1,27 @@ | |||||||
| import { mdiMenuDown, mdiMenuUp } from "@mdi/js"; | import { mdiMenuDown, mdiMenuUp } from "@mdi/js"; | ||||||
| import "@material/mwc-textfield"; |  | ||||||
| import "@material/mwc-formfield"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   TemplateResult, |  | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|  |   TemplateResult, | ||||||
| } from "lit"; | } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-button-menu"; | import "../ha-button-menu"; | ||||||
|  | import "../ha-check-list-item"; | ||||||
|  | import type { HaCheckListItem } from "../ha-check-list-item"; | ||||||
|  | import "../ha-checkbox"; | ||||||
|  | import type { HaCheckbox } from "../ha-checkbox"; | ||||||
|  | import "../ha-formfield"; | ||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
|  | import "../ha-textfield"; | ||||||
| import { | import { | ||||||
|   HaFormElement, |   HaFormElement, | ||||||
|   HaFormMultiSelectData, |   HaFormMultiSelectData, | ||||||
|   HaFormMultiSelectSchema, |   HaFormMultiSelectSchema, | ||||||
| } from "./types"; | } from "./types"; | ||||||
| import "../ha-checkbox"; |  | ||||||
| import type { HaCheckbox } from "../ha-checkbox"; |  | ||||||
|  |  | ||||||
| function optionValue(item: string | string[]): string { | function optionValue(item: string | string[]): string { | ||||||
|   return Array.isArray(item) ? item[0] : item; |   return Array.isArray(item) ? item[0] : item; | ||||||
| @@ -57,23 +59,23 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { | |||||||
|       : Object.entries(this.schema.options); |       : Object.entries(this.schema.options); | ||||||
|     const data = this.data || []; |     const data = this.data || []; | ||||||
|  |  | ||||||
|     const renderedOptions = options.map((item: string | [string, string]) => { |     // We will just render all checkboxes. | ||||||
|  |     if (options.length < SHOW_ALL_ENTRIES_LIMIT) { | ||||||
|  |       return html`<div> | ||||||
|  |         ${this.label}${options.map((item: string | [string, string]) => { | ||||||
|           const value = optionValue(item); |           const value = optionValue(item); | ||||||
|           return html` |           return html` | ||||||
|         <mwc-formfield .label=${optionLabel(item)}> |             <ha-formfield .label=${optionLabel(item)}> | ||||||
|               <ha-checkbox |               <ha-checkbox | ||||||
|                 .checked=${data.includes(value)} |                 .checked=${data.includes(value)} | ||||||
|                 .value=${value} |                 .value=${value} | ||||||
|                 .disabled=${this.disabled} |                 .disabled=${this.disabled} | ||||||
|                 @change=${this._valueChanged} |                 @change=${this._valueChanged} | ||||||
|               ></ha-checkbox> |               ></ha-checkbox> | ||||||
|         </mwc-formfield> |             </ha-formfield> | ||||||
|           `; |           `; | ||||||
|     }); |         })} | ||||||
|  |       </div> `; | ||||||
|     // We will just render all checkboxes. |  | ||||||
|     if (options.length < SHOW_ALL_ENTRIES_LIMIT) { |  | ||||||
|       return html`<div>${this.label}${renderedOptions}</div> `; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
| @@ -83,8 +85,10 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { | |||||||
|         corner="BOTTOM_START" |         corner="BOTTOM_START" | ||||||
|         @opened=${this._handleOpen} |         @opened=${this._handleOpen} | ||||||
|         @closed=${this._handleClose} |         @closed=${this._handleClose} | ||||||
|  |         multi | ||||||
|  |         activatable | ||||||
|       > |       > | ||||||
|         <mwc-textfield |         <ha-textfield | ||||||
|           slot="trigger" |           slot="trigger" | ||||||
|           .label=${this.label} |           .label=${this.label} | ||||||
|           .value=${data |           .value=${data | ||||||
| @@ -92,12 +96,25 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { | |||||||
|             .join(", ")} |             .join(", ")} | ||||||
|           .disabled=${this.disabled} |           .disabled=${this.disabled} | ||||||
|           tabindex="-1" |           tabindex="-1" | ||||||
|         ></mwc-textfield> |         ></ha-textfield> | ||||||
|         <ha-svg-icon |         <ha-svg-icon | ||||||
|           slot="trigger" |           slot="trigger" | ||||||
|           .path=${this._opened ? mdiMenuUp : mdiMenuDown} |           .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||||
|         ></ha-svg-icon> |         ></ha-svg-icon> | ||||||
|         ${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> |       </ha-button-menu> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -105,7 +122,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { | |||||||
|   protected firstUpdated() { |   protected firstUpdated() { | ||||||
|     this.updateComplete.then(() => { |     this.updateComplete.then(() => { | ||||||
|       const { formElement, mdcRoot } = |       const { formElement, mdcRoot } = | ||||||
|         this.shadowRoot?.querySelector("mwc-textfield") || ({} as any); |         this.shadowRoot?.querySelector("ha-textfield") || ({} as any); | ||||||
|       if (formElement) { |       if (formElement) { | ||||||
|         formElement.style.textOverflow = "ellipsis"; |         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 { |   private _valueChanged(ev: CustomEvent): void { | ||||||
|     const { value, checked } = ev.target as HaCheckbox; |     const { value, checked } = ev.target as HaCheckbox; | ||||||
|  |     this._handleValueChanged(value, checked); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleValueChanged(value, checked: boolean): void { | ||||||
|     let newValue: string[]; |     let newValue: string[]; | ||||||
|  |  | ||||||
|     if (checked) { |     if (checked) { | ||||||
| @@ -171,11 +202,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { | |||||||
|         display: block; |         display: block; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|       } |       } | ||||||
|       mwc-formfield { |       ha-formfield { | ||||||
|         display: block; |         display: block; | ||||||
|         padding-right: 16px; |         padding-right: 16px; | ||||||
|       } |       } | ||||||
|       mwc-textfield { |       ha-textfield { | ||||||
|         display: block; |         display: block; | ||||||
|         pointer-events: none; |         pointer-events: none; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -1,14 +1,13 @@ | |||||||
| import "@material/mwc-select"; |  | ||||||
| import type { Select } from "@material/mwc-select"; |  | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-radio"; |  | ||||||
| import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; |  | ||||||
|  |  | ||||||
| import { stopPropagation } from "../../common/dom/stop_propagation"; | import { stopPropagation } from "../../common/dom/stop_propagation"; | ||||||
|  | import "../ha-radio"; | ||||||
| import type { HaRadio } from "../ha-radio"; | import type { HaRadio } from "../ha-radio"; | ||||||
|  | import "../ha-select"; | ||||||
|  | import type { HaSelect } from "../ha-select"; | ||||||
|  | import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; | ||||||
|  |  | ||||||
| @customElement("ha-form-select") | @customElement("ha-form-select") | ||||||
| export class HaFormSelect extends LitElement implements HaFormElement { | export class HaFormSelect extends LitElement implements HaFormElement { | ||||||
| @@ -20,7 +19,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|   @query("mwc-select", true) private _input?: HTMLElement; |   @query("ha-select", true) private _input?: HTMLElement; | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     if (this._input) { |     if (this._input) { | ||||||
| @@ -29,7 +28,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this.schema.optional && this.schema.options!.length < 6) { |     if (this.schema.required && this.schema.options!.length < 6) { | ||||||
|       return html` |       return html` | ||||||
|         <div> |         <div> | ||||||
|           ${this.label} |           ${this.label} | ||||||
| @@ -50,7 +49,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <mwc-select |       <ha-select | ||||||
|         fixedMenuPosition |         fixedMenuPosition | ||||||
|         naturalMenuWidth |         naturalMenuWidth | ||||||
|         .label=${this.label} |         .label=${this.label} | ||||||
| @@ -59,7 +58,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|         @closed=${stopPropagation} |         @closed=${stopPropagation} | ||||||
|         @selected=${this._valueChanged} |         @selected=${this._valueChanged} | ||||||
|       > |       > | ||||||
|         ${this.schema.optional |         ${!this.schema.required | ||||||
|           ? html`<mwc-list-item value=""></mwc-list-item>` |           ? html`<mwc-list-item value=""></mwc-list-item>` | ||||||
|           : ""} |           : ""} | ||||||
|         ${this.schema.options!.map( |         ${this.schema.options!.map( | ||||||
| @@ -67,13 +66,13 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|             <mwc-list-item .value=${value}>${label}</mwc-list-item> |             <mwc-list-item .value=${value}>${label}</mwc-list-item> | ||||||
|           ` |           ` | ||||||
|         )} |         )} | ||||||
|       </mwc-select> |       </ha-select> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: CustomEvent) { |   private _valueChanged(ev: CustomEvent) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     let value: string | undefined = (ev.target as Select | HaRadio).value; |     let value: string | undefined = (ev.target as HaSelect | HaRadio).value; | ||||||
|  |  | ||||||
|     if (value === this.data) { |     if (value === this.data) { | ||||||
|       return; |       return; | ||||||
| @@ -90,7 +89,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       mwc-select, |       ha-select, | ||||||
|       mwc-formfield { |       mwc-formfield { | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -1,17 +1,17 @@ | |||||||
| import { mdiEye, mdiEyeOff } from "@mdi/js"; | import { mdiEye, mdiEyeOff } from "@mdi/js"; | ||||||
| import "@material/mwc-textfield"; |  | ||||||
| import type { TextField } from "@material/mwc-textfield"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   TemplateResult, |  | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|  |   TemplateResult, | ||||||
| } from "lit"; | } 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 { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-icon-button"; | import "../ha-icon-button"; | ||||||
|  | import "../ha-textfield"; | ||||||
|  | import type { HaTextField } from "../ha-textfield"; | ||||||
| import type { | import type { | ||||||
|   HaFormElement, |   HaFormElement, | ||||||
|   HaFormStringData, |   HaFormStringData, | ||||||
| @@ -32,7 +32,7 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   @state() private _unmaskedPassword = false; |   @state() private _unmaskedPassword = false; | ||||||
|  |  | ||||||
|   @query("mwc-textfield") private _input?: HTMLElement; |   @query("ha-textfield") private _input?: HaTextField; | ||||||
|  |  | ||||||
|   public focus(): void { |   public focus(): void { | ||||||
|     if (this._input) { |     if (this._input) { | ||||||
| @@ -45,7 +45,7 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|       this.schema.name.includes(field) |       this.schema.name.includes(field) | ||||||
|     ); |     ); | ||||||
|     return html` |     return html` | ||||||
|       <mwc-textfield |       <ha-textfield | ||||||
|         .type=${!isPassword |         .type=${!isPassword | ||||||
|           ? this._stringType |           ? this._stringType | ||||||
|           : this._unmaskedPassword |           : this._unmaskedPassword | ||||||
| @@ -62,7 +62,7 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|           : this.schema.description?.suffix} |           : this.schema.description?.suffix} | ||||||
|         .validationMessage=${this.schema.required ? "Required" : undefined} |         .validationMessage=${this.schema.required ? "Required" : undefined} | ||||||
|         @input=${this._valueChanged} |         @input=${this._valueChanged} | ||||||
|       ></mwc-textfield> |       ></ha-textfield> | ||||||
|       ${isPassword |       ${isPassword | ||||||
|         ? html`<ha-icon-button |         ? html`<ha-icon-button | ||||||
|             toggles |             toggles | ||||||
| @@ -85,11 +85,11 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: Event): void { |   private _valueChanged(ev: Event): void { | ||||||
|     let value: string | undefined = (ev.target as TextField).value; |     let value: string | undefined = (ev.target as HaTextField).value; | ||||||
|     if (this.data === value) { |     if (this.data === value) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (value === "" && this.schema.optional) { |     if (value === "" && !this.schema.required) { | ||||||
|       value = undefined; |       value = undefined; | ||||||
|     } |     } | ||||||
|     fireEvent(this, "value-changed", { |     fireEvent(this, "value-changed", { | ||||||
| @@ -118,7 +118,7 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|       :host([own-margin]) { |       :host([own-margin]) { | ||||||
|         margin-bottom: 5px; |         margin-bottom: 5px; | ||||||
|       } |       } | ||||||
|       mwc-textfield { |       ha-textfield { | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|       ha-icon-button { |       ha-icon-button { | ||||||
|   | |||||||
| @@ -1,10 +1,18 @@ | |||||||
| import { css, CSSResultGroup, html, LitElement } from "lit"; | import { | ||||||
|  |   css, | ||||||
|  |   CSSResultGroup, | ||||||
|  |   html, | ||||||
|  |   LitElement, | ||||||
|  |   PropertyValues, | ||||||
|  |   TemplateResult, | ||||||
|  | } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { dynamicElement } from "../../common/dom/dynamic-element-directive"; | import { dynamicElement } from "../../common/dom/dynamic-element-directive"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-alert"; | import "../ha-alert"; | ||||||
| import "./ha-form-boolean"; | import "./ha-form-boolean"; | ||||||
| import "./ha-form-constant"; | import "./ha-form-constant"; | ||||||
|  | import "./ha-form-grid"; | ||||||
| import "./ha-form-float"; | import "./ha-form-float"; | ||||||
| import "./ha-form-integer"; | import "./ha-form-integer"; | ||||||
| import "./ha-form-multi_select"; | import "./ha-form-multi_select"; | ||||||
| @@ -12,14 +20,20 @@ import "./ha-form-positive_time_period_dict"; | |||||||
| import "./ha-form-select"; | import "./ha-form-select"; | ||||||
| import "./ha-form-string"; | import "./ha-form-string"; | ||||||
| import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; | import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; | ||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  |  | ||||||
| const getValue = (obj, item) => (obj ? obj[item.name] : null); | const getValue = (obj, item) => | ||||||
|  |   obj ? (!item.name ? obj : obj[item.name]) : null; | ||||||
|  |  | ||||||
|  | let selectorImported = false; | ||||||
|  |  | ||||||
| @customElement("ha-form") | @customElement("ha-form") | ||||||
| export class HaForm extends LitElement implements HaFormElement { | export class HaForm extends LitElement implements HaFormElement { | ||||||
|   @property() public data!: HaFormDataContainer; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public schema!: HaFormSchema[]; |   @property({ attribute: false }) public data!: HaFormDataContainer; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public schema!: HaFormSchema[]; | ||||||
|  |  | ||||||
|   @property() public error?: Record<string, string>; |   @property() public error?: Record<string, string>; | ||||||
|  |  | ||||||
| @@ -27,7 +41,12 @@ export class HaForm extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   @property() public computeError?: (schema: HaFormSchema, error) => string; |   @property() public computeError?: (schema: HaFormSchema, error) => string; | ||||||
|  |  | ||||||
|   @property() public computeLabel?: (schema: HaFormSchema) => string; |   @property() public computeLabel?: ( | ||||||
|  |     schema: HaFormSchema, | ||||||
|  |     data?: HaFormDataContainer | ||||||
|  |   ) => string; | ||||||
|  |  | ||||||
|  |   @property() public computeHelper?: (schema: HaFormSchema) => string; | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     const root = this.shadowRoot?.querySelector(".root"); |     const root = this.shadowRoot?.querySelector(".root"); | ||||||
| @@ -42,7 +61,19 @@ export class HaForm extends LitElement implements HaFormElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   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(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <div class="root"> |       <div class="root"> | ||||||
|         ${this.error && this.error.base |         ${this.error && this.error.base | ||||||
| @@ -54,6 +85,7 @@ export class HaForm extends LitElement implements HaFormElement { | |||||||
|           : ""} |           : ""} | ||||||
|         ${this.schema.map((item) => { |         ${this.schema.map((item) => { | ||||||
|           const error = getValue(this.error, item); |           const error = getValue(this.error, item); | ||||||
|  |  | ||||||
|           return html` |           return html` | ||||||
|             ${error |             ${error | ||||||
|               ? html` |               ? html` | ||||||
| @@ -62,11 +94,25 @@ export class HaForm extends LitElement implements HaFormElement { | |||||||
|                   </ha-alert> |                   </ha-alert> | ||||||
|                 ` |                 ` | ||||||
|               : ""} |               : ""} | ||||||
|             ${dynamicElement(`ha-form-${item.type}`, { |             ${"selector" in item | ||||||
|  |               ? html`<ha-selector | ||||||
|  |                   .schema=${item} | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   .selector=${item.selector} | ||||||
|  |                   .value=${getValue(this.data, item)} | ||||||
|  |                   .label=${this._computeLabel(item, this.data)} | ||||||
|  |                   .disabled=${this.disabled} | ||||||
|  |                   .helper=${this._computeHelper(item)} | ||||||
|  |                   .required=${item.required || false} | ||||||
|  |                 ></ha-selector>` | ||||||
|  |               : dynamicElement(`ha-form-${item.type}`, { | ||||||
|                   schema: item, |                   schema: item, | ||||||
|                   data: getValue(this.data, item), |                   data: getValue(this.data, item), | ||||||
|               label: this._computeLabel(item), |                   label: this._computeLabel(item, this.data), | ||||||
|                   disabled: this.disabled, |                   disabled: this.disabled, | ||||||
|  |                   hass: this.hass, | ||||||
|  |                   computeLabel: this.computeLabel, | ||||||
|  |                   computeHelper: this.computeHelper, | ||||||
|                 })} |                 })} | ||||||
|           `; |           `; | ||||||
|         })} |         })} | ||||||
| @@ -80,21 +126,30 @@ export class HaForm extends LitElement implements HaFormElement { | |||||||
|     root.addEventListener("value-changed", (ev) => { |     root.addEventListener("value-changed", (ev) => { | ||||||
|       ev.stopPropagation(); |       ev.stopPropagation(); | ||||||
|       const schema = (ev.target as HaFormElement).schema as HaFormSchema; |       const schema = (ev.target as HaFormElement).schema as HaFormSchema; | ||||||
|  |  | ||||||
|  |       const newValue = !schema.name | ||||||
|  |         ? ev.detail.value | ||||||
|  |         : { [schema.name]: ev.detail.value }; | ||||||
|  |  | ||||||
|       fireEvent(this, "value-changed", { |       fireEvent(this, "value-changed", { | ||||||
|         value: { ...this.data, [schema.name]: ev.detail.value }, |         value: { ...this.data, ...newValue }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|     return root; |     return root; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _computeLabel(schema: HaFormSchema) { |   private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { | ||||||
|     return this.computeLabel |     return this.computeLabel | ||||||
|       ? this.computeLabel(schema) |       ? this.computeLabel(schema, data) | ||||||
|       : schema |       : schema | ||||||
|       ? schema.name |       ? schema.name | ||||||
|       : ""; |       : ""; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _computeHelper(schema: HaFormSchema) { | ||||||
|  |     return this.computeHelper ? this.computeHelper(schema) : ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { |   private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { | ||||||
|     return this.computeError ? this.computeError(error, schema) : error; |     return this.computeError ? this.computeError(error, schema) : error; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import type { LitElement } from "lit"; | import type { LitElement } from "lit"; | ||||||
|  | import { Selector } from "../../data/selector"; | ||||||
| import type { HaDurationData } from "../ha-duration-input"; | import type { HaDurationData } from "../ha-duration-input"; | ||||||
|  |  | ||||||
| export type HaFormSchema = | export type HaFormSchema = | ||||||
| @@ -9,14 +10,32 @@ export type HaFormSchema = | |||||||
|   | HaFormBooleanSchema |   | HaFormBooleanSchema | ||||||
|   | HaFormSelectSchema |   | HaFormSelectSchema | ||||||
|   | HaFormMultiSelectSchema |   | HaFormMultiSelectSchema | ||||||
|   | HaFormTimeSchema; |   | HaFormTimeSchema | ||||||
|  |   | HaFormSelector | ||||||
|  |   | HaFormGridSchema; | ||||||
|  |  | ||||||
| export interface HaFormBaseSchema { | export interface HaFormBaseSchema { | ||||||
|   name: string; |   name: string; | ||||||
|  |   // This value is applied if no data is submitted for this field | ||||||
|   default?: HaFormData; |   default?: HaFormData; | ||||||
|   required?: boolean; |   required?: boolean; | ||||||
|   optional?: boolean; |   description?: { | ||||||
|   description?: { suffix?: string; suggested_value?: HaFormData }; |     suffix?: string; | ||||||
|  |     // This value will be set initially when form is loaded | ||||||
|  |     suggested_value?: HaFormData; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface HaFormGridSchema extends HaFormBaseSchema { | ||||||
|  |   type: "grid"; | ||||||
|  |   name: ""; | ||||||
|  |   column_min_width?: string; | ||||||
|  |   schema: HaFormSchema[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface HaFormSelector extends HaFormBaseSchema { | ||||||
|  |   type?: never; | ||||||
|  |   selector: Selector; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface HaFormConstantSchema extends HaFormBaseSchema { | export interface HaFormConstantSchema extends HaFormBaseSchema { | ||||||
| @@ -38,7 +57,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema { | |||||||
|  |  | ||||||
| export interface HaFormMultiSelectSchema extends HaFormBaseSchema { | export interface HaFormMultiSelectSchema extends HaFormBaseSchema { | ||||||
|   type: "multi_select"; |   type: "multi_select"; | ||||||
|   options: Record<string, string> | string[]; |   options: Record<string, string> | string[] | Array<[string, string]>; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface HaFormFloatSchema extends HaFormBaseSchema { | export interface HaFormFloatSchema extends HaFormBaseSchema { | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { Formfield } from "@material/mwc-formfield"; | import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; | ||||||
| import { css, CSSResultGroup } from "lit"; | import { styles } from "@material/mwc-formfield/mwc-formfield.css"; | ||||||
|  | import { css } from "lit"; | ||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  |  | ||||||
| @customElement("ha-formfield") | @customElement("ha-formfield") | ||||||
| // @ts-expect-error | export class HaFormfield extends FormfieldBase { | ||||||
| export class HaFormfield extends Formfield { |  | ||||||
|   protected _labelClick() { |   protected _labelClick() { | ||||||
|     const input = this.input; |     const input = this.input; | ||||||
|     if (input) { |     if (input) { | ||||||
| @@ -23,9 +23,8 @@ export class HaFormfield extends Formfield { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected static get styles(): CSSResultGroup { |   static override styles = [ | ||||||
|     return [ |     styles, | ||||||
|       Formfield.styles, |  | ||||||
|     css` |     css` | ||||||
|       :host(:not([alignEnd])) ::slotted(ha-switch) { |       :host(:not([alignEnd])) ::slotted(ha-switch) { | ||||||
|         margin-right: 10px; |         margin-right: 10px; | ||||||
| @@ -37,7 +36,6 @@ export class HaFormfield extends Formfield { | |||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|   | |||||||
| @@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _error?: string; |   @state() private _error?: string; | ||||||
|  |  | ||||||
|  |   @state() private _errorIsFatal = false; | ||||||
|  |  | ||||||
|   private _hlsPolyfillInstance?: HlsLite; |   private _hlsPolyfillInstance?: HlsLite; | ||||||
|  |  | ||||||
|   private _exoPlayer = false; |   private _exoPlayer = false; | ||||||
| @@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement { | |||||||
|     super.connectedCallback(); |     super.connectedCallback(); | ||||||
|     HaHLSPlayer.streamCount += 1; |     HaHLSPlayer.streamCount += 1; | ||||||
|     if (this.hasUpdated) { |     if (this.hasUpdated) { | ||||||
|  |       this._resetError(); | ||||||
|       this._startHls(); |       this._startHls(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (this._error) { |  | ||||||
|       return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; |  | ||||||
|     } |  | ||||||
|     return html` |     return html` | ||||||
|       <video |       ${this._error | ||||||
|  |         ? html`<ha-alert | ||||||
|  |             alert-type="error" | ||||||
|  |             class=${this._errorIsFatal ? "fatal" : "retry"} | ||||||
|  |           > | ||||||
|  |             ${this._error} | ||||||
|  |           </ha-alert>` | ||||||
|  |         : ""} | ||||||
|  |       ${!this._errorIsFatal | ||||||
|  |         ? html`<video | ||||||
|             ?autoplay=${this.autoPlay} |             ?autoplay=${this.autoPlay} | ||||||
|             .muted=${this.muted} |             .muted=${this.muted} | ||||||
|             ?playsinline=${this.playsInline} |             ?playsinline=${this.playsInline} | ||||||
|             ?controls=${this.controls} |             ?controls=${this.controls} | ||||||
|       ></video> |           ></video>` | ||||||
|  |         : ""} | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     this._cleanUp(); |     this._cleanUp(); | ||||||
|  |     this._resetError(); | ||||||
|     this._startHls(); |     this._startHls(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _startHls(): Promise<void> { |   private async _startHls(): Promise<void> { | ||||||
|     this._error = undefined; |  | ||||||
|  |  | ||||||
|     const masterPlaylistPromise = fetch(this.url); |     const masterPlaylistPromise = fetch(this.url); | ||||||
|  |  | ||||||
|     const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) |     const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) | ||||||
| @@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!hlsSupported) { |     if (!hlsSupported) { | ||||||
|       this._error = this.hass.localize( |       this._setFatalError( | ||||||
|         "ui.components.media-browser.video_not_supported" |         this.hass.localize("ui.components.media-browser.video_not_supported") | ||||||
|       ); |       ); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement { | |||||||
|     this._hlsPolyfillInstance = hls; |     this._hlsPolyfillInstance = hls; | ||||||
|     hls.attachMedia(videoEl); |     hls.attachMedia(videoEl); | ||||||
|     hls.on(Hls.Events.MEDIA_ATTACHED, () => { |     hls.on(Hls.Events.MEDIA_ATTACHED, () => { | ||||||
|  |       this._resetError(); | ||||||
|       hls.loadSource(url); |       hls.loadSource(url); | ||||||
|     }); |     }); | ||||||
|     hls.on(Hls.Events.ERROR, (_, data: any) => { |     hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => { | ||||||
|  |       this._resetError(); | ||||||
|  |     }); | ||||||
|  |     hls.on(Hls.Events.ERROR, (_event, data: any) => { | ||||||
|  |       // Some errors are recovered automatically by the hls player itself, and the others handled | ||||||
|  |       // in this function require special actions to recover. Errors retried in this function | ||||||
|  |       // are done with backoff to not cause unecessary failures. | ||||||
|       if (!data.fatal) { |       if (!data.fatal) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement { | |||||||
|                 error += " (" + data.response.code + ")"; |                 error += " (" + data.response.code + ")"; | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|             this._error = error; |             this._setRetryableError(error); | ||||||
|             return; |             break; | ||||||
|           } |           } | ||||||
|           case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: |           case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: | ||||||
|             this._error = "Timeout while starting stream"; |             this._setRetryableError("Timeout while starting stream"); | ||||||
|             return; |             break; | ||||||
|           default: |           default: | ||||||
|             this._error = "Unknown stream network error (" + data.details + ")"; |             this._setRetryableError("Stream network error"); | ||||||
|             return; |             break; | ||||||
|         } |         } | ||||||
|         this._error = "Error with media stream contents (" + data.details + ")"; |         hls.startLoad(); | ||||||
|       } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { |       } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { | ||||||
|         this._error = "Error with media stream contents (" + data.details + ")"; |         this._setRetryableError("Error with media stream contents"); | ||||||
|  |         hls.recoverMediaError(); | ||||||
|       } else { |       } else { | ||||||
|         this._error = |         this._setFatalError("Error playing stream"); | ||||||
|           "Unknown error with stream (" + data.type + ", " + data.details + ")"; |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _resetError() { | ||||||
|  |     this._error = undefined; | ||||||
|  |     this._errorIsFatal = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _setFatalError(errorMessage: string) { | ||||||
|  |     this._error = errorMessage; | ||||||
|  |     this._errorIsFatal = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _setRetryableError(errorMessage: string) { | ||||||
|  |     this._error = errorMessage; | ||||||
|  |     this._errorIsFatal = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       :host, |       :host, | ||||||
| @@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement { | |||||||
|         max-height: var(--video-max-height, calc(100vh - 97px)); |         max-height: var(--video-max-height, calc(100vh - 97px)); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       ha-alert { |       .fatal { | ||||||
|         display: block; |         display: block; | ||||||
|         padding: 100px 16px; |         padding: 100px 16px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .retry { | ||||||
|  |         display: block; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { 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 { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { customIcons } from "../data/custom_icons"; | import { customIcons } from "../data/custom_icons"; | ||||||
| import { PolymerChangedEvent } from "../polymer-types"; | import { PolymerChangedEvent } from "../polymer-types"; | ||||||
|  | import { HomeAssistant } from "../types"; | ||||||
|  | import "./ha-combo-box"; | ||||||
|  | import type { HaComboBox } from "./ha-combo-box"; | ||||||
| import "./ha-icon"; | import "./ha-icon"; | ||||||
| import "./ha-icon-button"; |  | ||||||
|  |  | ||||||
| type IconItem = { | type IconItem = { | ||||||
|   icon: string; |   icon: string; | ||||||
| @@ -19,35 +16,17 @@ type IconItem = { | |||||||
| let iconItems: IconItem[] = []; | let iconItems: IconItem[] = []; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | // eslint-disable-next-line lit/prefer-static-styles | ||||||
| const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<style> | const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<mwc-list-item | ||||||
|     paper-icon-item { |   graphic="avatar" | ||||||
|       padding: 0; | > | ||||||
|       margin: -8px; |   <ha-icon .icon=${item.icon} slot="graphic"></ha-icon> | ||||||
|     } |   ${item.icon} | ||||||
|     #content { | </mwc-list-item>`; | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|     ha-svg-icon { |  | ||||||
|       padding-left: 2px; |  | ||||||
|       color: var(--secondary-text-color); |  | ||||||
|     } |  | ||||||
|     :host(:not([selected])) ha-svg-icon { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     :host([selected]) paper-icon-item { |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
|  |  | ||||||
|   <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> |  | ||||||
|   <paper-icon-item> |  | ||||||
|     <ha-icon .icon=${item.icon} slot="item-icon"></ha-icon> |  | ||||||
|     <paper-item-body>${item.icon}</paper-item-body> |  | ||||||
|   </paper-icon-item>`; |  | ||||||
|  |  | ||||||
| @customElement("ha-icon-picker") | @customElement("ha-icon-picker") | ||||||
| export class HaIconPicker extends LitElement { | export class HaIconPicker extends LitElement { | ||||||
|  |   @property() public hass?: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public value?: string; |   @property() public value?: string; | ||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
| @@ -64,51 +43,40 @@ export class HaIconPicker extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _opened = false; |   @state() private _opened = false; | ||||||
|  |  | ||||||
|   @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement; |   @query("ha-combo-box", true) private comboBox!: HaComboBox; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <ha-combo-box | ||||||
|  |         .hass=${this.hass} | ||||||
|         item-value-path="icon" |         item-value-path="icon" | ||||||
|         item-label-path="icon" |         item-label-path="icon" | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|         allow-custom-value |         allow-custom-value | ||||||
|         .filteredItems=${iconItems} |         .filteredItems=${iconItems} | ||||||
|         ${comboBoxRenderer(rowRenderer)} |         .label=${this.label} | ||||||
|  |         .disabled=${this.disabled} | ||||||
|  |         .placeholder=${this.placeholder} | ||||||
|  |         .errorMessage=${this.errorMessage} | ||||||
|  |         .invalid=${this.invalid} | ||||||
|  |         .renderer=${rowRenderer} | ||||||
|  |         icon | ||||||
|         @opened-changed=${this._openedChanged} |         @opened-changed=${this._openedChanged} | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|         @filter-changed=${this._filterChanged} |         @filter-changed=${this._filterChanged} | ||||||
|       > |  | ||||||
|         <paper-input |  | ||||||
|           .label=${this.label} |  | ||||||
|           .placeholder=${this.placeholder} |  | ||||||
|           .disabled=${this.disabled} |  | ||||||
|           class="input" |  | ||||||
|           autocapitalize="none" |  | ||||||
|           autocomplete="off" |  | ||||||
|           autocorrect="off" |  | ||||||
|           spellcheck="false" |  | ||||||
|           .errorMessage=${this.errorMessage} |  | ||||||
|           .invalid=${this.invalid} |  | ||||||
|       > |       > | ||||||
|         ${this._value || this.placeholder |         ${this._value || this.placeholder | ||||||
|           ? html` |           ? html` | ||||||
|                 <ha-icon .icon=${this._value || this.placeholder} slot="prefix"> |               <ha-icon .icon=${this._value || this.placeholder} slot="icon"> | ||||||
|               </ha-icon> |               </ha-icon> | ||||||
|             ` |             ` | ||||||
|           : this.fallbackPath |           : this.fallbackPath | ||||||
|           ? html`<ha-svg-icon |           ? html`<ha-svg-icon | ||||||
|               .path=${this.fallbackPath} |               .path=${this.fallbackPath} | ||||||
|                 slot="prefix" |               slot="icon" | ||||||
|             ></ha-svg-icon>` |             ></ha-svg-icon>` | ||||||
|           : ""} |           : ""} | ||||||
|           <ha-icon-button |       </ha-combo-box> | ||||||
|             .path=${this._opened ? mdiMenuUp : mdiMenuDown} |  | ||||||
|             slot="suffix" |  | ||||||
|             class="toggle-button" |  | ||||||
|           ></ha-icon-button> |  | ||||||
|         </paper-input> |  | ||||||
|       </vaadin-combo-box-light> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -150,6 +118,7 @@ export class HaIconPicker extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _valueChanged(ev: PolymerChangedEvent<string>) { |   private _valueChanged(ev: PolymerChangedEvent<string>) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|     this._setValue(ev.detail.value); |     this._setValue(ev.detail.value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -158,7 +127,7 @@ export class HaIconPicker extends LitElement { | |||||||
|     fireEvent( |     fireEvent( | ||||||
|       this, |       this, | ||||||
|       "value-changed", |       "value-changed", | ||||||
|       { value }, |       { value: this._value }, | ||||||
|       { |       { | ||||||
|         bubbles: false, |         bubbles: false, | ||||||
|         composed: false, |         composed: false, | ||||||
| @@ -205,17 +174,13 @@ export class HaIconPicker extends LitElement { | |||||||
|     return css` |     return css` | ||||||
|       ha-icon, |       ha-icon, | ||||||
|       ha-svg-icon { |       ha-svg-icon { | ||||||
|  |         color: var(--primary-text-color); | ||||||
|         position: relative; |         position: relative; | ||||||
|         bottom: 2px; |         bottom: 2px; | ||||||
|       } |       } | ||||||
|       *[slot="prefix"] { |       *[slot="prefix"] { | ||||||
|         margin-right: 8px; |         margin-right: 8px; | ||||||
|       } |       } | ||||||
|       paper-input > ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,28 +0,0 @@ | |||||||
| import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; |  | ||||||
| import { PolymerElement } from "@polymer/polymer"; |  | ||||||
| import { Constructor } from "../types"; |  | ||||||
|  |  | ||||||
| const paperDropdownClass = customElements.get( |  | ||||||
|   "paper-dropdown-menu" |  | ||||||
| ) as Constructor<PolymerElement>; |  | ||||||
|  |  | ||||||
| // patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183 |  | ||||||
| export class HaPaperDropdownClass extends paperDropdownClass { |  | ||||||
|   public ready() { |  | ||||||
|     super.ready(); |  | ||||||
|     // wait to check for direction since otherwise direction is wrong even though top level is RTL |  | ||||||
|     setTimeout(() => { |  | ||||||
|       if (window.getComputedStyle(this).direction === "rtl") { |  | ||||||
|         this.style.textAlign = "right"; |  | ||||||
|       } |  | ||||||
|     }, 100); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-paper-dropdown-menu": HaPaperDropdownClass; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass); |  | ||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { mdiImagePlus } from "@mdi/js"; | import { mdiImagePlus } from "@mdi/js"; | ||||||
| import "@polymer/paper-input/paper-input-container"; |  | ||||||
| import { html, LitElement, TemplateResult } from "lit"; | import { html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
|  | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import "@material/mwc-select/mwc-select"; |  | ||||||
| import "@material/mwc-textfield/mwc-textfield"; |  | ||||||
| import type { TextField } from "@material/mwc-textfield/mwc-textfield"; |  | ||||||
| import { mdiCamera } from "@mdi/js"; | import { mdiCamera } from "@mdi/js"; | ||||||
| import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; | import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| @@ -11,7 +9,8 @@ import { stopPropagation } from "../common/dom/stop_propagation"; | |||||||
| import { LocalizeFunc } from "../common/translations/localize"; | import { LocalizeFunc } from "../common/translations/localize"; | ||||||
| import "./ha-alert"; | import "./ha-alert"; | ||||||
| import "./ha-button-menu"; | import "./ha-button-menu"; | ||||||
| import "@material/mwc-button/mwc-button"; | import "./ha-textfield"; | ||||||
|  | import type { HaTextField } from "./ha-textfield"; | ||||||
|  |  | ||||||
| @customElement("ha-qr-scanner") | @customElement("ha-qr-scanner") | ||||||
| class HaQrScanner extends LitElement { | class HaQrScanner extends LitElement { | ||||||
| @@ -29,7 +28,7 @@ class HaQrScanner extends LitElement { | |||||||
|  |  | ||||||
|   @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; |   @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; | ||||||
|  |  | ||||||
|   @query("mwc-textfield") private _manualInput?: TextField; |   @query("ha-textfield") private _manualInput?: HaTextField; | ||||||
|  |  | ||||||
|   public disconnectedCallback(): void { |   public disconnectedCallback(): void { | ||||||
|     super.disconnectedCallback(); |     super.disconnectedCallback(); | ||||||
| @@ -102,11 +101,11 @@ class HaQrScanner extends LitElement { | |||||||
|           </ha-alert> |           </ha-alert> | ||||||
|           <p>${this.localize("ui.components.qr-scanner.manual_input")}</p> |           <p>${this.localize("ui.components.qr-scanner.manual_input")}</p> | ||||||
|           <div class="row"> |           <div class="row"> | ||||||
|             <mwc-textfield |             <ha-textfield | ||||||
|               .label=${this.localize("ui.components.qr-scanner.enter_qr_code")} |               .label=${this.localize("ui.components.qr-scanner.enter_qr_code")} | ||||||
|               @keyup=${this._manualKeyup} |               @keyup=${this._manualKeyup} | ||||||
|               @paste=${this._manualPaste} |               @paste=${this._manualPaste} | ||||||
|             ></mwc-textfield> |             ></ha-textfield> | ||||||
|             <mwc-button @click=${this._manualSubmit} |             <mwc-button @click=${this._manualSubmit} | ||||||
|               >${this.localize("ui.common.submit")}</mwc-button |               >${this.localize("ui.common.submit")}</mwc-button | ||||||
|             > |             > | ||||||
| @@ -161,7 +160,7 @@ class HaQrScanner extends LitElement { | |||||||
|  |  | ||||||
|   private _manualKeyup(ev: KeyboardEvent) { |   private _manualKeyup(ev: KeyboardEvent) { | ||||||
|     if (ev.key === "Enter") { |     if (ev.key === "Enter") { | ||||||
|       this._qrCodeScanned((ev.target as TextField).value); |       this._qrCodeScanned((ev.target as HaTextField).value); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -199,7 +198,7 @@ class HaQrScanner extends LitElement { | |||||||
|       display: flex; |       display: flex; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|     } |     } | ||||||
|     mwc-textfield { |     ha-textfield { | ||||||
|       flex: 1; |       flex: 1; | ||||||
|       margin-right: 8px; |       margin-right: 8px; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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"; | import { customElement } from "lit/decorators"; | ||||||
|  |  | ||||||
| @customElement("ha-radio") | @customElement("ha-radio") | ||||||
| export class HaRadio extends Radio { | export class HaRadio extends RadioBase { | ||||||
|   public firstUpdated() { |   static override styles = [ | ||||||
|     super.firstUpdated(); |     styles, | ||||||
|     this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); |     css` | ||||||
|  |       :host { | ||||||
|  |         --mdc-theme-secondary: var(--primary-color); | ||||||
|       } |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								src/components/ha-select.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/ha-select.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | import { SelectBase } from "@material/mwc-select/mwc-select-base"; | ||||||
|  | import { styles } from "@material/mwc-select/mwc-select.css"; | ||||||
|  | import { css, html, nothing } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import { debounce } from "../common/util/debounce"; | ||||||
|  | import { nextRender } from "../common/util/render-status"; | ||||||
|  |  | ||||||
|  | @customElement("ha-select") | ||||||
|  | export class HaSelect extends SelectBase { | ||||||
|  |   // @ts-ignore | ||||||
|  |   @property({ type: Boolean }) public icon?: boolean; | ||||||
|  |  | ||||||
|  |   protected override renderLeadingIcon() { | ||||||
|  |     if (!this.icon) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html`<span class="mdc-select__icon" | ||||||
|  |       ><slot name="icon"></slot | ||||||
|  |     ></span>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |     window.addEventListener("translations-updated", this._translationsUpdated); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   disconnectedCallback() { | ||||||
|  |     super.disconnectedCallback(); | ||||||
|  |     window.removeEventListener( | ||||||
|  |       "translations-updated", | ||||||
|  |       this._translationsUpdated | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _translationsUpdated = debounce(async () => { | ||||||
|  |     await nextRender(); | ||||||
|  |     this.layoutOptions(); | ||||||
|  |   }, 500); | ||||||
|  |  | ||||||
|  |   static override styles = [ | ||||||
|  |     styles, | ||||||
|  |     css` | ||||||
|  |       .mdc-select:not(.mdc-select--disabled) .mdc-select__icon { | ||||||
|  |         color: var(--secondary-text-color); | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-select": HaSelect; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { AddonSelector } from "../../data/selector"; | import { AddonSelector } from "../../data/selector"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| @@ -22,6 +22,12 @@ export class HaAddonSelector extends LitElement { | |||||||
|       allow-custom-entity |       allow-custom-entity | ||||||
|     ></ha-addon-picker>`; |     ></ha-addon-picker>`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     ha-addon-picker { | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								src/components/ha-selector/ha-selector-attribute.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/ha-selector/ha-selector-attribute.ts
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -35,9 +35,12 @@ export class HaBooleanSelector extends LitElement { | |||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|  |       :host { | ||||||
|  |         height: 56px; | ||||||
|  |         display: flex; | ||||||
|  |       } | ||||||
|       ha-formfield { |       ha-formfield { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         margin: 16px 0; |  | ||||||
|         --mdc-typography-body2-font-size: 1em; |         --mdc-typography-body2-font-size: 1em; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								src/components/ha-selector/ha-selector-duration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/components/ha-selector/ha-selector-duration.ts
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -50,7 +50,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   private _filterEntities = (entity: HassEntity): boolean => { |   private _filterEntities = (entity: HassEntity): boolean => { | ||||||
|     if (this.selector.entity?.domain) { |     if (this.selector.entity?.domain) { | ||||||
|       if (computeStateDomain(entity) !== this.selector.entity.domain) { |       const filterDomain = this.selector.entity.domain; | ||||||
|  |       const entityDomain = computeStateDomain(entity); | ||||||
|  |       if ( | ||||||
|  |         (Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) || | ||||||
|  |         entityDomain !== filterDomain | ||||||
|  |       ) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user