Compare commits

..

61 Commits

Author SHA1 Message Date
Bram Kragten
d98e373f64 Bumped version to 20260128.6 2026-02-04 15:41:09 +01:00
Paul Bottein
649516c9fa Change default icon for blank area if not icon configured (#29394) 2026-02-04 15:40:36 +01:00
Paul Bottein
bbc4fb96b2 Load domain translation when integration page load (#29391) 2026-02-04 15:40:35 +01:00
Paul Bottein
0ae639aeb0 Remove old lovelace overview from pickers (#29390) 2026-02-04 15:40:34 +01:00
karwosts
0e7e41065e Don't shrink ha-dropdown checkboxes (#29387) 2026-02-04 15:40:33 +01:00
Paul Bottein
685843f112 Add translations for new overview dialog (#29382) 2026-02-04 15:40:32 +01:00
Paul Bottein
5e1a99d94a Use area icon for area empty state (#29371) 2026-02-04 15:40:31 +01:00
Bram Kragten
d843349865 Bumped version to 20260128.5 2026-02-03 16:58:01 +01:00
Paul Bottein
ec23164aa9 Improve other devices page in home dashboard (#29370) 2026-02-03 16:57:45 +01:00
Paul Bottein
e74ef11101 Hide edit and delete actions for YAML dashboards in config (#29368)
YAML dashboards are defined in configuration files and cannot be
modified or deleted through the UI. This change ensures the edit
and delete actions are only shown for storage-mode dashboards.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:57:43 +01:00
Paul Bottein
a222f6a736 Add missing danger variant in dropdown item (#29359) 2026-02-03 16:57:40 +01:00
Petar Petrov
ef3dd16d45 Move dialog scrim to pseudo-element (#29357) 2026-02-03 16:57:39 +01:00
karwosts
5d4e1d205e Fix missing imports in devtools-statistics (#29355) 2026-02-03 16:57:38 +01:00
Darryn Capes-Davis
1ee5ebbe75 Fix CSS minification issue for ha-card (#29354) 2026-02-03 16:57:37 +01:00
Bram Kragten
59d705aa3d Bumped version to 20260128.4 2026-02-02 17:17:24 +01:00
Paul Bottein
332e108dae Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 17:17:17 +01:00
karwosts
3c15b29d0a Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:17:16 +01:00
Wendelin
130c708e23 Fix dropdown width in datatables (#29340) 2026-02-02 17:17:15 +01:00
Paul Bottein
588a14a8a7 Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 17:17:14 +01:00
Petar Petrov
a1ef6ad266 Remove redundant dialog backdrop color (#29337) 2026-02-02 17:17:13 +01:00
Aidan Timson
a6c1f87730 Ensure template renderer overflows on overflow (#29335) 2026-02-02 17:17:12 +01:00
Wendelin
49252a3808 Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 17:17:11 +01:00
Aidan Timson
c7877fe38f Show hint only if keyboard shortcuts is enabled (#29332)
Enabled by default, must be explicity disabled
2026-02-02 17:17:10 +01:00
Wendelin
e355a61d8f Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 17:17:08 +01:00
Linus Rath
f2e19e51ce Update untracked consumption threshold to 1W (#29310) 2026-02-02 17:17:07 +01:00
karwosts
fd9ab8f561 Use ha-form for condition template (#29301) 2026-02-02 17:17:06 +01:00
Kristel
faa1b3c98f bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 17:17:06 +01:00
Aidan Timson
acc4a84fc9 Fix scrolling for labs page (#29287) 2026-02-02 17:17:05 +01:00
karwosts
4d723dac37 Fix areas cannot be deleted (#29285) 2026-02-02 17:17:03 +01:00
Aidan Timson
f1d4d0ef98 Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-02-02 17:17:02 +01:00
Paul Bottein
88180a2708 Fix demo because of new default panel (#29279) 2026-02-02 17:17:01 +01:00
Aidan Timson
258d87e3d5 Add missing settings nav items for quick search (#29278)
* Add missing repairs quick search item

* Add voice assistants
2026-02-02 17:17:00 +01:00
Wendelin
55f22ba61a Implement fallback for dialog close event in Quick Search (#29260) 2026-02-02 17:16:59 +01:00
Aidan Timson
812f3ca8b9 Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-02-02 17:16:58 +01:00
Marcin Bauer
7f880d11a0 Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 17:16:57 +01:00
Bram Kragten
6b2452c538 Update compress.js 2026-02-02 17:15:44 +01:00
Paul Bottein
c2cbf8bd21 Bumped version to 20260128.3 2026-01-30 10:31:26 +01:00
Wendelin
224bcece9c Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:30:33 +01:00
Wendelin
dc84b7698f Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 10:30:32 +01:00
Wendelin
bc22e6a9bd Fix device download diagnostic via overflow (#29269)
fix diagnostic download link handling to simplify URL signing
2026-01-30 10:30:31 +01:00
Simon Lamon
d44874783a Remove duplicated text (#29265) 2026-01-30 10:30:30 +01:00
Paul Bottein
8d1bb5c867 Fix default lovelace yaml loading (#29240) 2026-01-30 10:30:29 +01:00
Bram Kragten
da1b528eee Bumped version to 20260128.2 2026-01-29 18:04:42 +01:00
Paul Bottein
756138408a Remove default title for new dashboards (#29259) 2026-01-29 18:04:09 +01:00
Paul Bottein
3c8f112565 Prevent action in tile container (#29257) 2026-01-29 18:04:08 +01:00
Paul Bottein
2521f3dde4 Fix actions in dashboard overflow menu (#29256) 2026-01-29 18:04:07 +01:00
TheJulianJES
56390aa01a Fix Matter dashboard using disabled and ignored config entries (#29254) 2026-01-29 17:52:32 +01:00
Paul Bottein
9aac5b19da Stop click propagation when clicking item in icon overflow (#29252) 2026-01-29 17:52:31 +01:00
Wendelin
24afc3dc88 Prevent quick search to close from hot keys (#29251) 2026-01-29 17:52:30 +01:00
Paul Bottein
873c7b2947 Remove unused theme option in distribution card (#29250) 2026-01-29 17:52:29 +01:00
Aidan Timson
648db4276b Add protocols to quick search (#29248)
Add protocols to quick search, extract logic and translations
2026-01-29 17:52:28 +01:00
Aidan Timson
f86c3e7856 Remove unused "app" item from quick search (#29244) 2026-01-29 17:52:27 +01:00
Aidan Timson
1d0251cc28 Fixes for picker combo box scrolling and selection (#29242) 2026-01-29 17:52:26 +01:00
Wendelin
518cf87847 Fix quick search apps (#29238) 2026-01-29 17:52:25 +01:00
ildar170975
81a9216c44 computeGroupEntitiesState(): fix condition (#29234)
* fix condition

* fix condition

* prettier
2026-01-29 17:52:24 +01:00
Paul Bottein
f0e10e0058 Fix default yaml lovelace panel loading (#29230) 2026-01-29 17:52:23 +01:00
Paul Bottein
5df8ea4f07 Add welcome banner for new overview dashboard (#29223) 2026-01-29 17:52:22 +01:00
Aidan Timson
73f081f5cc Add meta+click/enter support to quick search (#29220)
* Allow meta+click event from combobox

* Handle new tab events for navigations

* Add mod+enter support for new tab

* Helper function
2026-01-29 17:52:21 +01:00
Petar Petrov
f0d1db1da6 Add non standard power sensor support (#28845)
* Add non standard power sensor support

* remove useless code

* GridPowerSourceInput type for grid power source saving
2026-01-29 17:52:20 +01:00
Bram Kragten
c658eb414b Bumped version to 20260128.1 2026-01-28 17:52:10 +01:00
Bram Kragten
bac493b72b dont include brotli compression 2026-01-28 17:50:19 +01:00
240 changed files with 4206 additions and 3850 deletions

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:3.14
FROM mcr.microsoft.com/devcontainers/python:1-3.13
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: |
node_modules/.cache/prettier

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11

View File

@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
permissions:

View File

@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -84,7 +84,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
with:
abi: cp314
abi: cp313
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

View File

@@ -100,6 +100,7 @@ class HaLandingPage extends LandingPageBaseElement {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@@ -27,25 +27,25 @@
"type": "module",
"dependencies": {
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.2",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/language": "6.12.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.12",
"@codemirror/view": "6.39.11",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.2.1",
"@formatjs/intl-displaynames": "7.2.1",
"@formatjs/intl-durationformat": "0.10.1",
"@formatjs/intl-getcanonicallocales": "3.2.1",
"@formatjs/intl-listformat": "8.2.1",
"@formatjs/intl-locale": "5.2.1",
"@formatjs/intl-numberformat": "9.2.2",
"@formatjs/intl-pluralrules": "6.2.2",
"@formatjs/intl-relativetimeformat": "12.2.2",
"@formatjs/intl-datetimeformat": "7.2.0",
"@formatjs/intl-displaynames": "7.2.0",
"@formatjs/intl-durationformat": "0.10.0",
"@formatjs/intl-getcanonicallocales": "3.2.0",
"@formatjs/intl-listformat": "8.2.0",
"@formatjs/intl-locale": "5.2.0",
"@formatjs/intl-numberformat": "9.2.1",
"@formatjs/intl-pluralrules": "6.2.1",
"@formatjs/intl-relativetimeformat": "12.2.1",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -71,6 +71,7 @@
"@material/mwc-icon-button": "0.27.0",
"@material/mwc-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
@@ -88,7 +89,7 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vibrant/color": "4.0.4",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -111,7 +112,7 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.1.2",
"intl-messageformat": "11.1.1",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -121,7 +122,7 @@
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
@@ -145,17 +146,17 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.28.6",
"@babel/helper-define-polyfill-provider": "0.6.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.9",
"@lokalise/node-api": "15.6.1",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.6",
"@bundle-stats/plugin-webpack-filter": "4.21.8",
"@lokalise/node-api": "15.6.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@rsdoctor/rspack-plugin": "1.5.0",
"@rspack/core": "1.7.3",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -210,11 +211,11 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.7",
"tar": "7.5.6",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0",
"typescript-eslint": "8.53.1",
"vite-tsconfig-paths": "6.0.5",
"vitest": "4.0.18",
"webpack-stats-plugin": "1.1.3",
@@ -228,7 +229,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.3.0",
"globals": "17.1.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20260128.0"
version = "20260128.6"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
@@ -12,7 +12,7 @@ readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.14.0"
requires-python = ">=3.13.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"

View File

@@ -16,12 +16,6 @@ if [[ -n "$DEVCONTAINER" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default
nvm uninstall "$nodeCurrent"
fi
# install yarn if not already available
if ! command -v yarn &> /dev/null; then
npm install -g corepack
yes | yarn
fi
fi
if ! command -v yarn &> /dev/null; then

View File

@@ -194,6 +194,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@@ -1,15 +1,3 @@
let isViewTransitionDisabled = false;
try {
isViewTransitionDisabled =
window.localStorage.getItem("disableViewTransition") === "true";
} catch {
// ignore
}
export const setViewTransitionDisabled = (disabled: boolean): void => {
isViewTransitionDisabled = disabled;
};
/**
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
*
@@ -26,7 +14,7 @@ export const setViewTransitionDisabled = (disabled: boolean): void => {
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void
): Promise<void> => {
if (!document.startViewTransition || isViewTransitionDisabled) {
if (!document.startViewTransition) {
callback(false);
return Promise.resolve();
}

View File

@@ -19,7 +19,6 @@ import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
@@ -28,7 +27,6 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
@@ -94,18 +92,10 @@ export class HaChartBase extends LitElement {
private _resizeAnimationDuration?: number;
private _suspendResize = false;
private _layoutTransitionActive = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (this._suspendResize) {
this._shouldResizeChart = true;
return;
}
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
@@ -123,11 +113,8 @@ export class HaChartBase extends LitElement {
private _originalZrFlush?: () => void;
private _pendingSetup = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
}
@@ -139,13 +126,7 @@ export class HaChartBase extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._pendingSetup = true;
afterNextRender(() => {
if (this.isConnected && this._pendingSetup) {
this._pendingSetup = false;
this._setupChart();
}
});
this._setupChart();
}
this._listeners.push(
@@ -200,26 +181,6 @@ export class HaChartBase extends LitElement {
() => window.removeEventListener("keyup", handleKeyUp)
);
}
const handleLayoutTransition: EventListener = (ev) => {
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
this._layoutTransitionActive = Boolean(event.detail?.active);
this.toggleAttribute(
"layout-transition-active",
this._layoutTransitionActive
);
this._suspendResize = this._layoutTransitionActive;
if (!this._suspendResize) {
this._resizeChartIfNeeded();
}
};
window.addEventListener("hass-layout-transition", handleLayoutTransition);
this._listeners.push(() =>
window.removeEventListener(
"hass-layout-transition",
handleLayoutTransition
)
);
}
protected firstUpdated() {
@@ -1027,29 +988,19 @@ export class HaChartBase extends LitElement {
}
private _handleChartRenderFinished = () => {
this._resizeChartIfNeeded();
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
};
private _resizeChartIfNeeded() {
if (!this.chart || !this._shouldResizeChart) {
return;
}
if (this._suspendResize) {
return;
}
if (!this.chart.getZr().animation.isFinished()) {
return;
}
this.chart.resize({
animation:
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
@@ -1071,18 +1022,11 @@ export class HaChartBase extends LitElement {
display: block;
position: relative;
letter-spacing: normal;
overflow: visible;
}
:host([layout-transition-active]),
:host([layout-transition-active]) .container,
:host([layout-transition-active]) .chart-container {
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
position: relative;
overflow: visible;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
@@ -1090,7 +1034,6 @@ export class HaChartBase extends LitElement {
.chart-container {
width: 100%;
max-height: var(--chart-max-height, 350px);
overflow: visible;
}
.has-height .chart-container {
flex: 1;

View File

@@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ attribute: false }) public valueFormatter?: (
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
@property({ attribute: false }) public data?: SunburstNode;
@property({ attribute: false }) public valueFormatter?: (
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@@ -716,18 +716,6 @@ export class StateHistoryChartLine extends LitElement {
// Add an entry for final values
pushData(endTime, prevValues);
// For sensors, append current state if viewing recent data
const now = new Date();
// allow 1s of leeway for "now"
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
if (domain === "sensor" && isUpToNow && data.length === 1) {
const stateObj = this.hass.states[states.entity_id];
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
if (currentValue !== null) {
data[0].data!.push([now, currentValue]);
}
}
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});

View File

@@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: "hide-reset-button", type: Boolean })
public hideResetButton?: boolean;

View File

@@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ attribute: false }) public hoursToShow?: number;
@property({ attribute: false, type: Number }) public hoursToShow?: number;
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
@@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;

View File

@@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false })
@property({ attribute: false, type: Array })
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@@ -605,57 +605,6 @@ export class StatisticsChart extends LitElement {
}
});
// Append current state if viewing recent data
const now = new Date();
// allow 10m of leeway for "now", because stats are 5 minute aggregated
const isUpToNow = now.getTime() - endTime.getTime() <= 600000;
if (isUpToNow) {
// Skip external statistics (they have ":" in the ID)
if (!statistic_id.includes(":")) {
const stateObj = this.hass.states[statistic_id];
if (stateObj) {
const currentValue = parseFloat(stateObj.state);
if (
isFinite(currentValue) &&
!this._hiddenStats.has(statistic_id)
) {
// First, close out the last stat segment at prevEndTime
const lastEndTime = prevEndTime;
const lastValues = prevValues;
if (lastEndTime && lastValues) {
statDataSets.forEach((d, i) => {
d.data!.push(
this._transformDataValue([lastEndTime, ...lastValues[i]!])
);
});
}
// Then push the current state at now
statTypes.forEach((type, i) => {
const val: (number | null)[] = [];
if (type === "sum" || type === "change") {
// Skip cumulative types - need special calculation
val.push(null);
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands &&
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
) {
// For band chart, current value is both min and max, so diff is 0
val.push(0);
val.push(currentValue);
} else {
val.push(currentValue);
}
statDataSets[i].data!.push(
this._transformDataValue([now, ...val])
);
});
}
}
}
}
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
Array.prototype.push.apply(legendData, statLegendData);

View File

@@ -130,9 +130,9 @@ export class HaDataTable extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property({ type: String }) public id = "id";
@property({ attribute: false }) public noDataText?: string;
@property({ attribute: false, type: String }) public noDataText?: string;
@property({ attribute: false }) public searchLabel?: string;
@property({ attribute: false, type: String }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;

View File

@@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show only devices with entities from specific domains.

View File

@@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ attribute: false }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ type: Boolean })
public reorder = false;

View File

@@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show entities from specific domains.

View File

@@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false })
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ attribute: false }) public helpMissingEntityUrl =

View File

@@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ attribute: false }) public statisticIds?: string[];
@property({ attribute: false, type: Array }) public statisticIds?: string[];
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";

View File

@@ -48,6 +48,7 @@ export class HaAnsiToHtml extends LitElement {
static styles = css`
pre {
overflow-x: auto;
margin: 0;
}
pre.wrap {

View File

@@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
@property({ type: Boolean, attribute: "disable-speech" })
public disableSpeech = false;
@property({ attribute: false })
@property({ type: Boolean, attribute: false })
public startListening?: boolean;
@query("#message-input") private _messageInput!: HaTextField;

View File

@@ -2,12 +2,14 @@ import type { PropertyValueMap } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import type { AssistPipeline } from "../data/assist_pipeline";
import { listAssistPipelines } from "../data/assist_pipeline";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
const PREFERRED = "preferred";
const LAST_USED = "last_used";
@@ -39,31 +41,6 @@ export class HaAssistPipelinePicker extends LitElement {
return nothing;
}
const value = this.value ?? this._default;
const options: HaSelectOption[] = [
{
value: PREFERRED,
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
}),
},
];
if (this.includeLastUsed) {
options.unshift({
value: LAST_USED,
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
});
}
options.push(
...this._pipelines.map((pipeline) => ({
value: pipeline.id,
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
}))
);
return html`
<ha-select
.label=${this.label ||
@@ -72,8 +49,33 @@ export class HaAssistPipelinePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.includeLastUsed
? html`
<ha-list-item .value=${LAST_USED}>
${this.hass!.localize(
"ui.components.pipeline-picker.last_used"
)}
</ha-list-item>
`
: null}
<ha-list-item .value=${PREFERRED}>
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
})}
</ha-list-item>
${this._pipelines.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline.id}>
${pipeline.name}
(${formatLanguageCode(pipeline.language, this.hass.locale)})
</ha-list-item>`
)}
</ha-select>
`;
}
@@ -94,17 +96,17 @@ export class HaAssistPipelinePicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === this._default)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === this._default)
) {
return;
}
this.value = value === this._default ? undefined : value;
this.value = target.value === this._default ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -4,8 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-list-item";
import "./ha-select";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -258,10 +260,14 @@ export class HaBaseTimeInput extends LitElement {
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
.name=${"amPm"}
name="amPm"
naturalMenuWidth
fixedMenuPosition
@selected=${this._valueChanged}
.options=${["AM", "PM"]}
@closed=${stopPropagation}
>
<ha-list-item value="AM">AM</ha-list-item>
<ha-list-item value="PM">PM</ha-list-item>
</ha-select>`}
</div>
${this.helper
@@ -276,12 +282,10 @@ export class HaBaseTimeInput extends LitElement {
fireEvent(this, "value-changed");
}
private _valueChanged(ev: InputEvent | CustomEvent<{ value: string }>): void {
private _valueChanged(ev: InputEvent) {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm"
? (ev as CustomEvent<{ value: string }>).detail.value
: Number(textField.value);
textField.name === "amPm" ? textField.value : Number(textField.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
@@ -362,6 +366,10 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
ha-select {
--mdc-shape-small: 0;
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}

View File

@@ -2,10 +2,12 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare";
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
import { fetchBlueprints } from "../data/blueprint";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@customElement("ha-blueprint-picker")
@@ -53,16 +55,20 @@ class HaBluePrintPicker extends LitElement {
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
.disabled=${this.disabled}
@selected=${this._blueprintChanged}
.options=${this._processedBlueprints(this.blueprints).map(
(blueprint) => ({
value: blueprint.path,
label: blueprint.name,
})
)}
@closed=${stopPropagation}
>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<ha-list-item .value=${blueprint.path}>
${blueprint.name}
</ha-list-item>
`
)}
</ha-select>
`;
}
@@ -76,8 +82,8 @@ class HaBluePrintPicker extends LitElement {
}
}
private _blueprintChanged(ev: CustomEvent<{ value: string }>) {
const newValue = ev.detail.value;
private _blueprintChanged(ev) {
const newValue = ev.target.value;
if (newValue !== this.value) {
this.value = newValue;

View File

@@ -1,31 +1,24 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import type { PropertyValues } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon";
export interface SelectOption {
label: string;
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
import type { HaSvgIcon } from "./ha-svg-icon";
import "./ha-menu";
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
export class HaControlSelectMenu extends SelectBase {
@query(".select") protected mdcRoot!: HTMLElement;
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -33,83 +26,95 @@ export class HaControlSelectMenu extends LitElement {
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false;
@property({ type: Boolean })
public disabled = false;
@property() public options;
@property({ type: Boolean })
public required = false;
@property()
public label?: string;
@property()
public value?: string;
@property({ attribute: false }) public options: SelectOption[] = [];
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
if (this.disabled) {
return this._renderTrigger();
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
return html`
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
${this._renderTrigger()} ${this.options.map(this._renderOption)}
</ha-dropdown>
`;
}
private _renderTrigger() {
const selectedText = this.getValueObject(this.options, this.value)?.label;
public override render() {
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-no-value": !selectedText,
"select-invalid": !this.isUiValid,
"select-no-value": !this.selectedText,
};
return html`<button
?disabled=${this.disabled}
slot="trigger"
class="select-anchor ${classMap(classes)}"
>
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${selectedText ? html`<p class="value">${selectedText}</p>` : nothing}
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
const labelAttribute =
this.label && this.hideLabel ? this.label : undefined;
return html`
<div class="select ${classMap(classes)}">
<input
class="formElement"
.name=${this.name}
.value=${this.value}
hidden
?disabled=${this.disabled}
?required=${this.required}
/>
<!-- @ts-ignore -->
<div
class="select-anchor"
aria-autocomplete="none"
role="combobox"
aria-expanded=${this.menuOpen}
aria-invalid=${!this.isUiValid}
aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required}
aria-controls="listbox"
@focus=${this.onFocus}
@blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown}
>
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this._renderArrow()}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</div>
${this.renderMenu()}
</div>
${this._renderArrow()}
</button>`;
`;
}
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
class=${this.value === option.value ? "selected" : ""}
>${option.iconPath
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: nothing}
${option.label}</ha-dropdown-item
>`;
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
>
${this.renderMenuContent()}
</ha-menu>`;
}
private _renderArrow() {
if (!this.showArrow) {
return nothing;
}
if (!this.showArrow) return nothing;
return html`
<div class="icon">
@@ -119,42 +124,47 @@ export class HaControlSelectMenu extends LitElement {
}
private _renderIcon() {
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;
const defaultIcon = this.querySelector("[slot='icon']");
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
| HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html`
<div class="icon">
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
${icon && icon.localName === "ha-svg-icon" && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && icon.localName === "ha-icon" && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
</div>
`;
}
private _showDropdown() {
this.style.setProperty(
"--control-select-menu-width",
`${this._triggerButton.offsetWidth}px`
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private getValueObject = memoizeOne(
(options: SelectOption[], value?: string) =>
options.find((option) => option.value === value)
);
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
static override styles = [
css`
@@ -176,8 +186,6 @@ export class HaControlSelectMenu extends LitElement {
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;
@@ -203,12 +211,6 @@ export class HaControlSelectMenu extends LitElement {
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.25px;
}
.select-anchor:hover {
--control-select-menu-background-color: var(
--ha-color-on-neutral-quiet
);
}
.content {
display: flex;
flex-direction: column;
@@ -228,19 +230,16 @@ export class HaControlSelectMenu extends LitElement {
}
.label {
font-size: var(--ha-font-size-s);
font-size: 0.85em;
letter-spacing: 0.4px;
}
.select-no-value .label {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
.content .value {
font-size: var(--ha-font-size-m);
}
.select-anchor:focus-visible {
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
}
@@ -259,23 +258,10 @@ export class HaControlSelectMenu extends LitElement {
opacity: var(--control-select-menu-background-opacity);
}
.select-disabled.select-anchor {
.select-disabled .select-anchor {
cursor: not-allowed;
color: var(--disabled-color);
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown::part(menu) {
min-width: var(--control-select-menu-width);
}
`,
];
}

View File

@@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement {
@property({ type: Boolean }) public checked = false;
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
@property({ attribute: false }) pathOn?: string;
@property({ attribute: false, type: String }) pathOn?: string;
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ attribute: false }) pathOff?: string;
@property({ attribute: false, type: String }) pathOff?: string;
@property({ type: String })
public label?: string;

View File

@@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
@@ -13,8 +14,9 @@ import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -71,35 +73,37 @@ export class HaConversationAgentPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._agents.map((agent) => ({
value: agent.id,
label: agent.name,
disabled:
agent.supported_languages !== "*" &&
agent.supported_languages.length === 0,
}));
if (!this.required) {
options.unshift({
value: NONE,
label: this.hass.localize(
"ui.components.conversation-agent-picker.none"
),
});
}
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.conversation-agent-picker.conversation_agent"
"ui.components.coversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
></ha-select
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}</ha-select
>${(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
@@ -234,17 +238,17 @@ export class HaConversationAgentPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._agents!.find((agent) => agent.id === this.value)

View File

@@ -4,18 +4,6 @@ import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@@ -27,30 +15,6 @@ export class HaDrawer extends DrawerBase {
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = true;
fireEvent(window, "hass-layout-transition", {
active: true,
reason: "sidebar",
});
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = false;
fireEvent(window, "hass-layout-transition", {
active: false,
reason: "sidebar",
});
};
protected createAdapter() {
return {
...super.createAdapter(),
@@ -99,38 +63,6 @@ export class HaDrawer extends DrawerBase {
}
}
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
@@ -158,16 +90,6 @@ export class HaDrawer extends DrawerBase {
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
@@ -181,15 +103,6 @@ export class HaDrawer extends DrawerBase {
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
}
}
`,
];

View File

@@ -1,9 +1,9 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
import { css, type CSSResultGroup, html } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
/**
* Home Assistant dropdown item component

View File

@@ -1,9 +1,6 @@
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaDropdownItem } from "./ha-dropdown-item";
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
/**
* Home Assistant dropdown component

View File

@@ -31,7 +31,6 @@ import "./ha-expansion-panel";
import "./ha-icon";
import "./ha-list";
import "./ha-list-item";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
@customElement("ha-filter-categories")
export class HaFilterCategories extends SubscribeMixin(LitElement) {
@@ -175,7 +174,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
}
private _handleAction(ev: HaDropdownSelectEvent) {
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
const categoryId = (ev.currentTarget as any).categoryId;
const action = ev.detail.item.value;
switch (action) {

View File

@@ -11,7 +11,8 @@ import "../ha-formfield";
import "../ha-icon-button";
import "../ha-picker-field";
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
import type { HaDropdown } from "../ha-dropdown";
import type { HaDropdownItem } from "../ha-dropdown-item";
import type {
HaFormElement,
HaFormMultiSelectData,
@@ -107,7 +108,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
`;
}
protected _toggleItem(ev: HaDropdownSelectEvent) {
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.preventDefault(); // keep the dropdown open
const value = ev.detail.item.value;
const action = (ev.detail.item as any).action;

View File

@@ -17,7 +17,6 @@ import type {
HaFormOptionalActionsSchema,
HaFormSchema,
} from "./types";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
const NO_ACTIONS = [];
@@ -143,7 +142,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
`;
}
private _handleAddAction(ev: HaDropdownSelectEvent) {
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
const action = ev.detail.item.value;
this._displayActions = [...(this._displayActions ?? []), action];
}

View File

@@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
@property({ attribute: false })
public formatOptions?: Intl.NumberFormatOptions;
@property({ attribute: false }) public valueText?: string;
@property({ attribute: false, type: String }) public valueText?: string;
@property({ attribute: false }) public locale!: FrontendLocaleData;

View File

@@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
section?: string
) => (PickerComboBoxItem | string)[] | undefined;
@property({ attribute: false })
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })

View File

@@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ attribute: false })
@property({ type: Number, attribute: false })
public renderSize?: number;
@property({ type: Array })
public value?: [number, number];
@property({ attribute: false })
@property({ attribute: false, type: Number })
public colorBrightness?: number;
@property({ type: Number })
@@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
@property({ type: Number })
public ww?: number;
@property({ attribute: false })
@property({ attribute: false, type: Number })
public minKelvin?: number;
@property({ attribute: false })
@property({ attribute: false, type: Number })
public maxKelvin?: number;
@query("#canvas") private _canvas!: HTMLCanvasElement;

View File

@@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
@customElement("ha-icon-button-toolbar")
export class HaIconButtonToolbar extends LitElement {
@property({ attribute: false })
@property({ type: Array, attribute: false })
public items: (HaIconButtonToolbarItem | string)[] = [];
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];

View File

@@ -107,6 +107,9 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;

View File

@@ -134,50 +134,15 @@ export class HaMarkdown extends LitElement {
}
table[role="presentation"] {
--markdown-table-border-collapse: separate;
--markdown-table-border-width: 0;
--markdown-table-border-width: attr(border, 0);
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th,
td {
vertical-align: middle;
}
}
table[role="presentation"] td[valign="top"],
table[role="presentation"] th[valign="top"] {
vertical-align: top;
}
table[role="presentation"] td[valign="middle"],
table[role="presentation"] th[valign="middle"] {
vertical-align: middle;
}
table[role="presentation"] td[valign="bottom"],
table[role="presentation"] th[valign="bottom"] {
vertical-align: bottom;
}
table[role="presentation"] td[valign="baseline"],
table[role="presentation"] th[valign="baseline"] {
vertical-align: baseline;
}
@supports (border-width: attr(border px, 0)) {
table[role="presentation"] {
--markdown-table-border-width: attr(border px, 0);
}
table[role="presentation"] th,
table[role="presentation"] td {
th {
vertical-align: attr(valign, middle);
}
td {
vertical-align: attr(valign, middle);
}
}
table[role="presentation"][border="0"] {
--markdown-table-border-width: 0;
}
table[role="presentation"][border="1"] {
--markdown-table-border-width: 1px;
}
table[role="presentation"][border="2"] {
--markdown-table-border-width: 2px;
}
table[role="presentation"][border="3"] {
--markdown-table-border-width: 3px;
}
table {
border-collapse: var(--markdown-table-border-collapse, collapse);

45
src/components/ha-menu.ts Normal file
View File

@@ -0,0 +1,45 @@
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
import { styles } from "@material/mwc-menu/mwc-menu.css";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-list";
@customElement("ha-menu")
export class HaMenu extends MenuBase {
protected get listElement() {
if (!this.listElement_) {
this.listElement_ = this.renderRoot.querySelector("ha-list");
return this.listElement_;
}
return this.listElement_;
}
protected renderList() {
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
const classes = this.renderListClasses();
return html`<ha-list
rootTabbable
.innerAriaLabel=${this.innerAriaLabel}
.innerRole=${this.innerRole}
.multi=${this.multi}
class=${classMap(classes)}
.itemRoles=${itemRoles}
.wrapFocus=${this.wrapFocus}
.activatable=${this.activatable}
@action=${this.onAction}
>
<slot></slot>
</ha-list>`;
}
static styles = styles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-menu": HaMenu;
}
}

View File

@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { SupervisorMounts } from "../data/supervisor/mounts";
import {
@@ -16,7 +17,7 @@ import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -51,54 +52,60 @@ class HaMountPicker extends LitElement {
if (!this._mounts) {
return nothing;
}
const options: HaSelectOption[] = this._filterMounts(
this._mounts,
this.usage
).map((mount) => ({
value: mount.name,
label: mount.name,
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
}`,
iconPath:
mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore,
}));
if (this.usage === SupervisorMountUsage.BACKUP) {
const dataDiskOption = {
value: _BACKUP_DATA_DISK_,
iconPath: mdiHarddisk,
label:
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup",
};
if (
!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
) {
options.unshift(dataDiskOption);
} else {
options.push(dataDiskOption);
}
}
const dataDiskOption = html`<ha-list-item
graphic="icon"
.value=${_BACKUP_DATA_DISK_}
>
<span>
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup"}
</span>
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
</ha-list-item>`;
return html`
<ha-select
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.mount-picker.mount")
: this.label}
.value=${this.value}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
@selected=${this._mountChanged}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.usage === SupervisorMountUsage.BACKUP &&
(!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
? dataDiskOption
: nothing}
${this._filterMounts(this._mounts, this.usage).map(
(mount) =>
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
<span>${mount.name}</span>
<span slot="secondary"
>${mount.server}${mount.port
? `:${mount.port}`
: nothing}${mount.type === SupervisorMountType.NFS
? mount.path
: `:${mount.share}`}</span
>
<ha-svg-icon
slot="graphic"
.path=${mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore}
></ha-svg-icon>
</ha-list-item>`
)}
${this.usage === SupervisorMountUsage.BACKUP &&
this._mounts.default_backup_mount
? dataDiskOption
: nothing}
</ha-select>
`;
}
@@ -146,10 +153,16 @@ class HaMountPicker extends LitElement {
}
}
private _mountChanged(ev: CustomEvent<{ value: string }>) {
const newValue = ev.detail.value;
private get _value() {
return this.value || "";
}
if (newValue !== this.value) {
private _mountChanged(ev: Event) {
ev.stopPropagation();
const target = ev.target as HaSelect;
const newValue = target.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}

View File

@@ -11,7 +11,6 @@ import { fetchConfig } from "../data/lovelace/config/types";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import { findRelated, type RelatedResult } from "../data/search";
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
@@ -26,9 +25,8 @@ import {
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
const RELATED_SORT_PREFIX = {
area_view: "0_area_view",
area: "1_area_settings",
device: "2_device",
area: "0_area",
device: "1_device",
} as const;
interface NavigationItem extends PickerComboBoxItem {
@@ -434,31 +432,10 @@ export class HaNavigationPicker extends LitElement {
for (const areaId of relatedAreaIds) {
const area = this.hass.areas[areaId];
const primary = area?.name ?? areaId;
// Area dashboard view
const viewPath = `/home/${computeAreaPath(areaId)}`;
relatedItems.push({
id: viewPath,
primary,
secondary: viewPath,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,
sorting_label: createSortingLabel(
RELATED_SORT_PREFIX.area_view,
primary,
viewPath
),
group: "related",
});
// Area settings
const path = `/config/areas/area/${areaId}`;
relatedItems.push({
id: path,
primary: this.hass.localize(
"ui.components.navigation-picker.area_settings",
{ area: primary }
),
primary,
secondary: path,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,

View File

@@ -89,7 +89,7 @@ export class HaPasswordField extends LitElement {
@property({ type: Boolean }) readOnly = false;
// eslint-disable-next-line lit/no-native-attributes
@property({ attribute: false }) autocapitalize = "";
@property({ attribute: false, type: String }) autocapitalize = "";
@state() private _unmaskedPassword = false;

View File

@@ -119,7 +119,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
section?: string
) => PickerComboBoxItem[] | undefined;
@property({ attribute: false })
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })

View File

@@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
@property({ type: Number })
public margin = 4;
@property({ attribute: false })
@property({ attribute: false, type: Number })
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@property({ attribute: "center-image" }) public centerImage?: string;

View File

@@ -14,8 +14,8 @@ import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-dropdown";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
import "./ha-dropdown-item";
import type { HaDropdownItem } from "./ha-dropdown-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -259,7 +259,7 @@ class HaQrScanner extends LitElement {
this._qrCodeScanned(this._manualInput!.value);
}
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const cameraId = ev.detail?.item?.value;
if (cameraId) {
this._selectedCamera = cameraId;

View File

@@ -1,209 +1,187 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
export interface HaSelectOption {
value: string;
label?: string;
secondary?: string;
iconPath?: string;
disabled?: boolean;
}
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
import "./ha-menu";
@customElement("ha-select")
export class HaSelect extends LitElement {
@property({ type: Boolean }) public clearable = false;
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon = false;
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
@property({ type: Boolean, reflect: true }) public clearable = false;
@property() public label?: string;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@property() public helper?: string;
@property() public value?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public disabled = false;
@state() private _opened = false;
@query("ha-picker-field") private _triggerField!: HaPickerField;
private _getValueLabel = memoizeOne(
(
options: HaSelectOption[] | string[] | undefined,
value: string | undefined
) => {
if (!options || !value) {
return value;
}
for (const option of options) {
if (
(typeof option === "string" && option === value) ||
(typeof option !== "string" && option.value === value)
) {
return typeof option === "string"
? option
: option.label || option.value;
}
}
return value;
}
);
@property() public options;
protected override render() {
if (this.disabled) {
return this._renderField();
}
return html`
<ha-dropdown
placement="bottom"
@wa-select=${this._handleSelect}
@wa-show=${this._handleShow}
@wa-hide=${this._handleHide}
>
${this._renderField()}
${this.options
? this.options.map(
(option) => html`
<ha-dropdown-item
.value=${typeof option === "string" ? option : option.value}
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
>
${option.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${option.iconPath}
></ha-svg-icon>`
: nothing}
<div class="content">
${typeof option === "string"
? option
: option.label || option.value}
${option.secondary
? html`<div class="secondary">${option.secondary}</div>`
: nothing}
</div>
</ha-dropdown-item>
`
)
: html`<slot></slot>`}
</ha-dropdown>
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
`;
}
private _renderField() {
const valueLabel = this._getValueLabel(this.options, this.value);
return html`
<ha-picker-field
slot="trigger"
type="button"
class=${this._opened ? "opened" : ""}
compact
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${!this.clearable ||
this.required ||
this.disabled ||
!this.value}
>
</ha-picker-field>
`;
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
>
${this.renderMenuContent()}
</ha-menu>`;
}
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
if (value === this.value) {
return;
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
}
fireEvent(this, "selected", { value });
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
protected async firstUpdated() {
super.firstUpdated();
if (this.inlineArrow) {
this.shadowRoot
?.querySelector(".mdc-select__selected-text-container")
?.classList.add("inline-arrow");
}
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("inlineArrow")) {
const textContainerElement = this.shadowRoot?.querySelector(
".mdc-select__selected-text-container"
);
if (this.inlineArrow) {
textContainerElement?.classList.add("inline-arrow");
} else {
textContainerElement?.classList.remove("inline-arrow");
}
}
if (changedProperties.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
fireEvent(this, "selected", { value: undefined });
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
}
private _handleShow() {
this.style.setProperty(
"--select-menu-width",
`${this._triggerField.offsetWidth}px`
);
this._opened = true;
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private _handleHide() {
this._opened = false;
}
static styles = css`
:host {
position: relative;
}
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
flex-direction: column;
}
ha-dropdown-item .secondary {
font-size: var(--ha-font-size-s);
color: var(--ha-color-text-secondary);
}
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
:host ::slotted(ha-dropdown-item.selected),
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
static override styles = [
styles,
css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: var(--ha-space-4);
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: var(--ha-space-4);
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
}
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(
--select-selected-text-padding-end,
var(--ha-space-4)
);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
.inline-arrow {
flex-grow: 0;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
interface HASSDomEvents {
selected: { value: string | undefined };
}
}

View File

@@ -5,16 +5,17 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
import "../ha-dropdown-item";
import "../ha-formfield";
import "../ha-generic-picker";
import "../ha-input-helper-text";
import "../ha-list-item";
import "../ha-radio";
import "../ha-select";
import "../ha-select-box";
@@ -230,15 +231,24 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label ?? ""}
.value=${(this.value as string) ?? ""}
.value=${this.value ?? ""}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
clearable
@closed=${stopPropagation}
@selected=${this._valueChanged}
.options=${options}
>
${options.map(
(item: SelectOption) => html`
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
>${item.label}</ha-list-item
>
`
)}
</ha-select>
`;
}
@@ -285,7 +295,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
if (ev.detail?.value === undefined && this.value !== undefined) {
if (ev.detail?.index === -1 && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
@@ -375,7 +385,7 @@ export class HaSelectSelector extends LitElement {
ha-formfield {
display: block;
}
ha-dropdown-item[disabled] {
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {

View File

@@ -108,7 +108,6 @@ export class HaSettingsRow extends LitElement {
white-space: normal;
}
.prefix-wrap {
flex: 1;
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {

View File

@@ -492,22 +492,19 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="start">
${this._updatesCount + this._issuesCount}
</span>
`
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="start"
>${this._updatesCount + this._issuesCount}</span
>`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>`
: nothing}
</ha-md-list-item>
`;
@@ -527,15 +524,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
type="button"
>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
${notificationCount > 0
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
${!this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="start">${notificationCount}</span>`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${notificationCount > 0
${this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: nothing}
</ha-md-list-item>
@@ -744,8 +739,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
font-size: var(--ha-font-size-xl);
align-items: center;
overflow: hidden;
width: calc(56px + var(--safe-area-inset-left, 0px));
padding-left: calc(
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
);
@@ -754,7 +747,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
padding-inline-end: initial;
padding-top: var(--safe-area-inset-top, 0px);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) .menu {
width: calc(256px + var(--safe-area-inset-left, 0px));
@@ -769,22 +761,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
margin-left: 3px;
margin-inline-start: 3px;
margin-inline-end: initial;
flex: 1;
min-width: 0;
max-width: 0;
opacity: 0;
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
width: 100%;
display: none;
}
:host([narrow]) .title {
margin: 0;
padding: 0 var(--ha-space-4);
}
:host([expanded]) .title {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: initial;
}
.panels-list {
@@ -842,7 +827,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--ha-space-3);
--md-list-item-leading-icon-size: var(--ha-space-6);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item {
width: 248px;
@@ -883,22 +867,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
}
ha-md-list-item .item-text {
display: block;
max-width: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: none;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item .item-text {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -916,9 +889,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
background-color: var(--accent-color);
padding: 2px 6px;
color: var(--text-accent-color, var(--text-primary-color));
transition:
opacity var(--ha-animation-duration-normal) ease,
transform var(--ha-animation-duration-normal) ease;
}
ha-svg-icon + .badge {
@@ -930,12 +900,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
line-height: var(--ha-line-height-expanded);
padding: 0 var(--ha-space-1);
}
:host([expanded]) .badge[slot="start"],
:host(:not([expanded])) .badge[slot="end"] {
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: var(--ha-space-10);
@@ -974,15 +938,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@media (prefers-reduced-motion: reduce) {
.menu,
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
}
}
`,
];
}

View File

@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
@@ -59,30 +61,6 @@ export class HaSTTPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.stt-picker.none") || "None",
});
}
return html`
<ha-select
.label=${this.label ||
@@ -91,8 +69,33 @@ export class HaSTTPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@@ -141,17 +144,17 @@ export class HaSTTPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)

View File

@@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public compact = false;
@property({ attribute: false }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show only targets with entities from specific domains.

View File

@@ -1,10 +1,11 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, nothing, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { HomeAssistant } from "../types";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import "./ha-list-item";
const DEFAULT_THEME = "default";
@@ -24,26 +25,6 @@ export class HaThemePicker extends LitElement {
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
const options: HaSelectOption[] = Object.keys(
this.hass?.themes.themes || {}
).map((theme) => ({
value: theme,
}));
if (this.includeDefault) {
options.unshift({
value: DEFAULT_THEME,
label: "Home Assistant",
});
}
if (!this.required) {
options.unshift({
value: "remove",
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -52,8 +33,31 @@ export class HaThemePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
></ha-select>
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`
<ha-list-item value="remove">
${this.hass!.localize("ui.components.theme-picker.no_theme")}
</ha-list-item>
`
: nothing}
${this.includeDefault
? html`
<ha-list-item .value=${DEFAULT_THEME}>
Home Assistant
</ha-list-item>
`
: nothing}
${Object.keys(this.hass!.themes.themes)
.sort()
.map(
(theme) =>
html`<ha-list-item .value=${theme}>${theme}</ha-list-item>`
)}
</ha-select>
`;
}
@@ -63,11 +67,11 @@ export class HaThemePicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
if (!this.hass || ev.detail.value === "") {
private _changed(ev): void {
if (!this.hass || ev.target.value === "") {
return;
}
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -36,19 +36,10 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar__title {
font-size: var(--ha-font-size-xl);
padding-inline-start: var(--ha-space-6);

View File

@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
@@ -59,30 +61,6 @@ export class HaTTSPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id === value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (!this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.tts-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -91,8 +69,33 @@ export class HaTTSPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@@ -141,17 +144,17 @@ export class HaTTSPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)

View File

@@ -1,13 +1,15 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { TTSVoice } from "../data/tts";
import { listTTSVoices } from "../data/tts";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -29,25 +31,14 @@ export class HaTTSVoicePicker extends LitElement {
@state() _voices?: TTSVoice[] | null;
@query("ha-select") private _select?: HaSelect;
protected render() {
if (!this._voices) {
return nothing;
}
const value =
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
const options: HaSelectOption[] = (this._voices || []).map((voice) => ({
value: voice.voice_id,
label: voice.name,
}));
if (!this.required || !this.value) {
options.unshift({
value: NONE,
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -56,8 +47,21 @@ export class HaTTSVoicePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-voice-picker.none")}
</ha-list-item>`
: nothing}
${this._voices.map(
(voice) =>
html`<ha-list-item .value=${voice.voice_id}>
${voice.name}
</ha-list-item>`
)}
</ha-select>
`;
}
@@ -98,25 +102,34 @@ export class HaTTSVoicePicker extends LitElement {
}
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (
changedProperties.has("_voices") &&
this._select?.value !== this.value
) {
this._select?.layoutOptions();
fireEvent(this, "value-changed", { value: this._select?.value });
}
}
static styles = css`
ha-select {
width: 100%;
text-align: start;
display: block;
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -48,7 +48,7 @@ export class TopAppBarBaseBase extends BaseElement {
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@property({ attribute: false })
@property({ attribute: false, type: Object })
get scrollTarget() {
return this._scrollTarget || window;
}
@@ -288,19 +288,10 @@ export class TopAppBarBaseBase extends BaseElement {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}

View File

@@ -21,8 +21,8 @@ import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog-header";
import "../ha-dropdown";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import type { HaDropdownItem } from "../ha-dropdown-item";
import "../ha-icon-button-arrow-prev";
import "../ha-wa-dialog";
import "./ha-media-manage-button";
@@ -177,7 +177,7 @@ class DialogMediaPlayerBrowse extends LitElement {
this.classList.add("opened");
}
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
private async _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
switch (action) {
case "auto":

View File

@@ -15,7 +15,7 @@ export class HaTileContainer extends LitElement {
@property({ type: Boolean })
public vertical = false;
@property({ attribute: false })
@property({ type: Boolean, attribute: false })
public interactive = false;
@property({ attribute: false })

View File

@@ -53,7 +53,6 @@ export type ClimateEntity = HassEntityBase & {
current_humidity?: number;
target_humidity_low?: number;
target_humidity_high?: number;
target_humidity_step?: number;
min_humidity?: number;
max_humidity?: number;
fan_mode?: string;

View File

@@ -35,7 +35,7 @@ export const deserializeFilters = (value: DataTableFilters) => {
// returns true when this filter has *selected* options and the filter's name
// equals the given filterName
export const isFilterUsed = (
export const isUsedFilter = (
key: string,
filter: DataTableFilter,
filterName: string
@@ -49,7 +49,7 @@ export const isFilterUsed = (
// which has resulted in a list of items that match these selected opions
// (this list can be empty),
// and the filter's name equals (one of) the given filterName(s)
export const isRelatedItemsFilterUsed = (
export const isUsedRelatedItemsFilter = (
key: string,
filter: DataTableFilter,
filterName: string | string[]

View File

@@ -511,15 +511,6 @@ const getEnergyData = async (
"mean",
])
: {};
// If power stats 5 minute data is selected, then also fetch hourly data which
// will be used to back-fill any missing data points in the 5 minute data when
// the requested range is beyond the limit of short term statistics.
const _powerStatsHour: Statistics | Promise<Statistics> =
powerStatIds.length && finePeriod === "5minute"
? fetchStatistics(hass!, start, end, powerStatIds, "hour", powerUnits, [
"mean",
])
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
@@ -628,7 +619,6 @@ const getEnergyData = async (
const [
energyStats,
powerStats,
powerStatsHour,
waterStats,
energyStatsCompare,
waterStatsCompare,
@@ -637,44 +627,12 @@ const getEnergyData = async (
] = await Promise.all([
_energyStats,
_powerStats,
_powerStatsHour,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
// Back-fill any missing power statistics from hourly data if present
if (Object.keys(powerStatsHour).length) {
powerStatIds.forEach((powerId) => {
if (powerId in powerStatsHour) {
// If we have extra hourly power statistics for an ID, we may need to
// insert data into statistics
if (powerId in powerStats && powerStats[powerId].length) {
// We have 5-minute data. Only insert hourly values for time periods
// before the first 5-minute value.
const powerStatFirst = powerStats[powerId][0];
const powerStatHour = powerStatsHour[powerId];
let powerStatHourLast = 0;
for (const powerStat of powerStatHour) {
if (powerStat.end > powerStatFirst.start) {
break;
}
powerStatHourLast++;
}
powerStats[powerId] = [
...powerStatHour.slice(0, powerStatHourLast),
...powerStats[powerId],
];
} else {
// There was no 5-minute data, so simply insert full hourly data
powerStats[powerId] = powerStatsHour[powerId];
}
}
});
}
const stats = { ...energyStats, ...waterStats, ...powerStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };

View File

@@ -13,6 +13,7 @@ import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-date-input";
import "../../../components/ha-humidifier-state";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-time-input";
@@ -295,11 +296,17 @@ class EntityPreviewRow extends LitElement {
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${isUnavailableState(stateObj.state)}
.options=${stateObj.attributes.options?.map((option) => ({
value: option,
label: this.hass!.formatEntityState(stateObj, option),
})) || []}
naturalMenuWidth
>
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</ha-list-item>
`
)
: ""}
</ha-select>
`;
}

View File

@@ -91,11 +91,9 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
);
}

View File

@@ -95,11 +95,9 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
);
}

View File

@@ -86,11 +86,9 @@ export const showSubConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
) || field.name
return hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
step.description_placeholders
);
}

View File

@@ -1,18 +1,23 @@
import { mdiPlay, mdiStop } from "@mdi/js";
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-button";
import "../../../../components/ha-control-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import "../../../../components/ha-wa-dialog";
import { SirenEntityFeature } from "../../../../data/siren";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -23,25 +28,23 @@ class MoreInfoSirenAdvancedControls extends LitElement {
@state() _stateObj?: HassEntity;
@state() private _open = false;
@state() _tone?: string;
@state() _volume?: number;
@state() _duration?: number;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog({ stateObj }: { stateObj: HassEntity }) {
this._stateObj = stateObj;
this._open = true;
}
public closeDialog(): void {
this._open = false;
this._dialog?.close();
}
private _dialogClosed(): void {
this._open = false;
this._stateObj = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -62,33 +65,50 @@ class MoreInfoSirenAdvancedControls extends LitElement {
SirenEntityFeature.DURATION
);
return html`
<ha-wa-dialog
.open=${this._open}
.hass=${this.hass}
header-title=${this.hass.localize(
"ui.components.siren.advanced_controls"
)}
<ha-md-dialog
open
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<div>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this.hass.localize(
"ui.components.siren.advanced_controls"
)}</span
>
</ha-dialog-header>
<div slot="content">
<div class="options">
${supportsTones
? html`
<ha-select
.label=${this.hass.localize("ui.components.siren.tone")}
@selected=${this._handleToneChange}
@closed=${stopPropagation}
@change=${this._handleToneChange}
.value=${this._tone}
.options=${Object.entries(
this._stateObj!.attributes.available_tones
).map(([toneId, toneName]) => ({
value: Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId,
label: toneName,
}))}
>
${Object.entries(
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
)}
</ha-select>
`
: nothing}
@@ -133,21 +153,17 @@ class MoreInfoSirenAdvancedControls extends LitElement {
</ha-control-button>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
private _handleToneChange(ev: CustomEvent<{ value: string }>) {
this._tone = ev.detail.value;
private _handleToneChange(ev) {
this._tone = ev.target.value;
}
private _handleVolumeChange(ev) {

View File

@@ -8,7 +8,9 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
@@ -27,7 +29,6 @@ import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type MainControl = "temperature" | "humidity";
@@ -168,112 +169,182 @@ class MoreInfoClimate extends LitElement {
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
.options=${stateObj.attributes.hvac_modes
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
${html`
<ha-svg-icon
slot="icon"
.path=${climateHvacModeIcon(stateObj.state)}
></ha-svg-icon>
`}
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map((mode) => ({
value: mode,
iconPath: climateHvacModeIcon(mode),
label: this.hass.formatEntityState(stateObj, mode),
}))}
@wa-select=${this._handleOperationModeChanged}
>
<ha-svg-icon
slot="icon"
.path=${climateHvacModeIcon(stateObj.state)}
></ha-svg-icon>
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handlePresetmodeChanged}
.options=${stateObj.attributes.preset_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "preset_mode",
attributeValue: mode,
},
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.preset_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${stateObj.attributes.preset_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleFanModeChanged}
.options=${stateObj.attributes.fan_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "fan_mode",
attributeValue: mode,
},
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${stateObj.attributes.fan_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
`}
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleSwingmodeChanged}
.options=${stateObj.attributes.swing_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: mode,
},
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
${stateObj.attributes.swing_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${stateObj.attributes.swing_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
`}
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
@@ -281,34 +352,52 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.swing_horizontal_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_horizontal_mode"
)}
.value=${stateObj.attributes.swing_horizontal_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleSwingHorizontalmodeChanged}
.options=${stateObj.attributes.swing_horizontal_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_horizontal_mode",
attributeValue: mode,
},
})
)}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingHorizontalmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
${stateObj.attributes.swing_horizontal_mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${stateObj.attributes
.swing_horizontal_mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
`}
${stateObj.attributes.swing_horizontal_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
@@ -321,8 +410,8 @@ class MoreInfoClimate extends LitElement {
this._mainControl = ev.currentTarget.control;
}
private _handleFanModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleFanModeChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.fan_mode,
newVal,
@@ -331,15 +420,15 @@ class MoreInfoClimate extends LitElement {
);
}
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleOperationModeChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
hvac_mode: newVal,
});
}
private _handleSwingmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleSwingmodeChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_mode,
newVal,
@@ -348,8 +437,8 @@ class MoreInfoClimate extends LitElement {
);
}
private _handleSwingHorizontalmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleSwingHorizontalmodeChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_horizontal_mode,
newVal,
@@ -358,8 +447,8 @@ class MoreInfoClimate extends LitElement {
);
}
private _handlePresetmodeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value || null;
private _handlePresetmodeChanged(ev) {
const newVal = ev.target.value || null;
if (newVal) {
this._callServiceHelper(
this.stateObj!.attributes.preset_mode,

View File

@@ -9,6 +9,7 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
@@ -30,7 +31,6 @@ import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-fan")
class MoreInfoFan extends LitElement {
@@ -48,8 +48,8 @@ class MoreInfoFan extends LitElement {
});
};
private _handleDirection(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleDirection(ev) {
const newVal = ev.target.value;
const oldVal = this.stateObj?.attributes.direction;
if (!newVal || oldVal === newVal) return;
@@ -60,8 +60,8 @@ class MoreInfoFan extends LitElement {
});
}
private _handlePresetMode(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handlePresetMode(ev) {
const newVal = ev.target.value;
const oldVal = this._presetMode;
if (!newVal || oldVal === newVal) return;
@@ -73,8 +73,8 @@ class MoreInfoFan extends LitElement {
});
}
private _handleOscillating(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value === "true";
private _handleOscillating(ev) {
const newVal = ev.target.value === "true";
const oldVal = this.stateObj?.attributes.oscillating;
if (oldVal === newVal) return;
@@ -176,64 +176,65 @@ class MoreInfoFan extends LitElement {
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handlePresetMode}
.options=${this.stateObj.attributes.preset_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "preset_mode",
attributeValue: mode,
}
: undefined,
})
)}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${this.stateObj.attributes.preset_mode
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${this.stateObj.attributes.preset_mode}
></ha-attribute-icon>`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"direction"
)}
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleDirection}
.options=${["forward", "reverse"].map((direction) => ({
value: direction,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
direction
)
: direction,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "direction",
attributeValue: direction,
}
: undefined,
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
>
<ha-attribute-icon
slot="icon"
@@ -242,13 +243,40 @@ class MoreInfoFan extends LitElement {
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
></ha-attribute-icon>
<ha-list-item value="forward" graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
attributeValue="forward"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"forward"
)}
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
attributeValue="reverse"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
@@ -257,26 +285,37 @@ class MoreInfoFan extends LitElement {
? "true"
: "false"}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleOscillating}
.options=${["true", "false"].map((val) => ({
value: val,
iconPath:
val === "true"
? mdiArrowOscillating
: mdiArrowOscillatingOff,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
val === "true"
)
: val,
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillatingOff}
></ha-svg-icon>
<ha-list-item value="true" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiArrowOscillating}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
true
)}
</ha-list-item>
<ha-list-item value="false" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiArrowOscillatingOff}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
false
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}

View File

@@ -2,6 +2,7 @@ import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
@@ -13,7 +14,6 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@@ -74,48 +74,68 @@ class MoreInfoHumidifier extends LitElement {
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${hass}
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleStateChanged}
.options=${["off", "on"].map((fanState) => ({
value: fanState,
label: this.stateObj
? this.hass.formatEntityState(this.stateObj, fanState)
: fanState,
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${this.hass.formatEntityState(this.stateObj, "off")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.formatEntityState(this.stateObj, "on")}
</ha-list-item>
</ha-control-select-menu>
${supportModes
? html`
<ha-control-select-menu
.hass=${hass}
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleModeChanged}
.options=${stateObj.attributes.available_modes?.map((mode) => ({
value: mode,
label: stateObj
? this.hass.formatEntityAttributeValue(
stateObj,
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.mode
? html`
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${stateObj.attributes.mode}
></ha-attribute-icon>
`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj!,
"mode",
mode
)
: mode,
attributeIcon: stateObj
? {
stateObj,
attribute: "mode",
attributeValue: mode,
}
: undefined,
})) || []}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
@@ -123,8 +143,8 @@ class MoreInfoHumidifier extends LitElement {
`;
}
private _handleStateChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value || null;
private _handleStateChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
this.stateObj!.state,
newVal,
@@ -133,8 +153,8 @@ class MoreInfoHumidifier extends LitElement {
);
}
private _handleModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleModeChanged(ev) {
const newVal = ev.target.value || null;
this._mode = newVal;
this._callServiceHelper(
this.stateObj!.attributes.mode,

View File

@@ -9,6 +9,7 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
@@ -37,7 +38,6 @@ import "../components/lights/ha-more-info-light-favorite-colors";
import "../components/lights/light-color-rgb-picker";
import "../components/lights/light-color-temp-picker";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type MainControl = "brightness" | "color_temp" | "color";
@@ -253,35 +253,47 @@ class MoreInfoLight extends LitElement {
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleEffect}
.options=${this.stateObj.attributes.effect_list.map(
(effect) => ({
value: effect,
label: this.stateObj
? this.hass.formatEntityAttributeValue(
this.stateObj,
"effect",
effect
)
: effect,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "effect",
attributeValue: effect,
}
: undefined,
})
)}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
${this.stateObj.attributes.effect
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${this.stateObj.attributes.effect}
></ha-attribute-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiCreation}
></ha-svg-icon>`}
${this.stateObj.attributes.effect_list?.map(
(effect) => html`
<ha-list-item .value=${effect} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${effect}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
effect
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
@@ -305,8 +317,8 @@ class MoreInfoLight extends LitElement {
});
};
private _handleEffect(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleEffect(ev) {
const newVal = ev.target.value;
const oldVal = this._effect;
if (!newVal || oldVal === newVal) return;

View File

@@ -25,8 +25,8 @@ import { VolumeSliderController } from "../../../common/util/volume-slider";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-marquee-text";
@@ -202,11 +202,12 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
return html`<ha-dropdown>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
@wa-select=${this._handleSourceChange}
>
</ha-icon-button>
${this.stateObj.attributes.source_list!.map(
@@ -742,7 +743,7 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSourceChange(e: HaDropdownSelectEvent) {
private _handleSourceChange(e: CustomEvent<{ item: HaDropdownItem }>) {
const source = e.detail.item.value;
if (!source || this.stateObj!.attributes.source === source) {
return;
@@ -754,7 +755,7 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSoundModeChange(ev: HaDropdownSelectEvent) {
private _handleSoundModeChange(ev: CustomEvent<{ item: HaDropdownItem }>) {
const soundMode = ev.detail.item.value;
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
return;

View File

@@ -1,10 +1,12 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-select";
import type { RemoteEntity } from "../../../data/remote";
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
@customElement("more-info-remote")
class MoreInfoRemote extends LitElement {
@@ -28,24 +30,30 @@ class MoreInfoRemote extends LitElement {
)}
.value=${stateObj.attributes.current_activity || ""}
@selected=${this._handleActivityChanged}
.options=${stateObj.attributes.activity_list?.map((activity) => ({
value: activity,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"activity",
activity
),
}))}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.activity_list?.map(
(activity) => html`
<ha-list-item .value=${activity}>
${this.hass.formatEntityAttributeValue(
stateObj,
"activity",
activity
)}
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
`;
}
private _handleActivityChanged(ev: CustomEvent<{ value: string }>) {
private _handleActivityChanged(ev) {
const oldVal = this.stateObj!.attributes.current_activity;
const newVal = ev.detail.value;
const newVal = ev.target.value;
if (!newVal || oldVal === newVal) {
return;

View File

@@ -11,11 +11,13 @@ import {
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
@@ -170,17 +172,21 @@ class MoreInfoVacuum extends LitElement {
.disabled=${stateObj.state === UNAVAILABLE}
.value=${stateObj.attributes.fan_speed}
@selected=${this._handleFanSpeedChanged}
.options=${stateObj.attributes.fan_speed_list!.map(
(mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
),
})
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.fan_speed_list!.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
)}
</ha-list-item>
`
)}
</ha-select>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
@@ -285,9 +291,9 @@ class MoreInfoVacuum extends LitElement {
});
}
private _handleFanSpeedChanged(ev: CustomEvent<{ value: string }>) {
private _handleFanSpeedChanged(ev) {
const oldVal = this.stateObj!.attributes.fan_speed;
const newVal = ev.detail.value;
const newVal = ev.target.value;
if (!newVal || oldVal === newVal) {
return;

View File

@@ -2,10 +2,9 @@ import { mdiAccount, mdiAccountArrowRight, mdiWaterBoiler } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-list-item";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { WaterHeaterEntity } from "../../../data/water_heater";
@@ -17,6 +16,7 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import "../../../components/ha-attribute-icon";
@customElement("more-info-water_heater")
class MoreInfoWaterHeater extends LitElement {
@@ -74,25 +74,32 @@ class MoreInfoWaterHeater extends LitElement {
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.water_heater.mode")}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
@wa-select=${this._handleOperationModeChanged}
.options=${stateObj.attributes.operation_list
.concat()
.sort(compareWaterHeaterOperationMode)
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
attributeIcon: {
stateObj,
attribute: "operation_mode",
attributeValue: mode,
},
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
${stateObj.attributes.operation_list
.concat()
.sort(compareWaterHeaterOperationMode)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="operation_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
@@ -105,18 +112,31 @@ class MoreInfoWaterHeater extends LitElement {
)}
.value=${stateObj.attributes.away_mode}
.disabled=${stateObj.state === UNAVAILABLE}
@wa-select=${this._handleAwayModeChanged}
.options=${["on", "off"].map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
mode
),
iconPath: mode === "on" ? mdiAccountArrowRight : mdiAccount,
}))}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleAwayModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiAccount}></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiAccountArrowRight}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"on"
)}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"off"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
@@ -124,8 +144,8 @@ class MoreInfoWaterHeater extends LitElement {
`;
}
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value;
private _handleOperationModeChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.state,
newVal,
@@ -136,8 +156,8 @@ class MoreInfoWaterHeater extends LitElement {
);
}
private _handleAwayModeChanged(ev: HaDropdownSelectEvent) {
const newVal = ev.detail.item.value === "on";
private _handleAwayModeChanged(ev) {
const newVal = ev.target.value === "on";
const oldVal = this.stateObj!.attributes.away_mode === "on";
this._callServiceHelper(oldVal, newVal, "set_away_mode", {

View File

@@ -25,11 +25,11 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
getEntityContext,
getEntityEntryContext,
@@ -37,12 +37,11 @@ import {
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import { computeRTL } from "../../common/util/compute_rtl";
import { withViewTransition } from "../../common/util/view-transition";
import "../../components/ha-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-related-items";
@@ -318,7 +317,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._setView("related");
}
private _handleMenuAction(ev: HaDropdownSelectEvent) {
private _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
switch (action) {
case "device":
@@ -771,9 +770,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
}
private _enlarge() {
withViewTransition(() => {
this.large = !this.large;
});
this.large = !this.large;
}
private _handleOpened() {

View File

@@ -9,8 +9,8 @@ import { formatLanguageCode } from "../../common/language/format_language";
import "../../components/chips/ha-assist-chip";
import "../../components/ha-dialog";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import { getLanguageOptions } from "../../components/ha-language-picker";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite";
@@ -328,7 +328,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
}
}
private _handlePickLanguage(ev: HaDropdownSelectEvent) {
private _handlePickLanguage(ev: CustomEvent<{ item: HaDropdownItem }>) {
this._language = ev.detail.item.value;
}

View File

@@ -5,8 +5,8 @@ import "../../components/ha-button";
import "../../components/ha-spinner";
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-check")
export class HaVoiceAssistantSetupStepCheck extends LitElement {
@@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
"/voice_control/troubleshooting/#i-dont-get-a-voice-response"
)}
>
${this.hass.localize(
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.check.help"
)}</ha-button
>

View File

@@ -8,6 +8,7 @@ import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import "../../components/ha-list-item";
import "../../components/ha-select";
import "../../components/ha-tts-voice-picker";
import type { AssistPipeline } from "../../data/assist_pipeline";
@@ -114,15 +115,19 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.form.wake_word_id"
)}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this.assistConfiguration.active_wake_words[0]}
@selected=${this._wakeWordPicked}
.options=${this.assistConfiguration.available_wake_words.map(
(wakeword) => ({
value: wakeword.id,
label: wakeword.wake_word,
})
>
${this.assistConfiguration.available_wake_words.map(
(wakeword) =>
html`<ha-list-item .value=${wakeword.id}>
${wakeword.wake_word}
</ha-list-item>`
)}
></ha-select>
</ha-select>
<ha-button
appearance="plain"
size="small"
@@ -146,17 +151,16 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
)}
@closed=${stopPropagation}
.value=${pipelineEntity?.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._pipelinePicked}
.options=${pipelineEntity?.attributes.options.map(
(pipeline) => ({
value: pipeline,
label: this.hass.formatEntityState(
pipelineEntity,
pipeline
),
})
)}
>
${pipelineEntity?.attributes.options.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline}>
${this.hass.formatEntityState(pipelineEntity, pipeline)}
</ha-list-item>`
)}
</ha-select>
<ha-button
appearance="plain"
@@ -231,19 +235,16 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
this._deviceName = ev.target.value;
}
private async _wakeWordPicked(ev: CustomEvent<{ value: string }>) {
const option = ev.detail.value;
if (this.assistConfiguration) {
this.assistConfiguration.active_wake_words = [option];
}
private async _wakeWordPicked(ev) {
const option = ev.target.value;
await setWakeWords(this.hass, this.assistEntityId!, [option]);
}
private _pipelinePicked(ev: CustomEvent<{ value: string }>) {
private _pipelinePicked(ev) {
const stateObj = this.hass!.states[
this.assistConfiguration!.pipeline_entity_id
] as InputSelectEntity;
const option = ev.detail.value;
const option = ev.target.value;
if (
option === stateObj.state ||
!stateObj.attributes.options.includes(option)
@@ -383,11 +384,6 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.row ha-button {
width: 82px;
}
ha-select {
display: block;
text-align: start;
}
`,
];
}

View File

@@ -17,8 +17,8 @@ import "../../components/ha-button";
import "../../components/ha-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-icon-next";
import "../../components/ha-spinner";
@@ -215,7 +215,7 @@ export class HaVoiceCommandDialog extends LitElement {
this._preferredPipeline = preferred_pipeline || undefined;
}
private async _selectPipeline(ev: HaDropdownSelectEvent) {
private async _selectPipeline(ev: CustomEvent<{ item: HaDropdownItem }>) {
const pipelineId = ev.detail?.item?.value;
if (pipelineId) {
this._pipelineId = pipelineId;

View File

@@ -29,11 +29,11 @@
}
}
::view-transition-group(launch-screen) {
animation-duration: var(--ha-animation-duration-slow, 350ms);
animation-duration: var(--ha-animation-base-duration, 350ms);
animation-timing-function: ease-out;
}
::view-transition-old(launch-screen) {
animation: fade-out var(--ha-animation-duration-slow, 350ms) ease-out;
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
}
html {
background-color: var(--primary-background-color, #fafafa);

View File

@@ -30,8 +30,8 @@ import { showDataTableSettingsDialog } from "../components/data-table/show-dialo
import "../components/ha-dialog";
import "../components/ha-dialog-header";
import "../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../components/ha-dropdown";
import "../components/ha-dropdown-item";
import type { HaDropdownItem } from "../components/ha-dropdown-item";
import "../components/search-input-outlined";
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant, Route } from "../types";
@@ -134,7 +134,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
* String to show when there are no records in the data table.
* @type {String}
*/
@property({ attribute: false }) public noDataText?: string;
@property({ attribute: false, type: String }) public noDataText?: string;
/**
* Hides the data table and show an empty message.
@@ -590,7 +590,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._sortColumn = this._sortDirection ? ev.detail.column : undefined;
}
private _handleSortBy(ev: HaDropdownSelectEvent) {
private _handleSortBy(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.preventDefault(); // keep the dropdown open
const columnId = ev.detail.item.value;
@@ -609,7 +609,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
});
}
private _handleGroupBy(ev: HaDropdownSelectEvent) {
private _handleGroupBy(ev: CustomEvent<{ item: HaDropdownItem }>) {
const group = ev.detail.item.value;
if (group === "reset") {
@@ -662,7 +662,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._selectMode = true;
}
private _handleSelect(ev: HaDropdownSelectEvent) {
private _handleSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {

View File

@@ -144,6 +144,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
.label=${""}
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/getting-started/onboarding/"

View File

@@ -13,7 +13,7 @@ class IntegrationBadge extends LitElement {
@property({ attribute: "dark-optimized-icon", type: Boolean })
public darkOptimizedIcon = false;
@property({ attribute: false })
@property({ attribute: false, type: Boolean, reflect: true })
public clickable = false;
protected render(): TemplateResult {

View File

@@ -1,16 +1,19 @@
import type { SelectedDetail } from "@material/mwc-list";
import { TZDate } from "@date-fns/tz";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { ByWeekday, Options, WeekdayStr } from "rrule";
import { customElement, property, query, state } from "lit/decorators";
import type { Options, WeekdayStr, ByWeekday } from "rrule";
import { RRule, Weekday } from "rrule";
import { formatDate, formatTime } from "../../common/datetime/calc_date";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/chips/ha-chip-set";
import "../../components/chips/ha-filter-chip";
import "../../components/ha-date-input";
import "../../components/ha-list-item";
import "../../components/ha-select";
import type { HaSelect } from "../../components/ha-select";
import "../../components/ha-textfield";
import type { HomeAssistant } from "../../types";
import type {
@@ -30,6 +33,7 @@ import {
ruleByWeekDay,
untilValue,
} from "./recurrence";
import { formatDate, formatTime } from "../../common/datetime/calc_date";
@customElement("ha-recurrence-rule-editor")
export class RecurrenceRuleEditor extends LitElement {
@@ -67,6 +71,8 @@ export class RecurrenceRuleEditor extends LitElement {
@state() private _untilDay?: Date;
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
private _allWeekdays?: WeekdayStr[];
private _monthlyRepeatItems: MonthlyRepeatItem[] = [];
@@ -85,6 +91,14 @@ export class RecurrenceRuleEditor extends LitElement {
? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart)
: [];
this._computeWeekday();
const selectElement = this._monthlyRepeatSelect;
if (selectElement) {
const oldSelected = selectElement.index;
selectElement.select(-1);
this.updateComplete.then(() => {
selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected);
});
}
}
if (
@@ -170,16 +184,35 @@ export class RecurrenceRuleEditor extends LitElement {
id="freq"
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
@selected=${this._onRepeatSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._freq}
.options=${["none", "yearly", "monthly", "weekly", "daily"].map(
(freq) => ({
value: freq,
label: this.hass.localize(
`ui.components.calendar.event.repeat.freq.${freq}` as LocalizeKeys
),
})
)}
></ha-select>
>
<ha-list-item value="none">
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
</ha-list-item>
<ha-list-item value="yearly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.yearly"
)}
</ha-list-item>
<ha-list-item value="monthly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.monthly"
)}
</ha-list-item>
<ha-list-item value="weekly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.weekly"
)}
</ha-list-item>
<ha-list-item value="daily">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.daily"
)}
</ha-list-item>
</ha-select>
`;
}
@@ -194,8 +227,18 @@ export class RecurrenceRuleEditor extends LitElement {
)}
@selected=${this._onMonthlyDetailSelected}
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
.options=${this._monthlyRepeatItems}
></ha-select>`
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._monthlyRepeatItems!.map(
(item) => html`
<ha-list-item .value=${item.value} .item=${item}>
${item.label}
</ha-list-item>
`
)}
</ha-select>`
: nothing}
`;
}
@@ -256,13 +299,19 @@ export class RecurrenceRuleEditor extends LitElement {
)}
.value=${this._end}
@selected=${this._onEndSelected}
.options=${["never", "after", "on"].map((end) => ({
value: end,
label: this.hass.localize(
`ui.components.calendar.event.repeat.end.${end as RepeatEnd}`
),
}))}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item value="never">
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
</ha-list-item>
<ha-list-item value="after">
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
</ha-list-item>
</ha-select>
${this._end === "after"
? html`
@@ -311,8 +360,8 @@ export class RecurrenceRuleEditor extends LitElement {
this._interval = (e.target! as any).value;
}
private _onRepeatSelected(e: CustomEvent<{ value: string }>) {
this._freq = e.detail.value as RepeatFrequency;
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
this._freq = (e.target as HaSelect).value as RepeatFrequency;
if (this._freq === "yearly") {
this._interval = 1;
@@ -321,12 +370,12 @@ export class RecurrenceRuleEditor extends LitElement {
this._weekday.clear();
this._computeWeekday();
}
e.stopPropagation();
}
private _onMonthlyDetailSelected(e: CustomEvent<{ value: string }>) {
const selectedItem = this._monthlyRepeatItems.find(
(item) => item.value === e.detail.value
);
private _onMonthlyDetailSelected(e: CustomEvent<SelectedDetail<number>>) {
e.stopPropagation();
const selectedItem = this._monthlyRepeatItems[e.detail.index];
if (!selectedItem) {
return;
}
@@ -346,8 +395,8 @@ export class RecurrenceRuleEditor extends LitElement {
this.requestUpdate("_weekday");
}
private _onEndSelected(e: CustomEvent<{ value: string }>) {
const end = e.detail.value as RepeatEnd;
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
const end = (e.target as HaSelect).value as RepeatEnd;
if (end === this._end) {
return;
}

View File

@@ -186,6 +186,7 @@ class PanelClimate extends LitElement {
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);

View File

@@ -1,9 +1,11 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type {
HassioAddonDetails,
@@ -14,8 +16,8 @@ import type { HassioHardwareAudioDevice } from "../../../../../data/hassio/hardw
import { fetchHassioHardwareAudio } from "../../../../../data/hassio/hardware";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
@customElement("supervisor-app-audio")
class SupervisorAppAudio extends LitElement {
@@ -53,13 +55,19 @@ class SupervisorAppAudio extends LitElement {
"ui.panel.config.apps.configuration.audio.input"
)}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
.disabled=${this.disabled}
.options=${this._inputDevices.map((item) => ({
value: item.device || "",
label: item.name,
}))}
>
${this._inputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}>
${item.name}
</ha-list-item>
`
)}
</ha-select>`}
${this._outputDevices &&
html`<ha-select
@@ -67,13 +75,19 @@ class SupervisorAppAudio extends LitElement {
"ui.panel.config.apps.configuration.audio.output"
)}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
.disabled=${this.disabled}
.options=${this._outputDevices.map((item) => ({
value: item.device || "",
label: item.name,
}))}
>
${this._outputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}
>${item.name}</ha-list-item
>
`
)}
</ha-select>`}
</div>
<div class="card-actions">
@@ -102,7 +116,6 @@ class SupervisorAppAudio extends LitElement {
}
ha-select {
width: 100%;
display: block;
}
ha-select:last-child {
margin-top: var(--ha-space-2);
@@ -118,13 +131,13 @@ class SupervisorAppAudio extends LitElement {
}
}
private _setInputDevice(ev: CustomEvent<{ value: string }>): void {
const device = ev.detail.value;
private _setInputDevice(ev): void {
const device = ev.target.value;
this._selectedInput = device;
}
private _setOutputDevice(ev: CustomEvent<{ value: string }>): void {
const device = ev.detail.value;
private _setOutputDevice(ev): void {
const device = ev.target.value;
this._selectedOutput = device;
}

View File

@@ -9,8 +9,8 @@ import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-form/ha-form";
import type {
HaFormDataContainer,
@@ -347,7 +347,7 @@ class SupervisorAppConfig extends LitElement {
}
}
private _handleAction(ev: HaDropdownSelectEvent) {
private _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {

View File

@@ -7,8 +7,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/search-input";
import type {
@@ -202,7 +202,7 @@ export class HaConfigAppsAvailable extends LitElement {
})
);
private _handleAction(ev: HaDropdownSelectEvent) {
private _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {

View File

@@ -19,6 +19,7 @@ import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list";
@@ -52,7 +53,6 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
declare interface NameAndEntity<EntityType extends HassEntity> {
name: string;
@@ -605,7 +605,7 @@ class HaConfigAreaPage extends LitElement {
this._related = await findRelated(this.hass, "area", this.areaId);
}
private _handleMenuAction(ev: HaDropdownSelectEvent) {
private _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
const entry = (ev.detail?.item as any)?.data as AreaRegistryEntry;
switch (action) {

View File

@@ -58,7 +58,6 @@ import {
} from "./show-dialog-area-registry-detail";
import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const UNASSIGNED_FLOOR = "__unassigned__";
@@ -535,7 +534,7 @@ export class HaConfigAreasDashboard extends LitElement {
}, time);
}
private _handleFloorAction(ev: HaDropdownSelectEvent) {
private _handleFloorAction(ev: CustomEvent<{ item: { value: string } }>) {
const floor = (ev.currentTarget as any).floor;
const action = ev.detail.item.value;
switch (action) {
@@ -551,7 +550,9 @@ export class HaConfigAreasDashboard extends LitElement {
}
}
private _handleUnassignedAreasAction(ev: HaDropdownSelectEvent) {
private _handleUnassignedAreasAction(
ev: CustomEvent<{ item: { value: string } }>
) {
const action = ev.detail.item.value;
if (action === "reorder") {
this._showReorderDialog();

View File

@@ -36,6 +36,7 @@ import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-service-icon";
@@ -91,7 +92,6 @@ import "./types/ha-automation-action-set_conversation_response";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
export const getAutomationActionType = memoizeOne(
(action: Action | undefined) => {
@@ -862,7 +862,7 @@ export default class HaAutomationActionRow extends LitElement {
this._automationRowElement?.focus();
}
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;

View File

@@ -2,11 +2,13 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import { stringCompare } from "../../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
@@ -83,47 +85,37 @@ export class HaConditionAction
this.action.condition
);
const value =
this.action.condition in this._conditionDescriptions
? `${DYNAMIC_PREFIX}${this.action.condition}`
: this.action.condition;
let valueLabel = value;
const items = html`${this._processedTypes(
this._conditionDescriptions,
this.hass.localize
).map(([opt, label, condition]) => {
const selected = value === opt;
if (selected) {
valueLabel = label;
}
return html`
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
<ha-condition-icon
.hass=${this.hass}
slot="icon"
.condition=${condition}
></ha-condition-icon>
${label}
</ha-dropdown-item>
`;
})}`;
return html`
${this.inSidebar || (!this.inSidebar && !this.indent)
? html`
<ha-select
fixedMenuPosition
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.disabled=${this.disabled}
.value=${valueLabel}
.value=${this.action.condition in this._conditionDescriptions
? `${DYNAMIC_PREFIX}${this.action.condition}`
: this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
@closed=${stopPropagation}
>
${items}
${this._processedTypes(
this._conditionDescriptions,
this.hass.localize
).map(
([opt, label, condition]) => html`
<ha-list-item .value=${opt} graphic="icon">
${label}
<ha-condition-icon
.hass=${this.hass}
slot="graphic"
.condition=${condition}
></ha-condition-icon>
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
@@ -200,8 +192,8 @@ export class HaConditionAction
});
}
private _typeChanged(ev: CustomEvent<{ value: string }>) {
const type = ev.detail.value;
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
@@ -250,7 +242,6 @@ export class HaConditionAction
static styles = css`
ha-select {
margin-bottom: 24px;
display: block;
}
`;
}

View File

@@ -37,6 +37,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import type {
@@ -51,7 +52,6 @@ import type { ConditionDescriptions } from "../../../../data/condition";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { DeviceCondition } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import {
showAlertDialog,
@@ -76,7 +76,7 @@ import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import type { DeviceCondition } from "../../../../data/device/device_automation";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -813,7 +813,7 @@ export default class HaAutomationConditionRow extends LitElement {
this._automationRowElement?.focus();
}
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;

View File

@@ -35,6 +35,7 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
@@ -90,7 +91,6 @@ import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialo
import "./blueprint-automation-editor";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
declare global {
interface HTMLElementTagNameMap {
@@ -1212,7 +1212,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._undoRedoController.redo();
}
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
if (!action) {

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