mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-05 12:17:07 -06:00
Compare commits
2 Commits
dev
...
20260128.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c658eb414b | ||
|
|
bac493b72b |
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@ -251,6 +251,7 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
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@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
|
||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
||||
@ -51,8 +51,8 @@ const compressAppOtherZopfli = () =>
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
compressAppModernBrotli,
|
||||
compressAppOtherBrotli,
|
||||
// compressAppModernBrotli,
|
||||
// compressAppOtherBrotli,
|
||||
compressAppModernZopfli,
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
|
||||
@ -9,14 +9,11 @@ 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";
|
||||
@ -63,9 +60,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let sidebarChangeCallback;
|
||||
let changeFunction;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
sidebarChangeCallback?.({
|
||||
changeFunction?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@ -14,34 +16,15 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// 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) => {
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@ -29,7 +29,6 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@ -7,18 +7,8 @@ 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,
|
||||
result: msg.template,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@ -10,9 +10,7 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@ -100,6 +100,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
51
package.json
51
package.json
@ -27,25 +27,25 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@codemirror/view": "6.39.11",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
"@formatjs/intl-listformat": "8.2.1",
|
||||
"@formatjs/intl-locale": "5.2.1",
|
||||
"@formatjs/intl-numberformat": "9.2.2",
|
||||
"@formatjs/intl-pluralrules": "6.2.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.2",
|
||||
"@formatjs/intl-datetimeformat": "7.2.0",
|
||||
"@formatjs/intl-displaynames": "7.2.0",
|
||||
"@formatjs/intl-durationformat": "0.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.1",
|
||||
"@formatjs/intl-pluralrules": "6.2.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.1",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@ -71,6 +71,7 @@
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-menu": "0.27.0",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-snackbar": "0.27.0",
|
||||
@ -88,7 +89,7 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@ -111,7 +112,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@ -121,7 +122,7 @@
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
@ -145,17 +146,17 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.8",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.0",
|
||||
"@rspack/core": "1.7.3",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@ -198,7 +199,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "27.4.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@ -210,11 +211,11 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.7",
|
||||
"tar": "7.5.6",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"typescript-eslint": "8.53.1",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@ -228,7 +229,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.3.0",
|
||||
"globals": "17.1.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
|
||||
|
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.0"
|
||||
version = "20260128.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
@ -12,7 +12,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||
]
|
||||
requires-python = ">=3.14.0"
|
||||
requires-python = ">=3.13.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
@ -16,12 +16,6 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||
nvm uninstall "$nodeCurrent"
|
||||
fi
|
||||
|
||||
# install yarn if not already available
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
npm install -g corepack
|
||||
yes | yarn
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
|
||||
@ -194,6 +194,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
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,9 +8,7 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
const validState = states.some(
|
||||
(stateObj) => !isUnavailableState(stateObj.state)
|
||||
);
|
||||
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
|
||||
|
||||
if (!validState) {
|
||||
return UNAVAILABLE;
|
||||
|
||||
@ -16,7 +16,6 @@ export interface ShortcutConfig {
|
||||
* Default is false to avoid interrupting copy/paste.
|
||||
*/
|
||||
allowWhenTextSelected?: boolean;
|
||||
allowInInput?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,10 +29,7 @@ function registerShortcuts(
|
||||
|
||||
Object.entries(shortcuts).forEach(([key, config]) => {
|
||||
wrappedShortcuts[key] = (event: KeyboardEvent) => {
|
||||
if (
|
||||
!config.allowInInput &&
|
||||
!canOverrideAlphanumericInput(event.composedPath())
|
||||
) {
|
||||
if (!canOverrideAlphanumericInput(event.composedPath())) {
|
||||
return;
|
||||
}
|
||||
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
let isViewTransitionDisabled = false;
|
||||
try {
|
||||
isViewTransitionDisabled =
|
||||
window.localStorage.getItem("disableViewTransition") === "true";
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
export const setViewTransitionDisabled = (disabled: boolean): void => {
|
||||
isViewTransitionDisabled = disabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
@ -26,7 +14,7 @@ export const setViewTransitionDisabled = (disabled: boolean): void => {
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void
|
||||
): Promise<void> => {
|
||||
if (!document.startViewTransition || isViewTransitionDisabled) {
|
||||
if (!document.startViewTransition) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
@ -28,7 +27,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
@ -94,18 +92,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
|
||||
private _suspendResize = false;
|
||||
|
||||
private _layoutTransitionActive = false;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => {
|
||||
if (this.chart) {
|
||||
if (this._suspendResize) {
|
||||
this._shouldResizeChart = true;
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
this._shouldResizeChart = true;
|
||||
} else {
|
||||
@ -123,11 +113,8 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
private _pendingSetup = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
@ -139,13 +126,7 @@ export class HaChartBase extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._pendingSetup = true;
|
||||
afterNextRender(() => {
|
||||
if (this.isConnected && this._pendingSetup) {
|
||||
this._pendingSetup = false;
|
||||
this._setupChart();
|
||||
}
|
||||
});
|
||||
this._setupChart();
|
||||
}
|
||||
|
||||
this._listeners.push(
|
||||
@ -200,26 +181,6 @@ export class HaChartBase extends LitElement {
|
||||
() => window.removeEventListener("keyup", handleKeyUp)
|
||||
);
|
||||
}
|
||||
|
||||
const handleLayoutTransition: EventListener = (ev) => {
|
||||
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
|
||||
this._layoutTransitionActive = Boolean(event.detail?.active);
|
||||
this.toggleAttribute(
|
||||
"layout-transition-active",
|
||||
this._layoutTransitionActive
|
||||
);
|
||||
this._suspendResize = this._layoutTransitionActive;
|
||||
if (!this._suspendResize) {
|
||||
this._resizeChartIfNeeded();
|
||||
}
|
||||
};
|
||||
window.addEventListener("hass-layout-transition", handleLayoutTransition);
|
||||
this._listeners.push(() =>
|
||||
window.removeEventListener(
|
||||
"hass-layout-transition",
|
||||
handleLayoutTransition
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
@ -1027,29 +988,19 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
this._resizeChartIfNeeded();
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize({
|
||||
animation:
|
||||
this._reducedMotion ||
|
||||
typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private _resizeChartIfNeeded() {
|
||||
if (!this.chart || !this._shouldResizeChart) {
|
||||
return;
|
||||
}
|
||||
if (this._suspendResize) {
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
return;
|
||||
}
|
||||
this.chart.resize({
|
||||
animation:
|
||||
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
@ -1071,18 +1022,11 @@ export class HaChartBase extends LitElement {
|
||||
display: block;
|
||||
position: relative;
|
||||
letter-spacing: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
:host([layout-transition-active]),
|
||||
:host([layout-transition-active]) .container,
|
||||
:host([layout-transition-active]) .chart-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.container.has-height {
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
@ -1090,7 +1034,6 @@ export class HaChartBase extends LitElement {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
overflow: visible;
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
|
||||
@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@ -716,18 +716,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// Add an entry for final values
|
||||
pushData(endTime, prevValues);
|
||||
|
||||
// For sensors, append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 1s of leeway for "now"
|
||||
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
|
||||
if (domain === "sensor" && isUpToNow && data.length === 1) {
|
||||
const stateObj = this.hass.states[states.entity_id];
|
||||
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
|
||||
if (currentValue !== null) {
|
||||
data[0].data!.push([now, currentValue]);
|
||||
}
|
||||
}
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(datasets, data);
|
||||
});
|
||||
|
||||
@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-reset-button", type: Boolean })
|
||||
public hideResetButton?: boolean;
|
||||
|
||||
@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property({ attribute: false }) public hoursToShow?: number;
|
||||
@property({ attribute: false, type: Number }) public hoursToShow?: number;
|
||||
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
|
||||
@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Array })
|
||||
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
|
||||
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@ -605,57 +605,6 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
const isUpToNow = now.getTime() - endTime.getTime() <= 600000;
|
||||
if (isUpToNow) {
|
||||
// Skip external statistics (they have ":" in the ID)
|
||||
if (!statistic_id.includes(":")) {
|
||||
const stateObj = this.hass.states[statistic_id];
|
||||
if (stateObj) {
|
||||
const currentValue = parseFloat(stateObj.state);
|
||||
if (
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
if (type === "sum" || type === "change") {
|
||||
// Skip cumulative types - need special calculation
|
||||
val.push(null);
|
||||
} else if (
|
||||
type === bandTop &&
|
||||
this.chartType === "line" &&
|
||||
drawBands &&
|
||||
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
||||
) {
|
||||
// For band chart, current value is both min and max, so diff is 0
|
||||
val.push(0);
|
||||
val.push(currentValue);
|
||||
} else {
|
||||
val.push(currentValue);
|
||||
}
|
||||
statDataSets[i].data!.push(
|
||||
this._transformDataValue([now, ...val])
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(totalDataSets, statDataSets);
|
||||
Array.prototype.push.apply(legendData, statLegendData);
|
||||
|
||||
@ -130,9 +130,9 @@ export class HaDataTable extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: String }) public id = "id";
|
||||
|
||||
@property({ attribute: false }) public noDataText?: string;
|
||||
@property({ attribute: false, type: String }) public noDataText?: string;
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
@property({ attribute: false, type: String }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
@ -192,7 +192,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: ValueChangedEvent<string>) {
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
|
||||
@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
|
||||
@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public reorder = false;
|
||||
|
||||
@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
|
||||
@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Array })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
|
||||
@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ attribute: false }) public statisticIds?: string[];
|
||||
@property({ attribute: false, type: Array }) public statisticIds?: string[];
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@ -48,6 +48,7 @@ export class HaAnsiToHtml extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
pre.wrap {
|
||||
|
||||
@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
public disableSpeech = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ type: Boolean, attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@ -2,12 +2,14 @@ import type { PropertyValueMap } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import type { AssistPipeline } from "../data/assist_pipeline";
|
||||
import { listAssistPipelines } from "../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@ -39,31 +41,6 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const value = this.value ?? this._default;
|
||||
const options: HaSelectOption[] = [
|
||||
{
|
||||
value: PREFERRED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.includeLastUsed) {
|
||||
options.unshift({
|
||||
value: LAST_USED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
|
||||
});
|
||||
}
|
||||
|
||||
options.push(
|
||||
...this._pipelines.map((pipeline) => ({
|
||||
value: pipeline.id,
|
||||
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
|
||||
}))
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -72,8 +49,33 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${this.includeLastUsed
|
||||
? html`
|
||||
<ha-list-item .value=${LAST_USED}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.pipeline-picker.last_used"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: null}
|
||||
<ha-list-item .value=${PREFERRED}>
|
||||
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
})}
|
||||
</ha-list-item>
|
||||
${this._pipelines.map(
|
||||
(pipeline) =>
|
||||
html`<ha-list-item .value=${pipeline.id}>
|
||||
${pipeline.name}
|
||||
(${formatLanguageCode(pipeline.language, this.hass.locale)})
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -94,17 +96,17 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === this._default)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === this._default ? undefined : value;
|
||||
this.value = target.value === this._default ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
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";
|
||||
@ -259,10 +260,14 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
.name=${"amPm"}
|
||||
name="amPm"
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._valueChanged}
|
||||
.options=${["AM", "PM"]}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-list-item value="AM">AM</ha-list-item>
|
||||
<ha-list-item value="PM">PM</ha-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper
|
||||
@ -277,12 +282,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm"
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
textField.name === "amPm" ? textField.value : Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
@ -363,6 +366,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
ha-textfield:last-child {
|
||||
--text-field-border-top-right-radius: var(--mdc-shape-medium);
|
||||
}
|
||||
ha-select {
|
||||
--mdc-shape-small: 0;
|
||||
width: 85px;
|
||||
}
|
||||
:host([clearable]) .mdc-select__anchor {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
|
||||
import { fetchBlueprints } from "../data/blueprint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
@ -54,16 +55,20 @@ class HaBluePrintPicker extends LitElement {
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._blueprintChanged}
|
||||
.options=${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => ({
|
||||
value: blueprint.path,
|
||||
label: blueprint.name,
|
||||
})
|
||||
)}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<ha-list-item .value=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -77,8 +82,8 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.target.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
|
||||
@ -51,7 +51,6 @@ 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))
|
||||
@ -60,7 +59,6 @@ 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,31 +1,24 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-icon";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon";
|
||||
import type { HaIcon } from "./ha-icon";
|
||||
import "./ha-ripple";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
attributeIcon?: {
|
||||
stateObj: HassEntity;
|
||||
attribute: string;
|
||||
attributeValue?: string;
|
||||
};
|
||||
}
|
||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||
import "./ha-menu";
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
export class HaControlSelectMenu extends SelectBase {
|
||||
@query(".select") protected mdcRoot!: HTMLElement;
|
||||
|
||||
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow = false;
|
||||
@ -33,83 +26,95 @@ export class HaControlSelectMenu extends LitElement {
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
@property() public options;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
@property()
|
||||
public label?: string;
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
@property({ attribute: false }) public options: SelectOption[] = [];
|
||||
|
||||
@query("button") private _triggerButton!: HTMLButtonElement;
|
||||
|
||||
public override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderTrigger();
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
|
||||
${this._renderTrigger()} ${this.options.map(this._renderOption)}
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTrigger() {
|
||||
const selectedText = this.getValueObject(this.options, this.value)?.label;
|
||||
|
||||
public override render() {
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
"select-required": this.required,
|
||||
"select-no-value": !selectedText,
|
||||
"select-invalid": !this.isUiValid,
|
||||
"select-no-value": !this.selectedText,
|
||||
};
|
||||
|
||||
return html`<button
|
||||
?disabled=${this.disabled}
|
||||
slot="trigger"
|
||||
class="select-anchor ${classMap(classes)}"
|
||||
>
|
||||
${this._renderIcon()}
|
||||
<div class="content">
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
: html`<p id="label" class="label">${this.label}</p>`}
|
||||
${selectedText ? html`<p class="value">${selectedText}</p>` : nothing}
|
||||
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
||||
const labelAttribute =
|
||||
this.label && this.hideLabel ? this.label : undefined;
|
||||
|
||||
return html`
|
||||
<div class="select ${classMap(classes)}">
|
||||
<input
|
||||
class="formElement"
|
||||
.name=${this.name}
|
||||
.value=${this.value}
|
||||
hidden
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
/>
|
||||
<!-- @ts-ignore -->
|
||||
<div
|
||||
class="select-anchor"
|
||||
aria-autocomplete="none"
|
||||
role="combobox"
|
||||
aria-expanded=${this.menuOpen}
|
||||
aria-invalid=${!this.isUiValid}
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby=${ifDefined(labelledby)}
|
||||
aria-label=${ifDefined(labelAttribute)}
|
||||
aria-required=${this.required}
|
||||
aria-controls="listbox"
|
||||
@focus=${this.onFocus}
|
||||
@blur=${this.onBlur}
|
||||
@click=${this.onClick}
|
||||
@keydown=${this.onKeydown}
|
||||
>
|
||||
${this._renderIcon()}
|
||||
<div class="content">
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
: html`<p id="label" class="label">${this.label}</p>`}
|
||||
${this.selectedText
|
||||
? html`<p class="value">${this.selectedText}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderArrow()}
|
||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>
|
||||
${this._renderArrow()}
|
||||
</button>`;
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
class=${this.value === option.value ? "selected" : ""}
|
||||
>${option.iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
|
||||
: option.attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${option.attributeIcon.stateObj}
|
||||
.attribute=${option.attributeIcon.attribute}
|
||||
.attributeValue=${option.attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: nothing}
|
||||
${option.label}</ha-dropdown-item
|
||||
>`;
|
||||
protected override renderMenu() {
|
||||
const classes = this.getMenuClasses();
|
||||
return html`<ha-menu
|
||||
innerRole="listbox"
|
||||
wrapFocus
|
||||
class=${classMap(classes)}
|
||||
activatable
|
||||
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
|
||||
.open=${this.menuOpen}
|
||||
.anchor=${this.anchorElement}
|
||||
.fixed=${this.fixedMenuPosition}
|
||||
@selected=${this.onSelected}
|
||||
@opened=${this.onOpened}
|
||||
@closed=${this.onClosed}
|
||||
@items-updated=${this.onItemsUpdated}
|
||||
@keydown=${this.handleTypeahead}
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
}
|
||||
|
||||
private _renderArrow() {
|
||||
if (!this.showArrow) {
|
||||
return nothing;
|
||||
}
|
||||
if (!this.showArrow) return nothing;
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
@ -119,42 +124,47 @@ export class HaControlSelectMenu extends LitElement {
|
||||
}
|
||||
|
||||
private _renderIcon() {
|
||||
const { iconPath, icon, attributeIcon } =
|
||||
this.getValueObject(this.options, this.value) ?? {};
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
const defaultIcon = this.querySelector("[slot='icon']");
|
||||
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
|
||||
| HaSvgIcon
|
||||
| HaIcon
|
||||
| null;
|
||||
|
||||
if (!defaultIcon && !icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
|
||||
: icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
: attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${attributeIcon.stateObj}
|
||||
.attribute=${attributeIcon.attribute}
|
||||
.attributeValue=${attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: defaultIcon
|
||||
? html`<slot name="icon"></slot>`
|
||||
: nothing}
|
||||
${icon && icon.localName === "ha-svg-icon" && "path" in icon
|
||||
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
|
||||
: icon && icon.localName === "ha-icon" && "icon" in icon
|
||||
? html`<ha-icon .path=${icon.icon}></ha-icon>`
|
||||
: html`<slot name="icon"></slot>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showDropdown() {
|
||||
this.style.setProperty(
|
||||
"--control-select-menu-width",
|
||||
`${this._triggerButton.offsetWidth}px`
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private getValueObject = memoizeOne(
|
||||
(options: SelectOption[], value?: string) =>
|
||||
options.find((option) => option.value === value)
|
||||
);
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
@ -176,8 +186,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
border: none;
|
||||
text-align: left;
|
||||
height: var(--control-select-menu-height);
|
||||
padding: var(--control-select-menu-padding);
|
||||
overflow: hidden;
|
||||
@ -203,12 +211,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
.select-anchor:hover {
|
||||
--control-select-menu-background-color: var(
|
||||
--ha-color-on-neutral-quiet
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -228,19 +230,16 @@ export class HaControlSelectMenu extends LitElement {
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.select-no-value .label {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.content .value {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
.select-anchor:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
|
||||
}
|
||||
@ -259,23 +258,10 @@ export class HaControlSelectMenu extends LitElement {
|
||||
opacity: var(--control-select-menu-background-opacity);
|
||||
}
|
||||
|
||||
.select-disabled.select-anchor {
|
||||
.select-disabled .select-anchor {
|
||||
cursor: not-allowed;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--control-select-menu-width);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement {
|
||||
@property({ type: Boolean }) public checked = false;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
|
||||
@property({ attribute: false }) pathOn?: string;
|
||||
@property({ attribute: false, type: String }) pathOn?: string;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ attribute: false }) pathOff?: string;
|
||||
@property({ attribute: false, type: String }) pathOff?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { ConfigEntry, SubEntry } from "../data/config_entries";
|
||||
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
||||
@ -13,8 +14,9 @@ import { fetchIntegrationManifest } from "../data/integration";
|
||||
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -71,35 +73,37 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._agents.map((agent) => ({
|
||||
value: agent.id,
|
||||
label: agent.name,
|
||||
disabled:
|
||||
agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0,
|
||||
}));
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize(
|
||||
"ui.components.conversation-agent-picker.none"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize(
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
"ui.components.coversation-agent-picker.conversation_agent"
|
||||
)}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.coversation-agent-picker.none"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._agents.map(
|
||||
(agent) =>
|
||||
html`<ha-list-item
|
||||
.value=${agent.id}
|
||||
.disabled=${agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0}
|
||||
>
|
||||
${agent.name}
|
||||
</ha-list-item>`
|
||||
)}</ha-select
|
||||
>${(this._subConfigEntry &&
|
||||
this._configEntry?.supported_subentry_types[
|
||||
this._subConfigEntry.subentry_type
|
||||
@ -234,17 +238,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
|
||||
@ -65,10 +65,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = false;
|
||||
|
||||
public open(): void {
|
||||
this._openPicker();
|
||||
}
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public minimal = false;
|
||||
@ -310,15 +306,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
return dateRangePicker.vueComponent.$children[0];
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
if (!this._dateRangePicker.open) {
|
||||
const datePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker div.date-range-inputs"
|
||||
) as any;
|
||||
datePicker?.click();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputClick() {
|
||||
// close the date picker, so it will open again on the click event
|
||||
if (this._dateRangePicker.open) {
|
||||
|
||||
@ -76,18 +76,6 @@ 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)
|
||||
@ -96,9 +84,9 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__scrim {
|
||||
background-color: var(--mdc-dialog-scrim-color, 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__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
|
||||
@ -4,18 +4,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-layout-transition": { active: boolean; reason?: string };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-layout-transition": HASSDomEvent<
|
||||
HASSDomEvents["hass-layout-transition"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
const blockingElements = (document as any).$blockingElements;
|
||||
|
||||
@ -27,30 +15,6 @@ export class HaDrawer extends DrawerBase {
|
||||
|
||||
private _rtlStyle?: HTMLElement;
|
||||
|
||||
private _sidebarTransitionActive = false;
|
||||
|
||||
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = true;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: true,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = false;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: false,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
protected createAdapter() {
|
||||
return {
|
||||
...super.createAdapter(),
|
||||
@ -99,38 +63,6 @@ export class HaDrawer extends DrawerBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
private async _setupSwipe() {
|
||||
const hammer = await import("../resources/hammer");
|
||||
this._mc = new hammer.Manager(document, {
|
||||
@ -158,16 +90,6 @@ export class HaDrawer extends DrawerBase {
|
||||
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: initial !important;
|
||||
transition-property: transform, width;
|
||||
transition-duration:
|
||||
var(--mdc-drawer-transition-duration, 0.2s),
|
||||
var(--ha-animation-duration-normal);
|
||||
transition-timing-function:
|
||||
var(
|
||||
--mdc-drawer-transition-timing-function,
|
||||
cubic-bezier(0.4, 0, 0.2, 1)
|
||||
),
|
||||
ease;
|
||||
}
|
||||
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
|
||||
z-index: 200;
|
||||
@ -181,15 +103,6 @@ export class HaDrawer extends DrawerBase {
|
||||
direction: var(--direction);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@ -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,7 +37,6 @@ export class HaDropdownItem extends DropdownItem {
|
||||
|
||||
#check {
|
||||
visibility: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
*/
|
||||
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: T };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
|
||||
@ -31,7 +31,6 @@ import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@ -175,7 +174,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: HaDropdownSelectEvent) {
|
||||
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
|
||||
@ -11,7 +11,8 @@ import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-picker-field";
|
||||
|
||||
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import type { HaDropdown } from "../ha-dropdown";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormMultiSelectData,
|
||||
@ -107,7 +108,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected _toggleItem(ev: HaDropdownSelectEvent) {
|
||||
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.preventDefault(); // keep the dropdown open
|
||||
const value = ev.detail.item.value;
|
||||
const action = (ev.detail.item as any).action;
|
||||
|
||||
@ -17,7 +17,6 @@ import type {
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@ -143,7 +142,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAddAction(ev: HaDropdownSelectEvent) {
|
||||
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
const action = ev.detail.item.value;
|
||||
this._displayActions = [...(this._displayActions ?? []), action];
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public formatOptions?: Intl.NumberFormatOptions;
|
||||
|
||||
@property({ attribute: false }) public valueText?: string;
|
||||
@property({ attribute: false, type: String }) public valueText?: string;
|
||||
|
||||
@property({ attribute: false }) public locale!: FrontendLocaleData;
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
|
||||
@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ type: Number, attribute: false })
|
||||
public renderSize?: number;
|
||||
|
||||
@property({ type: Array })
|
||||
public value?: [number, number];
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Number })
|
||||
public colorBrightness?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Number })
|
||||
public ww?: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Number })
|
||||
public minKelvin?: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Number })
|
||||
public maxKelvin?: number;
|
||||
|
||||
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||
|
||||
@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
|
||||
|
||||
@customElement("ha-icon-button-toolbar")
|
||||
export class HaIconButtonToolbar extends LitElement {
|
||||
@property({ attribute: false })
|
||||
@property({ type: Array, attribute: false })
|
||||
public items: (HaIconButtonToolbarItem | string)[] = [];
|
||||
|
||||
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
|
||||
|
||||
@ -3,7 +3,6 @@ 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";
|
||||
@ -40,10 +39,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed representation for small screens -->
|
||||
<ha-dropdown
|
||||
@wa-show=${this._handleIconOverflowMenuOpened}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<ha-dropdown @wa-show=${this._handleIconOverflowMenuOpened}>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
||||
@ -107,6 +107,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@state() _defaultLanguages: string[] = [];
|
||||
|
||||
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
|
||||
|
||||
@ -134,50 +134,15 @@ export class HaMarkdown extends LitElement {
|
||||
}
|
||||
table[role="presentation"] {
|
||||
--markdown-table-border-collapse: separate;
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-border-width: attr(border, 0);
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
vertical-align: top;
|
||||
}
|
||||
table[role="presentation"] td[valign="middle"],
|
||||
table[role="presentation"] th[valign="middle"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="bottom"],
|
||||
table[role="presentation"] th[valign="bottom"] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
table[role="presentation"] td[valign="baseline"],
|
||||
table[role="presentation"] th[valign="baseline"] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@supports (border-width: attr(border px, 0)) {
|
||||
table[role="presentation"] {
|
||||
--markdown-table-border-width: attr(border px, 0);
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
th {
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
td {
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
}
|
||||
table[role="presentation"][border="0"] {
|
||||
--markdown-table-border-width: 0;
|
||||
}
|
||||
table[role="presentation"][border="1"] {
|
||||
--markdown-table-border-width: 1px;
|
||||
}
|
||||
table[role="presentation"][border="2"] {
|
||||
--markdown-table-border-width: 2px;
|
||||
}
|
||||
table[role="presentation"][border="3"] {
|
||||
--markdown-table-border-width: 3px;
|
||||
}
|
||||
table {
|
||||
border-collapse: var(--markdown-table-border-collapse, collapse);
|
||||
|
||||
263
src/components/ha-md-dialog.ts
Normal file
263
src/components/ha-md-dialog.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import { Dialog } from "@material/web/dialog/internal/dialog";
|
||||
import { styles } from "@material/web/dialog/internal/dialog-styles";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay a dialog with another dialog
|
||||
Dialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as HaMdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends Dialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
private _handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: var(--ha-font-weight-normal);
|
||||
--md-dialog-headline-size: var(--ha-font-size-xl);
|
||||
--md-dialog-supporting-text-size: var(--ha-font-size-m);
|
||||
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, 0);
|
||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
||||
margin-left: var(--safe-area-inset-left, 0);
|
||||
margin-right: var(--safe-area-inset-right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow: var(--dialog-content-overflow, auto);
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
||||
45
src/components/ha-menu.ts
Normal file
45
src/components/ha-menu.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
|
||||
import { styles } from "@material/mwc-menu/mwc-menu.css";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "./ha-list";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MenuBase {
|
||||
protected get listElement() {
|
||||
if (!this.listElement_) {
|
||||
this.listElement_ = this.renderRoot.querySelector("ha-list");
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
protected renderList() {
|
||||
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
|
||||
const classes = this.renderListClasses();
|
||||
|
||||
return html`<ha-list
|
||||
rootTabbable
|
||||
.innerAriaLabel=${this.innerAriaLabel}
|
||||
.innerRole=${this.innerRole}
|
||||
.multi=${this.multi}
|
||||
class=${classMap(classes)}
|
||||
.itemRoles=${itemRoles}
|
||||
.wrapFocus=${this.wrapFocus}
|
||||
.activatable=${this.activatable}
|
||||
@action=${this.onAction}
|
||||
>
|
||||
<slot></slot>
|
||||
</ha-list>`;
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu": HaMenu;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { SupervisorMounts } from "../data/supervisor/mounts";
|
||||
import {
|
||||
@ -14,9 +15,9 @@ import {
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@ -51,54 +52,60 @@ class HaMountPicker extends LitElement {
|
||||
if (!this._mounts) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._filterMounts(
|
||||
this._mounts,
|
||||
this.usage
|
||||
).map((mount) => ({
|
||||
value: mount.name,
|
||||
label: mount.name,
|
||||
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
|
||||
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
|
||||
}`,
|
||||
iconPath:
|
||||
mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore,
|
||||
}));
|
||||
|
||||
if (this.usage === SupervisorMountUsage.BACKUP) {
|
||||
const dataDiskOption = {
|
||||
value: _BACKUP_DATA_DISK_,
|
||||
iconPath: mdiHarddisk,
|
||||
label:
|
||||
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup",
|
||||
};
|
||||
if (
|
||||
!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
|
||||
) {
|
||||
options.unshift(dataDiskOption);
|
||||
} else {
|
||||
options.push(dataDiskOption);
|
||||
}
|
||||
}
|
||||
|
||||
const dataDiskOption = html`<ha-list-item
|
||||
graphic="icon"
|
||||
.value=${_BACKUP_DATA_DISK_}
|
||||
>
|
||||
<span>
|
||||
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup"}
|
||||
</span>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
|
||||
</ha-list-item>`;
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.mount-picker.mount")
|
||||
: this.label}
|
||||
.value=${this.value}
|
||||
.value=${this._value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
@selected=${this._mountChanged}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
(!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
${this._filterMounts(this._mounts, this.usage).map(
|
||||
(mount) =>
|
||||
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
|
||||
<span>${mount.name}</span>
|
||||
<span slot="secondary"
|
||||
>${mount.server}${mount.port
|
||||
? `:${mount.port}`
|
||||
: nothing}${mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
this._mounts.default_backup_mount
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -146,10 +153,16 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _mountChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
if (newValue !== this.value) {
|
||||
private _mountChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HaSelect;
|
||||
const newValue = target.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
|
||||
@ -26,9 +25,8 @@ import {
|
||||
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
|
||||
|
||||
const RELATED_SORT_PREFIX = {
|
||||
area_view: "0_area_view",
|
||||
area: "1_area_settings",
|
||||
device: "2_device",
|
||||
area: "0_area",
|
||||
device: "1_device",
|
||||
} as const;
|
||||
|
||||
interface NavigationItem extends PickerComboBoxItem {
|
||||
@ -262,7 +260,12 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
fetchConfig(
|
||||
this.hass!.connection,
|
||||
// path should be null to fetch default lovelace panel
|
||||
panel.url_path === "lovelace" ? null : panel.url_path,
|
||||
true
|
||||
)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
@ -434,31 +437,10 @@ export class HaNavigationPicker extends LitElement {
|
||||
for (const areaId of relatedAreaIds) {
|
||||
const area = this.hass.areas[areaId];
|
||||
const primary = area?.name ?? areaId;
|
||||
|
||||
// Area dashboard view
|
||||
const viewPath = `/home/${computeAreaPath(areaId)}`;
|
||||
relatedItems.push({
|
||||
id: viewPath,
|
||||
primary,
|
||||
secondary: viewPath,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.area_view,
|
||||
primary,
|
||||
viewPath
|
||||
),
|
||||
group: "related",
|
||||
});
|
||||
|
||||
// Area settings
|
||||
const path = `/config/areas/area/${areaId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: primary }
|
||||
),
|
||||
primary,
|
||||
secondary: path,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
|
||||
@ -89,7 +89,7 @@ export class HaPasswordField extends LitElement {
|
||||
@property({ type: Boolean }) readOnly = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ attribute: false }) autocapitalize = "";
|
||||
@property({ attribute: false, type: String }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ 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";
|
||||
@ -55,12 +54,6 @@ export interface PickerComboBoxItem {
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
isRelated?: boolean;
|
||||
}
|
||||
|
||||
export interface PickerComboBoxIndexSelectedDetail {
|
||||
index: number;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
@ -120,7 +113,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
section?: string
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
@ -286,7 +279,6 @@ 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}
|
||||
@ -425,19 +417,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _valueSelected = (ev: MouseEvent) => {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
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, newTab);
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number, newTab = false) {
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index, newTab });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
@ -509,10 +500,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._valuePinned = true;
|
||||
};
|
||||
|
||||
private _preventBlur(ev: Event) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _toggleSection(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._resetSelectedItem();
|
||||
@ -530,7 +517,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
this.virtualizerElement?.element(0)?.scrollIntoView();
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
private _registerKeyboardShortcuts() {
|
||||
@ -540,42 +529,15 @@ 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._initializeSelectedIndex();
|
||||
this._selectNextItem();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@ -594,14 +556,6 @@ 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
|
||||
@ -693,9 +647,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this.virtualizerElement
|
||||
?.element(this._selectedItemIndex)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.virtualizerElement
|
||||
@ -705,14 +657,6 @@ 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 &&
|
||||
@ -724,17 +668,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index, newTab);
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
this._initializeSelectedIndex();
|
||||
if (this._selectedItemIndex === -1) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if filter button is focused
|
||||
@ -744,7 +685,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex, newTab);
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
}
|
||||
};
|
||||
|
||||
@ -950,6 +891,6 @@ declare global {
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": PickerComboBoxIndexSelectedDetail;
|
||||
"index-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
|
||||
@property({ type: Number })
|
||||
public margin = 4;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Number })
|
||||
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
@property({ attribute: "center-image" }) public centerImage?: string;
|
||||
|
||||
@ -14,8 +14,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
@ -259,7 +259,7 @@ class HaQrScanner extends LitElement {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const cameraId = ev.detail?.item?.value;
|
||||
if (cameraId) {
|
||||
this._selectedCamera = cameraId;
|
||||
|
||||
@ -1,221 +1,187 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export interface HaSelectOption {
|
||||
value: string;
|
||||
label?: string;
|
||||
secondary?: string;
|
||||
iconPath?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event type for the ha-select component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
* @param Clearable - Whether the select is clearable (allows undefined values).
|
||||
*/
|
||||
export type HaSelectSelectEvent<
|
||||
T = string,
|
||||
Clearable extends boolean = false,
|
||||
> = CustomEvent<{
|
||||
value: Clearable extends true ? T | undefined : T;
|
||||
}>;
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { styles } from "@material/mwc-select/mwc-select.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends LitElement {
|
||||
@property({ type: Boolean }) public clearable = false;
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-picker-field") private _triggerField!: HaPickerField;
|
||||
|
||||
private _getValueLabel = memoizeOne(
|
||||
(
|
||||
options: HaSelectOption[] | string[] | undefined,
|
||||
value: string | undefined
|
||||
) => {
|
||||
if (!options || !value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
if (
|
||||
(typeof option === "string" && option === value) ||
|
||||
(typeof option !== "string" && option.value === value)
|
||||
) {
|
||||
return typeof option === "string"
|
||||
? option
|
||||
: option.label || option.value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
);
|
||||
@property() public options;
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderField();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown
|
||||
placement="bottom"
|
||||
@wa-select=${this._handleSelect}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-hide=${this._handleHide}
|
||||
>
|
||||
${this._renderField()}
|
||||
${this.options
|
||||
? this.options.map(
|
||||
(option) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${typeof option === "string" ? option : option.value}
|
||||
.disabled=${typeof option === "string"
|
||||
? false
|
||||
: (option.disabled ?? false)}
|
||||
class=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)
|
||||
? "selected"
|
||||
: ""}
|
||||
>
|
||||
${option.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${option.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${typeof option === "string"
|
||||
? option
|
||||
: option.label || option.value}
|
||||
${option.secondary
|
||||
? html`<div class="secondary">${option.secondary}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)
|
||||
: html`<slot></slot>`}
|
||||
</ha-dropdown>
|
||||
${super.render()}
|
||||
${this.clearable && !this.required && !this.disabled && this.value
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@click=${this._clearValue}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderField() {
|
||||
const valueLabel = this._getValueLabel(this.options, this.value);
|
||||
|
||||
return html`
|
||||
<ha-picker-field
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@clear=${this._clearValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${valueLabel}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${!this.clearable ||
|
||||
this.required ||
|
||||
this.disabled ||
|
||||
!this.value}
|
||||
>
|
||||
</ha-picker-field>
|
||||
`;
|
||||
protected override renderMenu() {
|
||||
const classes = this.getMenuClasses();
|
||||
return html`<ha-menu
|
||||
innerRole="listbox"
|
||||
wrapFocus
|
||||
class=${classMap(classes)}
|
||||
activatable
|
||||
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
|
||||
.open=${this.menuOpen}
|
||||
.anchor=${this.anchorElement}
|
||||
.fixed=${this.fixedMenuPosition}
|
||||
@selected=${this.onSelected}
|
||||
@opened=${this.onOpened}
|
||||
@closed=${this.onClosed}
|
||||
@items-updated=${this.onItemsUpdated}
|
||||
@keydown=${this.handleTypeahead}
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
}
|
||||
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
if (value === this.value) {
|
||||
return;
|
||||
protected override renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
}
|
||||
fireEvent(this, "selected", { value });
|
||||
|
||||
return html`<span class="mdc-select__icon"
|
||||
><slot name="icon"></slot
|
||||
></span>`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
super.firstUpdated();
|
||||
|
||||
if (this.inlineArrow) {
|
||||
this.shadowRoot
|
||||
?.querySelector(".mdc-select__selected-text-container")
|
||||
?.classList.add("inline-arrow");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("inlineArrow")) {
|
||||
const textContainerElement = this.shadowRoot?.querySelector(
|
||||
".mdc-select__selected-text-container"
|
||||
);
|
||||
if (this.inlineArrow) {
|
||||
textContainerElement?.classList.add("inline-arrow");
|
||||
} else {
|
||||
textContainerElement?.classList.remove("inline-arrow");
|
||||
}
|
||||
}
|
||||
if (changedProperties.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "selected", { value: undefined });
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
}
|
||||
|
||||
private _handleShow() {
|
||||
this.style.setProperty(
|
||||
"--select-menu-width",
|
||||
`${this._triggerField.offsetWidth}px`
|
||||
);
|
||||
this._opened = true;
|
||||
}
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
|
||||
private _handleHide() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown-item .content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-1);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-dropdown-item .secondary {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--select-menu-width);
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dropdown-item.selected),
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
.mdc-select--filled .mdc-select__anchor {
|
||||
height: var(--ha-select-height, 56px);
|
||||
}
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: var(--ha-space-4);
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
|
||||
inset-inline-start: 48px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select .mdc-select__anchor {
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
padding-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select__anchor .mdc-floating-label--float-above {
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
.mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 0px);
|
||||
}
|
||||
:host([clearable]) .mdc-select__selected-text-container {
|
||||
padding-inline-end: var(
|
||||
--select-selected-text-padding-end,
|
||||
var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 28px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 28px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.inline-arrow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select": HaSelect;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
selected: { value: string | undefined };
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,17 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-formfield";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-list-item";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
@ -230,15 +231,24 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label ?? ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.value=${this.value ?? ""}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
.options=${options}
|
||||
>
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
|
||||
>${item.label}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -285,7 +295,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.detail?.value === undefined && this.value !== undefined) {
|
||||
if (ev.detail?.index === -1 && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
@ -375,7 +385,7 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-dropdown-item[disabled] {
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
|
||||
@ -224,7 +224,6 @@ ${typeof this._templateResult.result === "object"
|
||||
margin-bottom: 0;
|
||||
direction: ltr;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -108,7 +108,6 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
flex: 1;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
|
||||
@ -492,22 +492,19 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="start">
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="start"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
@ -527,15 +524,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
type="button"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
${notificationCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
${!this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="start">${notificationCount}</span>`
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${notificationCount > 0
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
@ -744,8 +739,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: calc(56px + var(--safe-area-inset-left, 0px));
|
||||
padding-left: calc(
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
@ -754,7 +747,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
);
|
||||
padding-inline-end: initial;
|
||||
padding-top: var(--safe-area-inset-top, 0px);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + var(--safe-area-inset-left, 0px));
|
||||
@ -769,22 +761,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
margin-left: 3px;
|
||||
margin-inline-start: 3px;
|
||||
margin-inline-end: initial;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.panels-list {
|
||||
@ -842,7 +827,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-3);
|
||||
--md-list-item-leading-icon-size: var(--ha-space-6);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item {
|
||||
width: 248px;
|
||||
@ -883,22 +867,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: none;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
transition:
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -916,9 +889,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
background-color: var(--accent-color);
|
||||
padding: 2px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-normal) ease,
|
||||
transform var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
ha-svg-icon + .badge {
|
||||
@ -930,12 +900,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: 0 var(--ha-space-1);
|
||||
}
|
||||
:host([expanded]) .badge[slot="start"],
|
||||
:host(:not([expanded])) .badge[slot="end"] {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-md-list-item.user {
|
||||
--md-list-item-leading-icon-size: var(--ha-space-10);
|
||||
@ -974,15 +938,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
-webkit-transform: scaleX(var(--scale-direction));
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { STTEngine } from "../data/stt";
|
||||
import { listSTTEngines } from "../data/stt";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -59,30 +61,6 @@ export class HaSTTPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._engines
|
||||
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
|
||||
.map((engine) => {
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return {
|
||||
value: engine.engine_id,
|
||||
label,
|
||||
disabled: engine.supported_languages?.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.required || value === NONE) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize("ui.components.stt-picker.none") || "None",
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -91,8 +69,33 @@ export class HaSTTPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.stt-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -141,17 +144,17 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@ -53,7 +53,7 @@ import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public compact = false;
|
||||
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: ValueChangedEvent<string>) {
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import "./ha-list-item";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
@ -24,26 +25,6 @@ export class HaThemePicker extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options: HaSelectOption[] = Object.keys(
|
||||
this.hass?.themes.themes || {}
|
||||
).map((theme) => ({
|
||||
value: theme,
|
||||
}));
|
||||
|
||||
if (this.includeDefault) {
|
||||
options.unshift({
|
||||
value: DEFAULT_THEME,
|
||||
label: "Home Assistant",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: "remove",
|
||||
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -52,8 +33,31 @@ export class HaThemePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`
|
||||
<ha-list-item value="remove">
|
||||
${this.hass!.localize("ui.components.theme-picker.no_theme")}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeDefault
|
||||
? html`
|
||||
<ha-list-item .value=${DEFAULT_THEME}>
|
||||
Home Assistant
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
.sort()
|
||||
.map(
|
||||
(theme) =>
|
||||
html`<ha-list-item .value=${theme}>${theme}</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -63,11 +67,11 @@ export class HaThemePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
private _changed(ev): void {
|
||||
if (!this.hass || ev.target.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,19 +36,10 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
|
||||
@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { TTSEngine } from "../data/tts";
|
||||
import { listTTSEngines } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -59,30 +61,6 @@ export class HaTTSPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._engines
|
||||
.filter((engine) => !engine.deprecated || engine.engine_id === value)
|
||||
.map((engine) => {
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return {
|
||||
value: engine.engine_id,
|
||||
label,
|
||||
disabled: engine.supported_languages?.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (!this.required || value === NONE) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize("ui.components.tts-picker.none"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -91,8 +69,33 @@ export class HaTTSPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.tts-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -141,17 +144,17 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { TTSVoice } from "../data/tts";
|
||||
import { listTTSVoices } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@ -29,25 +31,14 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
|
||||
@state() _voices?: TTSVoice[] | null;
|
||||
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
protected render() {
|
||||
if (!this._voices) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
|
||||
|
||||
const options: HaSelectOption[] = (this._voices || []).map((voice) => ({
|
||||
value: voice.voice_id,
|
||||
label: voice.name,
|
||||
}));
|
||||
|
||||
if (!this.required || !this.value) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -56,8 +47,21 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.tts-voice-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._voices.map(
|
||||
(voice) =>
|
||||
html`<ha-list-item .value=${voice.voice_id}>
|
||||
${voice.name}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@ -98,25 +102,34 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("_voices") &&
|
||||
this._select?.value !== this.value
|
||||
) {
|
||||
this._select?.layoutOptions();
|
||||
fireEvent(this, "value-changed", { value: this._select?.value });
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, type: Object })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
@ -288,19 +288,10 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { extractApiErrorMessage } from "../../data/hassio/common";
|
||||
@ -17,9 +19,8 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-player-toggle";
|
||||
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||
|
||||
@ -37,11 +38,8 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: JoinMediaPlayersDialogParams): void {
|
||||
this._entityId = params.entityId;
|
||||
this._open = true;
|
||||
|
||||
const stateObj = this.hass.states[params.entityId] as
|
||||
| MediaPlayerEntity
|
||||
@ -56,11 +54,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._entityId = undefined;
|
||||
this._selectedEntities = [];
|
||||
this._groupMembers = [];
|
||||
@ -75,18 +68,23 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
const entityId = this._entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
flexContent
|
||||
.heading=${name}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
data-dialog="close"
|
||||
dialogAction="close"
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
@ -120,23 +118,21 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
></ha-media-player-toggle>`
|
||||
)}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -221,7 +217,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-6);
|
||||
row-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@ -21,8 +21,8 @@ import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-manage-button";
|
||||
@ -177,7 +177,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
this.classList.add("opened");
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
private async _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "auto":
|
||||
|
||||
@ -2,7 +2,6 @@ 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";
|
||||
@ -15,7 +14,7 @@ export class HaTileContainer extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public vertical = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ type: Boolean, attribute: false })
|
||||
public interactive = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@ -48,11 +47,7 @@ export class HaTileContainer extends LitElement {
|
||||
>
|
||||
<ha-ripple .disabled=${!this.interactive}></ha-ripple>
|
||||
</div>
|
||||
<div
|
||||
class="container ${containerOrientationClass}"
|
||||
@action=${stopPropagation}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<div class="container ${containerOrientationClass}">
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<slot name="icon"></slot>
|
||||
<slot name="info" id="info"></slot>
|
||||
|
||||
@ -53,7 +53,6 @@ export type ClimateEntity = HassEntityBase & {
|
||||
current_humidity?: number;
|
||||
target_humidity_low?: number;
|
||||
target_humidity_high?: number;
|
||||
target_humidity_step?: number;
|
||||
min_humidity?: number;
|
||||
max_humidity?: number;
|
||||
fan_mode?: string;
|
||||
|
||||
@ -35,7 +35,7 @@ export const deserializeFilters = (value: DataTableFilters) => {
|
||||
|
||||
// returns true when this filter has *selected* options and the filter's name
|
||||
// equals the given filterName
|
||||
export const isFilterUsed = (
|
||||
export const isUsedFilter = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string
|
||||
@ -49,7 +49,7 @@ export const isFilterUsed = (
|
||||
// which has resulted in a list of items that match these selected opions
|
||||
// (this list can be empty),
|
||||
// and the filter's name equals (one of) the given filterName(s)
|
||||
export const isRelatedItemsFilterUsed = (
|
||||
export const isUsedRelatedItemsFilter = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string | string[]
|
||||
|
||||
@ -131,30 +131,11 @@ export interface FlowToGridSourceEnergyPreference {
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
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 {
|
||||
// W meter
|
||||
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";
|
||||
|
||||
@ -178,7 +159,6 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
@ -511,15 +491,6 @@ const getEnergyData = async (
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
// If power stats 5 minute data is selected, then also fetch hourly data which
|
||||
// will be used to back-fill any missing data points in the 5 minute data when
|
||||
// the requested range is beyond the limit of short term statistics.
|
||||
const _powerStatsHour: Statistics | Promise<Statistics> =
|
||||
powerStatIds.length && finePeriod === "5minute"
|
||||
? fetchStatistics(hass!, start, end, powerStatIds, "hour", powerUnits, [
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||
@ -628,7 +599,6 @@ const getEnergyData = async (
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
powerStatsHour,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@ -637,44 +607,12 @@ const getEnergyData = async (
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_powerStats,
|
||||
_powerStatsHour,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
|
||||
// Back-fill any missing power statistics from hourly data if present
|
||||
if (Object.keys(powerStatsHour).length) {
|
||||
powerStatIds.forEach((powerId) => {
|
||||
if (powerId in powerStatsHour) {
|
||||
// If we have extra hourly power statistics for an ID, we may need to
|
||||
// insert data into statistics
|
||||
if (powerId in powerStats && powerStats[powerId].length) {
|
||||
// We have 5-minute data. Only insert hourly values for time periods
|
||||
// before the first 5-minute value.
|
||||
const powerStatFirst = powerStats[powerId][0];
|
||||
const powerStatHour = powerStatsHour[powerId];
|
||||
let powerStatHourLast = 0;
|
||||
for (const powerStat of powerStatHour) {
|
||||
if (powerStat.end > powerStatFirst.start) {
|
||||
break;
|
||||
}
|
||||
powerStatHourLast++;
|
||||
}
|
||||
powerStats[powerId] = [
|
||||
...powerStatHour.slice(0, powerStatHourLast),
|
||||
...powerStats[powerId],
|
||||
];
|
||||
} else {
|
||||
// There was no 5-minute data, so simply insert full hourly data
|
||||
powerStats[powerId] = powerStatsHour[powerId];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
|
||||
@ -13,13 +13,10 @@ 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,10 +1,6 @@
|
||||
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";
|
||||
@ -46,8 +42,3 @@ 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,26 +15,16 @@ 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 => {
|
||||
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 getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.default_panel ||
|
||||
hass.systemData?.default_panel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
import {
|
||||
mdiKeyboard,
|
||||
mdiNavigationVariant,
|
||||
mdiPuzzle,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
filterNavigationPages,
|
||||
type NavigationFilterOptions,
|
||||
} from "../common/config/filter_navigation_pages";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
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;
|
||||
@ -29,7 +27,6 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
export interface BaseNavigationCommand {
|
||||
path: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
icon_path?: string;
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
@ -48,14 +45,11 @@ export interface NavigationInfo extends PageNavigation {
|
||||
const generateNavigationPanelCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
panels: HomeAssistant["panels"],
|
||||
apps?: HassioAddonInfo[]
|
||||
addons?: HassioAddonInfo[]
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.entries(panels)
|
||||
.filter(
|
||||
([panelKey]) =>
|
||||
panelKey !== "_my_redirect" &&
|
||||
panelKey !== "hassio" &&
|
||||
panelKey !== "app"
|
||||
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
)
|
||||
.map(([_panelKey, panel]) => {
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
@ -65,10 +59,12 @@ const generateNavigationPanelCommands = (
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (apps) {
|
||||
const app = apps.find(({ slug }) => slug === panel.url_path);
|
||||
if (app) {
|
||||
image = app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined;
|
||||
if (addons) {
|
||||
const addon = addons.find(({ slug }) => slug === panel.url_path);
|
||||
if (addon) {
|
||||
image = addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,30 +98,33 @@ const getNavigationInfoFromConfig = (
|
||||
};
|
||||
|
||||
const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant,
|
||||
filterOptions: NavigationFilterOptions = {}
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
if (!hass.user?.is_admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: NavigationInfo[] = [];
|
||||
const allPages = Object.values(configSections).flat();
|
||||
const visiblePages = filterNavigationPages(hass, allPages, filterOptions);
|
||||
|
||||
for (const page of visiblePages) {
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
Object.values(configSections).forEach((sectionPages) => {
|
||||
sectionPages.forEach((page) => {
|
||||
if (!canShowPage(hass, page)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
|
||||
items.push(info);
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
@ -141,7 +140,7 @@ const finalizeNavigationCommands = (
|
||||
return {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
icon_path: item.iconPath || mdiNavigationVariant,
|
||||
secondary: item.secondary || secondary,
|
||||
secondary,
|
||||
sorting_label: `${item.primary}_${secondary}`,
|
||||
...item,
|
||||
};
|
||||
@ -149,42 +148,41 @@ const finalizeNavigationCommands = (
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant,
|
||||
apps?: HassioAddonInfo[],
|
||||
filterOptions: NavigationFilterOptions = {}
|
||||
addons?: HassioAddonInfo[]
|
||||
): NavigationComboBoxItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(
|
||||
hass.localize,
|
||||
hass.panels,
|
||||
apps
|
||||
addons
|
||||
);
|
||||
|
||||
const sectionItems = generateNavigationConfigSectionCommands(
|
||||
hass,
|
||||
filterOptions
|
||||
);
|
||||
const appItems: BaseNavigationCommand[] = [];
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
appItems.push({
|
||||
path: "/config/apps/available",
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
icon_path: mdiStorePlus,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_store"
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_store"
|
||||
),
|
||||
iconColor: "#F1C447",
|
||||
});
|
||||
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,
|
||||
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,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_info",
|
||||
{ app: app.name }
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_info",
|
||||
{ addon: addon.name }
|
||||
),
|
||||
secondary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.app_settings"
|
||||
),
|
||||
iconColor: "#F1C447",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -203,7 +201,7 @@ export const generateNavigationCommands = (
|
||||
return finalizeNavigationCommands(hass.localize, [
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...appItems,
|
||||
...supervisorItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
};
|
||||
@ -267,16 +265,16 @@ const generateServerControlCommands = (
|
||||
|
||||
return serverActions.map((action, index) => {
|
||||
const primary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.home_assistant_control.perform_action",
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
{
|
||||
action: hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.home_assistant_control.${action}`
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const secondary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.home_assistant_control"
|
||||
"ui.dialogs.quick-bar.commands.types.server_control"
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -13,6 +13,7 @@ import "../../../components/ha-cover-controls";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-humidifier-state";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
@ -295,11 +296,17 @@ class EntityPreviewRow extends LitElement {
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
})) || []}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option}>
|
||||
${this.hass!.formatEntityState(stateObj, option)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -91,11 +91,9 @@ export const showConfigFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -95,11 +95,9 @@ export const showOptionsFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -86,11 +86,9 @@ export const showSubConfigFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button-toggle";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import {
|
||||
@ -36,7 +41,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
@state() private _modes: LightPickerMode[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
@ -45,11 +50,10 @@ class DialogLightColorFavorite extends LitElement {
|
||||
this._dialogParams = dialogParams;
|
||||
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
|
||||
this._updateModes();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _updateModes() {
|
||||
@ -116,8 +120,16 @@ class DialogLightColorFavorite extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _cancel() {
|
||||
this._dialogParams?.cancel?.();
|
||||
}
|
||||
|
||||
private _cancelDialog() {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._dialogParams = undefined;
|
||||
this._entry = undefined;
|
||||
this._color = undefined;
|
||||
@ -126,7 +138,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
private async _save() {
|
||||
if (!this._color) {
|
||||
this.closeDialog();
|
||||
this._cancel();
|
||||
return;
|
||||
}
|
||||
this._dialogParams?.submit?.(this._color);
|
||||
@ -147,76 +159,83 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._dialogParams?.title}
|
||||
<ha-md-dialog
|
||||
open
|
||||
@cancel=${this._cancel}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
<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._dialogParams?.title}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button appearance="plain" @click=${this._cancelDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this._color}
|
||||
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -224,18 +243,23 @@ class DialogLightColorFavorite extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
|
||||
--ha-dialog-max-height: min(
|
||||
ha-md-dialog {
|
||||
min-width: 420px; /* prevent width jumps when switching modes */
|
||||
max-height: min(
|
||||
600px,
|
||||
100% - 48px
|
||||
); /* prevent scrolling on desktop */
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 100vw;
|
||||
--ha-dialog-max-height: calc(100% - 100px);
|
||||
ha-md-dialog {
|
||||
min-width: 100%;
|
||||
min-height: auto;
|
||||
max-height: calc(100% - 100px);
|
||||
margin-bottom: 0;
|
||||
|
||||
--md-dialog-container-shape-start-start: 28px;
|
||||
--md-dialog-container-shape-start-end: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ export interface LightColorFavoriteDialogParams {
|
||||
title: string;
|
||||
initialColor?: LightColor;
|
||||
submit?: (color?: LightColor) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadLightColorFavoriteDialog = () =>
|
||||
@ -17,6 +18,7 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
) =>
|
||||
new Promise<LightColor | null>((resolve) => {
|
||||
const origCancel = dialogParams.cancel;
|
||||
const origSubmit = dialogParams.submit;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
@ -24,6 +26,12 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogImport: loadLightColorFavoriteDialog,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (color: LightColor) => {
|
||||
resolve(color);
|
||||
if (origSubmit) {
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import { mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import { SirenEntityFeature } from "../../../../data/siren";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@ -24,25 +28,23 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
|
||||
@state() _stateObj?: HassEntity;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() _tone?: string;
|
||||
|
||||
@state() _volume?: number;
|
||||
|
||||
@state() _duration?: number;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog({ stateObj }: { stateObj: HassEntity }) {
|
||||
this._stateObj = stateObj;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._stateObj = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@ -63,33 +65,50 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
.hass=${this.hass}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<div>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="options">
|
||||
${supportsTones
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.components.siren.tone")}
|
||||
@selected=${this._handleToneChange}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleToneChange}
|
||||
.value=${this._tone}
|
||||
.options=${Object.entries(
|
||||
this._stateObj!.attributes.available_tones
|
||||
).map(([toneId, toneName]) => ({
|
||||
value: Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId,
|
||||
label: toneName,
|
||||
}))}
|
||||
>
|
||||
${Object.entries(
|
||||
this._stateObj.attributes.available_tones
|
||||
).map(
|
||||
([toneId, toneName]) => html`
|
||||
<ha-list-item
|
||||
.value=${Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId}
|
||||
>${toneName}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
@ -134,21 +153,17 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
</ha-control-button>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev: HaSelectSelectEvent) {
|
||||
this._tone = ev.detail.value;
|
||||
private _handleToneChange(ev) {
|
||||
this._tone = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleVolumeChange(ev) {
|
||||
|
||||
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