Merge pull request #11809 from home-assistant/dev (#11809)

* Fix config card rtl issues

* Remove optional field from ha-form schema type (#11538)

* Add entity id autocompletion to YAML code editors (#11099)

* Add selectors to ha-form (#11534)

* Allow translate gas total (#11547)

* Migrate combobox to mwc (#11546)

* New date picker (#11555)

* Link via device on device page (#11554)

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

* Add integration_discovery to discovery sources (#11564)

* Remember filter between navigation (#11565)

* Convert selectors to MWC (#11543)

* Covert area picker to combo-box (#11562)

* Convert entity picker to ha-combo (#11560)

* Convert entity picker to ha-combo

* Update ha-entity-picker.ts

* Handle empty better

* Clear value when no device/area/entity

* Update links on info page (#11590)

* Migrate (input) select entities to mwc (#11591)

* Convert HaFormSchemas to use selectors (#11589)

* Fix number selector (#11585)

* Convert entity-attribute picker to ha-combo-box (#11587)

* Convert icon picker to ha-combobox (#11586)

Co-authored-by: Zack <zackbarett@hey.com>

* Convert area-devices picker (#11588)

* Convert device automation picker to mwc (#11592)

Co-authored-by: Zack <zackbarett@hey.com>

* Fix clearing device in device action (#11594)

* dark mode fixes (#11595)

* Only show stable add-ons in the store if not advanced mode (#11596)

* Convert Automation Action Choose to HA Form (#11597)

* Convert Auatomation Action Choose to HA Form

* remove log

* Remove Import

* Replace checkboxes in list items with `check-list-item` (#11610)

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

* Make textarea grow on input (#11618)

* Update lit-virtualizer (#11623)

* Convert time inputs to Lit + mwc (#11609)

* Set initial focus for device, area, and entity dialogs (#11622)

* Add aria-label to table headers with no title (#11503)

* Add loadCardHelpers to cast scope (#11616)

* Update code editor to material 3 look (#11628)

* Set button role on button card and handle enter and space (#11627)

* Only load ha-selector when needed (#11630)

* Fix service control for older browsers (#11629)

* Migrate a bunch of paper-dropdowns (#11626)

* Merged too fast for Bram :) Code improv (#11632)

* Add support for opening camera media source (#11633)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Create error when trying to backup wile system in freeze (#11634)

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

* Add missing type to create device automation/script heading (#11635)

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

* Generate random webhook_id and add copy button (#11568)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Migrate search bar to mwc (#11637)

* fix data-table row handlers (#11638)

* Bunch of fixes and cleanup (#11636)

* State Trigger -> HA Form (#11631)

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

* Allow uploading media (#11615)

* Allow uploading media

* Update path

* Use current item we already have

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

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

* Use alert dialog and use button for add media

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

* Add Attribute Picker as a selector - add to state trigger (#11641)

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

* Time Pattern to HA Form (#11648)

* MQTT Trigger to Ha-Form (#11643)

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

* Convert Sun to Ha Form (#11647)

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

* Geo Location Trigger to HA - Form (#11644)

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

* HA Trigger to HA Form (#11645)

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

* Make HA Form set required to false for selectors (#11649)

* Fix Lovelace Empty Menu when not advanced or admin (#11660)

* Add support for media player assumed state (#11642)

* Improve search and filters on mobile + fix close button in search field (#11662)

Co-authored-by: Zack <zackbarett@hey.com>

* Allow adding Zigbee/Zwave device (#11650)

* Numerical State to HA-Form (#11646)

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

* Filter fixes (#11664)

* Add WORKSPACE_DIRECTORY environment variable to devcontainer and script.core (#11477)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>

* hotfix history view on missing state (#11663)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Improve robustness of hls media player (#11672)

* Revert compute state display show empty string as unknown (#11677)

* Set initial focus for some more dialogs (#11676)

* Limit types of media that can be uploaded to local media (#11683)

* Don't show toggle always on more info (#11640)

* Add TTS to media browser (#11679)

* Omit Device info and actions for connected controller nodes (#11673)

* Script Editor to Ha Form (#11601)

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

* Another round of paper-dropdown -> mwc-select conversion (#11674)

* Another round of paper-dropdown -> mwc-select conversion

* ha-pick-language-row -> Lit

* Update hui-view-editor.ts

* Cleanup imports

* hassio

* Add explicit imports

* hassio fixes (#11688)

* Dont exclude domain for area and device (#11689)

* Try to keep the browsing stack when changing players in media panel (#11681)

* Allow uploading multiple files (#11687)

* Bumped version to 20220214.0

* Group helpers not in an area in a single card (#11690)

* Improve `stripPrefixFromEntityName` to handle colon and space separator (#11691)

* Display transmitted messages in MQTT debug info dialog (#11531)

* Latest paper-dropdown -> mwc-select conversion (#11692)

* This adds back mobile click accessibility (#11693)

* Updated text part 2 (#11686)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Set initial focus for lovelace dialogs (#11667)

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

* Migrate all lovelace elements to mwc (#11695)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Fix import

* Clean up some imports (#11696)

* Convert triple dots to single char in translations (#11697)

* Fixes remote icon state color (#11698)

* Convert scene action to service call (#11705)

* Convert scene action to service call

* fix describeAction

* rename to metadata

* Update script.ts

* Fix mode selection in automation editor (#11707)

* Remove duplicate gallery page (#11711)

* Add bottom padding to config links list with safe-area-inset-bottom (#11704)

* Bump hls.js to v1.1.5 (#11712)

* Make zwave_js config panel inclusion state aware (#11556)

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

* Fix mwc-select in lovelace editors (#11708)

* Add signed add-on capability and adjust max rating (#11703)

* Add support for removing config entry from a device

* Tweak

* Fix lint error

* Tweak

* Prettier

* Add play media action (#11702)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Debounce refresh the cloud status if Google events happen (#11721)

* Remove custom MQTT delete device button (#11724)

* Apply suggestions from code review

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

* Update src/panels/config/devices/ha-config-device-page.ts

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

* Correct typing

* Prettier

* Remove useless Array.isArray check

* Remove custom Tasmota delete device button (#11725)

* Automation Conditions to conversion to ha-form or mwc (#11727)

* Set initial focus for energy dialogs (#11730)

* Entity Settings Page to MWC 3 (#11694)

* Show why relayer is reconnecting (#11732)

* Change words for trigger condition (#11733)

* Update media player more info (#11734)

* Pass hass to ha-form to enable selectors (#11739)

* Bumped version to 20220220.0

* Add link to the selector docs

* TTS form no longer showed due to import oopsie (#11742)

* Improve logo rendering for playing media in browser (#11741)

* Fix media upload on iOS (#11740)

* Handle inifinity media duration (#11749)

* Show when media is being loaded (#11750)

* Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731)

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

* Set initial focus for supervisor dialogs (#11710)

* Convert Automation Actions to mwc/ha-form + other automation items (#11753)

* Selector: remove text value when not required and empty (#11754)

* Convert date-range-picker to mwc (#11755)

* Radio Browser is now added during onboarding (#11756)

* Add support for the media browser My link (#11757)

* Show Home Assistant when creating partial backup (#11758)

* Fix zwave migration (#11751)

* Allow config entries to be reloaded when they are in setup_retry state (#11759)

* Area Card Editor to Ha Form (#11762)

* Fix WebRTC player stream playback when disconnected/connected (#11764)

* set theme to undefined when no theme (#11765)

* Paper input migrations (#11766)

* Only show description when set (#11772)

* Thermostat Editor to HA - Form (#11763)

* Thermostat - Ha Form

* Update hui-thermostat-card-editor.ts

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

* Alarm Card Editor to HA Form (#11760)

* Move to ha-form

* Update hui-alarm-panel-card-editor.ts

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

* Change icons for cover with device_class curtain (#11752)

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

* no need for memoize

* Include scoped custom element polyfill (#11776)

* Show triggered in automation editor (#11771)

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

* Allow changing volume media player entity (#11781)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add community section (#11779)

* Bumped version to 20220222.0

* Fix State Condition 'For' Data (#11782)

* entities card editor to MWC (#11785)

* Fix ripple corner radius for button card (#11780)

* Condition Card Editor to MWC (#11783)

* Show number of hidden items (#11786)

* Put volume slider in the middle of the button (#11788)

* Add media management dialog (#11787)

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

* Convert alarm control panel more info (#11791)

* Convert alarm control panel more info

* Update more-info-alarm_control_panel.ts

* Update src/dialogs/more-info/controls/more-info-alarm_control_panel.ts

* Apply suggestions from code review

Co-authored-by: Zack Barett <zackbarett@hey.com>

* import

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Migrate more-info configurator (#11792)

* Migrate more-info configurator

* Update more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Import

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Convert more info lock (#11794)

* Add Margin to Tip (#11790)

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

* Dont render double label on number selector (#11796)

* Input conversion in dev tools (#11795)

* Gauge Editor to Ha Form (#11793)

* Stop spinning when opening media in dialog (#11800)

* Fix Entities picker (#11802)

* Migrate single textfields (#11799)

* Migrate single textfields

* Update ha-config-name-form.ts

* Update dialog-area-registry-detail.ts

* Update manual-automation-editor.ts

* Update manual-automation-editor.ts

* required to number selector fix script

* review

* change repository url and project description (#11801)

* Calendar card to HA Form (#11784)

* Graph Footer to MWC (#11803)

* History Graph Editor to ha form (#11797)

* Glance editor to ha-form (#11804)

* Grid Card to HA Form (#11798)

* Button editor to ha-form (#11808)

* Bumped version to 20220223.0

* mwc-select -> ha-select (#11806)

Co-authored-by: Yosi Levy <yosilevy@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: Kuba Wolanin <hi@kubawolanin.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Zack <zackbarett@hey.com>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
Co-authored-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Thomas Lovén <thomasloven@gmail.com>
Co-authored-by: Eric Severance <esev@esev.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
Co-authored-by: lintaba <lintaba@gmail.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: kpine <keith.pine@gmail.com>
Co-authored-by: Brandon Rothweiler <brandonrothweiler@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Matthias de Baat <matthias.debaat@nabucasa.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Josh McCarty <josh@joshmccarty.com>
Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Pascal Winters <pascal@famwinters.com>
Co-authored-by: Robin Wittebol <robinwittebol@live.nl>
Co-authored-by: Tomasz <t.jagusz@gmail.com>
This commit is contained in:
Paulus Schoutsen 2022-02-23 11:11:41 -08:00 committed by GitHub
commit 09f8f816d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
398 changed files with 11446 additions and 10619 deletions

View File

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

View File

@ -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();
@ -26,4 +25,5 @@ index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc
+ _ET = (await import("event-target-shim")).default.EventTarget; + _ET = (await import("event-target-shim")).default.EventTarget;
+ } + }
+ return (ET = _ET); + return (ET = _ET);
} }
//# sourceMappingURL=EventTarget.js.map

View File

@ -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.
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/) [![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/frontend/master/docs/screenshot.png)](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)

View File

@ -10,7 +10,7 @@ module.exports.ignorePackages = ({ latestBuild }) => [
]; ];
// Files from NPM packages that we should replace with empty file // Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ latestBuild }) => 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,
}, },

View File

@ -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(),

View File

@ -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;

View File

@ -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);

View File

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

View File

@ -3,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 {

View File

@ -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() {

View File

@ -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";

View File

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

View File

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

View File

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

View File

@ -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/).

View File

@ -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;
} }

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -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" (item) => html`
attr-for-selected="device" <mwc-list-item .value=${item.device || ""}>
.selected=${this._selectedInput!} ${item.name}
> </mwc-list-item>
${this._inputDevices && `
this._inputDevices.map( )}
(item) => html` </ha-select>`}
<paper-item device=${item.device || ""}> ${this._outputDevices &&
${item.name} html`<ha-select
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
.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" (item) => html`
attr-for-selected="device" <mwc-list-item .value=${item.device || ""}
.selected=${this._selectedOutput!} >${item.name}</mwc-list-item
> >
${this._outputDevices && `
this._outputDevices.map( )}
(item) => html` </ha-select>`}
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`
)}
</paper-listbox>
</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;
} }

View File

@ -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 />

View File

@ -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,24 +175,23 @@ 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 <ha-formfield
? html` .label=${html`<supervisor-formfield-label
<ha-formfield label="Home Assistant"
.label=${html`<supervisor-formfield-label .iconPath=${mdiHomeAssistant}
label="Home Assistant" .version=${this.backup
.iconPath=${mdiHomeAssistant} ? this.backup.homeassistant
.version=${this.backup.homeassistant} : this.hass.config.version}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
> >
<ha-checkbox <ha-checkbox
.checked=${this.homeAssistant} .checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant} @click=${this.toggleHomeAssistant}
> >
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
`
: ""}
${foldersSection?.templates.length ${foldersSection?.templates.length
? html` ? html`
<ha-formfield <ha-formfield

View File

@ -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;
} }
`, `,
]; ];

View File

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

View File

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

View File

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

View File

@ -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) =>
(device) => html`<paper-item>${device}</paper-item>` html`<mwc-list-item .value=${device}
)} >${device}</mwc-list-item
</paper-listbox> >`
</paper-dropdown-menu> )}
</ha-select>
` `
: 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 {

View File

@ -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 {

View File

@ -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>
`; `;
} }

View File

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

View File

@ -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"
)} )}

View File

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

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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 ${logProviders.map(
slot="dropdown-content" (provider) => html`
attr-for-selected="provider" <mwc-list-item .value=${provider.key}>
.selected=${this._selectedLogProvider} ${provider.name}
> </mwc-list-item>
${logProviders.map( `
(provider) => html` )}
<paper-item provider=${provider.key}> </ha-select>
${provider.name}
</paper-item>
`
)}
</paper-listbox>
</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;
} }
`, `,
]; ];

View File

@ -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(

View File

@ -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,7 +116,7 @@
"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": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",

View File

@ -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"

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = home-assistant-frontend name = home-assistant-frontend
version = 20220203.1 version = 20220223.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

View File

@ -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;

View File

@ -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];

View File

@ -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") {
return stateObj.state === "on" || stateObj.state === "off"; 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 false;
} }
if (domain === "climate") { if (domain === "climate") {
return supportsFeature(stateObj, 4096); return supportsFeature(stateObj, 4096);
} }

View File

@ -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;

View File

@ -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;
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)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
}
} }
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length); return undefined;
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
}; };
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str; const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;

View File

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

View File

@ -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"],

View File

@ -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;
}; };

View File

@ -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 {
@ -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,111 +339,99 @@ 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) => {
// not sure how this happens...
if (!row) {
return html``;
}
if (row.append) {
return html`
<div class="mdc-data-table__row">${row.content}</div>
`;
}
if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `;
}
return html`
<div
aria-rowindex=${index! + 2}
role="row"
.rowId=${row[this.id]}
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected":
this._checkedRows.includes(String(row[this.id])),
clickable: this.clickable,
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id]))
? true
: undefined
)}
.selectable=${row.selectable !== false}
>
${this.selectable
? html`
<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
role="cell"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</div>
`
: ""}
${Object.entries(this.columns).map(
([key, column]) => {
if (column.hidden) {
return "";
}
return html`
<div
role="cell"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric":
column.type === "numeric",
"mdc-data-table__cell--icon":
column.type === "icon",
"mdc-data-table__cell--icon-button":
column.type === "icon-button",
"mdc-data-table__cell--overflow-menu":
column.type === "overflow-menu",
grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR),
})}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]:
column.width,
maxWidth: column.maxWidth
? column.maxWidth
: "",
})
: ""}
>
${column.template
? column.template(row[key], row)
: row[key]}
</div>
`;
}
)}
</div>
`;
},
})}
</div>
`} `}
</div> </div>
</div> </div>
`; `;
} }
private _renderRow = (
row: DataTableRowData,
index: number
): TemplateResult => {
// not sure how this happens...
if (!row) {
return html``;
}
if (row.append) {
return html` <div class="mdc-data-table__row">${row.content}</div> `;
}
if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `;
}
return html`
<div
aria-rowindex=${index + 2}
role="row"
.rowId=${row[this.id]}
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
clickable: this.clickable,
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id])) ? true : undefined
)}
.selectable=${row.selectable !== false}
>
${this.selectable
? html`
<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
role="cell"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(String(row[this.id]))}
>
</ha-checkbox>
</div>
`
: ""}
${Object.entries(this.columns).map(([key, column]) => {
if (column.hidden) {
return "";
}
return html`
<div
role="cell"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": column.type === "numeric",
"mdc-data-table__cell--icon": column.type === "icon",
"mdc-data-table__cell--icon-button":
column.type === "icon-button",
"mdc-data-table__cell--overflow-menu":
column.type === "overflow-menu",
grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR),
})}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]: column.width,
maxWidth: column.maxWidth ? column.maxWidth : "",
})
: ""}
>
${column.template ? column.template(row[key], row) : row[key]}
</div>
`;
})}
</div>
`;
};
private async _sortFilterData() { private async _sortFilterData() {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
this.curRequest++; this.curRequest++;
@ -536,7 +526,7 @@ export class HaDataTable extends LitElement {
} }
} }
private _handleRowCheckboxClick(ev: Event) { private _handleRowCheckboxClick = (ev: Event) => {
const checkbox = ev.currentTarget as HaCheckbox; const 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;
}
`, `,
]; ];
} }

View File

@ -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} .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
> >
<paper-input </ha-combo-box>
.label=${this.label === undefined && this.hass <mwc-button @click=${this._switchPicker}>
? this.hass.localize("ui.components.device-picker.device") Choose individual devices
: `${this.label} in area`} </mwc-button>
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value
? html`<ha-icon-button
class="clear-button"
.label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
.path=${mdiClose}
@click=${this._clearValue}
no-ripple
></ha-icon-button> `
: ""}
${areas.length > 0
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button"
></ha-icon-button>
`
: ""}
</div>
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
>Choose individual devices</mwc-button
>
`; `;
} }
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 {

View File

@ -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 ${value === NO_AUTOMATION_KEY
slot="dropdown-content" ? html`<mwc-list-item .value=${NO_AUTOMATION_KEY}>
.selected=${this._key} ${this.NO_AUTOMATION_TEXT}
attr-for-selected="key" </mwc-list-item>`
@iron-select=${this._automationChanged} : ""}
> ${value === UNKNOWN_AUTOMATION_KEY
<paper-item ? html`<mwc-list-item .value=${UNKNOWN_AUTOMATION_KEY}>
key=${NO_AUTOMATION_KEY} ${this.UNKNOWN_AUTOMATION_TEXT}
.automation=${this._createNoAutomation(this.deviceId)} </mwc-list-item>`
hidden : ""}
> ${this._automations.map(
${this.NO_AUTOMATION_TEXT} (automation, idx) => html`
</paper-item> <mwc-list-item .value=${`${automation.device_id}_${idx}`}>
<paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden> ${this._localizeDeviceAutomation(this.hass, automation)}
${this.UNKNOWN_AUTOMATION_TEXT} </mwc-list-item>
</paper-item> `
${this._automations.map( )}
(automation, idx) => html` </ha-select>
<paper-item
key=${`${this.deviceId}_${idx}`}
.automation=${automation}
>
${this._localizeDeviceAutomation(this.hass, automation)}
</paper-item>
`
)}
</paper-listbox>
</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;
} }
`; `;
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -1,54 +1,14 @@
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket"; import { 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}
.value=${this.value || ""}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.disabled=${this.disabled || !this.entityId}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
attr-for-value="bind-value" item-value-path="value"
${comboBoxRenderer(rowRenderer)} item-label-path="label"
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
<paper-input </ha-combo-box>
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value ? formatAttributeName(this._value) : ""}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
.path=${mdiClose}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button"
tabindex="-1"
></ha-icon-button>
</div>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
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;
}
`;
} }
} }

View File

@ -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 {

View File

@ -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,54 +64,24 @@ 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> ${item.state
paper-icon-item { ? html`<state-badge slot="graphic" .stateObj=${item.state}></state-badge>`
padding: 0; : ""}
margin: -8px; <span>${item.name}</span>
} <span slot="secondary"
#content { >${item.id === "" || item.id === "__missing"
display: flex; ? html`<a
align-items: center; target="_blank"
} rel="noopener noreferrer"
ha-svg-icon { href=${documentationUrl(this.hass, "/more-info/statistics/")}
padding-left: 2px; >${this.hass.localize(
color: var(--secondary-text-color); "ui.components.statistic-picker.learn_more"
} )}</a
:host(:not([selected])) ha-svg-icon { >`
display: none; : item.id}</span
} >
:host([selected]) paper-icon-item { </mwc-list-item>`;
margin-left: 0;
}
a {
color: var(--primary-color);
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
${item.state
? html`<state-badge
slot="item-icon"
.stateObj=${item.state}
></state-badge>`
: ""}
<paper-item-body two-line="">
${item.name}
<span secondary
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/more-info/statistics/")}
>${this.hass.localize(
"ui.components.statistic-picker.learn_more"
)}</a
>`
: item.id}</span
>
</paper-item-body>
</paper-icon-item>`;
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 {

View File

@ -1,4 +1,4 @@
import { html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { 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 {

View File

@ -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 {

View File

@ -1,19 +1,6 @@
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { 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)} .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged} @value-changed=${this._areaChanged}
> >
<paper-input </ha-combo-box>
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.components.area-picker.clear"
)}
.path=${mdiClose}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass.localize("ui.components.area-picker.toggle")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
@ -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 {

View 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;
}
}

View File

@ -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" ${this.hass.localize(
.selected=${this.value} "ui.components.blueprint-picker.select_blueprint"
attr-for-selected="data-blueprint-path"
@iron-select=${this._blueprintChanged}
>
<paper-item data-blueprint-path="">
${this.hass.localize(
"ui.components.blueprint-picker.select_blueprint"
)}
</paper-item>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<paper-item data-blueprint-path=${blueprint.path}>
${blueprint.name}
</paper-item>
`
)} )}
</paper-listbox> </mwc-list-item>
</paper-dropdown-menu-light> ${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<mwc-list-item .value=${blueprint.path}>
${blueprint.name}
</mwc-list-item>
`
)}
</ha-select>
`; `;
} }
@ -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;
}
`; `;
} }
} }

View File

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

View File

@ -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%;
} }
`; `;

View File

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

View File

@ -1,12 +1,18 @@
import { Checkbox } from "@material/mwc-checkbox"; import { CheckboxBase } from "@material/mwc-checkbox/mwc-checkbox-base";
import { styles } from "@material/mwc-checkbox/mwc-checkbox.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; 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 {

View File

@ -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,43 +123,92 @@ 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._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
];
if (!this.readOnly && this.autocompleteEntities && this.hass) {
extensions.push(
this._loadedCodeMirror.autocompletion({
override: [this._entityCompletions.bind(this)],
maxRenderedOptions: 10,
})
);
}
this.codemirror = new this._loadedCodeMirror.EditorView({ this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({ state: this._loadedCodeMirror.EditorState.create({
doc: this._value, doc: this._value,
extensions: [ extensions,
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
}), }),
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);
} }
`; `;

View File

@ -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}
> >
${this.value <slot name="icon" slot="leadingIcon"></slot>
? html` </ha-textfield>
<ha-icon-button ${this.value
.label=${this.hass.localize("ui.components.combo-box.clear")} ? html`<ha-svg-icon
.path=${mdiClose} aria-label=${this.hass?.localize("ui.components.combo-box.clear")}
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
aria-label=${this.hass?.localize("ui.components.combo-box.show")}
<ha-icon-button class="toggle-button"
.label=${this.hass.localize("ui.components.combo-box.show")} .path=${this._opened ? mdiMenuUp : mdiMenuDown}
.path=${this._opened ? mdiMenuUp : mdiMenuDown} @click=${this._toggleOpen}
slot="suffix" ></ha-svg-icon>
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
} }
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) =>
html`<mwc-list-item>
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</mwc-list-item>`;
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { value: undefined }); fireEvent(this, "value-changed", { value: undefined });
} }
private _toggleOpen(ev: Event) {
if (this._opened) {
this._comboBox?.close();
ev.stopPropagation();
} else {
this._comboBox?.inputElement.focus();
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value; // delay this so we can handle click event before setting _opened
setTimeout(() => {
this._opened = ev.detail.value;
}, 0);
// @ts-ignore // @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;
}
`; `;
} }
} }

View File

@ -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",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
weekdays: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
firstDayOfWeek: 0,
week: "Week",
calendar: "Calendar",
clear: "Clear",
today: "Today",
cancel: "Cancel",
formatTitle: (monthName, fullYear) => monthName + " " + fullYear,
formatDate: (d: { day: number; month: number; year: number }) =>
[
("0000" + String(d.year)).slice(-4),
("0" + String(d.month + 1)).slice(-2),
("0" + String(d.day)).slice(-2),
].join("-"),
parseDate: (text: string) => {
const parts = text.split("-");
const today = new Date();
let date;
let month = today.getMonth();
let year = today.getFullYear();
if (parts.length === 3) {
year = parseInt(parts[0]);
if (parts[0].length < 3 && year >= 0) {
year += year < 50 ? 2000 : 1900;
}
month = parseInt(parts[1]) - 1;
date = parseInt(parts[2]);
} else if (parts.length === 2) {
month = parseInt(parts[0]) - 1;
date = parseInt(parts[1]);
} else if (parts.length === 1) {
date = parseInt(parts[0]);
}
if (date !== undefined) { export interface datePickerDialogParams {
return { day: date, month, year }; value?: string;
} min?: string;
return undefined; max?: string;
}, locale?: string;
onChange: (value: string) => void;
}
const showDatePickerDialog = (
element: HTMLElement,
dialogParams: datePickerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-date-picker",
dialogImport: loadDatePickerDialog,
dialogParams,
});
}; };
@customElement("ha-date-input") @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
.label=${this.label}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._valueChanged} iconTrailing="calendar"
attr-for-value="value" @click=${this._openDialog}
.i18n=${i18n} .value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
> >
<paper-input <ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
.label=${this.label} </ha-textfield>`;
.disabled=${this.disabled}
no-label-float
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>
</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);
} }

View File

@ -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;
} }

View File

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

View File

@ -1,6 +1,7 @@
import { Dialog } from "@material/mwc-dialog"; import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js"; import { 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,77 +31,75 @@ 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); z-index: var(--dialog-z-index, 7);
z-index: var(--dialog-z-index, 7); -webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none); backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none); }
} .mdc-dialog__actions {
.mdc-dialog__actions { justify-content: var(--justify-action-buttons, flex-end);
justify-content: var(--justify-action-buttons, flex-end); padding-bottom: max(env(safe-area-inset-bottom), 8px);
padding-bottom: max(env(safe-area-inset-bottom), 8px); }
} .mdc-dialog__actions span:nth-child(1) {
.mdc-dialog__actions span:nth-child(1) { flex: var(--secondary-action-button-flex, unset);
flex: var(--secondary-action-button-flex, unset); }
} .mdc-dialog__actions span:nth-child(2) {
.mdc-dialog__actions span:nth-child(2) { flex: var(--primary-action-button-flex, unset);
flex: var(--primary-action-button-flex, unset); }
} .mdc-dialog__container {
.mdc-dialog__container { align-items: var(--vertial-align-dialog, center);
align-items: var(--vertial-align-dialog, center); }
} .mdc-dialog__title::before {
.mdc-dialog__title::before { display: block;
display: block; height: 20px;
height: 20px; }
} .mdc-dialog .mdc-dialog__content {
.mdc-dialog .mdc-dialog__content { position: var(--dialog-content-position, relative);
position: var(--dialog-content-position, relative); padding: var(--dialog-content-padding, 20px 24px);
padding: var(--dialog-content-padding, 20px 24px); }
} :host([hideactions]) .mdc-dialog .mdc-dialog__content {
:host([hideactions]) .mdc-dialog .mdc-dialog__content { padding-bottom: max(
padding-bottom: max( var(--dialog-content-padding, 20px),
var(--dialog-content-padding, 20px), env(safe-area-inset-bottom)
env(safe-area-inset-bottom) );
); }
} .mdc-dialog .mdc-dialog__surface {
.mdc-dialog .mdc-dialog__surface { position: var(--dialog-surface-position, relative);
position: var(--dialog-surface-position, relative); top: var(--dialog-surface-top);
top: var(--dialog-surface-top); min-height: var(--mdc-dialog-min-height, auto);
min-height: var(--mdc-dialog-min-height, auto); border-radius: var(
border-radius: var( --ha-dialog-border-radius,
--ha-dialog-border-radius, var(--ha-card-border-radius, 4px)
var(--ha-card-border-radius, 4px) );
); }
} :host([flexContent]) .mdc-dialog .mdc-dialog__content {
:host([flexContent]) .mdc-dialog .mdc-dialog__content { display: flex;
display: flex; flex-direction: column;
flex-direction: column; }
} .header_button {
.header_button { position: absolute;
position: absolute; right: 16px;
right: 16px; top: 10px;
top: 10px; text-decoration: none;
text-decoration: none; color: inherit;
color: inherit; }
} .header_title {
.header_title { margin-right: 40px;
margin-right: 40px; }
} [dir="rtl"].header_button {
[dir="rtl"].header_button { right: auto;
right: auto; left: 16px;
left: 16px; }
} [dir="rtl"].header_title {
[dir="rtl"].header_title { margin-left: 40px;
margin-left: 40px; margin-right: 0px;
margin-right: 0px; }
} `,
`, ];
];
}
} }
declare global { declare global {

View File

@ -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 };
private _parseDurationMillisec(value) { if (!this.enableMillisecond && !value.milliseconds) {
return value.toString().padStart(3, "0"); // @ts-ignore
} delete value.milliseconds;
} else if (value.milliseconds > 999) {
private _hourChanged(ev) { value.seconds += Math.floor(value.milliseconds / 1000);
this._durationChanged(ev, "hours"); value.milliseconds %= 1000;
}
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; if (value.seconds > 59) {
let minutes = this._minutes; value.minutes += Math.floor(value.seconds / 60);
value.seconds %= 60;
if (unit === "seconds" && value > 59) {
minutes += Math.floor(value / 60);
value %= 60;
} }
if (unit === "minutes" && value > 59) { if (value.minutes > 59) {
hours += Math.floor(value / 60); value.hours += Math.floor(value.minutes / 60);
value %= 60; value.minutes %= 60;
} }
const newValue: HaDurationData = {
hours,
minutes,
seconds: this._seconds,
};
if (this.enableMillisecond || this._milliseconds) {
newValue.milliseconds = this._milliseconds;
}
newValue[unit] = value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: newValue, value,
}); });
} }
} }

View File

@ -1,21 +1,21 @@
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import { css, html, LitElement, TemplateResult, PropertyValues } from "lit"; import { 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;
} }
`; `;

View 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;
}
}

View File

@ -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;
} }
`; `;

View File

@ -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]) => {
const value = optionValue(item);
return html`
<mwc-formfield .label=${optionLabel(item)}>
<ha-checkbox
.checked=${data.includes(value)}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-checkbox>
</mwc-formfield>
`;
});
// We will just render all checkboxes. // We will just render all checkboxes.
if (options.length < SHOW_ALL_ENTRIES_LIMIT) { if (options.length < SHOW_ALL_ENTRIES_LIMIT) {
return html`<div>${this.label}${renderedOptions}</div> `; return html`<div>
${this.label}${options.map((item: string | [string, string]) => {
const value = optionValue(item);
return html`
<ha-formfield .label=${optionLabel(item)}>
<ha-checkbox
.checked=${data.includes(value)}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-checkbox>
</ha-formfield>
`;
})}
</div> `;
} }
return html` 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;
} }

View File

@ -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;
} }

View File

@ -1,17 +1,17 @@
import { mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiEye, mdiEyeOff } from "@mdi/js";
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
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 {

View File

@ -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,12 +94,26 @@ export class HaForm extends LitElement implements HaFormElement {
</ha-alert> </ha-alert>
` `
: ""} : ""}
${dynamicElement(`ha-form-${item.type}`, { ${"selector" in item
schema: item, ? html`<ha-selector
data: getValue(this.data, item), .schema=${item}
label: this._computeLabel(item), .hass=${this.hass}
disabled: this.disabled, .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,
data: getValue(this.data, item),
label: this._computeLabel(item, this.data),
disabled: this.disabled,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
})}
`; `;
})} })}
</div> </div>
@ -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;
} }

View File

@ -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 {

View File

@ -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,20 +23,18 @@ 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; }
} :host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) {
:host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) { margin-left: 10px;
margin-left: 10px; margin-right: auto;
margin-right: auto; }
} `,
`, ];
];
}
} }
declare global { declare global {

View File

@ -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
?autoplay=${this.autoPlay} ? html`<ha-alert
.muted=${this.muted} alert-type="error"
?playsinline=${this.playsInline} class=${this._errorIsFatal ? "fatal" : "retry"}
?controls=${this.controls} >
></video> ${this._error}
</ha-alert>`
: ""}
${!this._errorIsFatal
? html`<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></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;
}
`; `;
} }
} }

View File

@ -1,16 +1,13 @@
import { mdiCheck, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { css, html, LitElement, TemplateResult } from "lit"; import { 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 ${this._value || this.placeholder
.label=${this.label} ? html`
.placeholder=${this.placeholder} <ha-icon .icon=${this._value || this.placeholder} slot="icon">
.disabled=${this.disabled} </ha-icon>
class="input" `
autocapitalize="none" : this.fallbackPath
autocomplete="off" ? html`<ha-svg-icon
autocorrect="off" .path=${this.fallbackPath}
spellcheck="false" slot="icon"
.errorMessage=${this.errorMessage} ></ha-svg-icon>`
.invalid=${this.invalid} : ""}
> </ha-combo-box>
${this._value || this.placeholder
? html`
<ha-icon .icon=${this._value || this.placeholder} slot="prefix">
</ha-icon>
`
: this.fallbackPath
? html`<ha-svg-icon
.path=${this.fallbackPath}
slot="prefix"
></ha-svg-icon>`
: ""}
<ha-icon-button
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
@ -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);
}
`; `;
} }
} }

View File

@ -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);

View File

@ -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";

View File

@ -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;
} }

View File

@ -1,12 +1,18 @@
import { Radio } from "@material/mwc-radio"; import { RadioBase } from "@material/mwc-radio/mwc-radio-base";
import { styles } from "@material/mwc-radio/mwc-radio.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; 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 {

View File

@ -0,0 +1,47 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { 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>`;
}
static override styles = [styles];
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);
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
}

View File

@ -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 {

View File

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

View File

@ -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;
} }
`; `;

View File

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

View File

@ -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;
} }
} }

View File

@ -0,0 +1,41 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: IconSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-icon": HaIconSelector;
}
}

View File

@ -0,0 +1,264 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { supportsFeature } from "../../common/entity/supports-feature";
import { getSignedPath } from "../../data/auth";
import {
MediaClassBrowserSettings,
MediaPickedEvent,
SUPPORT_BROWSE_MEDIA,
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-form/ha-form";
import type { HaFormSchema } from "../ha-form/types";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
const MANUAL_SCHEMA = [
{ name: "media_content_id", required: false, selector: { text: {} } },
{ name: "media_content_type", required: false, selector: { text: {} } },
];
@customElement("ha-selector-media")
export class HaMediaSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: MediaSelector;
@property({ attribute: false }) public value?: MediaSelectorValue;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state() private _thumbnailUrl?: string | null;
willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("value")) {
const thumbnail = this.value?.metadata?.thumbnail;
const oldThumbnail = (changedProps.get("value") as this["value"])
?.metadata?.thumbnail;
if (thumbnail === oldThumbnail) {
return;
}
if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else {
this._thumbnailUrl = thumbnail;
}
}
}
protected render() {
const stateObj = this.value?.entity_id
? this.hass.states[this.value.entity_id]
: undefined;
const supportsBrowse =
!this.value?.entity_id ||
(stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA));
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value?.entity_id}
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
include-domains='["media_player"]'
allow-custom-entity
@value-changed=${this._entityChanged}
></ha-entity-picker>
${!supportsBrowse
? html`<ha-alert>
${this.hass.localize(
"ui.components.selectors.media.browse_not_supported"
)}
</ha-alert>
<ha-form
.hass=${this.hass}
.data=${this.value}
.schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback}
></ha-form>`
: html`<ha-card
outlined
@click=${this._pickMedia}
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
>
<div
class="thumbnail ${classMap({
portrait:
!!this.value?.metadata?.media_class &&
MediaClassBrowserSettings[
this.value.metadata.children_media_class ||
this.value.metadata.media_class
].thumbnail_ratio === "portrait",
})}"
>
${this.value?.metadata?.thumbnail
? html`
<div
class="${classMap({
"centered-image":
!!this.value.metadata.media_class &&
["app", "directory"].includes(
this.value.metadata.media_class
),
})}
image"
style=${this._thumbnailUrl
? `background-image: url(${this._thumbnailUrl});`
: ""}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${!this.value?.media_content_id
? mdiPlus
: this.value?.metadata?.media_class
? MediaClassBrowserSettings[
this.value.metadata.media_class === "directory"
? this.value.metadata.children_media_class ||
this.value.metadata.media_class
: this.value.metadata.media_class
].icon
: mdiPlayBox}
></ha-svg-icon>
</div>
`}
</div>
<div class="title">
${!this.value?.media_content_id
? this.hass.localize("ui.components.selectors.media.pick_media")
: this.value.metadata?.title || this.value.media_content_id}
</div>
</ha-card>`}`;
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
private _entityChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
entity_id: ev.detail.value,
media_content_id: "",
media_content_type: "",
},
});
}
private _pickMedia() {
showMediaBrowserDialog(this, {
action: "pick",
entityId: this.value!.entity_id!,
navigateIds: this.value!.metadata?.navigateIds,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", {
value: {
...this.value,
media_content_id: pickedMedia.item.media_content_id,
media_content_type: pickedMedia.item.media_content_type,
metadata: {
title: pickedMedia.item.title,
thumbnail: pickedMedia.item.thumbnail,
media_class: pickedMedia.item.media_class,
children_media_class: pickedMedia.item.children_media_class,
navigateIds: pickedMedia.navigateIds?.map((id) => ({
media_content_type: id.media_content_type,
media_content_id: id.media_content_id,
})),
},
},
});
},
});
}
static get styles(): CSSResultGroup {
return css`
ha-entity-picker {
display: block;
margin-bottom: 16px;
}
mwc-button {
margin-top: 8px;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
ha-card {
position: relative;
width: 200px;
box-sizing: border-box;
cursor: pointer;
}
ha-card.disabled {
pointer-events: none;
color: var(--disabled-text-color);
}
ha-card .thumbnail {
width: 100%;
position: relative;
box-sizing: border-box;
transition: padding-bottom 0.1s ease-out;
padding-bottom: 100%;
}
ha-card .thumbnail.portrait {
padding-bottom: 150%;
}
ha-card .image {
border-radius: 3px 3px 0 0;
}
.folder {
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
}
.title {
font-size: 16px;
padding-top: 16px;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 16px;
padding-left: 16px;
padding-right: 4px;
white-space: nowrap;
}
.image {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.centered-image {
margin: 0 8px;
background-size: contain;
}
.icon-holder {
display: flex;
justify-content: center;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-media": HaMediaSelector;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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