mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-08 19:54:24 +00:00
Compare commits
116 Commits
remove-dom
...
refactor-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab0b1a4632 | ||
|
|
aab2304d86 | ||
|
|
c013f79826 | ||
|
|
60236c2fee | ||
|
|
20d53a2659 | ||
|
|
6dbc38386c | ||
|
|
ce5a19caa8 | ||
|
|
2cda06e7a6 | ||
|
|
65485ce8c9 | ||
|
|
b73ae60cea | ||
|
|
cef35c6c23 | ||
|
|
6b9685ec9f | ||
|
|
fc9289dc05 | ||
|
|
2a2bca2a61 | ||
|
|
1eda51ddbc | ||
|
|
22738f6d77 | ||
|
|
2f73351c35 | ||
|
|
44b442dc0e | ||
|
|
916731d0ee | ||
|
|
5113594d6b | ||
|
|
edd162df68 | ||
|
|
6278d9be2f | ||
|
|
ba2fef50d0 | ||
|
|
a9774e74cf | ||
|
|
ae3d6c77ca | ||
|
|
4f3feced1b | ||
|
|
49dd217935 | ||
|
|
522cffffa8 | ||
|
|
3124fbe08e | ||
|
|
c705d4e4a1 | ||
|
|
446661915b | ||
|
|
6048356e01 | ||
|
|
c44341331a | ||
|
|
2d46304960 | ||
|
|
b5ff6a991d | ||
|
|
28254ca0f2 | ||
|
|
8605c235ac | ||
|
|
8325161d39 | ||
|
|
90057854c8 | ||
|
|
04c8c82966 | ||
|
|
16b3add987 | ||
|
|
f0e171076e | ||
|
|
48f0b78b95 | ||
|
|
32728d91d7 | ||
|
|
0915a3e29c | ||
|
|
60c5888f6b | ||
|
|
fb599b8b16 | ||
|
|
b1a390789d | ||
|
|
76c0dd1f1f | ||
|
|
96dacfdeca | ||
|
|
5f28ed35d2 | ||
|
|
edd7b4c3dc | ||
|
|
cbea8bbf44 | ||
|
|
23a41e4384 | ||
|
|
f747580b43 | ||
|
|
98fc69674f | ||
|
|
ab1a58b3f3 | ||
|
|
a7ff89385e | ||
|
|
f3d41be3bf | ||
|
|
b73707751a | ||
|
|
61bff43cdb | ||
|
|
0a0d08fa19 | ||
|
|
ae29ba63ff | ||
|
|
0579cd8eb6 | ||
|
|
8c3eafec6d | ||
|
|
b5c2e12016 | ||
|
|
f7a13392cd | ||
|
|
a2cdd592f1 | ||
|
|
f04341a2a2 | ||
|
|
91bdc80a67 | ||
|
|
b4824cc0a7 | ||
|
|
28f375c0d4 | ||
|
|
da7ccac811 | ||
|
|
a8ad921efd | ||
|
|
3b8f219800 | ||
|
|
e36a2e1c70 | ||
|
|
e06ea1047c | ||
|
|
99cb997d08 | ||
|
|
ac3edd20f8 | ||
|
|
0d88d139f0 | ||
|
|
b8d08ccb05 | ||
|
|
7c20316ba5 | ||
|
|
fa633efc87 | ||
|
|
85d461f0fd | ||
|
|
b55e1c9988 | ||
|
|
1da349a36d | ||
|
|
74f7139a09 | ||
|
|
2911cc77fa | ||
|
|
ab20383a3a | ||
|
|
514cb9da9d | ||
|
|
7c52ac8ca7 | ||
|
|
07b4a44228 | ||
|
|
2b28a6c3f2 | ||
|
|
84f2e304cf | ||
|
|
18cd40ab01 | ||
|
|
8e3b1dc6ac | ||
|
|
5cc223a582 | ||
|
|
9a62a9217c | ||
|
|
70be747e9d | ||
|
|
bb57a91494 | ||
|
|
7e22e6c0e2 | ||
|
|
c93f910e56 | ||
|
|
8bf4ff5d25 | ||
|
|
debc3adf19 | ||
|
|
ae21017de8 | ||
|
|
f15f518cc2 | ||
|
|
0e44417051 | ||
|
|
3581b43336 | ||
|
|
32b9676f97 | ||
|
|
7876642f35 | ||
|
|
0e3bcfad5e | ||
|
|
cd1c273d5a | ||
|
|
d92ac4b4b7 | ||
|
|
bfecb1d4a9 | ||
|
|
69a8db00fa | ||
|
|
bbda7affdc |
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
|
||||
uses: home-assistant/actions/helpers/verify-version@5752577ea7cc5aefb064b0b21432f18fe4d6ba90 # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -6,9 +6,9 @@ import rootConfig from "../eslint.config.mjs";
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
"import-x/extensions": "off",
|
||||
"import-x/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
|
||||
@@ -99,6 +99,44 @@ const lokaliseProjects = {
|
||||
frontend: "3420425759f6d6d241f598.13594006",
|
||||
};
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async function pollProcess(lokaliseApi, projectId, processId) {
|
||||
while (true) {
|
||||
const process = await lokaliseApi
|
||||
.queuedProcesses()
|
||||
.get(processId, { project_id: projectId });
|
||||
|
||||
const project =
|
||||
projectId === lokaliseProjects.backend ? "backend" : "frontend";
|
||||
|
||||
if (process.status === "finished") {
|
||||
console.log(`Lokalise export process for ${project} finished`);
|
||||
return process;
|
||||
}
|
||||
|
||||
if (process.status === "failed" || process.status === "cancelled") {
|
||||
throw new Error(
|
||||
`Lokalise export process for ${project} ${process.status}: ${process.message}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Lokalise export process for ${project} in progress...`,
|
||||
process.status,
|
||||
process.details?.items_to_process
|
||||
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
|
||||
: ""
|
||||
);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, POLL_INTERVAL_MS);
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
let apiKey;
|
||||
try {
|
||||
@@ -118,55 +156,60 @@ gulp.task("fetch-lokalise", async function () {
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(lokaliseProjects).map(([project, projectId]) =>
|
||||
lokaliseApi
|
||||
.files()
|
||||
.download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
})
|
||||
.then((download) => fetch(download.bundle_url))
|
||||
.then((response) => {
|
||||
if (response.status === 200 || response.status === 0) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
|
||||
try {
|
||||
const exportProcess = await lokaliseApi
|
||||
.files()
|
||||
.async_download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
});
|
||||
|
||||
const finishedProcess = await pollProcess(
|
||||
lokaliseApi,
|
||||
projectId,
|
||||
exportProcess.process_id
|
||||
);
|
||||
|
||||
const bundleUrl = finishedProcess.details.download_url;
|
||||
|
||||
console.log(`Downloading translations from: ${bundleUrl}`);
|
||||
|
||||
const response = await fetch(bundleUrl);
|
||||
|
||||
if (response.status !== 200 && response.status !== 0) {
|
||||
throw new Error(response.statusText);
|
||||
})
|
||||
.then(JSZip.loadAsync)
|
||||
.then(async (contents) => {
|
||||
await mkdirPromise;
|
||||
return Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return Promise.resolve();
|
||||
}
|
||||
return file
|
||||
.async("nodebuffer")
|
||||
.then((content) =>
|
||||
fs.writeFile(
|
||||
path.join(
|
||||
inDir,
|
||||
project,
|
||||
filename.split("/").splice(-1)[0]
|
||||
),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Extracting translations...`);
|
||||
|
||||
const contents = await JSZip.loadAsync(await response.arrayBuffer());
|
||||
|
||||
await mkdirPromise;
|
||||
await Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return;
|
||||
}
|
||||
const content = await file.async("nodebuffer");
|
||||
await fs.writeFile(
|
||||
path.join(inDir, project, filename.split("/").splice(-1)[0]),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
);
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import presetEnv from "@babel/preset-env";
|
||||
import compilationTargets from "@babel/helper-compilation-targets";
|
||||
import coreJSCompat from "core-js-compat";
|
||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
|
||||
import { babelOptions } from "./bundle.cjs";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/input/ha-input";
|
||||
import "../../../../src/components/ha-button";
|
||||
|
||||
const seeFAQ = (qid) => html`
|
||||
@@ -123,11 +123,11 @@ export class HcConnect extends LitElement {
|
||||
To get started, enter your Home Assistant URL and click authorize.
|
||||
If you want a preview instead, click the show demo button.
|
||||
</p>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
label="Home Assistant URL"
|
||||
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -204,7 +204,7 @@ export class HcConnect extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleConnect() {
|
||||
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
|
||||
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
|
||||
const value = inputEl.value || "";
|
||||
this.error = undefined;
|
||||
|
||||
@@ -319,7 +319,7 @@ export class HcConnect extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -11,9 +11,9 @@ export const demoConfigs: (() => Promise<DemoConfig>)[] = [
|
||||
() => import("./jimpower").then((mod) => mod.demoJimpower),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
// eslint-disable-next-line import-x/no-mutable-exports
|
||||
export let selectedDemoConfigIndex = 0;
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
// eslint-disable-next-line import-x/no-mutable-exports
|
||||
export let selectedDemoConfig: Promise<DemoConfig> =
|
||||
demoConfigs[selectedDemoConfigIndex]();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
import { mdiTelevision } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
@@ -13,6 +12,7 @@ import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
import importX from "eslint-plugin-import-x";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -22,8 +22,27 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
// Load airbnb-base via FlatCompat for non-import rules only.
|
||||
// eslint-plugin-import is incompatible with ESLint 10 (uses removed APIs),
|
||||
// so we strip its plugin/rules/settings and use eslint-plugin-import-x instead.
|
||||
const airbnbConfigs = compat.extends("airbnb-base").map((config) => {
|
||||
const { plugins = {}, rules = {}, settings = {}, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
plugins: Object.fromEntries(
|
||||
Object.entries(plugins).filter(([key]) => key !== "import")
|
||||
),
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(rules).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
settings: Object.fromEntries(
|
||||
Object.entries(settings).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...compat.extends("airbnb-base"),
|
||||
...airbnbConfigs,
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
@@ -31,6 +50,7 @@ export default tseslint.config(
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
a11yConfigs.recommended,
|
||||
importX.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -58,7 +78,7 @@ export default tseslint.config(
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
"import-x/resolver": {
|
||||
webpack: {
|
||||
config: "./rspack.config.cjs",
|
||||
},
|
||||
@@ -87,12 +107,20 @@ export default tseslint.config(
|
||||
"prefer-destructuring": "off",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-cycle": "off",
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"no-use-before-define": "off",
|
||||
|
||||
"import/extensions": [
|
||||
// import-x rules (migrated from eslint-plugin-import / airbnb-base)
|
||||
"import-x/named": "off",
|
||||
"import-x/prefer-default-export": "off",
|
||||
"import-x/no-default-export": "off",
|
||||
"import-x/no-unresolved": "off",
|
||||
"import-x/no-cycle": "off",
|
||||
"import-x/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
@@ -100,12 +128,24 @@ export default tseslint.config(
|
||||
js: "never",
|
||||
},
|
||||
],
|
||||
"import-x/no-mutable-exports": "error",
|
||||
"import-x/no-amd": "error",
|
||||
"import-x/first": "error",
|
||||
"import-x/order": [
|
||||
"error",
|
||||
{ groups: [["builtin", "external", "internal"]] },
|
||||
],
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/no-absolute-path": "error",
|
||||
"import-x/no-dynamic-require": "error",
|
||||
"import-x/no-webpack-loader-syntax": "error",
|
||||
"import-x/no-named-default": "error",
|
||||
"import-x/no-self-import": "error",
|
||||
"import-x/no-useless-path-segments": ["error", { commonjs: true }],
|
||||
"import-x/no-import-module-exports": ["error", { exceptions: [] }],
|
||||
"import-x/no-relative-packages": "error",
|
||||
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
// TypeScript rules
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
@@ -185,7 +225,6 @@ export default tseslint.config(
|
||||
allowObjectTypes: "always",
|
||||
},
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -194,6 +233,12 @@ export default tseslint.config(
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/entrypoints/service-worker.ts"],
|
||||
languageOptions: {
|
||||
globals: globals.serviceworker,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -57,7 +57,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
|
||||
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
|
||||
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
|
||||
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
|
||||
| size | "small"/"medium" | "medium" | Sets the button size. |
|
||||
| size | "small"/"medium"/"large" | "medium" | Sets the button size. |
|
||||
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
|
||||
| disabled | Boolean | false | Disables the button and prevents user interaction. |
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import "../../../../src/components/input/ha-input";
|
||||
import "../../../../src/components/input/ha-input-copy";
|
||||
import "../../../../src/components/input/ha-input-multi";
|
||||
import "../../../../src/components/input/ha-input-search";
|
||||
import { localizeContext } from "../../../../src/data/context";
|
||||
import { internationalizationContext } from "../../../../src/data/context/context";
|
||||
|
||||
const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.copy": "Copy",
|
||||
@@ -26,11 +26,19 @@ const LOCALIZE_KEYS: Record<string, string> = {
|
||||
export class DemoHaInput extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
// Provides localizeContext for ha-input-copy, ha-input-multi and ha-input-search
|
||||
// Provides internationalizationContext for ha-input-copy, ha-input-multi and ha-input-search
|
||||
// eslint-disable-next-line no-new
|
||||
new ContextProvider(this, {
|
||||
context: localizeContext,
|
||||
initialValue: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
localize: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {} as any,
|
||||
translationMetadata: {} as any,
|
||||
loadBackendTranslation: (async () => (key: string) => key) as any,
|
||||
loadFragmentTranslation: (async () => (key: string) => key) as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
@@ -692,7 +692,11 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
([key, value]) => html`
|
||||
<ha-settings-row narrow slot=${slot}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description">${value?.description}</span>
|
||||
${value?.description
|
||||
? html`<span slot="description"
|
||||
>${value?.description}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
|
||||
73
gallery/src/pages/components/ha-textarea.markdown
Normal file
73
gallery/src/pages/components/ha-textarea.markdown
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Textarea
|
||||
---
|
||||
|
||||
# Textarea `<ha-textarea>`
|
||||
|
||||
A multiline text input component supporting Home Assistant theming and validation, based on webawesome textarea.
|
||||
Supports autogrow, hints, validation, and both material and outlined appearances.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example usage
|
||||
|
||||
```html
|
||||
<ha-textarea label="Description" value="Hello world"></ha-textarea>
|
||||
|
||||
<ha-textarea
|
||||
label="Notes"
|
||||
placeholder="Type here..."
|
||||
resize="auto"
|
||||
></ha-textarea>
|
||||
|
||||
<ha-textarea label="Required field" required></ha-textarea>
|
||||
|
||||
<ha-textarea label="Disabled" disabled value="Can't edit this"></ha-textarea>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This component is based on the webawesome textarea component.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `label`: Custom label content. Overrides the `label` property.
|
||||
- `hint`: Custom hint content. Overrides the `hint` property.
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------ | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
|
||||
| value | String | - | The current value of the textarea. |
|
||||
| label | String | "" | The textarea's label text. |
|
||||
| hint | String | "" | The textarea's hint/helper text. |
|
||||
| placeholder | String | "" | Placeholder text shown when the textarea is empty. |
|
||||
| rows | Number | 4 | The number of visible text rows. |
|
||||
| resize | "none"/"vertical"/"horizontal"/"both"/"auto" | "none" | Controls the textarea's resize behavior. |
|
||||
| readonly | Boolean | false | Makes the textarea readonly. |
|
||||
| disabled | Boolean | false | Disables the textarea and prevents user interaction. |
|
||||
| required | Boolean | false | Makes the textarea a required field. |
|
||||
| auto-validate | Boolean | false | Validates the textarea on blur instead of on form submit. |
|
||||
| invalid | Boolean | false | Marks the textarea as invalid. |
|
||||
| validation-message | String | "" | Custom validation message shown when the textarea is invalid. |
|
||||
| minlength | Number | - | The minimum length of input that will be considered valid. |
|
||||
| maxlength | Number | - | The maximum length of input that will be considered valid. |
|
||||
| name | String | - | The name of the textarea, submitted as a name/value pair with form data. |
|
||||
| autocapitalize | "off"/"none"/"on"/"sentences"/"words"/"characters" | "" | Controls whether and how text input is automatically capitalized. |
|
||||
| autocomplete | String | - | Indicates whether the browser's autocomplete feature should be used. |
|
||||
| autofocus | Boolean | false | Automatically focuses the textarea when the page loads. |
|
||||
| spellcheck | Boolean | true | Enables or disables the browser's spellcheck feature. |
|
||||
| inputmode | "none"/"text"/"decimal"/"numeric"/"tel"/"search"/"email"/"url" | "" | Hints at the type of data for showing an appropriate virtual keyboard. |
|
||||
| enterkeyhint | "enter"/"done"/"go"/"next"/"previous"/"search"/"send" | "" | Customizes the label or icon of the Enter key on virtual keyboards. |
|
||||
|
||||
#### CSS Parts
|
||||
|
||||
- `wa-base` - The underlying wa-textarea base wrapper.
|
||||
- `wa-hint` - The underlying wa-textarea hint container.
|
||||
- `wa-textarea` - The underlying wa-textarea textarea element.
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-textarea-padding-bottom` - Padding below the textarea host.
|
||||
- `--ha-textarea-max-height` - Maximum height of the textarea when using `resize="auto"`. Defaults to `200px`.
|
||||
- `--ha-textarea-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
151
gallery/src/pages/components/ha-textarea.ts
Normal file
151
gallery/src/pages/components/ha-textarea.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-textarea";
|
||||
|
||||
@customElement("demo-components-ha-textarea")
|
||||
export class DemoHaTextarea extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-textarea in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-textarea label="Default"></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With value"
|
||||
value="Hello world"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>Autogrow</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Autogrow empty"
|
||||
resize="auto"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="Autogrow with value"
|
||||
resize="auto"
|
||||
value="This textarea will grow as you type more content into it. Try adding more lines to see the effect."
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-textarea>
|
||||
<ha-textarea label="Required" required></ha-textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With hint"
|
||||
hint="Supports Markdown"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With rows"
|
||||
.rows=${6}
|
||||
placeholder="6 rows"
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>No label</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
placeholder="No label, just placeholder"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
placeholder="No label, autogrow"
|
||||
></ha-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
h3 {
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-1) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-textarea": DemoHaTextarea;
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,21 @@ const CONFIGS = [
|
||||
entity: sensor.not_working
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lower minimum",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_high
|
||||
needle: true
|
||||
severity:
|
||||
green: 0
|
||||
yellow: 0.45
|
||||
red: 0.9
|
||||
min: -0.05
|
||||
name: " "
|
||||
max: 1.9
|
||||
unit: GBP/h`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-gauge-card")
|
||||
|
||||
@@ -422,7 +422,6 @@ export class DemoEntityState extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.columns=${this._columns(this.hass)}
|
||||
.data=${this._rows()}
|
||||
auto-height
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-linear-progress";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -8,6 +7,7 @@ import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-fade-in";
|
||||
import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/progress/ha-progress-bar";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import "../../src/onboarding/onboarding-welcome-links";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
@@ -60,7 +60,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<mwc-linear-progress indeterminate></mwc-linear-progress>
|
||||
<ha-progress-bar indeterminate></ha-progress-bar>
|
||||
`
|
||||
: nothing}
|
||||
${networkIssue || this._networkInfoError
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer --quiet",
|
||||
],
|
||||
|
||||
38
package.json
38
package.json
@@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.40.0",
|
||||
"@codemirror/view": "6.41.0",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.3.1",
|
||||
@@ -65,16 +65,12 @@
|
||||
"@material/mwc-checkbox": "0.27.0",
|
||||
"@material/mwc-dialog": "0.27.0",
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
"@material/mwc-fab": "0.27.0",
|
||||
"@material/mwc-floating-label": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@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-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-switch": "0.27.0",
|
||||
"@material/mwc-textarea": "0.27.0",
|
||||
"@material/mwc-textfield": "0.27.0",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -82,14 +78,14 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.20",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.1.1",
|
||||
"barcode-detector": "3.1.2",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
@@ -102,7 +98,7 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"fuse.js": "7.3.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.15",
|
||||
@@ -116,7 +112,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.5",
|
||||
"marked": "17.0.6",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -144,16 +140,18 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.0",
|
||||
"@html-eslint/eslint-plugin": "0.58.1",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.59.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.5",
|
||||
"@rspack/core": "1.7.10",
|
||||
"@rsdoctor/rspack-plugin": "1.5.7",
|
||||
"@rspack/core": "1.7.11",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -174,11 +172,12 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.4",
|
||||
"eslint": "10.2.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -186,6 +185,7 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.4.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -207,8 +207,8 @@
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.2",
|
||||
"typescript": "6.0.2",
|
||||
"typescript-eslint": "8.58.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
|
||||
import type { Auth } from "home-assistant-js-websocket";
|
||||
import { castApiAvailable } from "./cast_framework";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { listenMediaQuery } from "../dom/media_query";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
||||
import type {
|
||||
Condition,
|
||||
ConditionContext,
|
||||
} from "../../panels/lovelace/common/validate-condition";
|
||||
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
||||
import { extractMediaQueries, extractTimeConditions } from "./extract";
|
||||
import { calculateNextTimeUpdate } from "./time-calculator";
|
||||
@@ -19,7 +22,8 @@ export function setupMediaQueryListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
const mediaQueries = extractMediaQueries(conditions);
|
||||
|
||||
@@ -36,7 +40,8 @@ export function setupMediaQueryListeners(
|
||||
if (hasOnlyMediaQuery) {
|
||||
onUpdate(matches);
|
||||
} else {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
const context = getContext?.() ?? {};
|
||||
const conditionsMet = checkConditionsMet(conditions, hass, context);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
});
|
||||
@@ -51,7 +56,8 @@ export function setupTimeListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
const timeConditions = extractTimeConditions(conditions);
|
||||
|
||||
@@ -70,7 +76,8 @@ export function setupTimeListeners(
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (delay <= MAX_TIMEOUT_DELAY) {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
const context = getContext?.() ?? {};
|
||||
const conditionsMet = checkConditionsMet(conditions, hass, context);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
scheduleUpdate();
|
||||
@@ -87,3 +94,17 @@ export function setupTimeListeners(
|
||||
scheduleUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all condition listeners (media query, time) for conditional visibility.
|
||||
*/
|
||||
export function setupConditionListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
setupMediaQueryListeners(conditions, hass, addListener, onUpdate, getContext);
|
||||
setupTimeListeners(conditions, hass, addListener, onUpdate, getContext);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const isLoadedIntegration = (
|
||||
) =>
|
||||
!page.component ||
|
||||
ensureArray(page.component).some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isNotLoadedIntegration = (
|
||||
@@ -23,7 +23,7 @@ export const isNotLoadedIntegration = (
|
||||
) =>
|
||||
!page.not_component ||
|
||||
!ensureArray(page.not_component).some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isCore = (page: PageNavigation) => page.core;
|
||||
|
||||
@@ -21,6 +21,9 @@ export const filterNavigationPages = (
|
||||
if (page.path === "#external-app-configuration") {
|
||||
return hass.auth.external?.config.hasSettingsScreen;
|
||||
}
|
||||
if (page.adminOnly && !hass.user?.is_admin) {
|
||||
return false;
|
||||
}
|
||||
// Only show Bluetooth page if there are Bluetooth config entries
|
||||
if (page.component === "bluetooth") {
|
||||
return options.hasBluetoothConfigEntries ?? false;
|
||||
|
||||
@@ -2,6 +2,6 @@ import type { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a component is loaded. */
|
||||
export const isComponentLoaded = (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
component: string
|
||||
): boolean => hass && hass.config.components.includes(component);
|
||||
): boolean => hassConfig && hassConfig.components.includes(component);
|
||||
|
||||
@@ -27,6 +27,7 @@ export type DateRange =
|
||||
| "this_year"
|
||||
| "now-7d"
|
||||
| "now-30d"
|
||||
| "now-365d"
|
||||
| "now-12m"
|
||||
| "now-1h"
|
||||
| "now-12h"
|
||||
@@ -102,6 +103,11 @@ export const calcDateRange = (
|
||||
),
|
||||
calcDate(today, endOfMonth, locale, hassConfig),
|
||||
];
|
||||
case "now-365d":
|
||||
return [
|
||||
calcDate(today, subDays, locale, hassConfig, 365),
|
||||
calcDate(today, subDays, locale, hassConfig, 0),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
calcDate(today, subHours, locale, hassConfig, 1),
|
||||
|
||||
@@ -38,6 +38,14 @@ export interface HASSDomEvent<T> extends Event {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
export type HASSDomTargetEvent<T extends EventTarget> = Event & {
|
||||
target: T;
|
||||
};
|
||||
|
||||
export type HASSDomCurrentTargetEvent<T extends EventTarget> = Event & {
|
||||
currentTarget: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a custom event with an optional detail value.
|
||||
*
|
||||
|
||||
@@ -7,7 +7,8 @@ export type LeafletModuleType = typeof import("leaflet");
|
||||
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement
|
||||
mapElement: HTMLElement,
|
||||
initialView?: { latitude: number; longitude: number; zoom?: number }
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
@@ -32,7 +33,12 @@ export const setupLeafletMap = async (
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
if (initialView) {
|
||||
map.setView(
|
||||
[initialView.latitude, initialView.longitude],
|
||||
initialView.zoom ?? 13
|
||||
);
|
||||
}
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
||||
|
||||
@@ -14,24 +14,25 @@ export const computeDeviceName = (
|
||||
|
||||
export const computeDeviceNameDisplay = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant,
|
||||
localize: HomeAssistant["localize"],
|
||||
hassStates: HomeAssistant["states"],
|
||||
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) =>
|
||||
computeDeviceName(device) ||
|
||||
(entities && fallbackDeviceName(hass, entities)) ||
|
||||
hass.localize("ui.panel.config.devices.unnamed_device", {
|
||||
type: hass.localize(
|
||||
(entities && fallbackDeviceName(hassStates, entities)) ||
|
||||
localize("ui.panel.config.devices.unnamed_device", {
|
||||
type: localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
),
|
||||
});
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
hass: HomeAssistant,
|
||||
hassStates: HomeAssistant["states"],
|
||||
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) => {
|
||||
for (const entity of entities || []) {
|
||||
const entityId = typeof entity === "string" ? entity : entity.entity_id;
|
||||
const stateObj = hass.states[entityId];
|
||||
const stateObj = hassStates[entityId];
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
interface DeviceContext {
|
||||
device: DeviceRegistryEntry;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getDeviceContext = (
|
||||
export const getDeviceArea = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): DeviceContext => {
|
||||
areas: HomeAssistant["areas"]
|
||||
): AreaRegistryEntry | undefined => {
|
||||
const areaId = device.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : undefined;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
device: device,
|
||||
area: area || null,
|
||||
floor: floor || null,
|
||||
};
|
||||
return areaId ? areas[areaId] : undefined;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const isDeletableEntity = (
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
return !!(
|
||||
isComponentLoaded(hass, domain) &&
|
||||
isComponentLoaded(hass.config, domain) &&
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
);
|
||||
@@ -56,7 +56,7 @@ export const deleteEntity = (
|
||||
const domain = computeDomain(entity_id);
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
if (isComponentLoaded(hass, domain)) {
|
||||
if (isComponentLoaded(hass.config, domain)) {
|
||||
if (
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
|
||||
@@ -242,14 +242,18 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
},
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
export const getStatesDomain = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
domain: string,
|
||||
attribute?: string | undefined
|
||||
): string[] => {
|
||||
const domain = computeStateDomain(state);
|
||||
const result: string[] = [];
|
||||
|
||||
if (!attribute) {
|
||||
// All entities can have unavailable states
|
||||
result.push(...UNAVAILABLE_STATES);
|
||||
}
|
||||
|
||||
if (!attribute && domain in FIXED_DOMAIN_STATES) {
|
||||
result.push(...FIXED_DOMAIN_STATES[domain]);
|
||||
} else if (
|
||||
@@ -260,21 +264,7 @@ export const getStates = (
|
||||
result.push(...FIXED_DOMAIN_ATTRIBUTE_STATES[domain][attribute]);
|
||||
}
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
@@ -293,6 +283,37 @@ export const getStates = (
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
const domain = computeStateDomain(state);
|
||||
const result: string[] = [];
|
||||
|
||||
// Fixed values based on a domain
|
||||
result.push(...getStatesDomain(hass, domain, attribute));
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
if (attribute === "event_type") {
|
||||
result.push(...state.attributes.event_types);
|
||||
@@ -353,9 +374,5 @@ export const getStates = (
|
||||
break;
|
||||
}
|
||||
|
||||
if (!attribute) {
|
||||
// All entities can have unavailable states
|
||||
result.push(...UNAVAILABLE_STATES);
|
||||
}
|
||||
return [...new Set(result)];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Indicates whether the current browser has native ElementInternals support.
|
||||
*/
|
||||
export const nativeElementInternalsSupported =
|
||||
Boolean(globalThis.ElementInternals) &&
|
||||
globalThis.HTMLElement?.prototype.attachInternals
|
||||
?.toString()
|
||||
.includes("[native code]");
|
||||
@@ -41,7 +41,7 @@ export const protocolIntegrationPicked = async (
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zwave_js") ||
|
||||
!isComponentLoaded(hass.config, "zwave_js") ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
@@ -90,7 +90,7 @@ export const protocolIntegrationPicked = async (
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zha") ||
|
||||
!isComponentLoaded(hass.config, "zha") ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
@@ -139,7 +139,7 @@ export const protocolIntegrationPicked = async (
|
||||
})
|
||||
).filter((e) => !e.disabled_by);
|
||||
if (
|
||||
!isComponentLoaded(hass, domain) ||
|
||||
!isComponentLoaded(hass.config, domain) ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
|
||||
@@ -10,13 +10,10 @@
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/28732
|
||||
*/
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { directive, Directive } from "lit-html/directive.js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { setCommittedValue } from "lit-html/directive-helpers.js";
|
||||
// eslint-disable-next-line lit/no-legacy-imports
|
||||
import { nothing } from "lit-html";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import type { Part } from "lit-html/directive.js";
|
||||
|
||||
class KeyedES5 extends Directive {
|
||||
|
||||
@@ -5,12 +5,41 @@ import {
|
||||
formatDateMonthYear,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
export function getPeriodicAxisLabelConfig(
|
||||
period: string,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
):
|
||||
| {
|
||||
formatter: (value: number) => string;
|
||||
}
|
||||
| undefined {
|
||||
if (period === "month") {
|
||||
return {
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (period === "year") {
|
||||
return {
|
||||
formatter: (value: number) =>
|
||||
formatDateYear(new Date(value), locale, config),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
locale: FrontendLocaleData,
|
||||
|
||||
@@ -18,15 +18,16 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { themesContext } from "../../data/context";
|
||||
import { uiContext } from "../../data/context/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, HomeAssistantUI } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
@@ -74,8 +75,11 @@ export class HaChartBase extends LitElement {
|
||||
public extraComponents?: any[];
|
||||
|
||||
@state()
|
||||
@consume({ context: themesContext, subscribe: true })
|
||||
_themes!: Themes;
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, Themes>({
|
||||
transformer: ({ themes }) => themes,
|
||||
})
|
||||
private _themes!: Themes;
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
@@ -91,6 +95,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
private _longPressTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _longPressTriggered = false;
|
||||
|
||||
private _shouldResizeChart = false;
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
@@ -128,6 +136,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._legendPointerCancel();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
@@ -302,22 +311,31 @@ export class HaChartBase extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
private _getLegendItems() {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend).find(
|
||||
(l) => l.show && l.type === "custom"
|
||||
) as CustomLegendOption | undefined;
|
||||
if (!legend) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items =
|
||||
return (
|
||||
legend.data ||
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => ({ id: d.id, name: d.name }));
|
||||
.map((d) => ({ id: d.id, name: d.name }))
|
||||
);
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data!);
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -362,6 +380,11 @@ export class HaChartBase extends LitElement {
|
||||
return html`<li
|
||||
.id=${id}
|
||||
@click=${this._legendClick}
|
||||
@pointerdown=${this._legendPointerDown}
|
||||
@pointerup=${this._legendPointerCancel}
|
||||
@pointerleave=${this._legendPointerCancel}
|
||||
@pointercancel=${this._legendPointerCancel}
|
||||
@contextmenu=${this._legendContextMenu}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
.title=${name}
|
||||
>
|
||||
@@ -587,10 +610,7 @@ export class HaChartBase extends LitElement {
|
||||
id: "dataZoom",
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
|
||||
// It rescales the Y-axis to the visible data while keeping one point
|
||||
// just outside each boundary to avoid line gaps at the zoom edges.
|
||||
filterMode: "boundaryFilter" as any,
|
||||
filterMode: this._getDataZoomFilterMode() as any,
|
||||
xAxisIndex: 0,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
@@ -598,6 +618,23 @@ export class HaChartBase extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
|
||||
// It rescales the Y-axis to the visible data while keeping one point
|
||||
// just outside each boundary to avoid line gaps at the zoom edges.
|
||||
// Use "filter" for bar charts since boundaryFilter causes rendering issues.
|
||||
// Use "weakFilter" for other types (e.g. custom/timeline) so bars
|
||||
// spanning the visible range boundary are kept.
|
||||
private _getDataZoomFilterMode(): string {
|
||||
const series = ensureArray(this.data);
|
||||
if (series.every((s) => s.type === "line")) {
|
||||
return "boundaryFilter";
|
||||
}
|
||||
if (series.some((s) => s.type === "bar")) {
|
||||
return "filter";
|
||||
}
|
||||
return "weakFilter";
|
||||
}
|
||||
|
||||
private _createOptions(): ECOption {
|
||||
let xAxis = this.options?.xAxis;
|
||||
if (xAxis) {
|
||||
@@ -632,7 +669,7 @@ export class HaChartBase extends LitElement {
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
minInterval: axis.minInterval ?? minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
@@ -1022,11 +1059,52 @@ export class HaChartBase extends LitElement {
|
||||
fireEvent(this, "chart-zoom", { start, end });
|
||||
}
|
||||
|
||||
private _legendClick(ev: any) {
|
||||
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
|
||||
private _legendPointerDown(ev: PointerEvent) {
|
||||
// Mouse uses Ctrl/Cmd+click instead
|
||||
if (ev.pointerType === "mouse") {
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this._longPressTriggered = false;
|
||||
this._longPressTimer = setTimeout(() => {
|
||||
this._longPressTriggered = true;
|
||||
this._longPressTimer = undefined;
|
||||
this._soloLegend(id);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _legendPointerCancel() {
|
||||
if (this._longPressTimer) {
|
||||
clearTimeout(this._longPressTimer);
|
||||
this._longPressTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _legendContextMenu(ev: Event) {
|
||||
if (this._longPressTimer || this._longPressTriggered) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private _legendClick(ev: MouseEvent) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._longPressTriggered) {
|
||||
this._longPressTriggered = false;
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
|
||||
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
if (soloModifier) {
|
||||
this._soloLegend(id);
|
||||
return;
|
||||
}
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
@@ -1041,6 +1119,60 @@ export class HaChartBase extends LitElement {
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _soloLegend(id: string) {
|
||||
const allIds = this._getAllLegendIds();
|
||||
const clickedIds = this._getAllIdsFromLegend(this.options, id);
|
||||
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
|
||||
|
||||
const clickedIsOnlyVisible =
|
||||
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
|
||||
otherIds.every((i) => this._hiddenDatasets.has(i));
|
||||
|
||||
if (clickedIsOnlyVisible) {
|
||||
// Already solo'd on this item — restore all series to visible
|
||||
for (const hiddenId of [...this._hiddenDatasets]) {
|
||||
this._hiddenDatasets.delete(hiddenId);
|
||||
fireEvent(this, "dataset-unhidden", { id: hiddenId });
|
||||
}
|
||||
} else {
|
||||
// Solo: hide every other series, unhide clicked if it was hidden
|
||||
for (const otherId of otherIds) {
|
||||
if (!this._hiddenDatasets.has(otherId)) {
|
||||
this._hiddenDatasets.add(otherId);
|
||||
fireEvent(this, "dataset-hidden", { id: otherId });
|
||||
}
|
||||
}
|
||||
for (const clickedId of clickedIds) {
|
||||
if (this._hiddenDatasets.has(clickedId)) {
|
||||
this._hiddenDatasets.delete(clickedId);
|
||||
fireEvent(this, "dataset-unhidden", { id: clickedId });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _getAllLegendIds(): string[] {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
const allIds = new Set<string>();
|
||||
for (const item of items) {
|
||||
const primaryId =
|
||||
typeof item === "string"
|
||||
? item
|
||||
: ((item.id as string) ?? (item.name as string) ?? "");
|
||||
for (const expandedId of this._getAllIdsFromLegend(
|
||||
this.options,
|
||||
primaryId
|
||||
)) {
|
||||
allIds.add(expandedId);
|
||||
}
|
||||
}
|
||||
return [...allIds];
|
||||
}
|
||||
|
||||
private _toggleExpandedLegend() {
|
||||
this.expandLegend = !this.expandLegend;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface NetworkData {
|
||||
categories?: { name: string; symbol: string }[];
|
||||
}
|
||||
|
||||
const PHYSICS_DISABLE_THRESHOLD = 512;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@@ -94,7 +96,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled = true;
|
||||
@state() private _physicsEnabled?: boolean;
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
@@ -122,6 +124,14 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (this._physicsEnabled === undefined && this.data?.nodes?.length > 1) {
|
||||
this._physicsEnabled =
|
||||
this.data.nodes.length <= PHYSICS_DISABLE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!GraphChart || !this.data.nodes?.length) {
|
||||
return nothing;
|
||||
@@ -138,7 +148,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled,
|
||||
this._physicsEnabled ?? false,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import type {
|
||||
@@ -12,12 +12,12 @@ import type {
|
||||
} from "../../data/history";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
||||
import "../ha-fab";
|
||||
import "../ha-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-history-chart-line";
|
||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
@@ -105,7 +105,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
if (!isComponentLoaded(this.hass.config, "history")) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
@@ -150,16 +150,14 @@ export class StateHistoryCharts extends LitElement {
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`}
|
||||
${this.syncCharts && this._hasZoomedCharts
|
||||
? html`<ha-fab
|
||||
slot="fab"
|
||||
? html`<ha-button
|
||||
size="large"
|
||||
class="reset-button"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
@click=${this._handleGlobalZoomReset}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.history_charts.zoom_reset")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
@@ -448,6 +446,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
bottom: calc(24px + var(--safe-area-inset-bottom));
|
||||
right: calc(24px + var(--safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
|
||||
@@ -148,7 +149,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
if (!isComponentLoaded(this.hass.config, "history")) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
@@ -293,6 +294,22 @@ export class StatisticsChart extends LitElement {
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
...(this.period === "month" && {
|
||||
minInterval: 28 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"month",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
...(this.period === "year" && {
|
||||
minInterval: 365 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"year",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -15,15 +16,20 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { internationalizationContext } from "../../data/context/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
@@ -101,12 +107,13 @@ export interface DataTableRowData {
|
||||
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
|
||||
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -160,6 +167,10 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
|
||||
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
|
||||
|
||||
@query("lit-virtualizer") private _scroller?: HTMLElement;
|
||||
|
||||
@state() private _collapsedGroups: string[] = [];
|
||||
|
||||
@state() private _lastSelectedRowId: string | null = null;
|
||||
@@ -236,16 +247,28 @@ export class HaDataTable extends LitElement {
|
||||
this.updateComplete.then(() => this._calcTableHeight());
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
|
||||
if (!header) {
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this._headerRow) {
|
||||
return;
|
||||
}
|
||||
if (header.scrollWidth > header.clientWidth) {
|
||||
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
|
||||
|
||||
if (this._headerRow.scrollWidth > this._headerRow.clientWidth) {
|
||||
this.style.setProperty(
|
||||
"--table-row-width",
|
||||
`${this._headerRow.scrollWidth}px`
|
||||
);
|
||||
} else {
|
||||
this.style.removeProperty("--table-row-width");
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("selectable") ||
|
||||
(!this.autoHeight &&
|
||||
document.activeElement &&
|
||||
AUTO_FOCUS_ALLOWED_ACTIVE_TAGS.includes(document.activeElement.tagName))
|
||||
) {
|
||||
this._focusScroller();
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
@@ -378,8 +401,6 @@ export class HaDataTable extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const localize = this.localizeFunc || this.hass.localize;
|
||||
|
||||
const columns = this._sortedColumns(this.columns, this.columnOrder);
|
||||
|
||||
const renderRow = (row: DataTableRowData, index: number) =>
|
||||
@@ -503,7 +524,10 @@ export class HaDataTable extends LitElement {
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
${this.noDataText ||
|
||||
localize("ui.components.data-table.no-data")}
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -512,10 +536,12 @@ export class HaDataTable extends LitElement {
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._groupData(
|
||||
this._filteredData,
|
||||
localize,
|
||||
this._i18n?.localize,
|
||||
this._i18n?.locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
@@ -685,7 +711,7 @@ export class HaDataTable extends LitElement {
|
||||
this._sortColumns[this.sortColumn],
|
||||
this.sortDirection,
|
||||
this.sortColumn,
|
||||
this.hass.locale.language
|
||||
this._i18n?.locale?.language
|
||||
)
|
||||
: filteredData;
|
||||
|
||||
@@ -711,7 +737,8 @@ export class HaDataTable extends LitElement {
|
||||
private _groupData = memoizeOne(
|
||||
(
|
||||
data: DataTableRowData[],
|
||||
localize: LocalizeFunc,
|
||||
localize: LocalizeFunc | undefined,
|
||||
locale: FrontendLocaleData | undefined,
|
||||
appendRow,
|
||||
groupColumn: string | undefined,
|
||||
groupOrder: string[] | undefined,
|
||||
@@ -735,11 +762,7 @@ export class HaDataTable extends LitElement {
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!groupOrder && isGroupSortColumn) {
|
||||
const comparison = stringCompare(
|
||||
a,
|
||||
b,
|
||||
this.hass.locale.language
|
||||
);
|
||||
const comparison = stringCompare(a, b, locale?.language);
|
||||
if (sortDirection === "asc") {
|
||||
return comparison;
|
||||
}
|
||||
@@ -760,7 +783,7 @@ export class HaDataTable extends LitElement {
|
||||
return stringCompare(
|
||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||
this.hass.locale.language
|
||||
locale?.language
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
@@ -787,14 +810,15 @@ export class HaDataTable extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
.label=${this.hass.localize(
|
||||
.label=${localize?.(
|
||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||
)}
|
||||
) || (collapsed ? "Expand" : "Collapse")}
|
||||
class=${collapsed ? "collapsed" : ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? localize("ui.components.data-table.ungrouped")
|
||||
? localize?.("ui.components.data-table.ungrouped") ||
|
||||
"Ungrouped"
|
||||
: groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
@@ -825,8 +849,10 @@ export class HaDataTable extends LitElement {
|
||||
): Promise<DataTableRowData[]> => filterData(data, columns, filter)
|
||||
);
|
||||
|
||||
private _handleHeaderClick(ev: Event) {
|
||||
const columnId = (ev.currentTarget as any).columnId;
|
||||
private _handleHeaderClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { columnId: string }>
|
||||
) {
|
||||
const columnId = ev.currentTarget.columnId;
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
@@ -844,11 +870,12 @@ export class HaDataTable extends LitElement {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
|
||||
this._focusScroller();
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
if (checkbox.checked) {
|
||||
private _handleHeaderRowCheckboxClick(ev: HASSDomTargetEvent<HaCheckbox>) {
|
||||
if (ev.target.checked) {
|
||||
this.selectAll();
|
||||
} else {
|
||||
this._checkedRows = [];
|
||||
@@ -857,13 +884,15 @@ export class HaDataTable extends LitElement {
|
||||
this._lastSelectedRowId = null;
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClicked = (ev: Event) => {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const rowId = (checkbox as any).rowId;
|
||||
private _handleRowCheckboxClicked = (
|
||||
ev: HASSDomCurrentTargetEvent<HaCheckbox & { rowId: string }>
|
||||
) => {
|
||||
const rowId = ev.currentTarget.rowId;
|
||||
|
||||
const groupedData = this._groupData(
|
||||
this._filteredData,
|
||||
this.localizeFunc || this.hass.localize,
|
||||
this._i18n?.localize,
|
||||
this._i18n?.locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
@@ -895,7 +924,7 @@ export class HaDataTable extends LitElement {
|
||||
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
|
||||
];
|
||||
}
|
||||
} else if (!checkbox.checked) {
|
||||
} else if (!ev.currentTarget.checked) {
|
||||
if (!this._checkedRows.includes(rowId)) {
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
}
|
||||
@@ -933,7 +962,9 @@ export class HaDataTable extends LitElement {
|
||||
return checkedRows;
|
||||
}
|
||||
|
||||
private _handleRowClick = (ev: Event) => {
|
||||
private _handleRowClick = (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { rowId: string }>
|
||||
) => {
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
@@ -949,14 +980,13 @@ export class HaDataTable extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const rowId = (ev.currentTarget as any).rowId;
|
||||
const rowId = ev.currentTarget.rowId;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
};
|
||||
|
||||
private _setTitle(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
if (target.scrollWidth > target.offsetWidth) {
|
||||
target.setAttribute("title", target.innerText);
|
||||
private _setTitle(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
|
||||
if (ev.currentTarget.scrollWidth > ev.currentTarget.offsetWidth) {
|
||||
ev.currentTarget.setAttribute("title", ev.currentTarget.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,6 +1008,12 @@ export class HaDataTable extends LitElement {
|
||||
this._debounceSearch((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private _focusScroller(): void {
|
||||
this._scroller?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _calcTableHeight() {
|
||||
if (this.autoHeight) {
|
||||
return;
|
||||
@@ -987,23 +1023,27 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
this._savedScrollPos = e.target.scrollTop;
|
||||
|
||||
this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
if (this._headerRow) {
|
||||
this._headerRow.scrollLeft = e.target.scrollLeft;
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _scrollContent(e: Event) {
|
||||
this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
private _scrollContent(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
if (!this._scroller) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scroller.scrollLeft = e.target.scrollLeft;
|
||||
}
|
||||
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
const groupName = (ev.currentTarget as any).group;
|
||||
private _collapseGroup = (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
|
||||
) => {
|
||||
const groupName = ev.currentTarget.group;
|
||||
if (this._collapsedGroups.includes(groupName)) {
|
||||
this._collapsedGroups = this._collapsedGroups.filter(
|
||||
(grp) => grp !== groupName
|
||||
@@ -1426,6 +1466,11 @@ export class HaDataTable extends LitElement {
|
||||
contain: size layout !important;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
lit-virtualizer:focus,
|
||||
lit-virtualizer:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
@@ -12,14 +13,19 @@ import {
|
||||
formatDateYear,
|
||||
formatISODateOnly,
|
||||
} from "../../common/datetime/format_date";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
configContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
} from "../../data/context";
|
||||
internationalizationContext,
|
||||
} from "../../data/context/context";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import type { HomeAssistantConfig, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-filter-chip";
|
||||
import type { HaFilterChip } from "../chips/ha-filter-chip";
|
||||
import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
@@ -27,12 +33,12 @@ import "../ha-icon-button-prev";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
import { datePickerStyles, dateRangePickerStyles } from "./styles";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("date-range-picker")
|
||||
export class DateRangePicker extends LitElement {
|
||||
export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: false }) public ranges?: DateRangePickerRanges | false;
|
||||
|
||||
@property({ attribute: false }) public startDate?: Date;
|
||||
@@ -43,16 +49,15 @@ export class DateRangePicker extends LitElement {
|
||||
public timePicker = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale!: ContextType<typeof localeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private hassConfig!: ContextType<typeof configContext>;
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
/** used to show month in calendar-range header */
|
||||
@state() private _pickerMonth?: string;
|
||||
@@ -82,12 +87,20 @@ export class DateRangePicker extends LitElement {
|
||||
? formatCallyDateRange(
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n?.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: undefined;
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
|
||||
if (this.timePicker && this.startDate && this.endDate) {
|
||||
this._timeValue = {
|
||||
@@ -103,26 +116,48 @@ export class DateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderRanges() {
|
||||
if (this._isMobileSize) {
|
||||
return html`
|
||||
<ha-chip-set class="ha-scrollbar">
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-filter-chip
|
||||
.index=${index}
|
||||
.range=${range}
|
||||
@click=${this._clickDateRangeChip}
|
||||
>
|
||||
${name}
|
||||
</ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div class="picker">
|
||||
${this.ranges !== false && this.ranges
|
||||
? html`<div class="date-range-ranges">
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>`
|
||||
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
|
||||
: nothing}
|
||||
<div class="range">
|
||||
<calendar-range
|
||||
.value=${this._dateValue}
|
||||
.locale=${this.locale.language}
|
||||
.locale=${this._i18n.locale.language}
|
||||
.focusedDate=${this._focusDate}
|
||||
@focusday=${this._focusChanged}
|
||||
@change=${this._handleChange}
|
||||
show-outside-days
|
||||
.firstDayOfWeek=${firstWeekdayIndex(this.locale)}
|
||||
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
|
||||
>
|
||||
<ha-icon-button-prev
|
||||
tabindex="-1"
|
||||
@@ -135,7 +170,7 @@ export class DateRangePicker extends LitElement {
|
||||
<ha-icon-button
|
||||
@click=${this._focusToday}
|
||||
.path=${mdiCalendarToday}
|
||||
.label=${this.localize("ui.dialogs.date-picker.today")}
|
||||
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-icon-button-next
|
||||
@@ -149,9 +184,9 @@ export class DateRangePicker extends LitElement {
|
||||
<div class="times">
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
|
||||
.locale=${this.locale}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_from"
|
||||
)}
|
||||
id="from"
|
||||
@@ -160,9 +195,9 @@ export class DateRangePicker extends LitElement {
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
.locale=${this.locale}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_to"
|
||||
)}
|
||||
id="to"
|
||||
@@ -176,19 +211,33 @@ export class DateRangePicker extends LitElement {
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button appearance="plain" @click=${this._cancel}
|
||||
>${this.localize("ui.common.cancel")}</ha-button
|
||||
>${this._i18n.localize("ui.common.cancel")}</ha-button
|
||||
>
|
||||
<ha-button .disabled=${!this._dateValue} @click=${this._save}
|
||||
>${this.localize("ui.components.date-range-picker.select")}</ha-button
|
||||
>${this._i18n.localize(
|
||||
"ui.components.date-range-picker.select"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _focusToday() {
|
||||
const date = new Date();
|
||||
this._focusDate = formatISODateOnly(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._focusDate = formatISODateOnly(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
}
|
||||
|
||||
private _cancel() {
|
||||
@@ -228,12 +277,12 @@ export class DateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.locale.time_zone === TimeZone.server) {
|
||||
if (this._i18n.locale.time_zone === TimeZone.server) {
|
||||
startDate = new Date(
|
||||
new TZDate(startDate, this.hassConfig.time_zone).getTime()
|
||||
new TZDate(startDate, this._hassConfig.time_zone).getTime()
|
||||
);
|
||||
endDate = new Date(
|
||||
new TZDate(endDate, this.hassConfig.time_zone).getTime()
|
||||
new TZDate(endDate, this._hassConfig.time_zone).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,8 +308,16 @@ export class DateRangePicker extends LitElement {
|
||||
|
||||
private _focusChanged(ev: CustomEvent<Date>) {
|
||||
const date = ev.detail;
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._focusDate = undefined;
|
||||
}
|
||||
|
||||
@@ -270,18 +327,30 @@ export class DateRangePicker extends LitElement {
|
||||
this._focusDate = undefined;
|
||||
}
|
||||
|
||||
private _clickDateRangeChip(ev: Event) {
|
||||
const chip = ev.target as HaFilterChip & {
|
||||
index: number;
|
||||
range: [Date, Date];
|
||||
};
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
startDate: dateRange[0],
|
||||
endDate: dateRange[1],
|
||||
startDate: range[0],
|
||||
endDate: range[1],
|
||||
},
|
||||
});
|
||||
fireEvent(this, "preset-selected", {
|
||||
index: ev.detail.index,
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -306,6 +375,7 @@ export class DateRangePicker extends LitElement {
|
||||
static styles = [
|
||||
datePickerStyles,
|
||||
dateRangePickerStyles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.picker {
|
||||
display: flex;
|
||||
@@ -313,7 +383,7 @@ export class DateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-right: var(--ha-border-width-sm) solid var(--divider-color);
|
||||
min-width: 140px;
|
||||
flex: 0 1 30%;
|
||||
}
|
||||
@@ -327,16 +397,21 @@ export class DateRangePicker extends LitElement {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 460px) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.picker {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
flex-basis: 180px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
border-right: none;
|
||||
overflow-y: scroll;
|
||||
margin-top: var(--ha-space-5);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.range {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import "cally";
|
||||
import { isThisYear } from "date-fns";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -14,12 +15,13 @@ import {
|
||||
formatShortDateTime,
|
||||
formatShortDateTimeWithYear,
|
||||
} from "../../common/datetime/format_date_time";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
configContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
} from "../../data/context";
|
||||
internationalizationContext,
|
||||
} from "../../data/context/context";
|
||||
import type { HomeAssistantConfig } from "../../types";
|
||||
import "../ha-bottom-sheet";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
@@ -43,16 +45,15 @@ const EXTENDED_RANGE_KEYS: DateRange[] = [
|
||||
@customElement("ha-date-range-picker")
|
||||
export class HaDateRangePicker extends LitElement {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale!: ContextType<typeof localeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private hassConfig!: ContextType<typeof configContext>;
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
@property({ attribute: false }) public startDate!: Date;
|
||||
|
||||
@@ -117,8 +118,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
this._ranges = {};
|
||||
rangeKeys.forEach((key) => {
|
||||
this._ranges![
|
||||
this.localize(`ui.components.date-range-picker.ranges.${key}`)
|
||||
] = calcDateRange(this.locale, this.hassConfig, key);
|
||||
this._i18n.localize(`ui.components.date-range-picker.ranges.${key}`)
|
||||
] = calcDateRange(this._i18n.locale, this._hassConfig, key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,47 +134,50 @@ export class HaDateRangePicker extends LitElement {
|
||||
${!this.minimal
|
||||
? html`<ha-textarea
|
||||
id="field"
|
||||
mobile-multiline
|
||||
rows="1"
|
||||
resize="auto"
|
||||
@click=${this._openPicker}
|
||||
@keydown=${this._handleKeydown}
|
||||
.value=${(isThisYear(this.startDate)
|
||||
? formatShortDateTime(
|
||||
this.startDate,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.startDate,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)) +
|
||||
(window.innerWidth >= 459 ? " - " : " - \n") +
|
||||
(isThisYear(this.endDate)
|
||||
? formatShortDateTime(
|
||||
this.endDate,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.endDate,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
))}
|
||||
.label=${this.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
) +
|
||||
" - " +
|
||||
this.localize("ui.components.date-range-picker.end_date")}
|
||||
this._i18n.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
readonly
|
||||
></ha-textarea>
|
||||
<ha-icon-button-prev
|
||||
.label=${this.localize("ui.common.previous")}
|
||||
.label=${this._i18n.localize("ui.common.previous")}
|
||||
@click=${this._handlePrev}
|
||||
>
|
||||
</ha-icon-button-prev>
|
||||
<ha-icon-button-next
|
||||
.label=${this.localize("ui.common.next")}
|
||||
.label=${this._i18n.localize("ui.common.next")}
|
||||
@click=${this._handleNext}
|
||||
>
|
||||
</ha-icon-button-next>`
|
||||
@@ -181,7 +185,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
@click=${this._openPicker}
|
||||
.disabled=${this.disabled}
|
||||
id="field"
|
||||
.label=${this.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.select_date_range"
|
||||
)}
|
||||
.path=${mdiCalendar}
|
||||
@@ -289,8 +293,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
forward,
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this.startDate = start;
|
||||
this.endDate = end;
|
||||
@@ -336,14 +340,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
private _setTextareaFocusStyle(focused: boolean) {
|
||||
const textarea = this.renderRoot.querySelector("ha-textarea");
|
||||
if (textarea) {
|
||||
const foundation = (textarea as any).mdcFoundation;
|
||||
if (foundation) {
|
||||
if (focused) {
|
||||
foundation.activateFocus();
|
||||
} else {
|
||||
foundation.deactivateFocus();
|
||||
}
|
||||
}
|
||||
textarea.setFocused(focused);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiBackspace, mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
@@ -10,12 +11,13 @@ import {
|
||||
formatDateYear,
|
||||
formatISODateOnly,
|
||||
} from "../../common/datetime/format_date";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import {
|
||||
configContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
} from "../../data/context";
|
||||
internationalizationContext,
|
||||
} from "../../data/context/context";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { HomeAssistantConfig } from "../../types";
|
||||
import "../ha-button";
|
||||
import type { DatePickerDialogParams } from "../ha-date-input";
|
||||
import "../ha-dialog";
|
||||
@@ -40,16 +42,15 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale!: ContextType<typeof localeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private hassConfig!: ContextType<typeof configContext>;
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
@state() private _value?: {
|
||||
year: string;
|
||||
@@ -74,14 +75,26 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
? new Date(`${this.params.value.split("T")[0]}T00:00:00`)
|
||||
: new Date();
|
||||
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
|
||||
this._value = this.params.value
|
||||
? {
|
||||
year: this._pickerYear,
|
||||
title: formatDateShort(date, this.locale, this.hassConfig),
|
||||
dateString: formatISODateOnly(date, this.locale, this.hassConfig),
|
||||
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
|
||||
dateString: formatISODateOnly(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
@@ -95,7 +108,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
open
|
||||
width="small"
|
||||
.headerTitle=${this._value?.title ||
|
||||
this.localize("ui.dialogs.date-picker.title")}
|
||||
this._i18n.localize("ui.dialogs.date-picker.title")}
|
||||
.headerSubtitle=${this._value?.year}
|
||||
header-subtitle-position="above"
|
||||
>
|
||||
@@ -103,7 +116,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiBackspace}
|
||||
.label=${this.localize("ui.dialogs.date-picker.clear")}
|
||||
.label=${this._i18n.localize("ui.dialogs.date-picker.clear")}
|
||||
slot="headerActionItems"
|
||||
@click=${this._clear}
|
||||
></ha-icon-button>
|
||||
@@ -131,7 +144,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
<ha-icon-button
|
||||
@click=${this._setToday}
|
||||
.path=${mdiCalendarToday}
|
||||
.label=${this.localize("ui.dialogs.date-picker.today")}
|
||||
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-icon-button-next tabindex="-1" slot="next"></ha-icon-button-next>
|
||||
@@ -143,10 +156,10 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.localize("ui.common.cancel")}
|
||||
${this._i18n.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.localize("ui.common.ok")}
|
||||
${this._i18n.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>`;
|
||||
@@ -164,23 +177,39 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
? new Date(`${value.split("T")[0]}T00:00:00`)
|
||||
: new Date();
|
||||
this._value = {
|
||||
year: formatDateYear(date, this.locale, this.hassConfig),
|
||||
title: formatDateShort(date, this.locale, this.hassConfig),
|
||||
year: formatDateYear(date, this._i18n.locale, this._hassConfig),
|
||||
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
|
||||
dateString:
|
||||
value || formatISODateOnly(date, this.locale, this.hassConfig),
|
||||
value || formatISODateOnly(date, this._i18n.locale, this._hassConfig),
|
||||
};
|
||||
|
||||
if (setFocusDay) {
|
||||
this._focusDate = this._value.dateString;
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _focusChanged(ev: CustomEvent<Date>) {
|
||||
const date = ev.detail;
|
||||
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
|
||||
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._focusDate = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import { fullEntitiesContext } from "../../data/context/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
type DevicePickerItem,
|
||||
} from "../../data/device/device_picker";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
@@ -154,7 +154,7 @@ export class HaDevicePicker extends LitElement {
|
||||
return html`<span slot="headline">${deviceId}</span>`;
|
||||
}
|
||||
|
||||
const { area } = getDeviceContext(device, this.hass);
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
@@ -38,6 +38,8 @@ export class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
@@ -124,7 +126,8 @@ export class HaEntityStatePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.disabled=${this.disabled ||
|
||||
(!this.entityId && this.noEntity === false)}
|
||||
.autofocus=${this.autofocus}
|
||||
.required=${this.required}
|
||||
.label=${this.label ??
|
||||
|
||||
@@ -94,7 +94,7 @@ class HaAddonPicker extends LitElement {
|
||||
|
||||
private async _getApps() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._addons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
@@ -38,6 +39,8 @@ class HaAlert extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
public render() {
|
||||
@@ -65,7 +68,7 @@ class HaAlert extends LitElement {
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismissClicked}
|
||||
label="Dismiss alert"
|
||||
.label=${this.localize!("ui.common.dismiss_alert")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
@@ -267,7 +267,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
: item.domain
|
||||
? html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
.deviceClass=${item.deviceClass}
|
||||
></ha-domain-icon>`
|
||||
|
||||
@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
* @cssprop --ha-button-height - The height of the button.
|
||||
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
|
||||
*
|
||||
* @attr {("small"|"medium")} size - Sets the button size.
|
||||
* @attr {("small"|"medium"|"large")} size - Sets the button size.
|
||||
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
|
||||
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
|
||||
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||
@@ -62,6 +62,7 @@ export class HaButton extends Button {
|
||||
transition: background-color var(--ha-animation-duration-fast)
|
||||
ease-out;
|
||||
text-wrap: wrap;
|
||||
box-shadow: var(--ha-button-box-shadow);
|
||||
}
|
||||
|
||||
:host([size="small"]) .button {
|
||||
@@ -73,6 +74,14 @@ export class HaButton extends Button {
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([size="large"]) .button {
|
||||
--wa-form-control-height: var(
|
||||
--ha-button-height,
|
||||
var(--button-height, 48px)
|
||||
);
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
--button-color-fill-normal-active: var(
|
||||
--ha-color-fill-primary-normal-active
|
||||
|
||||
@@ -7,7 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { localizeContext } from "../data/context";
|
||||
import { internationalizationContext } from "../data/context/context";
|
||||
import type { UiColorExtraOption } from "../data/selector";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
@@ -55,8 +55,8 @@ export class HaColorPicker extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
render() {
|
||||
const effectiveValue = this.value ?? this.defaultColor ?? "";
|
||||
@@ -73,7 +73,7 @@ export class HaColorPicker extends LitElement {
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.notFoundLabel=${this.localize?.(
|
||||
.notFoundLabel=${this._i18n?.localize?.(
|
||||
"ui.components.color-picker.no_colors_found"
|
||||
)}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
@@ -103,7 +103,7 @@ export class HaColorPicker extends LitElement {
|
||||
{
|
||||
id: searchString,
|
||||
primary:
|
||||
this.localize?.("ui.components.color-picker.custom_color") ||
|
||||
this._i18n?.localize?.("ui.components.color-picker.custom_color") ||
|
||||
"Custom color",
|
||||
secondary: searchString,
|
||||
},
|
||||
@@ -130,14 +130,15 @@ export class HaColorPicker extends LitElement {
|
||||
const items: PickerComboBoxItem[] = [];
|
||||
|
||||
const defaultSuffix =
|
||||
this.localize?.("ui.components.color-picker.default") || "Default";
|
||||
this._i18n?.localize?.("ui.components.color-picker.default") ||
|
||||
"Default";
|
||||
|
||||
const addDefaultSuffix = (label: string, isDefault: boolean) =>
|
||||
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
|
||||
|
||||
if (includeNone) {
|
||||
const noneLabel =
|
||||
this.localize?.("ui.components.color-picker.none") || "None";
|
||||
this._i18n?.localize?.("ui.components.color-picker.none") || "None";
|
||||
items.push({
|
||||
id: "none",
|
||||
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
|
||||
@@ -147,7 +148,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
if (includeState) {
|
||||
const stateLabel =
|
||||
this.localize?.("ui.components.color-picker.state") || "State";
|
||||
this._i18n?.localize?.("ui.components.color-picker.state") || "State";
|
||||
items.push({
|
||||
id: "state",
|
||||
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
|
||||
@@ -170,7 +171,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
Array.from(THEME_COLORS).forEach((color) => {
|
||||
const themeLabel =
|
||||
this.localize?.(
|
||||
this._i18n?.localize?.(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color;
|
||||
items.push({
|
||||
@@ -227,7 +228,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.localize?.("ui.components.color-picker.none") || "None"}
|
||||
${this._i18n?.localize?.("ui.components.color-picker.none") || "None"}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -235,7 +236,8 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.localize?.("ui.components.color-picker.state") || "State"}
|
||||
${this._i18n?.localize?.("ui.components.color-picker.state") ||
|
||||
"State"}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -243,7 +245,7 @@ export class HaColorPicker extends LitElement {
|
||||
const extraOption = this.extraOptions?.find((o) => o.value === value);
|
||||
const label =
|
||||
extraOption?.label ||
|
||||
this.localize?.(
|
||||
this._i18n?.localize?.(
|
||||
`ui.components.color-picker.colors.${value}` as LocalizeKeys
|
||||
) ||
|
||||
value;
|
||||
|
||||
@@ -79,7 +79,6 @@ class HaConfigEntryPicker extends LitElement {
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
@@ -115,7 +114,6 @@ class HaConfigEntryPicker extends LitElement {
|
||||
slot="headline"
|
||||
>${item?.icon
|
||||
? html`<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`
|
||||
|
||||
@@ -123,6 +123,9 @@ export class HaDateInput extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
min-width: 0px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import { localizeContext } from "../data/context";
|
||||
import { internationalizationContext } from "../data/context/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-dialog-header";
|
||||
@@ -123,13 +123,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// @state()
|
||||
// @consume({ context: authContext, subscribe: true })
|
||||
// private auth?: ContextType<typeof authContext>;
|
||||
// @consume({ context: configContext, subscribe: true })
|
||||
// private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
@@ -184,7 +184,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.localize?.("ui.common.close") ?? "Close"}
|
||||
.label=${this._i18n?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
@@ -222,13 +222,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
// if (this._hassConfig?.auth.external && isIosApp(this._hassConfig.auth.external)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-dialog-autofocus";
|
||||
// }
|
||||
// this.auth.external.fireMessage({
|
||||
// this._hassConfig.auth.external.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
uiContext,
|
||||
} from "../data/context/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
domainIcon,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import "./ha-icon";
|
||||
|
||||
@customElement("ha-domain-icon")
|
||||
export class HaDomainIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public domain?: string;
|
||||
|
||||
@property({ attribute: false }) public deviceClass?: string;
|
||||
@@ -25,6 +28,18 @@ export class HaDomainIcon extends LitElement {
|
||||
@property({ attribute: "brand-fallback", type: Boolean })
|
||||
public brandFallback?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _hassUi?: ContextType<typeof uiContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -34,12 +49,13 @@ export class HaDomainIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._hassConfig) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = domainIcon(
|
||||
this.hass,
|
||||
this._connection.connection,
|
||||
this._hassConfig.config,
|
||||
this.domain,
|
||||
this.deviceClass,
|
||||
this.state
|
||||
@@ -65,9 +81,9 @@ export class HaDomainIcon extends LitElement {
|
||||
{
|
||||
domain: this.domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
darkOptimized: this._hassUi?.themes.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
this._hassConfig?.auth.data.hassUrl
|
||||
);
|
||||
return html`
|
||||
<img
|
||||
|
||||
@@ -100,6 +100,9 @@ export class HaDropdown extends Dropdown {
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
wa-popup::part(popup) {
|
||||
z-index: 200;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { FabBase } from "@material/mwc-fab/mwc-fab-base";
|
||||
import { styles } from "@material/mwc-fab/mwc-fab.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css } from "lit";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-fab")
|
||||
export class HaFab extends FabBase {
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-typography-button-text-transform: none;
|
||||
--mdc-typography-button-font-size: var(--ha-font-size-l);
|
||||
--mdc-typography-button-font-family: var(--ha-font-family-body);
|
||||
--mdc-typography-button-font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
:host .mdc-fab--extended {
|
||||
border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
}
|
||||
:host .mdc-fab.mdc-fab--extended .ripple {
|
||||
border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
}
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
margin-inline-start: -8px;
|
||||
margin-inline-end: 12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:disabled {
|
||||
--mdc-theme-secondary: var(--disabled-text-color);
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
mainWindow.document.dir === "rtl"
|
||||
? css`
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-fab": HaFab;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -12,6 +11,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./progress/ha-progress-bar";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -100,10 +100,11 @@ export class HaFileUpload extends LitElement {
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<mwc-linear-progress
|
||||
<ha-progress-bar
|
||||
.indeterminate=${!this.progress}
|
||||
.progress=${this.progress ? this.progress / 100 : undefined}
|
||||
></mwc-linear-progress>
|
||||
.value=${this.progress}
|
||||
loading
|
||||
></ha-progress-bar>
|
||||
</div>`
|
||||
: html`<label
|
||||
for=${this.value ? "" : "input"}
|
||||
@@ -319,7 +320,7 @@ export class HaFileUpload extends LitElement {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--ha-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
ha-progress-bar {
|
||||
width: 100%;
|
||||
padding: 8px 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -101,7 +101,11 @@ export class HaFilterDevices extends LitElement {
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceNameDisplay(device, this.hass)}
|
||||
${computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
@@ -151,14 +155,18 @@ export class HaFilterDevices extends LitElement {
|
||||
.filter(
|
||||
(device) =>
|
||||
!filter ||
|
||||
computeDeviceNameDisplay(device, this.hass)
|
||||
computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeDeviceNameDisplay(a, this.hass),
|
||||
computeDeviceNameDisplay(b, this.hass),
|
||||
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
|
||||
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
@@ -72,7 +72,6 @@ export class HaFilterDomains extends LitElement {
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -9,7 +9,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { labelsContext } from "../data/context/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -100,7 +100,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
inputMode="numeric"
|
||||
.label=${this.label}
|
||||
.hint=${this.helper}
|
||||
.value=${this.data !== undefined ? this.data.toString() : ""}
|
||||
.value=${this.data?.toString() ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, LitElement, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -54,6 +54,7 @@ export class HaGauge extends LitElement {
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
}
|
||||
this._segment_label = this._getSegmentLabel();
|
||||
this._rescaleSvg();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +71,7 @@ export class HaGauge extends LitElement {
|
||||
}
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this._getSegmentLabel();
|
||||
this._rescaleSvg();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -88,87 +90,91 @@ export class HaGauge extends LitElement {
|
||||
/>
|
||||
|
||||
|
||||
${
|
||||
this.levels
|
||||
? [...this.levels]
|
||||
.sort((a, b) => a.level - b.level)
|
||||
.map((level, i, arr) => {
|
||||
const startLevel = i === 0 ? this.min : arr[i].level;
|
||||
const endLevel = i + 1 < arr.length ? arr[i + 1].level : this.max;
|
||||
${
|
||||
this.levels
|
||||
? (() => {
|
||||
const sortedLevels = [...this.levels].sort(
|
||||
(a, b) => a.level - b.level
|
||||
);
|
||||
|
||||
const startAngle = getAngle(startLevel, this.min, this.max);
|
||||
const endAngle = getAngle(endLevel, this.min, this.max);
|
||||
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
||||
if (
|
||||
sortedLevels.length > 0 &&
|
||||
sortedLevels[0].level !== this.min
|
||||
) {
|
||||
sortedLevels.unshift({
|
||||
level: this.min,
|
||||
stroke: "var(--info-color)",
|
||||
});
|
||||
}
|
||||
|
||||
const x1 = -arcRadius * Math.cos((startAngle * Math.PI) / 180);
|
||||
const y1 = -arcRadius * Math.sin((startAngle * Math.PI) / 180);
|
||||
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
|
||||
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
|
||||
return sortedLevels.map((level, i, arr) => {
|
||||
const startLevel = level.level;
|
||||
const endLevel =
|
||||
i + 1 < arr.length ? arr[i + 1].level : this.max;
|
||||
|
||||
const firstSegment = i === 0;
|
||||
const lastSegment = i === arr.length - 1;
|
||||
const startAngle = getAngle(startLevel, this.min, this.max);
|
||||
const endAngle = getAngle(endLevel, this.min, this.max);
|
||||
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
||||
|
||||
const paths: TemplateResult[] = [];
|
||||
const x1 =
|
||||
-arcRadius * Math.cos((startAngle * Math.PI) / 180);
|
||||
const y1 =
|
||||
-arcRadius * Math.sin((startAngle * Math.PI) / 180);
|
||||
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
|
||||
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
|
||||
|
||||
if (firstSegment) {
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: round"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
} else if (lastSegment) {
|
||||
const offsetAngle = 0.5;
|
||||
const midAngle = endAngle - offsetAngle;
|
||||
const xm = -arcRadius * Math.cos((midAngle * Math.PI) / 180);
|
||||
const ym = -arcRadius * Math.sin((midAngle * Math.PI) / 180);
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === arr.length - 1;
|
||||
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}"
|
||||
/>
|
||||
`);
|
||||
if (isFirst) {
|
||||
return svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: round"
|
||||
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
} else {
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
}
|
||||
if (isLast) {
|
||||
const offsetAngle = 0.5;
|
||||
const midAngle = endAngle - offsetAngle;
|
||||
const xm =
|
||||
-arcRadius * Math.cos((midAngle * Math.PI) / 180);
|
||||
const ym =
|
||||
-arcRadius * Math.sin((midAngle * Math.PI) / 180);
|
||||
|
||||
return paths;
|
||||
})
|
||||
: ""
|
||||
}
|
||||
return svg`
|
||||
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}" />
|
||||
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
|
||||
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}" />
|
||||
`;
|
||||
}
|
||||
|
||||
return svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
></path>
|
||||
`;
|
||||
});
|
||||
})()
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
this.needle
|
||||
? svg`
|
||||
<line
|
||||
class="needle"
|
||||
x1="-35.0"
|
||||
y1="0"
|
||||
x2="-45.0"
|
||||
y2="0"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
/>
|
||||
<path
|
||||
class="needle"
|
||||
d="M -34,-3 L -48,-1 A 1,1,0,0,0,-48,1 L -34,3 A 2,2,0,0,0,-34,-3 Z"
|
||||
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
/>
|
||||
`
|
||||
: svg`
|
||||
<path
|
||||
@@ -179,7 +185,8 @@ export class HaGauge extends LitElement {
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text
|
||||
class="value-text"
|
||||
x="0"
|
||||
@@ -204,6 +211,18 @@ export class HaGauge extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _rescaleSvg() {
|
||||
// Set the viewbox of the SVG containing the value to perfectly
|
||||
// fit the text
|
||||
// That way it will auto-scale correctly
|
||||
const svgRoot = this.shadowRoot!.querySelector(".text")!;
|
||||
const box = svgRoot.querySelector("text")!.getBBox()!;
|
||||
svgRoot.setAttribute(
|
||||
"viewBox",
|
||||
`${box.x} ${box.y} ${box.width} ${box.height}`
|
||||
);
|
||||
}
|
||||
|
||||
private _getSegmentLabel() {
|
||||
if (this.levels) {
|
||||
[...this.levels].sort((a, b) => a.level - b.level);
|
||||
@@ -224,32 +243,43 @@ export class HaGauge extends LitElement {
|
||||
.levels-base {
|
||||
fill: none;
|
||||
stroke: var(--primary-background-color);
|
||||
stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 12;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 12;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.value {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 12;
|
||||
stroke: var(--gauge-color);
|
||||
stroke-linecap: round;
|
||||
stroke-linecap: butt;
|
||||
transition: stroke-dashoffset 1s ease 0s;
|
||||
}
|
||||
|
||||
.needle {
|
||||
stroke: var(--primary-text-color);
|
||||
stroke-width: 2;
|
||||
fill: var(--primary-text-color);
|
||||
stroke: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
stroke-width: 1;
|
||||
stroke-linecap: round;
|
||||
transform-origin: 0 0;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
max-height: 40%;
|
||||
max-width: 55%;
|
||||
left: 50%;
|
||||
bottom: 10%;
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
fill: var(--primary-text-color);
|
||||
|
||||
@@ -140,7 +140,7 @@ class HaHLSPlayer extends LitElement {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
|
||||
if (!isComponentLoaded(this.hass!, "stream")) {
|
||||
if (!isComponentLoaded(this.hass.config, "stream")) {
|
||||
this._setFatalError("Streaming component is not loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { labelsContext } from "../data/context/context";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { labelsContext } from "../data/context/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { updateLabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, render, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@@ -14,9 +14,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 { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@@ -129,7 +129,7 @@ class HaMountPicker extends LitElement {
|
||||
|
||||
private async _getMounts() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
this._mounts = await fetchSupervisorMounts(this.hass);
|
||||
if (this.usage === SupervisorMountUsage.BACKUP && !this.value) {
|
||||
this.value = this._mounts.default_backup_mount || _BACKUP_DATA_DISK_;
|
||||
|
||||
@@ -132,7 +132,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
@@ -158,7 +157,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -15,7 +15,7 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { localeContext, localizeContext } from "../data/context";
|
||||
import { internationalizationContext } from "../data/context/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
@@ -162,12 +162,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale!: ContextType<typeof localeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
@@ -222,9 +218,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
const searchLabel =
|
||||
this.label ??
|
||||
(this.allowCustomValue
|
||||
? (this.localize?.("ui.components.combo-box.search_or_custom") ??
|
||||
? (this.i18n.localize?.("ui.components.combo-box.search_or_custom") ??
|
||||
"Search | Add custom value")
|
||||
: (this.localize?.("ui.common.search") ?? "Search"));
|
||||
: (this.i18n.localize?.("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-input-search
|
||||
appearance="outlined"
|
||||
@@ -351,7 +347,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return caseInsensitiveStringCompare(
|
||||
sortLabelA,
|
||||
sortLabelB,
|
||||
this.locale?.language ?? navigator.language
|
||||
this.i18n.locale?.language ?? navigator.language
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -368,7 +364,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
id: this._search,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${this._search}"`,
|
||||
icon_path: mdiPlus,
|
||||
@@ -402,10 +398,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
? typeof this.notFoundLabel === "function"
|
||||
? this.notFoundLabel(this._search)
|
||||
: this.notFoundLabel ||
|
||||
this.localize?.("ui.components.combo-box.no_match") ||
|
||||
this.i18n.localize?.("ui.components.combo-box.no_match") ||
|
||||
"No matching items found"
|
||||
: this.emptyLabel ||
|
||||
this.localize?.("ui.components.combo-box.no_items") ||
|
||||
this.i18n.localize?.("ui.components.combo-box.no_items") ||
|
||||
"No items available"}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
@@ -497,7 +493,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
id: searchString,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
@@ -798,11 +794,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-3);
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-combo-box-item {
|
||||
@@ -873,12 +869,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-3) var(--ha-space-3);
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-3);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) .sections {
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
.sections ha-filter-chip {
|
||||
@@ -915,10 +911,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title-wrapper {
|
||||
height: 0;
|
||||
position: relative;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -11,9 +11,8 @@ import {
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { localizeContext } from "../data/context";
|
||||
import { internationalizationContext } from "../data/context/context";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
@@ -34,8 +33,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
@@ -89,7 +88,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
${this.unknown
|
||||
? html`<div slot="supporting-text" class="unknown">
|
||||
${this.unknownItemText ||
|
||||
this.localize("ui.components.combo-box.unknown_item")}
|
||||
this._i18n?.localize("ui.components.combo-box.unknown_item")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${showClearIcon
|
||||
|
||||
@@ -2,7 +2,7 @@ import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import { fullEntitiesContext } from "../../data/context/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { Action } from "../../data/script";
|
||||
import { migrateAutomationAction } from "../../data/script";
|
||||
|
||||
144
src/components/ha-selector/ha-selector-period.ts
Normal file
144
src/components/ha-selector/ha-selector-period.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { PeriodKey, PeriodSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
const PERIODS = {
|
||||
none: undefined,
|
||||
today: { calendar: { period: "day" } },
|
||||
yesterday: { calendar: { period: "day", offset: -1 } },
|
||||
tomorrow: { calendar: { period: "day", offset: 1 } },
|
||||
this_week: { calendar: { period: "week" } },
|
||||
last_week: { calendar: { period: "week", offset: -1 } },
|
||||
next_week: { calendar: { period: "week", offset: 1 } },
|
||||
this_month: { calendar: { period: "month" } },
|
||||
last_month: { calendar: { period: "month", offset: -1 } },
|
||||
next_month: { calendar: { period: "month", offset: 1 } },
|
||||
this_year: { calendar: { period: "year" } },
|
||||
last_year: { calendar: { period: "year", offset: -1 } },
|
||||
next_7d: { calendar: { period: "day", offset: 7 } },
|
||||
next_30d: { calendar: { period: "day", offset: 30 } },
|
||||
} as const;
|
||||
|
||||
@customElement("ha-selector-period")
|
||||
export class HaPeriodSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: PeriodSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: unknown;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
selectedPeriodKey: PeriodKey | undefined,
|
||||
selector: PeriodSelector,
|
||||
localize: LocalizeFunc
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "period",
|
||||
required: this.required,
|
||||
selector:
|
||||
selectedPeriodKey && selectedPeriodKey in this._periods(selector)
|
||||
? {
|
||||
select: {
|
||||
multiple: false,
|
||||
options: Object.keys(this._periods(selector)).map(
|
||||
(periodKey) => ({
|
||||
value: periodKey,
|
||||
label:
|
||||
localize(
|
||||
`ui.components.selectors.period.periods.${periodKey as PeriodKey}`
|
||||
) || periodKey,
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
: { object: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = this._data(this.value, this.selector);
|
||||
|
||||
const schema = this._schema(
|
||||
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
|
||||
this.selector,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _periods = memoizeOne((selector: PeriodSelector) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(PERIODS).filter(([key]) =>
|
||||
selector.period?.options?.includes(key as any)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private _data = memoizeOne((value: unknown, selector: PeriodSelector) => {
|
||||
for (const [periodKey, period] of Object.entries(this._periods(selector))) {
|
||||
if (deepEqual(period, value)) {
|
||||
return { period: periodKey };
|
||||
}
|
||||
}
|
||||
return { period: value };
|
||||
});
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (typeof newValue.period === "string") {
|
||||
const periods = this._periods(this.selector);
|
||||
if (newValue.period in periods) {
|
||||
const period = this._periods(this.selector)[newValue.period];
|
||||
fireEvent(this, "value-changed", { value: period });
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "value-changed", { value: newValue.period });
|
||||
}
|
||||
}
|
||||
|
||||
private _computeHelperCallback = () => this.helper;
|
||||
|
||||
private _computeLabelCallback = () => this.label;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-period": HaPeriodSelector;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.noEntity=${this.selector.state?.no_entity ?? false}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
|
||||
@@ -65,15 +65,14 @@ export class HaTextSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@input=${this._handleChange}
|
||||
autocapitalize="none"
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
spellcheck="false"
|
||||
.required=${this.required}
|
||||
autogrow
|
||||
resize="auto"
|
||||
></ha-textarea>`;
|
||||
}
|
||||
return html`<ha-input
|
||||
|
||||
@@ -41,6 +41,7 @@ const LOAD_ELEMENTS = {
|
||||
number: () => import("./ha-selector-number"),
|
||||
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
period: () => import("./ha-selector-period"),
|
||||
qr_code: () => import("./ha-selector-qr-code"),
|
||||
select: () => import("./ha-selector-select"),
|
||||
selector: () => import("./ha-selector-selector"),
|
||||
|
||||
@@ -516,17 +516,10 @@ export class HaServiceControl extends LitElement {
|
||||
`}
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.components.service-control.target")}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-settings-row")
|
||||
@@ -16,17 +17,28 @@ export class HaSettingsRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public empty = false;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"description"
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hasDescription = this._hasSlotController.test("description");
|
||||
|
||||
return html`
|
||||
<div class="prefix-wrap">
|
||||
<slot name="prefix"></slot>
|
||||
<div
|
||||
class="body"
|
||||
?two-line=${!this.threeLine}
|
||||
?two-line=${!this.threeLine && hasDescription}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div class="secondary"><slot name="description"></slot></div>
|
||||
${hasDescription
|
||||
? html`<span class="secondary"
|
||||
><slot name="description"></slot
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -23,7 +23,10 @@ export class HaSlider extends Slider {
|
||||
--marker-height: calc(var(--ha-slider-track-size, 4px) / 2);
|
||||
--marker-width: calc(var(--ha-slider-track-size, 4px) / 2);
|
||||
--wa-color-surface-default: var(--card-background-color);
|
||||
--wa-color-neutral-fill-normal: var(--disabled-color);
|
||||
--wa-color-neutral-fill-normal: var(
|
||||
--ha-slider-track-color,
|
||||
var(--disabled-color)
|
||||
);
|
||||
--wa-tooltip-background-color: var(
|
||||
--ha-tooltip-background-color,
|
||||
var(--secondary-background-color)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, css, LitElement, nothing } from "lit";
|
||||
import { mdiStarFourPoints } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
|
||||
import { customElement, state, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type {
|
||||
AITaskPreferences,
|
||||
GenDataTask,
|
||||
GenDataTaskResult,
|
||||
} from "../data/ai_task";
|
||||
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./chips/ha-assist-chip";
|
||||
import "./ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -56,7 +56,7 @@ export class HaSuggestWithAIButton extends LitElement {
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||
if (!this.hass || !isComponentLoaded(this.hass.config, "ai_task")) {
|
||||
return;
|
||||
}
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
|
||||
@@ -13,6 +13,24 @@ export class HaTabGroup extends TabGroup {
|
||||
|
||||
@property({ attribute: "tab-only", type: Boolean }) tabOnly = true;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Prevent the tab group from consuming Alt+Arrow and Cmd+Arrow keys,
|
||||
// which browsers use for back/forward navigation.
|
||||
this.addEventListener("keydown", this._handleKeyDown, true);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown, true);
|
||||
}
|
||||
|
||||
private _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.metaKey) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
protected override handleClick(event: MouseEvent) {
|
||||
if (this._dragScrollController.scrolled) {
|
||||
return;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor_picker";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { labelsContext } from "../data/context/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
|
||||
@@ -1,66 +1,249 @@
|
||||
import { TextAreaBase } from "@material/mwc-textarea/mwc-textarea-base";
|
||||
import { styles as textfieldStyles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { styles as textareaStyles } from "@material/mwc-textarea/mwc-textarea.css";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import type WaTextarea from "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { WaInputMixin, waInputStyles } from "./input/wa-input-mixin";
|
||||
|
||||
/**
|
||||
* Home Assistant textarea component
|
||||
*
|
||||
* @element ha-textarea
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A multi-line text input component supporting Home Assistant theming and validation, based on webawesome textarea.
|
||||
*
|
||||
* @slot label - Custom label content. Overrides the `label` property.
|
||||
* @slot hint - Custom hint content. Overrides the `hint` property.
|
||||
*
|
||||
* @csspart wa-base - The underlying wa-textarea base wrapper.
|
||||
* @csspart wa-hint - The underlying wa-textarea hint container.
|
||||
* @csspart wa-textarea - The underlying wa-textarea textarea element.
|
||||
*
|
||||
* @cssprop --ha-textarea-padding-bottom - Padding below the textarea host.
|
||||
* @cssprop --ha-textarea-max-height - Maximum height of the textarea when using `resize="auto"`. Defaults to `200px`.
|
||||
* @cssprop --ha-textarea-required-marker - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
*
|
||||
* @attr {string} label - The textarea's label text.
|
||||
* @attr {string} hint - The textarea's hint/helper text.
|
||||
* @attr {string} placeholder - Placeholder text shown when the textarea is empty.
|
||||
* @attr {boolean} readonly - Makes the textarea readonly.
|
||||
* @attr {boolean} disabled - Disables the textarea and prevents user interaction.
|
||||
* @attr {boolean} required - Makes the textarea a required field.
|
||||
* @attr {number} rows - Number of visible text rows.
|
||||
* @attr {number} minlength - Minimum number of characters required.
|
||||
* @attr {number} maxlength - Maximum number of characters allowed.
|
||||
* @attr {("none"|"vertical"|"horizontal"|"both"|"auto")} resize - Controls the textarea's resize behavior. Defaults to `"none"`.
|
||||
* @attr {boolean} auto-validate - Validates the textarea on blur instead of on form submit.
|
||||
* @attr {boolean} invalid - Marks the textarea as invalid.
|
||||
* @attr {string} validation-message - Custom validation message shown when the textarea is invalid.
|
||||
*/
|
||||
@customElement("ha-textarea")
|
||||
export class HaTextArea extends TextAreaBase {
|
||||
@property({ type: Boolean, reflect: true }) autogrow = false;
|
||||
export class HaTextArea extends WaInputMixin(LitElement) {
|
||||
@property({ type: Number })
|
||||
public rows?: number;
|
||||
|
||||
updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this.autogrow && changedProperties.has("value")) {
|
||||
this.mdcRoot.dataset.value = this.value + '=\u200B"'; // add a zero-width space to correctly wrap
|
||||
@property()
|
||||
public resize: "none" | "vertical" | "horizontal" | "both" | "auto" = "none";
|
||||
|
||||
@query("wa-textarea")
|
||||
private _textarea?: WaTextarea;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"label",
|
||||
"hint"
|
||||
);
|
||||
|
||||
protected get _formControl(): WaTextarea | undefined {
|
||||
return this._textarea;
|
||||
}
|
||||
|
||||
protected readonly _requiredMarkerCSSVar = "--ha-textarea-required-marker";
|
||||
|
||||
/** Programmatically toggle focus styling (used by ha-date-range-picker). */
|
||||
public setFocused(focused: boolean): void {
|
||||
if (focused) {
|
||||
this.toggleAttribute("focused", true);
|
||||
} else {
|
||||
this.removeAttribute("focused");
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
textfieldStyles,
|
||||
textareaStyles,
|
||||
protected render() {
|
||||
const hasLabelSlot = this.label
|
||||
? false
|
||||
: this._hasSlotController.test("label");
|
||||
|
||||
const hasHintSlot = this.hint
|
||||
? false
|
||||
: this._hasSlotController.test("hint");
|
||||
|
||||
return html`
|
||||
<wa-textarea
|
||||
.value=${this.value ?? null}
|
||||
.placeholder=${this.placeholder}
|
||||
.readonly=${this.readonly}
|
||||
.required=${this.required}
|
||||
.rows=${this.rows ?? 4}
|
||||
.resize=${this.resize}
|
||||
.disabled=${this.disabled}
|
||||
name=${ifDefined(this.name)}
|
||||
autocapitalize=${ifDefined(this.autocapitalize || undefined)}
|
||||
autocomplete=${ifDefined(this.autocomplete)}
|
||||
.autofocus=${this.autofocus}
|
||||
.spellcheck=${this.spellcheck}
|
||||
inputmode=${ifDefined(this.inputmode || undefined)}
|
||||
enterkeyhint=${ifDefined(this.enterkeyhint || undefined)}
|
||||
minlength=${ifDefined(this.minlength)}
|
||||
maxlength=${ifDefined(this.maxlength)}
|
||||
class=${classMap({
|
||||
input: true,
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
(this.label && this.placeholder),
|
||||
"no-label": !this.label,
|
||||
"hint-hidden":
|
||||
!this.hint &&
|
||||
!hasHintSlot &&
|
||||
!this.required &&
|
||||
!this._invalid &&
|
||||
!this.invalid,
|
||||
})}
|
||||
@input=${this._handleInput}
|
||||
@change=${this._handleChange}
|
||||
@blur=${this._handleBlur}
|
||||
@wa-invalid=${this._handleInvalid}
|
||||
exportparts="base:wa-base, hint:wa-hint, textarea:wa-textarea"
|
||||
>
|
||||
${this.label || hasLabelSlot
|
||||
? html`<slot name="label" slot="label"
|
||||
>${this.label
|
||||
? this._renderLabel(this.label, this.required)
|
||||
: nothing}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<div
|
||||
slot="hint"
|
||||
class=${classMap({
|
||||
error: this.invalid || this._invalid,
|
||||
})}
|
||||
role=${ifDefined(this.invalid || this._invalid ? "alert" : undefined)}
|
||||
aria-live="polite"
|
||||
>
|
||||
${this._invalid || this.invalid
|
||||
? this.validationMessage || this._textarea?.validationMessage
|
||||
: this.hint ||
|
||||
(hasHintSlot ? html`<slot name="hint"></slot>` : nothing)}
|
||||
</div>
|
||||
</wa-textarea>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-text-field-fill-color: var(--ha-color-form-background);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-bottom: var(--ha-textarea-padding-bottom);
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field {
|
||||
position: relative;
|
||||
min-height: 74px;
|
||||
min-width: 178px;
|
||||
max-height: 200px;
|
||||
|
||||
/* Label styling */
|
||||
wa-textarea::part(label) {
|
||||
width: calc(100% - var(--ha-space-2));
|
||||
background-color: var(--ha-color-form-background);
|
||||
transition:
|
||||
all var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
padding-inline-end: var(--ha-space-3);
|
||||
margin: var(--ha-space-1) var(--ha-space-1) 0;
|
||||
padding-top: var(--ha-space-4);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field:after {
|
||||
content: attr(data-value);
|
||||
margin-top: 23px;
|
||||
margin-bottom: 9px;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
min-height: 42px;
|
||||
padding: 0px 32px 0 16px;
|
||||
letter-spacing: var(
|
||||
--mdc-typography-subtitle1-letter-spacing,
|
||||
0.009375em
|
||||
);
|
||||
visibility: hidden;
|
||||
white-space: pre-wrap;
|
||||
|
||||
:host(:focus-within) wa-textarea::part(label),
|
||||
:host([focused]) wa-textarea::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field__input {
|
||||
|
||||
wa-textarea.label-raised::part(label),
|
||||
:host(:focus-within) wa-textarea::part(label),
|
||||
:host([focused]) wa-textarea::part(label) {
|
||||
padding-top: var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
wa-textarea.no-label::part(label) {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base styling */
|
||||
wa-textarea::part(base) {
|
||||
min-height: 56px;
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
wa-textarea.no-label::part(base) {
|
||||
padding-top: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-textarea::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: calc(100% - 32px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field.mdc-text-field--no-label:after {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:host(:focus-within) wa-textarea::part(base)::after,
|
||||
:host([focused]) wa-textarea::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start) top;
|
||||
|
||||
:host(:focus-within) wa-textarea.invalid::part(base)::after,
|
||||
wa-textarea.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
@media only screen and (min-width: 459px) {
|
||||
:host([mobile-multiline]) .mdc-text-field__input {
|
||||
white-space: nowrap;
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
/* Textarea element styling */
|
||||
wa-textarea::part(textarea) {
|
||||
padding: 0 var(--ha-space-4);
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
:host([resize="auto"]) wa-textarea::part(textarea) {
|
||||
max-height: var(--ha-textarea-max-height, 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
wa-textarea:hover::part(base),
|
||||
wa-textarea:hover::part(label) {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
wa-textarea[disabled]::part(textarea) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
wa-textarea[disabled]::part(base),
|
||||
wa-textarea[disabled]::part(label) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
/**
|
||||
* Legacy wrapper around ha-input that preserves the mwc-textfield API.
|
||||
* New code should use ha-input directly.
|
||||
* @deprecated Use ha-input instead.
|
||||
*/
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends LitElement {
|
||||
@property({ type: String })
|
||||
public value = "";
|
||||
|
||||
@property({ type: String })
|
||||
public type:
|
||||
| "text"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "email"
|
||||
| "password"
|
||||
| "date"
|
||||
| "month"
|
||||
| "week"
|
||||
| "time"
|
||||
| "datetime-local"
|
||||
| "number"
|
||||
| "color" = "text";
|
||||
|
||||
@property({ type: String })
|
||||
public label = "";
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = "";
|
||||
|
||||
@property({ type: String })
|
||||
public prefix = "";
|
||||
|
||||
@property({ type: String })
|
||||
public suffix = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
public icon = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
public iconTrailing = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
@property({ type: Number, attribute: "minlength" })
|
||||
public minLength = -1;
|
||||
|
||||
@property({ type: Number, attribute: "maxlength" })
|
||||
public maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public outlined = false;
|
||||
|
||||
@property({ type: String })
|
||||
public helper = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "validateoninitialrender" })
|
||||
public validateOnInitialRender = false;
|
||||
|
||||
@property({ type: String, attribute: "validationmessage" })
|
||||
public validationMessage = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "autovalidate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: String })
|
||||
public pattern = "";
|
||||
|
||||
@property()
|
||||
public min: number | string = "";
|
||||
|
||||
@property()
|
||||
public max: number | string = "";
|
||||
|
||||
@property()
|
||||
public step: number | "any" | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
public size: number | null = null;
|
||||
|
||||
@property({ type: Boolean, attribute: "helperpersistent" })
|
||||
public helperPersistent = false;
|
||||
|
||||
@property({ attribute: "charcounter" })
|
||||
public charCounter: boolean | "external" | "internal" = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "endaligned" })
|
||||
public endAligned = false;
|
||||
|
||||
@property({ type: String, attribute: "inputmode" })
|
||||
public inputMode = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "readonly" })
|
||||
public readOnly = false;
|
||||
|
||||
@property({ type: String })
|
||||
public name = "";
|
||||
|
||||
@property({ type: String })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize = "";
|
||||
|
||||
// --- ha-textfield-specific properties ---
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" })
|
||||
public errorMessage?: string;
|
||||
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@query("ha-input")
|
||||
private _haInput?: HaInput;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public get formElement(): HTMLInputElement | undefined {
|
||||
return (this._haInput as any)?._input?.input;
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this._haInput?.select();
|
||||
}
|
||||
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._haInput?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._haInput?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._haInput?.checkValidity() ?? true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._haInput?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
this.validationMessage = message;
|
||||
this.invalid = !!message;
|
||||
}
|
||||
|
||||
/** No-op. Preserved for backward compatibility. */
|
||||
public layout(): void {
|
||||
// no-op — mwc-textfield needed this for notched outline recalculation
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.validateOnInitialRender) {
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("invalid") && this._haInput) {
|
||||
if (
|
||||
this.invalid ||
|
||||
(changedProperties.get("invalid") !== undefined && !this.invalid)
|
||||
) {
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
const errorMsg = this.errorMessage || this.validationMessage;
|
||||
return html`
|
||||
<ha-input
|
||||
.type=${this.type}
|
||||
.value=${this.value || undefined}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.readonly=${this.readOnly}
|
||||
.pattern=${this.pattern || undefined}
|
||||
.minlength=${this.minLength > 0 ? this.minLength : undefined}
|
||||
.maxlength=${this.maxLength > 0 ? this.maxLength : undefined}
|
||||
.min=${this.min !== "" ? this.min : undefined}
|
||||
.max=${this.max !== "" ? this.max : undefined}
|
||||
.step=${this.step ?? undefined}
|
||||
.name=${this.name || undefined}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.spellcheck=${this.inputSpellcheck === "true"}
|
||||
.inputmode=${this.inputMode}
|
||||
.autocapitalize=${this.autocapitalize || ""}
|
||||
.invalid=${this.invalid}
|
||||
.validationMessage=${errorMsg || ""}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.hint=${this.helper}
|
||||
.withoutSpinButtons=${this.type === "number"}
|
||||
.insetLabel=${this.prefix}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onChange}
|
||||
>
|
||||
${this.icon
|
||||
? html`<slot name="leadingIcon" slot="start"></slot>`
|
||||
: nothing}
|
||||
${this.prefix
|
||||
? html`<span class="prefix" slot="start">${this.prefix}</span>`
|
||||
: nothing}
|
||||
${this.suffix
|
||||
? html`<span class="suffix" slot="end">${this.suffix}</span>`
|
||||
: nothing}
|
||||
${this.iconTrailing
|
||||
? html`<slot name="trailingIcon" slot="end"></slot>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onInput(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ha-input {
|
||||
--ha-input-padding-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
padding-top: var(--ha-space-3);
|
||||
margin-inline-end: var(--text-field-prefix-padding-right);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-textfield": HaTextField;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||
import type { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||
import { strings } from "@material/top-app-bar/constants";
|
||||
// eslint-disable-next-line import-x/no-named-as-default
|
||||
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||
import { html, css, nothing } from "lit";
|
||||
import { property, query, customElement } from "lit/decorators";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { internationalizationContext } from "../../data/context/context";
|
||||
import { showToast } from "../../util/toast";
|
||||
import "../ha-button";
|
||||
import "../ha-icon-button";
|
||||
@@ -59,8 +59,8 @@ export class HaInputCopy extends LitElement {
|
||||
false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _showMasked = true;
|
||||
|
||||
@@ -90,7 +90,7 @@ export class HaInputCopy extends LitElement {
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
class="toggle-unmasked"
|
||||
.label=${this.localize(
|
||||
.label=${this._i18n.localize(
|
||||
`ui.common.${this._showMasked ? "show" : "hide"}`
|
||||
)}
|
||||
@click=${this._toggleMasked}
|
||||
@@ -101,7 +101,7 @@ export class HaInputCopy extends LitElement {
|
||||
</div>
|
||||
<ha-button @click=${this._copy} appearance="plain" size="small">
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.label || this.localize("ui.common.copy")}
|
||||
${this.label || this._i18n.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
@@ -119,7 +119,7 @@ export class HaInputCopy extends LitElement {
|
||||
private async _copy(): Promise<void> {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message: this.localize("ui.common.copied_clipboard"),
|
||||
message: this._i18n.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { internationalizationContext } from "../../data/context/context";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../ha-button";
|
||||
import "../ha-icon-button";
|
||||
@@ -64,8 +64,8 @@ class HaInputMulti extends LitElement {
|
||||
public updateOnBlur = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize?: ContextType<typeof localizeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -109,7 +109,7 @@ class HaInputMulti extends LitElement {
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
.label=${this.removeLabel ??
|
||||
this.localize?.("ui.common.remove") ??
|
||||
this._i18n?.localize("ui.common.remove") ??
|
||||
"Remove"}
|
||||
@click=${this._removeItem}
|
||||
.path=${mdiDeleteOutline}
|
||||
@@ -137,10 +137,10 @@ class HaInputMulti extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.addLabel ??
|
||||
(this.label
|
||||
? this.localize?.("ui.components.multi-textfield.add_item", {
|
||||
? this._i18n?.localize("ui.components.multi-textfield.add_item", {
|
||||
item: this.label,
|
||||
})
|
||||
: this.localize?.("ui.common.add")) ??
|
||||
: this._i18n?.localize("ui.common.add")) ??
|
||||
"Add"}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { internationalizationContext } from "../../data/context/context";
|
||||
import { HaInput } from "./ha-input";
|
||||
|
||||
/**
|
||||
@@ -18,8 +18,8 @@ import { HaInput } from "./ha-input";
|
||||
@customElement("ha-input-search")
|
||||
export class HaInputSearch extends HaInput {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -33,9 +33,9 @@ export class HaInputSearch extends HaInput {
|
||||
if (
|
||||
!this.label &&
|
||||
!this.placeholder &&
|
||||
(!this.hasUpdated || changedProps.has("localize"))
|
||||
(!this.hasUpdated || changedProps.has("_i18n"))
|
||||
) {
|
||||
this.placeholder = this.localize("ui.common.search");
|
||||
this.placeholder = this._i18n.localize("ui.common.search");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import { WaInputMixin, waInputStyles } from "./wa-input-mixin";
|
||||
|
||||
export type InputType =
|
||||
| "date"
|
||||
@@ -77,35 +77,16 @@ export type InputType =
|
||||
* @attr {string} validation-message - Custom validation message shown when the input is invalid.
|
||||
*/
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
export class HaInput extends WaInputMixin(LitElement) {
|
||||
@property({ reflect: true }) appearance: "material" | "outlined" = "material";
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: InputType = "text";
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
|
||||
/** The input's hint. */
|
||||
@property()
|
||||
public hint? = "";
|
||||
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean, attribute: "with-clear" })
|
||||
public withClear = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property()
|
||||
public placeholder = "";
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. */
|
||||
@property({ type: Boolean, attribute: "password-toggle" })
|
||||
public passwordToggle = false;
|
||||
@@ -118,22 +99,10 @@ export class HaInput extends LitElement {
|
||||
@property({ type: Boolean, attribute: "without-spin-buttons" })
|
||||
public withoutSpinButtons = false;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required = false;
|
||||
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property()
|
||||
public pattern?: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public min?: number | string;
|
||||
@@ -146,88 +115,13 @@ export class HaInput extends LitElement {
|
||||
@property()
|
||||
public step?: number | "any";
|
||||
|
||||
/** Controls whether and how text input is automatically capitalized. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = false;
|
||||
|
||||
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
/** Tells the browser what type of data will be entered by the user. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/** Disables the form control. */
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
/** Custom validation message to show when the input is invalid. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage? = "";
|
||||
|
||||
/** When true, validates the input on blur instead of on form submit. */
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "inset-label" })
|
||||
public insetLabel = false;
|
||||
|
||||
@state()
|
||||
private _invalid = false;
|
||||
|
||||
@query("wa-input")
|
||||
private _input?: WaInput;
|
||||
|
||||
@@ -235,40 +129,12 @@ export class HaInput extends LitElement {
|
||||
this,
|
||||
"label",
|
||||
"hint",
|
||||
"input"
|
||||
"input",
|
||||
"start"
|
||||
);
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
public select(): void {
|
||||
this._input?.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._input?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._input?.setRangeText(replacement, start, end, selectMode);
|
||||
protected get _formControl(): WaInput | undefined {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element. */
|
||||
@@ -286,17 +152,6 @@ export class HaInput extends LitElement {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._input?.checkValidity() ?? true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const valid = this.checkValidity();
|
||||
|
||||
this._invalid = !valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected override async firstUpdated(
|
||||
changedProperties: PropertyValues
|
||||
): Promise<void> {
|
||||
@@ -318,6 +173,8 @@ export class HaInput extends LitElement {
|
||||
? false
|
||||
: this._hasSlotController.test("hint");
|
||||
|
||||
const hasStartSlot = this._hasSlotController.test("start");
|
||||
|
||||
return html`
|
||||
<wa-input
|
||||
.type=${this.type}
|
||||
@@ -345,10 +202,12 @@ export class HaInput extends LitElement {
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
class=${classMap({
|
||||
input: true,
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
(this.label && this.placeholder),
|
||||
(this.label && this.placeholder) ||
|
||||
(hasStartSlot && this.insetLabel),
|
||||
"no-label": !this.label,
|
||||
"hint-hidden":
|
||||
!this.hint &&
|
||||
@@ -365,7 +224,9 @@ export class HaInput extends LitElement {
|
||||
>
|
||||
${this.label || hasLabelSlot
|
||||
? html`<slot name="label" slot="label"
|
||||
>${this._renderLabel(this.label, this.required)}</slot
|
||||
>${this.label
|
||||
? this._renderLabel(this.label, this.required)
|
||||
: nothing}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<slot name="start" slot="start" @slotchange=${this._syncStartSlotWidth}>
|
||||
@@ -412,27 +273,6 @@ export class HaInput extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
if (this._invalid && this._input?.checkValidity()) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
if (this.autoValidate) {
|
||||
this._invalid = !this._input?.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInvalid() {
|
||||
this._invalid = true;
|
||||
}
|
||||
|
||||
private _syncStartSlotWidth = () => {
|
||||
const startEl = this._input?.shadowRoot?.querySelector(
|
||||
'[part~="start"]'
|
||||
@@ -453,204 +293,133 @@ export class HaInput extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _renderLabel = memoizeOne((label: string, required: boolean) => {
|
||||
if (!required) {
|
||||
return label;
|
||||
}
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--ha-input-padding-top);
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
:host([appearance="outlined"]) {
|
||||
padding-bottom: var(--ha-input-padding-bottom);
|
||||
}
|
||||
|
||||
let marker = getComputedStyle(this).getPropertyValue(
|
||||
"--ha-input-required-marker"
|
||||
);
|
||||
wa-input::part(label) {
|
||||
padding-inline-start: calc(
|
||||
var(--start-slot-width, 0px) + var(--ha-space-4)
|
||||
);
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
padding-top: var(--ha-space-5);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
marker = "*";
|
||||
}
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
if (marker.startsWith('"') && marker.endsWith('"')) {
|
||||
marker = marker.slice(1, -1);
|
||||
}
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::part(label),
|
||||
:host([type="date"]) wa-input::part(label) {
|
||||
padding-top: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
return label;
|
||||
}
|
||||
wa-input::part(base) {
|
||||
height: 56px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
return `${label}${marker}`;
|
||||
});
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--ha-input-padding-top);
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
:host([appearance="outlined"]) {
|
||||
padding-bottom: var(--ha-input-padding-bottom);
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
--wa-transition-fast: var(--wa-transition-normal);
|
||||
position: relative;
|
||||
}
|
||||
:host([appearance="outlined"]) wa-input::part(base) {
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
transition: border-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input::part(label) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-family: var(--ha-font-family-body);
|
||||
transition: all var(--wa-transition-normal) ease-in-out;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
padding-inline-start: calc(
|
||||
var(--start-slot-width, 0px) + var(--ha-space-4)
|
||||
);
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
padding-top: var(--ha-space-5);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
:host([appearance="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input.invalid::part(label),
|
||||
wa-input.invalid:not([disabled])::part(label) {
|
||||
color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([appearance="material"]:focus-within)
|
||||
wa-input.invalid::part(base)::after,
|
||||
:host([appearance="material"])
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::part(label),
|
||||
:host([type="date"]) wa-input::part(label) {
|
||||
padding-top: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
wa-input::part(input) {
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-inline-start: var(--input-padding-inline-start, 0);
|
||||
}
|
||||
|
||||
wa-input::part(base) {
|
||||
height: 56px;
|
||||
background-color: var(--ha-color-form-background);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border: none;
|
||||
padding: 0 var(--ha-space-4);
|
||||
position: relative;
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
wa-input.no-label::part(input) {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host([type="color"]) wa-input::part(input) {
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(input) {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
wa-input::part(input)::placeholder {
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
wa-input::part(base):hover {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base) {
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
transition: border-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
:host([appearance="outlined"]) wa-input::part(base):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
:host([appearance="outlined"]:focus-within) wa-input::part(base) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([appearance="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:host([appearance="material"]:focus-within)
|
||||
wa-input.invalid::part(base)::after,
|
||||
:host([appearance="material"])
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
wa-input::part(input) {
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-inline-start: var(--input-padding-inline-start, 0);
|
||||
}
|
||||
|
||||
wa-input.no-label::part(input) {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host([type="color"]) wa-input::part(input) {
|
||||
padding-top: var(--ha-space-6);
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(input) {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
wa-input::part(input)::placeholder {
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(base) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
wa-input::part(base):hover {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
:host([appearance="outlined"]:focus-within) wa-input::part(base) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
wa-input.hint-hidden::part(hint) {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
`;
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
357
src/components/input/wa-input-mixin.ts
Normal file
357
src/components/input/wa-input-mixin.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { type LitElement, css } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { Constructor } from "../../types";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
/**
|
||||
* Minimal interface for the inner wa-input / wa-textarea element.
|
||||
*/
|
||||
export interface WaInput {
|
||||
value: string | null;
|
||||
select(): void;
|
||||
setSelectionRange(
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none"
|
||||
): void;
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void;
|
||||
checkValidity(): boolean;
|
||||
validationMessage: string;
|
||||
}
|
||||
|
||||
export interface WaInputMixinInterface {
|
||||
value?: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
placeholder: string;
|
||||
readonly: boolean;
|
||||
required: boolean;
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "";
|
||||
autocomplete?: string;
|
||||
autofocus: boolean;
|
||||
spellcheck: boolean;
|
||||
inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "";
|
||||
enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "";
|
||||
name?: string;
|
||||
disabled: boolean;
|
||||
validationMessage?: string;
|
||||
autoValidate: boolean;
|
||||
invalid: boolean;
|
||||
select(): void;
|
||||
setSelectionRange(
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none"
|
||||
): void;
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void;
|
||||
checkValidity(): boolean;
|
||||
reportValidity(): boolean;
|
||||
}
|
||||
|
||||
export const WaInputMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class FormControlMixinClass extends superClass {
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
@property()
|
||||
public label? = "";
|
||||
|
||||
@property()
|
||||
public hint? = "";
|
||||
|
||||
@property()
|
||||
public placeholder? = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage? = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@state()
|
||||
protected _invalid = false;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Override in subclass to return the inner wa-input / wa-textarea element.
|
||||
*/
|
||||
protected get _formControl(): WaInput | undefined {
|
||||
throw new Error("_formControl getter must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* Override in subclass to set the CSS custom property name
|
||||
* used for the required-marker character (e.g. "--ha-input-required-marker").
|
||||
*/
|
||||
protected readonly _requiredMarkerCSSVar: string =
|
||||
"--ha-input-required-marker";
|
||||
|
||||
public select(): void {
|
||||
this._formControl?.select();
|
||||
}
|
||||
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._formControl?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._formControl?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return nativeElementInternalsSupported
|
||||
? (this._formControl?.checkValidity() ?? true)
|
||||
: true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const valid = this.checkValidity();
|
||||
this._invalid = !valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected _handleInput(): void {
|
||||
this.value = this._formControl?.value ?? undefined;
|
||||
if (this._invalid && this._formControl?.checkValidity()) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleChange(): void {
|
||||
this.value = this._formControl?.value ?? undefined;
|
||||
}
|
||||
|
||||
protected _handleBlur(): void {
|
||||
if (this.autoValidate) {
|
||||
this._invalid = !this._formControl?.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleInvalid(): void {
|
||||
this._invalid = true;
|
||||
}
|
||||
|
||||
protected _renderLabel = memoizeOne((label: string, required: boolean) => {
|
||||
if (!required) {
|
||||
return label;
|
||||
}
|
||||
|
||||
let marker = getComputedStyle(this).getPropertyValue(
|
||||
this._requiredMarkerCSSVar
|
||||
);
|
||||
|
||||
if (!marker) {
|
||||
marker = "*";
|
||||
}
|
||||
|
||||
if (marker.startsWith('"') && marker.endsWith('"')) {
|
||||
marker = marker.slice(1, -1);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}${marker}`;
|
||||
});
|
||||
}
|
||||
|
||||
return FormControlMixinClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared styles for form controls (ha-input / ha-textarea).
|
||||
* Both components add the `control` CSS class to the inner wa-input / wa-textarea
|
||||
* element so these rules can target them with a single selector.
|
||||
*/
|
||||
export const waInputStyles = css`
|
||||
/* Inner element reset */
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
--wa-transition-fast: var(--wa-transition-normal);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Label base */
|
||||
.input::part(label) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-family: var(--ha-font-family-body);
|
||||
transition: all var(--wa-transition-normal) ease-in-out;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
/* Invalid label */
|
||||
:host(:focus-within) .input.invalid::part(label),
|
||||
.input.invalid:not([disabled])::part(label) {
|
||||
color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
|
||||
/* Base common */
|
||||
.input::part(base) {
|
||||
background-color: var(--ha-color-form-background);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border: none;
|
||||
position: relative;
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
/* Focus outline removal */
|
||||
:host(:focus-within) .input::part(base) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hint */
|
||||
.input::part(hint) {
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.input.hint-hidden::part(hint) {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Error hint text */
|
||||
.error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
`;
|
||||
@@ -254,7 +254,11 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
this._loading = true;
|
||||
try {
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
|
||||
latitude: this.hass?.config.latitude ?? 52.3731339,
|
||||
longitude: this.hass?.config.longitude ?? 4.8903147,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
this._updateMapStyle();
|
||||
this.leafletMap.on("click", (ev) => {
|
||||
if (this._clickCount === 0) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { animate } from "@lit-labs/motion";
|
||||
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckboxMarkedOutline,
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -26,8 +26,8 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-spinner";
|
||||
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
|
||||
)}
|
||||
</ha-list>
|
||||
`}
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
${isComponentLoaded(this.hass.config, "hassio")
|
||||
? html`<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.tip_media_storage",
|
||||
|
||||
@@ -79,6 +79,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
flexcontent
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._dialogOpened}
|
||||
@@ -230,6 +231,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 65px - var(--safe-area-inset-y)
|
||||
);
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
:host(.opened) ha-media-player-browse {
|
||||
@@ -248,7 +251,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 145px - var(--safe-area-inset-y)
|
||||
);
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-textarea
|
||||
autogrow
|
||||
resize="auto"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.message"
|
||||
)}
|
||||
@@ -200,7 +200,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
}
|
||||
|
||||
private async _ttsClicked(): Promise<void> {
|
||||
const message = this.shadowRoot!.querySelector("ha-textarea")!.value;
|
||||
const message = this.shadowRoot!.querySelector("ha-textarea")!.value ?? "";
|
||||
this._message = message;
|
||||
const item = { ...this.item };
|
||||
const query = new URLSearchParams();
|
||||
|
||||
@@ -49,7 +49,6 @@ import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-card";
|
||||
import "../ha-fab";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
@@ -446,24 +445,20 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
currentItem.media_content_id
|
||||
))
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
<ha-button
|
||||
class="fab"
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
.title=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</ha-fab>
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -1363,11 +1358,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
height 0.4s,
|
||||
padding-bottom 0.4s;
|
||||
}
|
||||
ha-fab {
|
||||
.fab {
|
||||
position: absolute;
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
bottom: -20px;
|
||||
right: 20px;
|
||||
bottom: calc(var(--ha-space-5) * -1);
|
||||
right: var(--ha-space-5);
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
:host([narrow]) .header-info ha-button {
|
||||
margin-top: 16px;
|
||||
@@ -1429,11 +1424,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding-bottom: initial;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:host([scrolled]) ha-fab {
|
||||
bottom: 0px;
|
||||
right: -24px;
|
||||
--mdc-fab-box-shadow: none;
|
||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||||
:host([scrolled]) .fab {
|
||||
bottom: 0;
|
||||
right: calc(var(--ha-space-6) * -1);
|
||||
--ha-button-box-shadow: none;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
|
||||
92
src/components/progress/ha-progress-bar.ts
Normal file
92
src/components/progress/ha-progress-bar.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import ProgressBar from "@home-assistant/webawesome/dist/components/progress-bar/progress-bar";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant progress bar component
|
||||
*
|
||||
* @element ha-progress-bar
|
||||
* @extends {ProgressBar}
|
||||
*
|
||||
* @summary
|
||||
* A stylable progress bar component based on webawesome progress bar.
|
||||
* Supports regular, indeterminate, and loading states with Home Assistant theming.
|
||||
*
|
||||
* @cssprop --ha-progress-bar-indicator-color - Color of the filled progress indicator.
|
||||
* @cssprop --ha-progress-bar-track-color - Color of the progress track.
|
||||
* @cssprop --ha-progress-bar-track-height - Height of the progress track. Defaults to `16px`.
|
||||
* @cssprop --ha-progress-bar-border-radius - Border radius of the progress bar. Defaults to `var(--ha-border-radius-pill)`.
|
||||
* @cssprop --ha-progress-bar-animation-duration - Animation duration for indeterminate/loading highlight. Defaults to `2.5s`.
|
||||
*
|
||||
* @attr {boolean} loading - Shows the loading highlight animation on top of the indicator.
|
||||
* @attr {boolean} indeterminate - Shows indeterminate progress animation (inherited from ProgressBar).
|
||||
*/
|
||||
@customElement("ha-progress-bar")
|
||||
export class HaProgressBar extends ProgressBar {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
loading = false;
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
ProgressBar.styles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-progress-bar-indicator-color,
|
||||
var(--ha-color-on-primary-normal)
|
||||
);
|
||||
--track-color: var(
|
||||
--ha-progress-bar-track-color,
|
||||
var(--ha-color-fill-neutral-normal-hover)
|
||||
);
|
||||
--track-height: var(--ha-progress-bar-track-height, 16px);
|
||||
--wa-transition-slow: var(--ha-animation-duration-slow);
|
||||
}
|
||||
.progress-bar {
|
||||
border-radius: var(
|
||||
--ha-progress-bar-border-radius,
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
}
|
||||
@keyframes slide-highlight {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
:host([indeterminate]) .indicator {
|
||||
animation: wa-progress-indeterminate
|
||||
var(--ha-progress-bar-animation-duration, 2.5s) infinite
|
||||
cubic-bezier(0.37, 0, 0.63, 1);
|
||||
}
|
||||
:host([indeterminate]) .indicator::after,
|
||||
:host([loading]) .indicator::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--ha-color-fill-primary-normal-hover) 45%,
|
||||
var(--ha-color-fill-primary-normal-active) 50%,
|
||||
var(--ha-color-fill-primary-normal-hover) 55%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0.4;
|
||||
animation: slide-highlight
|
||||
var(--ha-progress-bar-animation-duration, 2.5s) infinite
|
||||
cubic-bezier(0.37, 0, 0.63, 1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-progress-bar": HaProgressBar;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { getEntityContext } from "../../common/entity/context/get_entity_context
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import { labelsContext } from "../../data/context/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
@@ -524,7 +524,13 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
|
||||
return {
|
||||
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
||||
name: device
|
||||
? computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
: item,
|
||||
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
||||
fallbackIconPath: mdiDevices,
|
||||
notFound: !device,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import { labelsContext } from "../../data/context/context";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { TargetType } from "../../data/target";
|
||||
@@ -161,7 +161,13 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
}
|
||||
|
||||
return {
|
||||
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
|
||||
name: device
|
||||
? computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
: itemId,
|
||||
fallbackIconPath: mdiDevices,
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user