mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-05 03:12:00 -06:00
Compare commits
107 Commits
20260128.1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29cee99f10 | ||
|
|
47341e93fc | ||
|
|
cbae7d6e2f | ||
|
|
ebff35d17f | ||
|
|
aec4a06156 | ||
|
|
917f2b4434 | ||
|
|
79ec6b972e | ||
|
|
9e35befa99 | ||
|
|
75160d67d3 | ||
|
|
b145d09041 | ||
|
|
f3f7a1e46a | ||
|
|
091315d9a9 | ||
|
|
75b830cdf9 | ||
|
|
e4b8352832 | ||
|
|
4e193187f9 | ||
|
|
5394b3b8cf | ||
|
|
2ab867986a | ||
|
|
a1c3a6c662 | ||
|
|
11296adbd4 | ||
|
|
4e04f4284e | ||
|
|
a0cc0d9cca | ||
|
|
c925053bb8 | ||
|
|
22a7aa8f8e | ||
|
|
3a5f719a3e | ||
|
|
7b7182c147 | ||
|
|
0eb7229819 | ||
|
|
fcc6f1b5e9 | ||
|
|
19dc2a5865 | ||
|
|
ae63530123 | ||
|
|
97e1f47af9 | ||
|
|
346d916944 | ||
|
|
ab326b3277 | ||
|
|
55e251c04f | ||
|
|
b902f3e6f5 | ||
|
|
9daac6c49f | ||
|
|
65ea0c9121 | ||
|
|
505cc698f6 | ||
|
|
b0f9b31dae | ||
|
|
2b2966a214 | ||
|
|
558b251e32 | ||
|
|
df28eaa99e | ||
|
|
40342c9cfd | ||
|
|
c6573b9c1b | ||
|
|
54c46ad362 | ||
|
|
3376036392 | ||
|
|
ff5ecc047a | ||
|
|
a8393cddd4 | ||
|
|
edfc33039b | ||
|
|
14f8d982a9 | ||
|
|
847a040fa7 | ||
|
|
8611359481 | ||
|
|
f473ebf18c | ||
|
|
9010898742 | ||
|
|
1e30394bf3 | ||
|
|
bca2cb0c1e | ||
|
|
29317eb842 | ||
|
|
a8327ef59a | ||
|
|
01c8832024 | ||
|
|
a8c633e627 | ||
|
|
b659671814 | ||
|
|
3060cdf355 | ||
|
|
215241df56 | ||
|
|
f6852894b0 | ||
|
|
86d7205a3a | ||
|
|
9c16ceda71 | ||
|
|
288789a604 | ||
|
|
09139d5bec | ||
|
|
2d90be9af3 | ||
|
|
1c6464663e | ||
|
|
189f0b9472 | ||
|
|
771f5eaff4 | ||
|
|
a4cb3b5b01 | ||
|
|
eb1ae99a1f | ||
|
|
655643eb3f | ||
|
|
02eb1e6832 | ||
|
|
62f7a2eea1 | ||
|
|
a7f9b93018 | ||
|
|
3e7011e2c8 | ||
|
|
97f89bd983 | ||
|
|
9aedaeabbf | ||
|
|
7e839dc895 | ||
|
|
1f6c916d11 | ||
|
|
71bd12bb90 | ||
|
|
e741c14482 | ||
|
|
a496448ed9 | ||
|
|
c772285358 | ||
|
|
4fcdd09935 | ||
|
|
157e89e5e7 | ||
|
|
c223f932f3 | ||
|
|
fcd63e7cba | ||
|
|
c1bab376c8 | ||
|
|
01f5df6671 | ||
|
|
241a655765 | ||
|
|
9841a6341a | ||
|
|
44c917b4b7 | ||
|
|
bff785ca68 | ||
|
|
1f15724024 | ||
|
|
ceaa3b8c17 | ||
|
|
7ed3ac1e24 | ||
|
|
cfcb649a6f | ||
|
|
ae036f4084 | ||
|
|
6cca48e79d | ||
|
|
feb9ce421d | ||
|
|
9639403865 | ||
|
|
8e8cb095b1 | ||
|
|
60079ce999 | ||
|
|
4f3196adb9 |
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -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@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
# ℹ️ 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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
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: cp313
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
||||
@ -51,8 +51,8 @@ const compressAppOtherZopfli = () =>
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
// compressAppModernBrotli,
|
||||
// compressAppOtherBrotli,
|
||||
compressAppModernBrotli,
|
||||
compressAppOtherBrotli,
|
||||
compressAppModernZopfli,
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
|
||||
@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "./stubs/device_registry";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
import { mockLovelace } from "./stubs/lovelace";
|
||||
@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
@ -27,4 +27,25 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS(
|
||||
"frontend/subscribe_system_data",
|
||||
(_msg, currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: currentHass.systemData,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
}
|
||||
);
|
||||
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
|
||||
onChange?.({
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
};
|
||||
|
||||
@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, _hass, onChange) => {
|
||||
let result = msg.template;
|
||||
// Simple variable substitution for demo purposes
|
||||
if (msg.variables) {
|
||||
for (const [key, value] of Object.entries(msg.variables)) {
|
||||
result = result.replace(
|
||||
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
onChange!({
|
||||
result: msg.template,
|
||||
result,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
51
package.json
51
package.json
@ -27,25 +27,25 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@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.11",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@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",
|
||||
"@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",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@ -71,7 +71,6 @@
|
||||
"@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",
|
||||
@ -89,7 +88,7 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@ -112,7 +111,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"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",
|
||||
@ -122,7 +121,7 @@
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
@ -146,17 +145,17 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@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",
|
||||
"@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",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.0",
|
||||
"@rspack/core": "1.7.3",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@ -199,7 +198,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.4.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@ -211,11 +210,11 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.6",
|
||||
"tar": "7.5.7",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.53.1",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@ -229,7 +228,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.1.0",
|
||||
"globals": "17.3.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"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260128.1"
|
||||
version = "20260128.0"
|
||||
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.13.0"
|
||||
requires-python = ">=3.14.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
@ -16,6 +16,12 @@ 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
|
||||
|
||||
@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
29
src/common/config/filter_navigation_pages.ts
Normal file
29
src/common/config/filter_navigation_pages.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { canShowPage } from "./can_show_page";
|
||||
|
||||
export interface NavigationFilterOptions {
|
||||
/** Whether there are Bluetooth config entries (pre-fetched by caller) */
|
||||
hasBluetoothConfigEntries?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters navigation pages based on visibility rules.
|
||||
* Handles special cases like Bluetooth (requires config entries)
|
||||
* and external app configuration.
|
||||
*/
|
||||
export const filterNavigationPages = (
|
||||
hass: HomeAssistant,
|
||||
pages: PageNavigation[],
|
||||
options: NavigationFilterOptions = {}
|
||||
): PageNavigation[] =>
|
||||
pages.filter((page) => {
|
||||
if (page.path === "#external-app-configuration") {
|
||||
return hass.auth.external?.config.hasSettingsScreen;
|
||||
}
|
||||
// Only show Bluetooth page if there are Bluetooth config entries
|
||||
if (page.component === "bluetooth") {
|
||||
return options.hasBluetoothConfigEntries ?? false;
|
||||
}
|
||||
return canShowPage(hass, page);
|
||||
});
|
||||
@ -8,7 +8,9 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
|
||||
const validState = states.some(
|
||||
(stateObj) => !isUnavailableState(stateObj.state)
|
||||
);
|
||||
|
||||
if (!validState) {
|
||||
return UNAVAILABLE;
|
||||
|
||||
@ -16,6 +16,7 @@ export interface ShortcutConfig {
|
||||
* Default is false to avoid interrupting copy/paste.
|
||||
*/
|
||||
allowWhenTextSelected?: boolean;
|
||||
allowInInput?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,7 +30,10 @@ function registerShortcuts(
|
||||
|
||||
Object.entries(shortcuts).forEach(([key, config]) => {
|
||||
wrappedShortcuts[key] = (event: KeyboardEvent) => {
|
||||
if (!canOverrideAlphanumericInput(event.composedPath())) {
|
||||
if (
|
||||
!config.allowInInput &&
|
||||
!canOverrideAlphanumericInput(event.composedPath())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
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.
|
||||
*
|
||||
@ -14,7 +26,7 @@
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void
|
||||
): Promise<void> => {
|
||||
if (!document.startViewTransition) {
|
||||
if (!document.startViewTransition || isViewTransitionDisabled) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ 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";
|
||||
@ -27,6 +28,7 @@ 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";
|
||||
@ -92,10 +94,18 @@ 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 {
|
||||
@ -113,8 +123,11 @@ 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()!();
|
||||
}
|
||||
@ -126,7 +139,13 @@ export class HaChartBase extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
this._pendingSetup = true;
|
||||
afterNextRender(() => {
|
||||
if (this.isConnected && this._pendingSetup) {
|
||||
this._pendingSetup = false;
|
||||
this._setupChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._listeners.push(
|
||||
@ -181,6 +200,26 @@ 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() {
|
||||
@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize({
|
||||
animation:
|
||||
this._reducedMotion ||
|
||||
typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
this._resizeChartIfNeeded();
|
||||
};
|
||||
|
||||
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
|
||||
@ -1022,11 +1071,18 @@ 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);
|
||||
@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
overflow: visible;
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
|
||||
@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@ -716,6 +716,18 @@ 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);
|
||||
});
|
||||
|
||||
@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-reset-button", type: Boolean })
|
||||
public hideResetButton?: boolean;
|
||||
|
||||
@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property({ attribute: false, type: Number }) public hoursToShow?: number;
|
||||
@property({ attribute: false }) 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, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
|
||||
@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@ -605,6 +605,57 @@ 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);
|
||||
|
||||
@ -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, type: String }) public noDataText?: string;
|
||||
@property({ attribute: false }) public noDataText?: string;
|
||||
|
||||
@property({ attribute: false, type: String }) public searchLabel?: string;
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
|
||||
@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public reorder = false;
|
||||
|
||||
@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
|
||||
@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
|
||||
@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ attribute: false, type: Array }) public statisticIds?: string[];
|
||||
@property({ attribute: false }) public statisticIds?: string[];
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@ -48,7 +48,6 @@ export class HaAnsiToHtml extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
pre.wrap {
|
||||
|
||||
@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
public disableSpeech = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@ -2,14 +2,12 @@ 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 { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@ -41,6 +39,31 @@ 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 ||
|
||||
@ -49,33 +72,8 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === this._default)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === this._default ? undefined : target.value;
|
||||
this.value = value === this._default ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,8 @@ 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";
|
||||
@ -260,14 +258,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
name="amPm"
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
.name=${"amPm"}
|
||||
@selected=${this._valueChanged}
|
||||
@closed=${stopPropagation}
|
||||
.options=${["AM", "PM"]}
|
||||
>
|
||||
<ha-list-item value="AM">AM</ha-list-item>
|
||||
<ha-list-item value="PM">PM</ha-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper
|
||||
@ -282,10 +276,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
private _valueChanged(ev: InputEvent | CustomEvent<{ value: string }>): void {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm" ? textField.value : Number(textField.value);
|
||||
textField.name === "amPm"
|
||||
? (ev as CustomEvent<{ value: string }>).detail.value
|
||||
: Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
@ -366,10 +362,6 @@ 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);
|
||||
}
|
||||
|
||||
@ -2,12 +2,10 @@ 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")
|
||||
@ -55,20 +53,16 @@ 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}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<ha-list-item .value=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
.options=${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => ({
|
||||
value: blueprint.path,
|
||||
label: blueprint.name,
|
||||
})
|
||||
)}
|
||||
>
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -82,8 +76,8 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.target.value;
|
||||
private _blueprintChanged(ev: CustomEvent<{ value: string }>) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
|
||||
@ -51,6 +51,7 @@ export class HaCard extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
/* clean-css ignore:start */
|
||||
:host
|
||||
::slotted(
|
||||
.card-content:not(:nth-child(1 of .card-content, .card-header))
|
||||
@ -59,6 +60,7 @@ export class HaCard extends LitElement {
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
/* clean-css ignore:end */
|
||||
|
||||
:host ::slotted(.card-content) {
|
||||
padding: var(--ha-space-4);
|
||||
|
||||
@ -1,24 +1,31 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-icon";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon";
|
||||
import type { HaIcon } from "./ha-icon";
|
||||
import "./ha-ripple";
|
||||
import "./ha-svg-icon";
|
||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||
import "./ha-menu";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
attributeIcon?: {
|
||||
stateObj: HassEntity;
|
||||
attribute: string;
|
||||
attributeValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends SelectBase {
|
||||
@query(".select") protected mdcRoot!: HTMLElement;
|
||||
|
||||
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||
export class HaControlSelectMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow = false;
|
||||
@ -26,95 +33,83 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
|
||||
@property() public options;
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
@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() {
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
"select-required": this.required,
|
||||
"select-invalid": !this.isUiValid,
|
||||
"select-no-value": !this.selectedText,
|
||||
};
|
||||
|
||||
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
||||
const labelAttribute =
|
||||
this.label && this.hideLabel ? this.label : undefined;
|
||||
if (this.disabled) {
|
||||
return this._renderTrigger();
|
||||
}
|
||||
|
||||
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>
|
||||
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
|
||||
${this._renderTrigger()} ${this.options.map(this._renderOption)}
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
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}
|
||||
private _renderTrigger() {
|
||||
const selectedText = this.getValueObject(this.options, this.value)?.label;
|
||||
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
"select-required": this.required,
|
||||
"select-no-value": !selectedText,
|
||||
};
|
||||
|
||||
return html`<button
|
||||
?disabled=${this.disabled}
|
||||
slot="trigger"
|
||||
class="select-anchor ${classMap(classes)}"
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
${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}
|
||||
</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
|
||||
>`;
|
||||
|
||||
private _renderArrow() {
|
||||
if (!this.showArrow) return nothing;
|
||||
if (!this.showArrow) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
@ -124,47 +119,42 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
private _renderIcon() {
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
const { iconPath, icon, attributeIcon } =
|
||||
this.getValueObject(this.options, this.value) ?? {};
|
||||
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">
|
||||
${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>`}
|
||||
${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}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
private _showDropdown() {
|
||||
this.style.setProperty(
|
||||
"--control-select-menu-width",
|
||||
`${this._triggerButton.offsetWidth}px`
|
||||
);
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
private getValueObject = memoizeOne(
|
||||
(options: SelectOption[], value?: string) =>
|
||||
options.find((option) => option.value === value)
|
||||
);
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
@ -186,6 +176,8 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
-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;
|
||||
@ -211,6 +203,12 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
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;
|
||||
@ -230,16 +228,19 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
font-size: var(--ha-font-size-s);
|
||||
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);
|
||||
}
|
||||
@ -258,10 +259,23 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@ -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, type: String }) pathOn?: string;
|
||||
@property({ attribute: false }) 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, type: String }) pathOff?: string;
|
||||
@property({ attribute: false }) pathOff?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@ -3,7 +3,6 @@ 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";
|
||||
@ -14,9 +13,8 @@ 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 { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -73,37 +71,35 @@ 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.coversation-agent-picker.conversation_agent"
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
)}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@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
|
||||
.options=${options}
|
||||
></ha-select
|
||||
>${(this._subConfigEntry &&
|
||||
this._configEntry?.supported_subentry_types[
|
||||
this._subConfigEntry.subentry_type
|
||||
@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
|
||||
@ -76,6 +76,18 @@ export class HaDialog extends DialogBase {
|
||||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
@ -84,9 +96,9 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__scrim {
|
||||
background-color: var(--mdc-dialog-scrim-color, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
|
||||
@ -4,6 +4,18 @@ 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;
|
||||
|
||||
@ -15,6 +27,30 @@ 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(),
|
||||
@ -63,6 +99,38 @@ 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, {
|
||||
@ -90,6 +158,16 @@ 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;
|
||||
@ -103,6 +181,15 @@ 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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@ -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
|
||||
@ -37,6 +37,7 @@ export class HaDropdownItem extends DropdownItem {
|
||||
|
||||
#check {
|
||||
visibility: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
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
|
||||
|
||||
@ -31,6 +31,7 @@ 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) {
|
||||
@ -174,7 +175,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
private _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
|
||||
@ -11,8 +11,7 @@ import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-picker-field";
|
||||
|
||||
import type { HaDropdown } from "../ha-dropdown";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormMultiSelectData,
|
||||
@ -108,7 +107,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
protected _toggleItem(ev: HaDropdownSelectEvent) {
|
||||
ev.preventDefault(); // keep the dropdown open
|
||||
const value = ev.detail.item.value;
|
||||
const action = (ev.detail.item as any).action;
|
||||
|
||||
@ -17,6 +17,7 @@ import type {
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@ -142,7 +143,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
private _handleAddAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
this._displayActions = [...(this._displayActions ?? []), action];
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public formatOptions?: Intl.NumberFormatOptions;
|
||||
|
||||
@property({ attribute: false, type: String }) public valueText?: string;
|
||||
@property({ attribute: false }) public valueText?: string;
|
||||
|
||||
@property({ attribute: false }) public locale!: FrontendLocaleData;
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
|
||||
@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Number, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public renderSize?: number;
|
||||
|
||||
@property({ type: Array })
|
||||
public value?: [number, number];
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public colorBrightness?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Number })
|
||||
public ww?: number;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public minKelvin?: number;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public maxKelvin?: number;
|
||||
|
||||
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||
|
||||
@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
|
||||
|
||||
@customElement("ha-icon-button-toolbar")
|
||||
export class HaIconButtonToolbar extends LitElement {
|
||||
@property({ type: Array, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public items: (HaIconButtonToolbarItem | string)[] = [];
|
||||
|
||||
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
|
||||
|
||||
@ -3,6 +3,7 @@ import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
@ -39,7 +40,10 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed representation for small screens -->
|
||||
<ha-dropdown @wa-show=${this._handleIconOverflowMenuOpened}>
|
||||
<ha-dropdown
|
||||
@wa-show=${this._handleIconOverflowMenuOpened}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
||||
@ -107,9 +107,6 @@ 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;
|
||||
|
||||
@ -134,16 +134,51 @@ export class HaMarkdown extends LitElement {
|
||||
}
|
||||
table[role="presentation"] {
|
||||
--markdown-table-border-collapse: separate;
|
||||
--markdown-table-border-width: attr(border, 0);
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th {
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ 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 {
|
||||
@ -17,7 +16,7 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@ -52,60 +51,54 @@ class HaMountPicker extends LitElement {
|
||||
if (!this._mounts) {
|
||||
return nothing;
|
||||
}
|
||||
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>`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
@ -153,16 +146,10 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
private _mountChanged(ev: CustomEvent<{ value: string }>) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
private _mountChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HaSelect;
|
||||
const newValue = target.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
if (newValue !== this.value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ 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";
|
||||
@ -25,8 +26,9 @@ import {
|
||||
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
|
||||
|
||||
const RELATED_SORT_PREFIX = {
|
||||
area: "0_area",
|
||||
device: "1_device",
|
||||
area_view: "0_area_view",
|
||||
area: "1_area_settings",
|
||||
device: "2_device",
|
||||
} as const;
|
||||
|
||||
interface NavigationItem extends PickerComboBoxItem {
|
||||
@ -260,12 +262,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(
|
||||
this.hass!.connection,
|
||||
// path should be null to fetch default lovelace panel
|
||||
panel.url_path === "lovelace" ? null : panel.url_path,
|
||||
true
|
||||
)
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
@ -437,10 +434,31 @@ 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,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: primary }
|
||||
),
|
||||
secondary: path,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
|
||||
@ -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, type: String }) autocapitalize = "";
|
||||
@property({ attribute: false }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isTouch } from "../util/is_touch";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
@ -56,6 +57,11 @@ export interface PickerComboBoxItem {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface PickerComboBoxIndexSelectedDetail {
|
||||
index: number;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
const PADDING_ID = "___padding___";
|
||||
|
||||
@ -113,7 +119,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
section?: string
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
@ -279,6 +285,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
section === "separator"
|
||||
? html`<div class="separator"></div>`
|
||||
: html`<ha-filter-chip
|
||||
@mousedown=${isTouch ? undefined : this._preventBlur}
|
||||
@click=${this._toggleSection}
|
||||
.section-id=${section.id}
|
||||
.selected=${this._selectedSection === section.id}
|
||||
@ -417,18 +424,19 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _valueSelected = (ev: Event) => {
|
||||
private _valueSelected = (ev: MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const newValue = value?.trim();
|
||||
const newTab = ev.ctrlKey || ev.metaKey;
|
||||
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
this._fireSelectedEvents(newValue, index, newTab);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
private _fireSelectedEvents(value: string, index: number, newTab = false) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
fireEvent(this, "index-selected", { index, newTab });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
@ -500,6 +508,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._valuePinned = true;
|
||||
};
|
||||
|
||||
private _preventBlur(ev: Event) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _toggleSection(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._resetSelectedItem();
|
||||
@ -517,9 +529,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
this.virtualizerElement?.element(0)?.scrollIntoView();
|
||||
}
|
||||
|
||||
private _registerKeyboardShortcuts() {
|
||||
@ -529,15 +539,42 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
Home: this._selectFirstItem,
|
||||
End: this._selectLastItem,
|
||||
Enter: this._pickSelectedItem,
|
||||
"$mod+Enter": this._pickSelectedItemNewTab,
|
||||
});
|
||||
}
|
||||
|
||||
private _focusList() {
|
||||
if (this._selectedItemIndex === -1) {
|
||||
this._selectNextItem();
|
||||
this._initializeSelectedIndex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize keyboard selection to the currently selected value,
|
||||
* or fall back to the first item when searching (skipping section titles).
|
||||
*/
|
||||
private _initializeSelectedIndex(): void {
|
||||
if (!this.virtualizerElement?.items?.length) {
|
||||
return;
|
||||
}
|
||||
const initialIndex = this._getInitialSelectedIndex();
|
||||
// Only initialize to first item if searching, otherwise require a selected value
|
||||
if (initialIndex === 0 && !this._search) {
|
||||
return;
|
||||
}
|
||||
let index = initialIndex;
|
||||
// Skip section titles (strings)
|
||||
if (typeof this.virtualizerElement.items[index] === "string") {
|
||||
index += 1;
|
||||
}
|
||||
// Bounds check: ensure index is valid after skipping section title
|
||||
if (index >= this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
this._selectedItemIndex = index;
|
||||
this._scrollToSelectedItem();
|
||||
}
|
||||
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
@ -556,6 +593,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If no item is selected yet, start from the currently selected value
|
||||
if (this._selectedItemIndex === -1) {
|
||||
this._initializeSelectedIndex();
|
||||
if (this._selectedItemIndex !== -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextIndex =
|
||||
maxItems === this._selectedItemIndex
|
||||
? this._selectedItemIndex
|
||||
@ -647,7 +692,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
this.virtualizerElement
|
||||
?.element(this._selectedItemIndex)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.virtualizerElement
|
||||
@ -657,6 +704,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
this._pickItem(ev, false);
|
||||
};
|
||||
|
||||
private _pickSelectedItemNewTab = (ev: KeyboardEvent) => {
|
||||
this._pickItem(ev, true);
|
||||
};
|
||||
|
||||
private _pickItem = (ev: KeyboardEvent, newTab: boolean) => {
|
||||
ev.stopPropagation();
|
||||
if (
|
||||
this.virtualizerElement?.items?.length !== undefined &&
|
||||
@ -668,14 +723,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
this._fireSelectedEvents(item.id, index, newTab);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
return;
|
||||
this._initializeSelectedIndex();
|
||||
if (this._selectedItemIndex === -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if filter button is focused
|
||||
@ -685,7 +743,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex, newTab);
|
||||
}
|
||||
};
|
||||
|
||||
@ -891,6 +949,6 @@ declare global {
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": { index: number };
|
||||
"index-selected": PickerComboBoxIndexSelectedDetail;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
|
||||
@property({ type: Number })
|
||||
public margin = 4;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
@property({ attribute: "center-image" }) public centerImage?: string;
|
||||
|
||||
@ -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: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
const cameraId = ev.detail?.item?.value;
|
||||
if (cameraId) {
|
||||
this._selectedCamera = cameraId;
|
||||
|
||||
@ -1,187 +1,209 @@
|
||||
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";
|
||||
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;
|
||||
}
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
export class HaSelect extends LitElement {
|
||||
@property({ type: Boolean }) public clearable = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public options;
|
||||
@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;
|
||||
}
|
||||
);
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderField();
|
||||
}
|
||||
|
||||
return html`
|
||||
${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}
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
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 _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 renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
fireEvent(this, "selected", { value });
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
|
||||
fireEvent(this, "selected", { value: undefined });
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
private _handleShow() {
|
||||
this.style.setProperty(
|
||||
"--select-menu-width",
|
||||
`${this._triggerField.offsetWidth}px`
|
||||
);
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select": HaSelect;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
selected: { value: string | undefined };
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,17 +5,16 @@ 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";
|
||||
@ -231,24 +230,15 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label ?? ""}
|
||||
.value=${this.value ?? ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.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>
|
||||
`;
|
||||
}
|
||||
@ -295,7 +285,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.detail?.index === -1 && this.value !== undefined) {
|
||||
if (ev.detail?.value === undefined && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
@ -385,7 +375,7 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
|
||||
@ -224,6 +224,7 @@ ${typeof this._templateResult.result === "object"
|
||||
margin-bottom: 0;
|
||||
direction: ltr;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -108,6 +108,7 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
flex: 1;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
|
||||
@ -492,19 +492,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="start"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
${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.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
@ -524,13 +527,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
type="button"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="start">${notificationCount}</span>`
|
||||
${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
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
${notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
@ -739,6 +744,8 @@ 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)
|
||||
);
|
||||
@ -747,6 +754,7 @@ 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));
|
||||
@ -761,15 +769,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
margin-left: 3px;
|
||||
margin-inline-start: 3px;
|
||||
margin-inline-end: initial;
|
||||
width: 100%;
|
||||
display: none;
|
||||
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;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
}
|
||||
|
||||
.panels-list {
|
||||
@ -827,6 +842,7 @@ 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;
|
||||
@ -867,11 +883,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: none;
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
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;
|
||||
@ -889,6 +916,9 @@ 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 {
|
||||
@ -900,6 +930,12 @@ 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);
|
||||
@ -938,6 +974,15 @@ 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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@ -2,16 +2,14 @@ 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 { computeDomain } from "../common/entity/compute_domain";
|
||||
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 { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -61,6 +59,30 @@ 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 ||
|
||||
@ -69,33 +91,8 @@ export class HaSTTPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@ -144,17 +141,17 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public compact = false;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
@ -25,6 +24,26 @@ 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 ||
|
||||
@ -33,31 +52,8 @@ export class HaThemePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@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>
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -67,11 +63,11 @@ export class HaThemePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
if (!this.hass || ev.target.value === "") {
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,10 +36,19 @@ 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);
|
||||
|
||||
@ -2,16 +2,14 @@ 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 { computeDomain } from "../common/entity/compute_domain";
|
||||
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 { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -61,6 +59,30 @@ 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 ||
|
||||
@ -69,33 +91,8 @@ export class HaTTSPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@ -144,17 +141,17 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
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 { TTSVoice } from "../data/tts";
|
||||
import { listTTSVoices } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -31,14 +29,25 @@ 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 ||
|
||||
@ -47,21 +56,8 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@ -102,34 +98,25 @@ 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): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
@property({ attribute: false, type: Object })
|
||||
@property({ attribute: false })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
@ -288,10 +288,19 @@ 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;
|
||||
}
|
||||
|
||||
@ -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: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "auto":
|
||||
|
||||
@ -2,6 +2,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { ActionHandlerOptions } from "../../data/lovelace/action_handler";
|
||||
import { actionHandler } from "../../panels/lovelace/common/directives/action-handler-directive";
|
||||
import "../ha-ripple";
|
||||
@ -14,7 +15,7 @@ export class HaTileContainer extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public vertical = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public interactive = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@ -47,7 +48,11 @@ export class HaTileContainer extends LitElement {
|
||||
>
|
||||
<ha-ripple .disabled=${!this.interactive}></ha-ripple>
|
||||
</div>
|
||||
<div class="container ${containerOrientationClass}">
|
||||
<div
|
||||
class="container ${containerOrientationClass}"
|
||||
@action=${stopPropagation}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<slot name="icon"></slot>
|
||||
<slot name="info" id="info"></slot>
|
||||
|
||||
@ -53,6 +53,7 @@ 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;
|
||||
|
||||
@ -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 isUsedFilter = (
|
||||
export const isFilterUsed = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string
|
||||
@ -49,7 +49,7 @@ export const isUsedFilter = (
|
||||
// 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 isUsedRelatedItemsFilter = (
|
||||
export const isRelatedItemsFilterUsed = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string | string[]
|
||||
|
||||
@ -131,11 +131,30 @@ export interface FlowToGridSourceEnergyPreference {
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
// W meter
|
||||
stat_rate: string;
|
||||
export interface PowerConfig {
|
||||
stat_rate?: string; // Standard single sensor
|
||||
stat_rate_inverted?: string; // Inverted single sensor
|
||||
stat_rate_from?: string; // Battery: discharge / Grid: consumption
|
||||
stat_rate_to?: string; // Battery: charge / Grid: return
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
stat_rate: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for saving grid power sources.
|
||||
* Core requires EITHER stat_rate (legacy) OR power_config (new format).
|
||||
* When reading from backend, stat_rate is always populated.
|
||||
*/
|
||||
export type GridPowerSourceInput = Omit<
|
||||
GridPowerSourceEnergyPreference,
|
||||
"stat_rate"
|
||||
> & {
|
||||
stat_rate?: string;
|
||||
};
|
||||
|
||||
export interface GridSourceTypeEnergyPreference {
|
||||
type: "grid";
|
||||
|
||||
@ -159,6 +178,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
@ -491,6 +511,15 @@ 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, [
|
||||
@ -599,6 +628,7 @@ const getEnergyData = async (
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
powerStatsHour,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@ -607,12 +637,44 @@ 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 };
|
||||
|
||||
@ -13,10 +13,13 @@ export interface SidebarFrontendUserData {
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
default_panel?: string;
|
||||
onboarded_version?: string;
|
||||
onboarded_date?: string;
|
||||
}
|
||||
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
welcome_banner_dismissed?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export interface LovelaceInfo {
|
||||
resource_mode: "yaml" | "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceResource {
|
||||
id: string;
|
||||
type: "css" | "js" | "module" | "html";
|
||||
@ -42,3 +46,8 @@ export const deleteResource = (hass: HomeAssistant, id: string) =>
|
||||
type: "lovelace/resources/delete",
|
||||
resource_id: id,
|
||||
});
|
||||
|
||||
export const fetchLovelaceInfo = (hass: HomeAssistant): Promise<LovelaceInfo> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/info",
|
||||
});
|
||||
|
||||
@ -15,16 +15,26 @@ import type { LocalizeKeys } from "../common/translations/localize";
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "home";
|
||||
|
||||
export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean =>
|
||||
Boolean(hass.panels.lovelace?.config);
|
||||
|
||||
export const getLegacyDefaultPanelUrlPath = (): string | null => {
|
||||
const defaultPanel = window.localStorage.getItem("defaultPanel");
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : null;
|
||||
};
|
||||
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.default_panel ||
|
||||
hass.systemData?.default_panel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
|
||||
const defaultPanel =
|
||||
hass.userData?.default_panel ||
|
||||
hass.systemData?.default_panel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
// If default panel is lovelace and no old overview exists, fall back to home
|
||||
if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) {
|
||||
return DEFAULT_PANEL;
|
||||
}
|
||||
return defaultPanel;
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
import {
|
||||
mdiKeyboard,
|
||||
mdiNavigationVariant,
|
||||
mdiPuzzle,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import {
|
||||
filterNavigationPages,
|
||||
type NavigationFilterOptions,
|
||||
} from "../common/config/filter_navigation_pages";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HassioAddonInfo } from "./hassio/addon";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
|
||||
export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
@ -27,6 +29,7 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
export interface BaseNavigationCommand {
|
||||
path: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
icon_path?: string;
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
@ -45,11 +48,14 @@ export interface NavigationInfo extends PageNavigation {
|
||||
const generateNavigationPanelCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
panels: HomeAssistant["panels"],
|
||||
addons?: HassioAddonInfo[]
|
||||
apps?: HassioAddonInfo[]
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.entries(panels)
|
||||
.filter(
|
||||
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
([panelKey]) =>
|
||||
panelKey !== "_my_redirect" &&
|
||||
panelKey !== "hassio" &&
|
||||
panelKey !== "app"
|
||||
)
|
||||
.map(([_panelKey, panel]) => {
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
@ -59,12 +65,10 @@ const generateNavigationPanelCommands = (
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (addons) {
|
||||
const addon = addons.find(({ slug }) => slug === panel.url_path);
|
||||
if (addon) {
|
||||
image = addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined;
|
||||
if (apps) {
|
||||
const app = apps.find(({ slug }) => slug === panel.url_path);
|
||||
if (app) {
|
||||
image = app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,33 +102,30 @@ const getNavigationInfoFromConfig = (
|
||||
};
|
||||
|
||||
const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
hass: HomeAssistant,
|
||||
filterOptions: NavigationFilterOptions = {}
|
||||
): BaseNavigationCommand[] => {
|
||||
if (!hass.user?.is_admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: NavigationInfo[] = [];
|
||||
const allPages = Object.values(configSections).flat();
|
||||
const visiblePages = filterNavigationPages(hass, allPages, filterOptions);
|
||||
|
||||
Object.values(configSections).forEach((sectionPages) => {
|
||||
sectionPages.forEach((page) => {
|
||||
if (!canShowPage(hass, page)) {
|
||||
return;
|
||||
}
|
||||
for (const page of visiblePages) {
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (items.some((e) => e.path === info.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (items.some((e) => e.path === info.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
});
|
||||
});
|
||||
items.push(info);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
@ -140,7 +141,7 @@ const finalizeNavigationCommands = (
|
||||
return {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
icon_path: item.iconPath || mdiNavigationVariant,
|
||||
secondary,
|
||||
secondary: item.secondary || secondary,
|
||||
sorting_label: `${item.primary}_${secondary}`,
|
||||
...item,
|
||||
};
|
||||
@ -148,41 +149,42 @@ const finalizeNavigationCommands = (
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant,
|
||||
addons?: HassioAddonInfo[]
|
||||
apps?: HassioAddonInfo[],
|
||||
filterOptions: NavigationFilterOptions = {}
|
||||
): NavigationComboBoxItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(
|
||||
hass.localize,
|
||||
hass.panels,
|
||||
addons
|
||||
apps
|
||||
);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
|
||||
const sectionItems = generateNavigationConfigSectionCommands(
|
||||
hass,
|
||||
filterOptions
|
||||
);
|
||||
const appItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
appItems.push({
|
||||
path: "/config/apps/available",
|
||||
icon_path: mdiStorePlus,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_store"
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_store"
|
||||
),
|
||||
iconColor: "#F1C447",
|
||||
});
|
||||
supervisorItems.push({
|
||||
path: "/hassio/dashboard",
|
||||
icon_path: mdiPuzzle,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
|
||||
),
|
||||
});
|
||||
if (addons) {
|
||||
for (const addon of addons.filter((a) => a.version)) {
|
||||
supervisorItems.push({
|
||||
path: `/hassio/addon/${addon.slug}`,
|
||||
image: addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined,
|
||||
if (apps) {
|
||||
for (const app of apps.filter((a) => a.version)) {
|
||||
appItems.push({
|
||||
path: `/config/app/${app.slug}`,
|
||||
image: app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_info",
|
||||
{ addon: addon.name }
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_info",
|
||||
{ app: app.name }
|
||||
),
|
||||
secondary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.app_settings"
|
||||
),
|
||||
iconColor: "#F1C447",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -201,7 +203,7 @@ export const generateNavigationCommands = (
|
||||
return finalizeNavigationCommands(hass.localize, [
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...supervisorItems,
|
||||
...appItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
};
|
||||
@ -265,16 +267,16 @@ const generateServerControlCommands = (
|
||||
|
||||
return serverActions.map((action, index) => {
|
||||
const primary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"ui.dialogs.quick-bar.commands.home_assistant_control.perform_action",
|
||||
{
|
||||
action: hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
`ui.dialogs.quick-bar.commands.home_assistant_control.${action}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const secondary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.server_control"
|
||||
"ui.dialogs.quick-bar.commands.types.home_assistant_control"
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -13,7 +13,6 @@ 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";
|
||||
@ -296,17 +295,11 @@ class EntityPreviewRow extends LitElement {
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
naturalMenuWidth
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
})) || []}
|
||||
>
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option}>
|
||||
${this.hass!.formatEntityState(stateObj, option)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -91,9 +91,11 @@ 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
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -95,9 +95,11 @@ 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
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -86,9 +86,11 @@ 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
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,23 +1,18 @@
|
||||
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { 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, query, state } from "lit/decorators";
|
||||
import { customElement, property, 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-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
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";
|
||||
@ -28,23 +23,25 @@ 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._dialog?.close();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._stateObj = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@ -65,50 +62,33 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
.hass=${this.hass}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<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>
|
||||
<div class="options">
|
||||
${supportsTones
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.components.siren.tone")}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleToneChange}
|
||||
@selected=${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}
|
||||
@ -153,17 +133,21 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
</ha-control-button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev) {
|
||||
this._tone = ev.target.value;
|
||||
private _handleToneChange(ev: CustomEvent<{ value: string }>) {
|
||||
this._tone = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleVolumeChange(ev) {
|
||||
|
||||
@ -8,9 +8,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";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
@ -29,6 +27,7 @@ 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";
|
||||
|
||||
@ -169,182 +168,112 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleOperationModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${climateHvacModeIcon(stateObj.state)}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.hvac_modes
|
||||
.options=${stateObj.attributes.hvac_modes
|
||||
.concat()
|
||||
.sort(compareClimateHvacModes)
|
||||
.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>
|
||||
`
|
||||
)}
|
||||
.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>
|
||||
</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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handlePresetmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@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,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${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-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleFanModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@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,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${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-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
</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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleSwingmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@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,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${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-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@ -352,52 +281,34 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleSwingHorizontalmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${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>
|
||||
`
|
||||
@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,
|
||||
},
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@ -410,8 +321,8 @@ class MoreInfoClimate extends LitElement {
|
||||
this._mainControl = ev.currentTarget.control;
|
||||
}
|
||||
|
||||
private _handleFanModeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleFanModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.fan_mode,
|
||||
newVal,
|
||||
@ -420,15 +331,15 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleOperationModeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
|
||||
hvac_mode: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSwingmodeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleSwingmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.swing_mode,
|
||||
newVal,
|
||||
@ -437,8 +348,8 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSwingHorizontalmodeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleSwingHorizontalmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.swing_horizontal_mode,
|
||||
newVal,
|
||||
@ -447,8 +358,8 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handlePresetmodeChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handlePresetmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value || null;
|
||||
if (newVal) {
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.preset_mode,
|
||||
|
||||
@ -9,7 +9,6 @@ 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";
|
||||
@ -31,6 +30,7 @@ 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) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleDirection(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this.stateObj?.attributes.direction;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
@ -60,8 +60,8 @@ class MoreInfoFan extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handlePresetMode(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handlePresetMode(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this._presetMode;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
@ -73,8 +73,8 @@ class MoreInfoFan extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleOscillating(ev) {
|
||||
const newVal = ev.target.value === "true";
|
||||
private _handleOscillating(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value === "true";
|
||||
const oldVal = this.stateObj?.attributes.oscillating;
|
||||
|
||||
if (oldVal === newVal) return;
|
||||
@ -176,65 +176,64 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handlePresetMode}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${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>
|
||||
`
|
||||
@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,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleDirection}
|
||||
@closed=${stopPropagation}
|
||||
@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,
|
||||
}))}
|
||||
>
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
@ -243,40 +242,13 @@ 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"
|
||||
@ -285,37 +257,26 @@ class MoreInfoFan extends LitElement {
|
||||
? "true"
|
||||
: "false"}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleOscillating}
|
||||
@closed=${stopPropagation}
|
||||
@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,
|
||||
}))}
|
||||
>
|
||||
<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}
|
||||
|
||||
@ -2,7 +2,6 @@ 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";
|
||||
@ -14,6 +13,7 @@ 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,68 +74,48 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleStateChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleStateChanged}
|
||||
.options=${["off", "on"].map((fanState) => ({
|
||||
value: fanState,
|
||||
label: this.stateObj
|
||||
? this.hass.formatEntityState(this.stateObj, fanState)
|
||||
: fanState,
|
||||
}))}
|
||||
>
|
||||
<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}
|
||||
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!,
|
||||
@wa-select=${this._handleModeChanged}
|
||||
.options=${stateObj.attributes.available_modes?.map((mode) => ({
|
||||
value: mode,
|
||||
label: stateObj
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
)
|
||||
: mode,
|
||||
attributeIcon: stateObj
|
||||
? {
|
||||
stateObj,
|
||||
attribute: "mode",
|
||||
attributeValue: mode,
|
||||
}
|
||||
: undefined,
|
||||
})) || []}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@ -143,8 +123,8 @@ class MoreInfoHumidifier extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleStateChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handleStateChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value || null;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.state,
|
||||
newVal,
|
||||
@ -153,8 +133,8 @@ class MoreInfoHumidifier extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleModeChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handleModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._mode = newVal;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.mode,
|
||||
|
||||
@ -9,7 +9,6 @@ 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";
|
||||
@ -38,6 +37,7 @@ 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,47 +253,35 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleEffect}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${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>
|
||||
`
|
||||
@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,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@ -317,8 +305,8 @@ class MoreInfoLight extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleEffect(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleEffect(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this._effect;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
@ -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,12 +202,11 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown>
|
||||
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
<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(
|
||||
@ -743,7 +742,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceChange(e: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleSourceChange(e: HaDropdownSelectEvent) {
|
||||
const source = e.detail.item.value;
|
||||
if (!source || this.stateObj!.attributes.source === source) {
|
||||
return;
|
||||
@ -755,7 +754,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeChange(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleSoundModeChange(ev: HaDropdownSelectEvent) {
|
||||
const soundMode = ev.detail.item.value;
|
||||
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
|
||||
return;
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
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 {
|
||||
@ -30,30 +28,24 @@ class MoreInfoRemote extends LitElement {
|
||||
)}
|
||||
.value=${stateObj.attributes.current_activity || ""}
|
||||
@selected=${this._handleActivityChanged}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
.options=${stateObj.attributes.activity_list?.map((activity) => ({
|
||||
value: activity,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"activity",
|
||||
activity
|
||||
),
|
||||
}))}
|
||||
>
|
||||
${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) {
|
||||
private _handleActivityChanged(ev: CustomEvent<{ value: string }>) {
|
||||
const oldVal = this.stateObj!.attributes.current_activity;
|
||||
const newVal = ev.target.value;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) {
|
||||
return;
|
||||
|
||||
@ -11,13 +11,11 @@ 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";
|
||||
@ -172,21 +170,17 @@ class MoreInfoVacuum extends LitElement {
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.value=${stateObj.attributes.fan_speed}
|
||||
@selected=${this._handleFanSpeedChanged}
|
||||
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>
|
||||
`
|
||||
.options=${stateObj.attributes.fan_speed_list!.map(
|
||||
(mode) => ({
|
||||
value: mode,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_speed",
|
||||
mode
|
||||
),
|
||||
})
|
||||
)}
|
||||
>
|
||||
</ha-select>
|
||||
<div
|
||||
style="justify-content: center; align-self: center; padding-top: 1.3em"
|
||||
@ -291,9 +285,9 @@ class MoreInfoVacuum extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFanSpeedChanged(ev) {
|
||||
private _handleFanSpeedChanged(ev: CustomEvent<{ value: string }>) {
|
||||
const oldVal = this.stateObj!.attributes.fan_speed;
|
||||
const newVal = ev.target.value;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) {
|
||||
return;
|
||||
|
||||
@ -2,9 +2,10 @@ 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";
|
||||
@ -16,7 +17,6 @@ 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,32 +74,25 @@ 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}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleOperationModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
|
||||
${stateObj.attributes.operation_list
|
||||
@wa-select=${this._handleOperationModeChanged}
|
||||
.options=${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>
|
||||
`
|
||||
)}
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityState(stateObj, mode),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "operation_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@ -112,31 +105,18 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
)}
|
||||
.value=${stateObj.attributes.away_mode}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleAwayModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAwayModeChanged}
|
||||
.options=${["on", "off"].map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"away_mode",
|
||||
mode
|
||||
),
|
||||
iconPath: mode === "on" ? mdiAccountArrowRight : mdiAccount,
|
||||
}))}
|
||||
>
|
||||
<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}
|
||||
@ -144,8 +124,8 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleOperationModeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.state,
|
||||
newVal,
|
||||
@ -156,8 +136,8 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleAwayModeChanged(ev) {
|
||||
const newVal = ev.target.value === "on";
|
||||
private _handleAwayModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value === "on";
|
||||
const oldVal = this.stateObj!.attributes.away_mode === "on";
|
||||
|
||||
this._callServiceHelper(oldVal, newVal, "set_away_mode", {
|
||||
|
||||
@ -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,11 +37,12 @@ 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";
|
||||
@ -317,7 +318,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._setView("related");
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "device":
|
||||
@ -770,7 +771,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
withViewTransition(() => {
|
||||
this.large = !this.large;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { NavigationFilterOptions } from "../../common/config/filter_navigation_pages";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
@ -15,6 +17,7 @@ import "../../components/ha-icon";
|
||||
import "../../components/ha-picker-combo-box";
|
||||
import type {
|
||||
HaPickerComboBox,
|
||||
PickerComboBoxIndexSelectedDetail,
|
||||
PickerComboBoxItem,
|
||||
} from "../../components/ha-picker-combo-box";
|
||||
import "../../components/ha-spinner";
|
||||
@ -48,8 +51,10 @@ import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../../resources/fuseMultiTerm";
|
||||
import { buttonLinkStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isIosApp } from "../../util/is_ios";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar";
|
||||
@ -64,7 +69,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _hint?: string;
|
||||
@state() private _showHint = false;
|
||||
|
||||
@state() private _selectedSection?: QuickBarSection;
|
||||
|
||||
@ -80,8 +85,12 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _addons?: HassioAddonInfo[];
|
||||
|
||||
private _navigationFilterOptions: NavigationFilterOptions = {};
|
||||
|
||||
private _translationsLoaded = false;
|
||||
|
||||
private _itemSelected = false;
|
||||
|
||||
// #region lifecycle
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
if (!this._translationsLoaded) {
|
||||
@ -90,7 +99,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
this._initialize();
|
||||
this._selectedSection = params.mode;
|
||||
this._hint = params.hint;
|
||||
this._showHint = params.showHint ?? false;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@ -104,6 +113,12 @@ export class QuickBar extends LitElement {
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
// Derive Bluetooth config entries status for navigation filtering
|
||||
this._navigationFilterOptions = {
|
||||
hasBluetoothConfigEntries: configEntries.some(
|
||||
(entry) => entry.domain === "bluetooth"
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching config entries for quick bar", err);
|
||||
@ -152,15 +167,28 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = undefined;
|
||||
this._opened = false;
|
||||
this._open = false;
|
||||
this._itemSelected = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
};
|
||||
|
||||
// fallback in case the closed event is not fired
|
||||
private _dialogCloseStarted = () => {
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this._opened) {
|
||||
this._dialogClosed();
|
||||
}
|
||||
},
|
||||
350 // close animation timeout is 300ms
|
||||
);
|
||||
};
|
||||
|
||||
// #endregion lifecycle
|
||||
|
||||
// #region render
|
||||
|
||||
protected render() {
|
||||
if (!this._open) {
|
||||
if (!this._open && !this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@ -210,6 +238,7 @@ export class QuickBar extends LitElement {
|
||||
hideActions
|
||||
@wa-show=${this._showTriggered}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-hide=${this._dialogCloseStarted}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${!this._loading && this._opened
|
||||
@ -230,9 +259,17 @@ export class QuickBar extends LitElement {
|
||||
clearable
|
||||
></ha-picker-combo-box>`
|
||||
: nothing}
|
||||
${this._hint
|
||||
${this._showHint
|
||||
? html`<ha-tip slot="footer" .hass=${this.hass}
|
||||
>${this._hint}</ha-tip
|
||||
>${this.hass.localize("ui.tips.key_shortcut_quick_search", {
|
||||
keyboard_shortcut: html`<button
|
||||
class="link"
|
||||
@click=${this._openShortcutDialog}
|
||||
>
|
||||
${this.hass.localize("ui.tips.keyboard_shortcut")}
|
||||
</button>`,
|
||||
modifier: isMac ? "⌘" : "Ctrl",
|
||||
})}</ha-tip
|
||||
>`
|
||||
: nothing}
|
||||
</ha-adaptive-dialog>
|
||||
@ -281,6 +318,9 @@ export class QuickBar extends LitElement {
|
||||
slot="start"
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.image}
|
||||
style=${"iconColor" in item && item.iconColor
|
||||
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
|
||||
: ""}
|
||||
/>
|
||||
`
|
||||
: item.icon
|
||||
@ -393,7 +433,8 @@ export class QuickBar extends LitElement {
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
this.hass,
|
||||
this._addons
|
||||
this._addons,
|
||||
this._navigationFilterOptions
|
||||
).sort(this._sortBySortingLabel);
|
||||
|
||||
if (filter) {
|
||||
@ -559,7 +600,11 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
|
||||
private _generateNavigationCommandsMemoized = memoizeOne(
|
||||
generateNavigationCommands
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
apps: HassioAddonInfo[] | undefined,
|
||||
filterOptions: NavigationFilterOptions
|
||||
) => generateNavigationCommands(hass, apps, filterOptions)
|
||||
);
|
||||
|
||||
private _generateActionCommandsMemoized = memoizeOne(generateActionCommands);
|
||||
@ -613,13 +658,29 @@ export class QuickBar extends LitElement {
|
||||
|
||||
// #region interaction
|
||||
|
||||
private async _handleItemSelected(ev: CustomEvent<{ index: number }>) {
|
||||
if (this._comboBox && this._comboBox.virtualizerElement) {
|
||||
const index = ev.detail.index;
|
||||
private _navigate(path: string, newTab = false) {
|
||||
if (newTab) {
|
||||
window.open(path, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(path);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleItemSelected(
|
||||
ev: CustomEvent<PickerComboBoxIndexSelectedDetail>
|
||||
) {
|
||||
if (
|
||||
!this._itemSelected &&
|
||||
this._comboBox &&
|
||||
this._comboBox.virtualizerElement
|
||||
) {
|
||||
const { index, newTab } = ev.detail;
|
||||
const item = this._comboBox.virtualizerElement.items[
|
||||
index
|
||||
] as PickerComboBoxItem;
|
||||
|
||||
this._itemSelected = true;
|
||||
|
||||
// entity selected
|
||||
if (item && "stateObj" in item) {
|
||||
this.closeDialog();
|
||||
@ -631,15 +692,17 @@ export class QuickBar extends LitElement {
|
||||
|
||||
// device selected
|
||||
if (item && item.id.startsWith(`device${SEPARATOR}`)) {
|
||||
const path = `/config/devices/device/${item.id.split(SEPARATOR)[1]}`;
|
||||
this.closeDialog();
|
||||
navigate(`/config/devices/device/${item.id.split(SEPARATOR)[1]}`);
|
||||
this._navigate(path, newTab);
|
||||
return;
|
||||
}
|
||||
|
||||
// area selected
|
||||
if (item && item.id.startsWith(`area${SEPARATOR}`)) {
|
||||
const path = `/config/areas/area/${item.id.split(SEPARATOR)[1]}`;
|
||||
this.closeDialog();
|
||||
navigate(`/config/areas/area/${item.id.split(SEPARATOR)[1]}`);
|
||||
this._navigate(path, newTab);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -693,53 +756,65 @@ export class QuickBar extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate((item as NavigationComboBoxItem).path);
|
||||
const path = (item as NavigationComboBoxItem).path;
|
||||
this._navigate(path, newTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _openShortcutDialog(ev: Event): void {
|
||||
ev.preventDefault();
|
||||
showShortcutsDialog(this);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
// #endregion interaction
|
||||
|
||||
// #region styles
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--dialog-surface-margin-top: var(--ha-space-10);
|
||||
--ha-dialog-min-height: 620px;
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100vh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100dvh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-max-height: calc(
|
||||
100vh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-max-height: calc(
|
||||
100dvh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--dialog-content-padding: 0;
|
||||
--safe-area-inset-bottom: 0px;
|
||||
}
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
:host {
|
||||
--dialog-surface-margin-top: var(--ha-space-10);
|
||||
--ha-dialog-min-height: 620px;
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100vh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-height: calc(
|
||||
100dvh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-max-height: calc(
|
||||
100vh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--ha-bottom-sheet-max-height: calc(
|
||||
100dvh - max(var(--safe-area-inset-top), 48px)
|
||||
);
|
||||
--dialog-content-padding: 0;
|
||||
--safe-area-inset-bottom: 0px;
|
||||
}
|
||||
|
||||
ha-tip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
ha-tip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
ha-tip a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-tip a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 690px) {
|
||||
ha-tip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@media all and (max-width: 450px), all and (max-height: 690px) {
|
||||
ha-tip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// #endregion styles
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { closeDialog } from "../make-dialog-manager";
|
||||
|
||||
export type QuickBarSection =
|
||||
| "entity"
|
||||
@ -10,7 +11,7 @@ export type QuickBarSection =
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
hint?: string;
|
||||
showHint?: boolean;
|
||||
}
|
||||
|
||||
export const loadQuickBar = () => import("./ha-quick-bar");
|
||||
@ -26,3 +27,7 @@ export const showQuickBar = (
|
||||
addHistory: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const closeQuickBar = (): void => {
|
||||
closeDialog("ha-quick-bar");
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user